From 7eb87afab8c6047b9555d0c64cc4b6d326e1f794 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 10 Oct 2022 14:00:26 +0300 Subject: [PATCH] cli: unify parameters parsing Share parameters parsing code between 'contract invokefunction' and 'vm run' commands. It allows VM CLI to parse more complicated parameter types including arrays and file-backed bytestrings. --- cli/cmdargs/parser.go | 70 +++++++++++++++++++++++++++ cli/smartcontract/smart_contract.go | 67 +------------------------- cli/vm/cli.go | 73 ++++++----------------------- cli/vm/cli_test.go | 12 +++++ pkg/smartcontract/parameter.go | 9 ++++ pkg/smartcontract/parameter_test.go | 65 ++++++++++++++++--------- pkg/vm/stackitem/item.go | 12 +++++ pkg/vm/stackitem/item_test.go | 6 ++- 8 files changed, 167 insertions(+), 147 deletions(-) diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index ab8f8a34b..3868466a2 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -24,6 +24,76 @@ const ( ArrayEndSeparator = "]" ) +const ( + // ParamsParsingDoc is a documentation for parameters parsing. + ParamsParsingDoc = ` Arguments always do have regular Neo smart contract parameter types, either + specified explicitly or being inferred from the value. To specify the type + manually use "type:value" syntax where the type is one of the following: + 'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'. + Array types are also supported: use special space-separated '[' and ']' + symbols around array values to denote array bounds. Nested arrays are also + supported. + + There is ability to provide an argument of 'bytearray' type via file. Use a + special 'filebytes' argument type for this with a filepath specified after + the colon, e.g. 'filebytes:my_file.txt'. + + Given values are type-checked against given types with the following + restrictions applied: + * 'signature' type values should be hex-encoded and have a (decoded) + length of 64 bytes. + * 'bool' type values are 'true' and 'false'. + * 'int' values are decimal integers that can be successfully converted + from the string. + * 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after + decoding) strings. + * 'hash256' type values should be hex-encoded and have a (decoded) + length of 32 bytes. + * 'bytes' type values are any hex-encoded things. + * 'filebytes' type values are filenames with the argument value inside. + * 'key' type values are hex-encoded marshalled public keys. + * 'string' type values are any valid UTF-8 strings. In the value's part of + the string the colon looses it's special meaning as a separator between + type and value and is taken literally. + + If no type is explicitly specified, it is inferred from the value using the + following logic: + - anything that can be interpreted as a decimal integer gets + an 'int' type + - 'true' and 'false' strings get 'bool' type + - valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160' + type + - valid hex-encoded public keys get 'key' type + - 32 bytes long hex-encoded values get 'hash256' type + - 64 bytes long hex-encoded values get 'signature' type + - any other valid hex-encoded values get 'bytes' type + - anything else is a 'string' + + Backslash character is used as an escape character and allows to use colon in + an implicitly typed string. For any other characters it has no special + meaning, to get a literal backslash in the string use the '\\' sequence. + + Examples: + * 'int:42' is an integer with a value of 42 + * '42' is an integer with a value of 42 + * 'bad' is a string with a value of 'bad' + * 'dead' is a byte array with a value of 'dead' + * 'string:dead' is a string with a value of 'dead' + * 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt + * 'AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y' is a hash160 with a value + of '23ba2703c53263e8d6e522dc32203339dcd8eee9' + * '\4\2' is an integer with a value of 42 + * '\\4\2' is a string with a value of '\42' + * 'string:string' is a string with a value of 'string' + * 'string\:string' is a string with a value of 'string:string' + * '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a + key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' + * '[ a b c ]' is an array with strings values 'a', 'b' and 'c' + * '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b', + array of two strings 'c' and 'd', string 'e' + * '[ ]' is an empty array` +) + // GetSignersFromContext returns signers parsed from context args starting // from the specified offset. func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) { diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 15a5e85cc..51cc2d9e6 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -209,72 +209,7 @@ func NewCommands() []cli.Command { follow the regular convention of smart contract arguments (method string and an array of other arguments). - Arguments always do have regular Neo smart contract parameter types, either - specified explicitly or being inferred from the value. To specify the type - manually use "type:value" syntax where the type is one of the following: - 'signature', 'bool', 'int', 'hash160', 'hash256', 'bytes', 'key' or 'string'. - Array types are also supported: use special space-separated '[' and ']' - symbols around array values to denote array bounds. Nested arrays are also - supported. - - There is ability to provide an argument of 'bytearray' type via file. Use a - special 'filebytes' argument type for this with a filepath specified after - the colon, e.g. 'filebytes:my_file.txt'. - - Given values are type-checked against given types with the following - restrictions applied: - * 'signature' type values should be hex-encoded and have a (decoded) - length of 64 bytes. - * 'bool' type values are 'true' and 'false'. - * 'int' values are decimal integers that can be successfully converted - from the string. - * 'hash160' values are Neo addresses and hex-encoded 20-bytes long (after - decoding) strings. - * 'hash256' type values should be hex-encoded and have a (decoded) - length of 32 bytes. - * 'bytes' type values are any hex-encoded things. - * 'filebytes' type values are filenames with the argument value inside. - * 'key' type values are hex-encoded marshalled public keys. - * 'string' type values are any valid UTF-8 strings. In the value's part of - the string the colon looses it's special meaning as a separator between - type and value and is taken literally. - - If no type is explicitly specified, it is inferred from the value using the - following logic: - - anything that can be interpreted as a decimal integer gets - an 'int' type - - 'true' and 'false' strings get 'bool' type - - valid Neo addresses and 20 bytes long hex-encoded strings get 'hash160' - type - - valid hex-encoded public keys get 'key' type - - 32 bytes long hex-encoded values get 'hash256' type - - 64 bytes long hex-encoded values get 'signature' type - - any other valid hex-encoded values get 'bytes' type - - anything else is a 'string' - - Backslash character is used as an escape character and allows to use colon in - an implicitly typed string. For any other characters it has no special - meaning, to get a literal backslash in the string use the '\\' sequence. - - Examples: - * 'int:42' is an integer with a value of 42 - * '42' is an integer with a value of 42 - * 'bad' is a string with a value of 'bad' - * 'dead' is a byte array with a value of 'dead' - * 'string:dead' is a string with a value of 'dead' - * 'filebytes:my_data.txt' is bytes decoded from a content of my_data.txt - * 'AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y' is a hash160 with a value - of '23ba2703c53263e8d6e522dc32203339dcd8eee9' - * '\4\2' is an integer with a value of 42 - * '\\4\2' is a string with a value of '\42' - * 'string:string' is a string with a value of 'string' - * 'string\:string' is a string with a value of 'string:string' - * '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a - key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' - * '[ a b c ]' is an array with strings values 'a', 'b' and 'c' - * '[ a b [ c d ] e ]' is an array with 4 values: string 'a', string 'b', - array of two strings 'c' and 'd', string 'e' - * '[ ]' is an empty array +` + cmdargs.ParamsParsingDoc + ` Signers represent a set of Uint160 hashes with witness scopes and are used to verify hashes in System.Runtime.CheckWitness syscall. First signer is treated diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 846dc809d..e947c3ef4 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -18,6 +18,7 @@ import ( "github.com/chzyer/readline" "github.com/kballard/go-shellquote" + "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/paramcontext" @@ -52,11 +53,6 @@ const ( exitFuncKey = "exitFunc" readlineInstanceKey = "readlineKey" printLogoKey = "printLogoKey" - boolType = "bool" - boolFalse = "false" - boolTrue = "true" - intType = "int" - stringType = "string" ) // Various flag names. @@ -221,18 +217,12 @@ and converted to other formats. Strings are escaped and output in quotes.`, is a contract method, specified in manifest. It can be '_' which will push parameters onto the stack and execute from the current offset. is a parameter (can be repeated multiple times) that can be specified - as :, where type can be: - '` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values - '` + intType + `': supports integers as values - '` + stringType + `': supports strings as values (that are pushed as a byte array - values to the stack) - or can be just , for which the type will be detected automatically - following these rules: '` + boolTrue + `' and '` + boolFalse + `' are treated as respective - boolean values, everything that can be converted to integer is treated as - integer and everything else is treated like a string. + using the same rules as for 'contract testinvokefunction' command: + +` + cmdargs.ParamsParsingDoc + ` Example: -> run put ` + stringType + `:"Something to put"`, +> run put string:"Something to put"`, Action: handleRun, }, { @@ -862,9 +852,16 @@ func handleRun(c *cli.Context) error { runCurrent = args[0] != "_" ) - params, err = parseArgs(args[1:]) + _, scParams, err := cmdargs.ParseParams(args[1:], true) if err != nil { - return err + return fmt.Errorf("%w: %v", ErrInvalidParameter, err) + } + params = make([]stackitem.Item, len(scParams)) + for i := range scParams { + params[i], err = scParams[i].ToStackItem() + if err != nil { + return fmt.Errorf("failed to convert parameter #%d to stackitem: %w", i, err) + } } if runCurrent { if m == nil { @@ -1265,48 +1262,6 @@ func Parse(args []string) (string, error) { return buf.String(), nil } -func parseArgs(args []string) ([]stackitem.Item, error) { - items := make([]stackitem.Item, len(args)) - for i, arg := range args { - var typ, value string - typeAndVal := strings.Split(arg, ":") - if len(typeAndVal) < 2 { - if typeAndVal[0] == boolFalse || typeAndVal[0] == boolTrue { - typ = boolType - } else if _, err := strconv.Atoi(typeAndVal[0]); err == nil { - typ = intType - } else { - typ = stringType - } - value = typeAndVal[0] - } else { - typ = typeAndVal[0] - value = typeAndVal[1] - } - - switch typ { - case boolType: - if value == boolFalse { - items[i] = stackitem.NewBool(false) - } else if value == boolTrue { - items[i] = stackitem.NewBool(true) - } else { - return nil, fmt.Errorf("%w: invalid bool value", ErrInvalidParameter) - } - case intType: - val, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return nil, fmt.Errorf("%w: invalid integer value", ErrInvalidParameter) - } - items[i] = stackitem.NewBigInteger(big.NewInt(val)) - case stringType: - items[i] = stackitem.NewByteArray([]byte(value)) - } - } - - return items, nil -} - const logo = ` _ ____________ __________ _ ____ ___ / | / / ____/ __ \ / ____/ __ \ | | / / |/ / diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index 7186d02cd..de7cf7a04 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" gio "io" + "math/big" "os" "path/filepath" "strings" @@ -428,6 +429,9 @@ func TestRunWithDifferentArguments(t *testing.T) { } func GetString(arg string) string { return arg + } + func GetArr(arg []interface{}) []interface{}{ + return arg }` tmpDir := t.TempDir() @@ -449,6 +453,7 @@ func TestRunWithDifferentArguments(t *testing.T) { "run _ 1 2", "loadbase64 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.MUL)}), "run _ 21 2", + "loadgo "+filename, "run getArr [ 1 2 3 ]", ) e.checkNextLine(t, "READY: loaded \\d.* instructions") @@ -480,6 +485,13 @@ func TestRunWithDifferentArguments(t *testing.T) { e.checkNextLine(t, "READY: loaded \\d.* instructions") e.checkStack(t, 42) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, []stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(1)), + stackitem.NewBigInteger(big.NewInt(2)), + stackitem.NewBigInteger(big.NewInt(3)), + }) } func TestPrintOps(t *testing.T) { diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index dd7c1a396..06556eca1 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -403,3 +403,12 @@ func ExpandParameterToEmitable(param Parameter) (interface{}, error) { return param.Value, nil } } + +// ToStackItem converts smartcontract parameter to stackitem.Item. +func (p *Parameter) ToStackItem() (stackitem.Item, error) { + e, err := ExpandParameterToEmitable(*p) + if err != nil { + return nil, err + } + return stackitem.Make(e), nil +} diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index e07218777..295ed8f93 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" @@ -446,47 +448,57 @@ func hexToBase64(s string) string { return base64.StdEncoding.EncodeToString(b) } -func TestExpandParameterToEmitable(t *testing.T) { +func TestExpandParameterToEmitableToStackitem(t *testing.T) { pk, _ := keys.NewPrivateKey() testCases := []struct { - In Parameter - Expected interface{} + In Parameter + Expected interface{} + ExpectedStackitem stackitem.Item }{ { - In: Parameter{Type: BoolType, Value: true}, - Expected: true, + In: Parameter{Type: BoolType, Value: true}, + Expected: true, + ExpectedStackitem: stackitem.NewBool(true), }, { - In: Parameter{Type: IntegerType, Value: big.NewInt(123)}, - Expected: big.NewInt(123), + In: Parameter{Type: IntegerType, Value: big.NewInt(123)}, + Expected: big.NewInt(123), + ExpectedStackitem: stackitem.NewBigInteger(big.NewInt(123)), }, { - In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}}, - Expected: []byte{1, 2, 3}, + In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}}, + Expected: []byte{1, 2, 3}, + ExpectedStackitem: stackitem.NewByteArray([]byte{1, 2, 3}), }, { - In: Parameter{Type: StringType, Value: "writing's on the wall"}, - Expected: "writing's on the wall", + In: Parameter{Type: StringType, Value: "writing's on the wall"}, + Expected: "writing's on the wall", + ExpectedStackitem: stackitem.NewByteArray([]byte("writing's on the wall")), }, { - In: Parameter{Type: Hash160Type, Value: util.Uint160{1, 2, 3}}, - Expected: util.Uint160{1, 2, 3}, + In: Parameter{Type: Hash160Type, Value: util.Uint160{1, 2, 3}}, + Expected: util.Uint160{1, 2, 3}, + ExpectedStackitem: stackitem.NewByteArray(util.Uint160{1, 2, 3}.BytesBE()), }, { - In: Parameter{Type: Hash256Type, Value: util.Uint256{1, 2, 3}}, - Expected: util.Uint256{1, 2, 3}, + In: Parameter{Type: Hash256Type, Value: util.Uint256{1, 2, 3}}, + Expected: util.Uint256{1, 2, 3}, + ExpectedStackitem: stackitem.NewByteArray(util.Uint256{1, 2, 3}.BytesBE()), }, { - In: Parameter{Type: PublicKeyType, Value: pk.PublicKey().Bytes()}, - Expected: pk.PublicKey().Bytes(), + In: Parameter{Type: PublicKeyType, Value: pk.PublicKey().Bytes()}, + Expected: pk.PublicKey().Bytes(), + ExpectedStackitem: stackitem.NewByteArray(pk.PublicKey().Bytes()), }, { - In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}}, - Expected: []byte{1, 2, 3}, + In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}}, + Expected: []byte{1, 2, 3}, + ExpectedStackitem: stackitem.NewByteArray([]byte{1, 2, 3}), }, { - In: Parameter{Type: AnyType}, - Expected: nil, + In: Parameter{Type: AnyType}, + Expected: nil, + ExpectedStackitem: stackitem.Null{}, }, { In: Parameter{Type: ArrayType, Value: []Parameter{ @@ -509,6 +521,13 @@ func TestExpandParameterToEmitable(t *testing.T) { }, }}, Expected: []interface{}{big.NewInt(123), []byte{1, 2, 3}, []interface{}{true}}, + ExpectedStackitem: stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(123)), + stackitem.NewByteArray([]byte{1, 2, 3}), + stackitem.NewArray([]stackitem.Item{ + stackitem.NewBool(true), + }), + }), }, } bw := io.NewBufBinWriter() @@ -519,6 +538,10 @@ func TestExpandParameterToEmitable(t *testing.T) { emit.Array(bw.BinWriter, actual) require.NoError(t, bw.Err) + + actualSI, err := testCase.In.ToStackItem() + require.NoError(t, err) + require.Equal(t, testCase.ExpectedStackitem, actualSI) } errCases := []Parameter{ {Type: UnknownType}, diff --git a/pkg/vm/stackitem/item.go b/pkg/vm/stackitem/item.go index 33f234955..6e645bf28 100644 --- a/pkg/vm/stackitem/item.go +++ b/pkg/vm/stackitem/item.go @@ -122,6 +122,18 @@ func Make(v interface{}) Item { a = append(a, Make(i)) } return Make(a) + case []interface{}: + res := make([]Item, len(val)) + for i := range val { + res[i] = Make(val[i]) + } + return Make(res) + case util.Uint160: + return Make(val.BytesBE()) + case util.Uint256: + return Make(val.BytesBE()) + case nil: + return Null{} default: i64T := reflect.TypeOf(int64(0)) if reflect.TypeOf(val).ConvertibleTo(i64T) { diff --git a/pkg/vm/stackitem/item_test.go b/pkg/vm/stackitem/item_test.go index 5a6843334..210f0a83d 100644 --- a/pkg/vm/stackitem/item_test.go +++ b/pkg/vm/stackitem/item_test.go @@ -77,13 +77,17 @@ var makeStackItemTestCases = []struct { input: []int{1, 2, 3}, result: &Array{value: []Item{(*BigInteger)(big.NewInt(1)), (*BigInteger)(big.NewInt(2)), (*BigInteger)(big.NewInt(3))}}, }, + { + input: nil, + result: Null{}, + }, } var makeStackItemErrorCases = []struct { input interface{} }{ { - input: nil, + input: map[int]int{1: 2}, }, }