diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index ab8f8a34b..b90fa60d7 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -24,23 +24,148 @@ 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. Null parameter is supported via 'nil' keyword without additional + type specification. + + 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 + - 'nil' string gets 'Any' NEP-14 parameter type and nil value which corresponds + to Null stackitem + - '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 + * 'nil' is a parameter with Any NEP-14 type and nil value (corresponds to Null stackitem) + * '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` + + // SignersParsingDoc is a documentation for signers parsing. + SignersParsingDoc = ` 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 + as a sender. To specify signers use signer[:scope] syntax where + * 'signer' is a signer's address (as Neo address or hex-encoded 160 bit (20 byte) + LE value with or without '0x' prefix). + * 'scope' is a comma-separated set of cosigner's scopes, which could be: + - 'None' - default witness scope which may be used for the sender + to only pay fee for the transaction. + - 'Global' - allows this witness in all contexts. This cannot be combined + with other flags. + - 'CalledByEntry' - means that this condition must hold: EntryScriptHash + == CallingScriptHash. The witness/permission/signature + given on first invocation will automatically expire if + entering deeper internal invokes. This can be default + safe choice for native NEO/GAS. + - 'CustomContracts' - define valid custom contract hashes for witness check. + Hashes are be provided as hex-encoded LE value string. + At lest one hash must be provided. Multiple hashes + are separated by ':'. + - 'CustomGroups' - define custom public keys for group members. Public keys are + provided as short-form (1-byte prefix + 32 bytes) hex-encoded + values. At least one key must be provided. Multiple keys + are separated by ':'. + + If no scopes were specified, 'CalledByEntry' used as default. If no signers were + specified, no array is passed. Note that scopes are properly handled by + neo-go RPC server only. C# implementation does not support scopes capability. + + Examples: + * 'NNQk4QXsxvsrr3GSozoWBUxEmfag7B6hz5' + * 'NVquyZHoPirw6zAEPvY1ZezxM493zMWQqs:Global' + * '0x0000000009070e030d0f0e020d0c06050e030c02' + * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,` + + `CustomGroups:0206d7495ceb34c197093b5fc1cccf1996ada05e69ef67e765462a7f5d88ee14d0' + * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,` + + `CustomContracts:1011120009070e030d0f0e020d0c06050e030c02:0x1211100009070e030d0f0e020d0c06050e030c02'` +) + // GetSignersFromContext returns signers parsed from context args starting // from the specified offset. func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) { args := ctx.Args() - var signers []transaction.Signer + var ( + signers []transaction.Signer + err error + ) if args.Present() && len(args) > offset { - for i, c := range args[offset:] { - cosigner, err := parseCosigner(c) - if err != nil { - return nil, cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i, err), 1) - } - signers = append(signers, cosigner) + signers, err = ParseSigners(args[offset:]) + if err != nil { + return nil, cli.NewExitError(err, 1) } } return signers, nil } +// ParseSigners returns array of signers parsed from their string representation. +func ParseSigners(args []string) ([]transaction.Signer, error) { + var signers []transaction.Signer + for i, c := range args { + cosigner, err := parseCosigner(c) + if err != nil { + return nil, fmt.Errorf("failed to parse signer #%d: %w", i, err) + } + signers = append(signers, cosigner) + } + return signers, nil +} + func parseCosigner(c string) (transaction.Signer, error) { var ( err error diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 15a5e85cc..8a468ed1a 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -209,109 +209,9 @@ 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. +` + cmdargs.ParamsParsingDoc + ` - 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 - - 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 - as a sender. To specify signers use signer[:scope] syntax where - * 'signer' is a signer's address (as Neo address or hex-encoded 160 bit (20 byte) - LE value with or without '0x' prefix). - * 'scope' is a comma-separated set of cosigner's scopes, which could be: - - 'None' - default witness scope which may be used for the sender - to only pay fee for the transaction. - - 'Global' - allows this witness in all contexts. This cannot be combined - with other flags. - - 'CalledByEntry' - means that this condition must hold: EntryScriptHash - == CallingScriptHash. The witness/permission/signature - given on first invocation will automatically expire if - entering deeper internal invokes. This can be default - safe choice for native NEO/GAS. - - 'CustomContracts' - define valid custom contract hashes for witness check. - Hashes are be provided as hex-encoded LE value string. - At lest one hash must be provided. Multiple hashes - are separated by ':'. - - 'CustomGroups' - define custom public keys for group members. Public keys are - provided as short-form (1-byte prefix + 32 bytes) hex-encoded - values. At least one key must be provided. Multiple keys - are separated by ':'. - - If no scopes were specified, 'CalledByEntry' used as default. If no signers were - specified, no array is passed. Note that scopes are properly handled by - neo-go RPC server only. C# implementation does not support scopes capability. - - Examples: - * 'NNQk4QXsxvsrr3GSozoWBUxEmfag7B6hz5' - * 'NVquyZHoPirw6zAEPvY1ZezxM493zMWQqs:Global' - * '0x0000000009070e030d0f0e020d0c06050e030c02' - * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,` + - `CustomGroups:0206d7495ceb34c197093b5fc1cccf1996ada05e69ef67e765462a7f5d88ee14d0' - * '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,` + - `CustomContracts:1011120009070e030d0f0e020d0c06050e030c02:0x1211100009070e030d0f0e020d0c06050e030c02' +` + cmdargs.SignersParsingDoc + ` `, Action: testInvokeFunction, Flags: testInvokeFunctionFlags, diff --git a/cli/vm/cli.go b/cli/vm/cli.go index ad4f3a184..27f01a7f6 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -18,8 +18,10 @@ 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" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core" @@ -27,11 +29,13 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util/slice" @@ -50,11 +54,6 @@ const ( exitFuncKey = "exitFunc" readlineInstanceKey = "readlineKey" printLogoKey = "printLogoKey" - boolType = "bool" - boolFalse = "false" - boolTrue = "true" - intType = "int" - stringType = "string" ) // Various flag names. @@ -75,184 +74,243 @@ var commands = []cli.Command{ { Name: "exit", Usage: "Exit the VM prompt", - Description: "Exit the VM prompt", + UsageText: "exit", + Description: "Exit the VM prompt.", Action: handleExit, }, { Name: "ip", Usage: "Show current instruction", - Description: "Show current instruction", + UsageText: "ip", + Description: "Show current instruction.", Action: handleIP, }, { Name: "break", Usage: "Place a breakpoint", UsageText: `break `, - Description: `break - is mandatory parameter, example: + Description: ` is mandatory parameter. + +Example: > break 12`, Action: handleBreak, }, + { + Name: "jump", + Usage: "Jump to the specified instruction (absolute IP value)", + UsageText: `jump `, + Description: ` is mandatory parameter (absolute IP value). + +Example: +> jump 12`, + Action: handleJump, + }, { Name: "estack", Usage: "Show evaluation stack contents", - Description: "Show evaluation stack contents", + UsageText: "estack", + Description: "Show evaluation stack contents.", Action: handleXStack, }, { Name: "istack", Usage: "Show invocation stack contents", - Description: "Show invocation stack contents", + UsageText: "istack", + Description: "Show invocation stack contents.", Action: handleXStack, }, { Name: "sslot", Usage: "Show static slot contents", - Description: "Show static slot contents", + UsageText: "sslot", + Description: "Show static slot contents.", Action: handleSlots, }, { Name: "lslot", Usage: "Show local slot contents", + UsageText: "lslot", Description: "Show local slot contents", Action: handleSlots, }, { Name: "aslot", Usage: "Show arguments slot contents", - Description: "Show arguments slot contents", + UsageText: "aslot", + Description: "Show arguments slot contents.", Action: handleSlots, }, { Name: "loadnef", - Usage: "Load a NEF-consistent script into the VM", - UsageText: `loadnef `, + Usage: "Load a NEF-consistent script into the VM optionally attaching to it provided signers with scopes", + UsageText: `loadnef [--historic ] [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loadnef [--historic ] -both parameters are mandatory, example: + Description: ` and parameters are mandatory. + +` + cmdargs.SignersParsingDoc + ` + +Example: > loadnef /path/to/script.nef /path/to/manifest.json`, Action: handleLoadNEF, }, { Name: "loadbase64", - Usage: "Load a base64-encoded script string into the VM", - UsageText: `loadbase64 [--historic ] `, + Usage: "Load a base64-encoded script string into the VM optionally attaching to it provided signers with scopes", + UsageText: `loadbase64 [--historic ] [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loadbase64 [--historic ] + Description: ` is mandatory parameter. - is mandatory parameter, example: +` + cmdargs.SignersParsingDoc + ` + +Example: > loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`, Action: handleLoadBase64, }, { Name: "loadhex", - Usage: "Load a hex-encoded script string into the VM", - UsageText: `loadhex [--historic ] `, + Usage: "Load a hex-encoded script string into the VM optionally attaching to it provided signers with scopes", + UsageText: `loadhex [--historic ] [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loadhex [--historic ] + Description: ` is mandatory parameter. - is mandatory parameter, example: +` + cmdargs.SignersParsingDoc + ` + +Example: > loadhex 0c0c48656c6c6f20776f726c6421`, Action: handleLoadHex, }, { Name: "loadgo", - Usage: "Compile and load a Go file with the manifest into the VM", - UsageText: `loadgo [--historic ] `, + Usage: "Compile and load a Go file with the manifest into the VM optionally attaching to it provided signers with scopes", + UsageText: `loadgo [--historic ] [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loadgo [--historic ] + Description: ` is mandatory parameter. - is mandatory parameter, example: +` + cmdargs.SignersParsingDoc + ` + +Example: > loadgo /path/to/file.go`, Action: handleLoadGo, }, { - Name: "reset", - Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state", - Flags: []cli.Flag{historicFlag}, - Action: handleReset, + Name: "loadtx", + Usage: "Load transaction into the VM from chain or from parameter context file", + UsageText: `loadtx [--historic ] `, + Flags: []cli.Flag{historicFlag}, + Description: `Load transaction into the VM from chain or from parameter context file. +The transaction script will be loaded into VM; the resulting execution context will use the provided transaction as script container including its signers, hash and nonce. + + is mandatory parameter. + +Example: +> loadtx /path/to/file`, + Action: handleLoadTx, + }, + { + Name: "loaddeployed", + Usage: "Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes", + UsageText: `loaddeployed [--historic ] [, ...]`, + Flags: []cli.Flag{historicFlag}, + Description: `Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes. +If '--historic' flag specified, then the historic contract state (historic script and manifest) will be loaded. + + is mandatory parameter. + +` + cmdargs.SignersParsingDoc + ` + +Example: +> loaddeployed 0x0000000009070e030d0f0e020d0c06050e030c02`, + Action: handleLoadDeployed, + }, + { + Name: "reset", + Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state", + UsageText: "reset", + Flags: []cli.Flag{historicFlag}, + Description: "Unload compiled script from the VM and reset context to proper (possibly, historic) state.", + Action: handleReset, }, { Name: "parse", Usage: "Parse provided argument and convert it into other possible formats", UsageText: `parse `, - Description: `parse - - is an argument which is tried to be interpreted as an item of different types + Description: ` is an argument which is tried to be interpreted as an item of different types and converted to other formats. Strings are escaped and output in quotes.`, Action: handleParse, }, { Name: "run", - Usage: "Execute the current loaded script", + Usage: "Usage Execute the current loaded script", UsageText: `run [ [...]]`, - Description: `run [ [...]] - - is a contract method, specified in manifest. It can be '_' which will push + Description: ` 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 int:5 string:some_string_value`, Action: handleRun, }, { Name: "cont", Usage: "Continue execution of the current loaded script", - Description: "Continue execution of the current loaded script", + UsageText: "cont", + Description: "Continue execution of the current loaded script.", Action: handleCont, }, { Name: "step", Usage: "Step (n) instruction in the program", UsageText: `step []`, - Description: `step [] - is optional parameter to specify number of instructions to run, example: + Description: ` is optional parameter to specify number of instructions to run. + +Example: > step 10`, Action: handleStep, }, { - Name: "stepinto", - Usage: "Stepinto instruction to take in the debugger", - Description: `Usage: stepInto -example: + Name: "stepinto", + Usage: "Stepinto instruction to take in the debugger", + UsageText: "stepinto", + Description: `Stepinto instruction to take in the debugger. + +Example: > stepinto`, Action: handleStepInto, }, { - Name: "stepout", - Usage: "Stepout instruction to take in the debugger", - Description: `stepOut -example: + Name: "stepout", + Usage: "Stepout instruction to take in the debugger", + UsageText: "stepout", + Description: `Stepout instruction to take in the debugger. + +Example: > stepout`, Action: handleStepOut, }, { - Name: "stepover", - Usage: "Stepover instruction to take in the debugger", - Description: `stepOver -example: + Name: "stepover", + Usage: "Stepover instruction to take in the debugger", + UsageText: "stepover", + Description: `Stepover instruction to take in the debugger. + +Example: > stepover`, Action: handleStepOver, }, { Name: "ops", Usage: "Dump opcodes of the current loaded program", + UsageText: "ops", Description: "Dump opcodes of the current loaded program", Action: handleOps, }, { Name: "events", Usage: "Dump events emitted by the current loaded program", + UsageText: "events", Description: "Dump events emitted by the current loaded program", Action: handleEvents, }, @@ -266,23 +324,15 @@ example: Usage: "Print the whole blockchain node configuration.", }, }, - Description: `env [-v] - -Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration). + Description: `Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration). Example: > env -v`, Action: handleEnv, }, { - Name: "storage", - Usage: "Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation. " + - "Can be used if no script is loaded. " + - "Hex-encoded storage items prefix may be specified (empty by default to return the whole set of storage items). " + - "If seek prefix is not empty, then it's trimmed from the resulting keys." + - "Items are sorted. Backwards seek direction may be specified (false by default, which means forwards storage seek direction). " + - "It is possible to dump only those storage items that were added or changed during current script invocation (use --diff flag for it). " + - "To dump the whole set of storage changes including removed items use 'changes' command.", + Name: "storage", + Usage: "Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation", UsageText: `storage [] [--backwards] [--diff]`, Flags: []cli.Flag{ cli.BoolFlag{ @@ -294,9 +344,7 @@ Example: Usage: "Dump only those storage items that were added or changed during the current script invocation. Note that this call won't show removed storage items, use 'changes' command for that.", }, }, - Description: `storage [--backwards] [--diff] - -Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation. + Description: `Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation. Can be used if no script is loaded. Hex-encoded storage items prefix may be specified (empty by default to return the whole set of storage items). If seek prefix is not empty, then it's trimmed from the resulting keys. @@ -309,16 +357,10 @@ Example: Action: handleStorage, }, { - Name: "changes", - Usage: "Dump storage changes as is at the current stage of loaded script invocation. " + - "If no script is loaded or executed, then no changes are present. " + - "The contract hash, address or ID may be specified as the first parameter to dump the specified contract storage changes. " + - "Hex-encoded search prefix (without contract ID) may be specified to dump matching storage changes. " + - "Resulting values are not sorted.", + Name: "changes", + Usage: "Dump storage changes as is at the current stage of loaded script invocation", UsageText: `changes [ []]`, - Description: `changes [ []] - -Dump storage changes as is at the current stage of loaded script invocation. + Description: `Dump storage changes as is at the current stage of loaded script invocation. If no script is loaded or executed, then no changes are present. The contract hash, address or ID may be specified as the first parameter to dump the specified contract storage changes. Hex-encoded search prefix (without contract ID) may be specified to dump matching storage changes. @@ -521,21 +563,44 @@ func handleBreak(c *cli.Context) error { if !checkVMIsReady(c.App) { return nil } - v := getVMFromContext(c.App) - args := c.Args() - if len(args) != 1 { - return fmt.Errorf("%w: ", ErrMissingParameter) - } - n, err := strconv.Atoi(args[0]) + n, err := getInstructionParameter(c) if err != nil { - return fmt.Errorf("%w: %s", ErrInvalidParameter, err) + return err } + v := getVMFromContext(c.App) v.AddBreakPoint(n) fmt.Fprintf(c.App.Writer, "breakpoint added at instruction %d\n", n) return nil } +func handleJump(c *cli.Context) error { + if !checkVMIsReady(c.App) { + return nil + } + n, err := getInstructionParameter(c) + if err != nil { + return err + } + + v := getVMFromContext(c.App) + v.Context().Jump(n) + fmt.Fprintf(c.App.Writer, "jumped to instruction %d\n", n) + return nil +} + +func getInstructionParameter(c *cli.Context) (int, error) { + args := c.Args() + if len(args) != 1 { + return 0, fmt.Errorf("%w: ", ErrMissingParameter) + } + n, err := strconv.Atoi(args[0]) + if err != nil { + return 0, fmt.Errorf("%w: %s", ErrInvalidParameter, err) + } + return n, nil +} + func handleXStack(c *cli.Context) error { v := getVMFromContext(c.App) var stackDump string @@ -574,32 +639,44 @@ func handleSlots(c *cli.Context) error { // prepareVM retrieves --historic flag from context (if set) and resets app state // (to the specified historic height if given). -func prepareVM(c *cli.Context) error { +func prepareVM(c *cli.Context, tx *transaction.Transaction) error { if c.IsSet(historicFlagFullName) { height := c.Int(historicFlagFullName) - return resetState(c.App, uint32(height)) + return resetState(c.App, tx, uint32(height)) } - return resetState(c.App) + return resetState(c.App, tx) } func handleLoadNEF(c *cli.Context) error { - err := prepareVM(c) - if err != nil { - return err - } - v := getVMFromContext(c.App) args := c.Args() if len(args) < 2 { return fmt.Errorf("%w: ", ErrMissingParameter) } - if err := v.LoadFileWithFlags(args[0], callflag.All); err != nil { - return fmt.Errorf("failed to read nef: %w", err) + b, err := os.ReadFile(args[0]) + if err != nil { + return err + } + nef, err := nef.FileFromBytes(b) + if err != nil { + return fmt.Errorf("failed to decode NEF file: %w", err) } m, err := getManifestFromFile(args[1]) if err != nil { return fmt.Errorf("failed to read manifest: %w", err) } + var signers []transaction.Signer + if len(args) > 2 { + signers, err = cmdargs.ParseSigners(c.Args()[2:]) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidParameter, err) + } + } + err = prepareVM(c, createFakeTransaction(nef.Script, signers)) + if err != nil { + return err + } + v := getVMFromContext(c.App) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) setManifestInContext(c.App, m) changePrompt(c.App) @@ -607,11 +684,6 @@ func handleLoadNEF(c *cli.Context) error { } func handleLoadBase64(c *cli.Context) error { - err := prepareVM(c) - if err != nil { - return err - } - v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { return fmt.Errorf("%w: ", ErrMissingParameter) @@ -620,18 +692,32 @@ func handleLoadBase64(c *cli.Context) error { if err != nil { return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } - v.LoadWithFlags(b, callflag.All) + var signers []transaction.Signer + if len(args) > 1 { + signers, err = cmdargs.ParseSigners(args[1:]) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidParameter, err) + } + } + err = prepareVM(c, createFakeTransaction(b, signers)) + if err != nil { + return err + } + v := getVMFromContext(c.App) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil } -func handleLoadHex(c *cli.Context) error { - err := prepareVM(c) - if err != nil { - return err +// createFakeTransaction creates fake transaction with prefilled script, VUB and signers. +func createFakeTransaction(script []byte, signers []transaction.Signer) *transaction.Transaction { + return &transaction.Transaction{ + Script: script, + Signers: signers, } - v := getVMFromContext(c.App) +} + +func handleLoadHex(c *cli.Context) error { args := c.Args() if len(args) < 1 { return fmt.Errorf("%w: ", ErrMissingParameter) @@ -640,18 +726,24 @@ func handleLoadHex(c *cli.Context) error { if err != nil { return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } - v.LoadWithFlags(b, callflag.All) + var signers []transaction.Signer + if len(args) > 1 { + signers, err = cmdargs.ParseSigners(args[1:]) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidParameter, err) + } + } + err = prepareVM(c, createFakeTransaction(b, signers)) + if err != nil { + return err + } + v := getVMFromContext(c.App) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil } func handleLoadGo(c *cli.Context) error { - err := prepareVM(c) - if err != nil { - return err - } - v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { return fmt.Errorf("%w: ", ErrMissingParameter) @@ -668,16 +760,112 @@ func handleLoadGo(c *cli.Context) error { if err != nil { return fmt.Errorf("can't create manifest: %w", err) } - setManifestInContext(c.App, m) + var signers []transaction.Signer + if len(args) > 1 { + signers, err = cmdargs.ParseSigners(args[1:]) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidParameter, err) + } + } - v.LoadWithFlags(b.Script, callflag.All) + err = prepareVM(c, createFakeTransaction(b.Script, signers)) + if err != nil { + return err + } + v := getVMFromContext(c.App) + setManifestInContext(c.App, m) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil } +func handleLoadTx(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return fmt.Errorf("%w: ", ErrMissingParameter) + } + + var ( + tx *transaction.Transaction + err error + ) + h, err := util.Uint256DecodeStringLE(strings.TrimPrefix(args[0], "0x")) + if err != nil { + pc, err := paramcontext.Read(args[0]) + if err != nil { + return fmt.Errorf("invalid tx hash or path to parameter context: %w", err) + } + var ok bool + tx, ok = pc.Verifiable.(*transaction.Transaction) + if !ok { + return errors.New("failed to retrieve transaction from parameter context: verifiable item is not a transaction") + } + } else { + bc := getChainFromContext(c.App) + tx, _, err = bc.GetTransaction(h) + if err != nil { + return fmt.Errorf("failed to get transaction from chain: %w", err) + } + } + err = prepareVM(c, tx) + if err != nil { + return err + } + v := getVMFromContext(c.App) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c.App) + return nil +} + +func handleLoadDeployed(c *cli.Context) error { + err := prepareVM(c, nil) // prepare historic IC if needed (for further historic contract state retrieving). + if err != nil { + return err + } + if !c.Args().Present() { + return errors.New("contract hash, address or ID is mandatory argument") + } + hashOrID := c.Args().Get(0) + ic := getInteropContextFromContext(c.App) + h, err := flags.ParseAddress(hashOrID) + if err != nil { + i, err := strconv.ParseInt(hashOrID, 10, 32) + if err != nil { + return fmt.Errorf("failed to parse contract hash, address or ID: %w", err) + } + bc := getChainFromContext(c.App) + h, err = bc.GetContractScriptHash(int32(i)) // @fixme: can be improved after #2702 to retrieve historic state of destroyed contract by ID. + if err != nil { + return fmt.Errorf("failed to retrieve contract hash by ID: %w", err) + } + } + cs, err := ic.GetContract(h) // will return historic contract state. + if err != nil { + return fmt.Errorf("contract %s not found: %w", h.StringLE(), err) + } + + var signers []transaction.Signer + if len(c.Args()) > 1 { + signers, err = cmdargs.ParseSigners(c.Args()[1:]) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidParameter, err) + } + } + err = prepareVM(c, createFakeTransaction(cs.NEF.Script, signers)) // prepare VM one more time for proper IC initialization. + if err != nil { + return err + } + ic = getInteropContextFromContext(c.App) // fetch newly-created IC. + ic.ReuseVM(ic.VM) // clear previously loaded program and context. + ic.VM.LoadScriptWithHash(cs.NEF.Script, cs.Hash, callflag.All) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", ic.VM.Context().LenInstr()) + setManifestInContext(c.App, &cs.Manifest) + changePrompt(c.App) + return nil +} + func handleReset(c *cli.Context) error { - err := prepareVM(c) + err := prepareVM(c, nil) if err != nil { return err } @@ -692,8 +880,9 @@ func finalizeInteropContext(app *cli.App) { } // resetInteropContext calls finalizer for current interop context and replaces -// it with the newly created one. -func resetInteropContext(app *cli.App, height ...uint32) error { +// it with the newly created one. If transaction is provided, then its script is +// loaded into bound VM. +func resetInteropContext(app *cli.App, tx *transaction.Transaction, height ...uint32) error { finalizeInteropContext(app) bc := getChainFromContext(app) var ( @@ -701,16 +890,25 @@ func resetInteropContext(app *cli.App, height ...uint32) error { err error ) if len(height) != 0 { - newIc, err = bc.GetTestHistoricVM(trigger.Application, nil, height[0]+1) + if tx != nil { + tx.ValidUntilBlock = height[0] + 1 + } + newIc, err = bc.GetTestHistoricVM(trigger.Application, tx, height[0]+1) if err != nil { return fmt.Errorf("failed to create historic VM for height %d: %w", height[0], err) } } else { - newIc, err = bc.GetTestVM(trigger.Application, nil, nil) + if tx != nil { + tx.ValidUntilBlock = bc.BlockHeight() + 1 + } + newIc, err = bc.GetTestVM(trigger.Application, tx, nil) if err != nil { return fmt.Errorf("failed to create VM: %w", err) } } + if tx != nil { + newIc.VM.LoadWithFlags(tx.Script, callflag.All) + } setInteropContextInContext(app, newIc) return nil @@ -723,8 +921,8 @@ func resetManifest(app *cli.App) { // resetState resets state of the app (clear interop context and manifest) so that it's ready // to load new program. -func resetState(app *cli.App, height ...uint32) error { - err := resetInteropContext(app, height...) +func resetState(app *cli.App, tx *transaction.Transaction, height ...uint32) error { + err := resetInteropContext(app, tx, height...) if err != nil { return err } @@ -757,9 +955,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 { @@ -1014,29 +1219,11 @@ func handleChanges(c *cli.Context) error { // getDumpArgs is a helper function that retrieves contract ID and search prefix (if given). func getDumpArgs(c *cli.Context) (int32, []byte, error) { - if !c.Args().Present() { - return 0, nil, errors.New("contract hash, address or ID is mandatory argument") - } - hashOrID := c.Args().Get(0) - var ( - ic = getInteropContextFromContext(c.App) - id int32 - prefix []byte - ) - h, err := flags.ParseAddress(hashOrID) + id, err := getContractID(c) if err != nil { - i, err := strconv.ParseInt(hashOrID, 10, 32) - if err != nil { - return 0, nil, fmt.Errorf("failed to parse contract hash, address or ID: %w", err) - } - id = int32(i) - } else { - cs, err := ic.GetContract(h) - if err != nil { - return 0, nil, fmt.Errorf("contract %s not found: %w", h.StringLE(), err) - } - id = cs.ID + return 0, nil, err } + var prefix []byte if c.NArg() > 1 { prefix, err = hex.DecodeString(c.Args().Get(1)) if err != nil { @@ -1046,6 +1233,29 @@ func getDumpArgs(c *cli.Context) (int32, []byte, error) { return id, prefix, nil } +// getContractID returns contract ID parsed from the first argument which can be ID, +// hash or address. +func getContractID(c *cli.Context) (int32, error) { + if !c.Args().Present() { + return 0, errors.New("contract hash, address or ID is mandatory argument") + } + hashOrID := c.Args().Get(0) + var ic = getInteropContextFromContext(c.App) + h, err := flags.ParseAddress(hashOrID) + if err != nil { + i, err := strconv.ParseInt(hashOrID, 10, 32) + if err != nil { + return 0, fmt.Errorf("failed to parse contract hash, address or ID: %w", err) + } + return int32(i), nil + } + cs, err := ic.GetContract(h) + if err != nil { + return 0, fmt.Errorf("contract %s not found: %w", h.StringLE(), err) + } + return cs.ID, nil +} + func dumpEvents(app *cli.App) (string, error) { ic := getInteropContextFromContext(app) if len(ic.Notifications) == 0 { @@ -1155,48 +1365,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 256625e1f..4552f9253 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -5,20 +5,25 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" gio "io" + "math/big" "os" "path/filepath" + "strconv" "strings" "sync" "testing" "time" "github.com/chzyer/readline" + "github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" @@ -277,27 +282,79 @@ func (e *executor) checkSlot(t *testing.T, items ...interface{}) { func TestLoad(t *testing.T) { script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)} + + ownerAddress := "NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB" + ownerAcc, err := address.StringToUint160(ownerAddress) + require.NoError(t, err) + sideAcc := util.Uint160{1, 2, 3} + buff := io.NewBufBinWriter() + emit.Bytes(buff.BinWriter, ownerAcc.BytesBE()) + emit.Syscall(buff.BinWriter, interopnames.SystemRuntimeCheckWitness) + checkWitnessScript := buff.Bytes() + t.Run("loadhex", func(t *testing.T) { e := newTestVMCLI(t) e.runProg(t, "loadhex", "loadhex notahex", - "loadhex "+hex.EncodeToString(script)) + "loadhex "+hex.EncodeToString(script), + "loadhex "+hex.EncodeToString(checkWitnessScript)+" "+ownerAddress, // owner:DefaultScope => true + "run", + "loadhex "+hex.EncodeToString(checkWitnessScript)+" "+ownerAddress+":None", // owner:None => false + "run", + "loadhex "+hex.EncodeToString(checkWitnessScript)+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true + "run", + "loadhex "+hex.EncodeToString(checkWitnessScript)+" 0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true + "run", + "loadhex "+hex.EncodeToString(checkWitnessScript)+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false + "run", + ) e.checkError(t, ErrMissingParameter) e.checkError(t, ErrInvalidParameter) e.checkNextLine(t, "READY: loaded 3 instructions") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) }) t.Run("loadbase64", func(t *testing.T) { e := newTestVMCLI(t) e.runProg(t, "loadbase64", "loadbase64 not_a_base64", - "loadbase64 "+base64.StdEncoding.EncodeToString(script)) + "loadbase64 "+base64.StdEncoding.EncodeToString(script), + "loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+ownerAddress, // owner:DefaultScope => true + "run", + "loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+ownerAddress+":None", // owner:None => false + "run", + "loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true + "run", + "loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" 0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true + "run", + "loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false + "run", + ) e.checkError(t, ErrMissingParameter) e.checkError(t, ErrInvalidParameter) e.checkNextLine(t, "READY: loaded 3 instructions") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) }) src := `package kek @@ -339,14 +396,7 @@ go 1.17`) checkLoadgo(t, "simple", "vmtestcontract.go", "vmtestcontract_err.go") checkLoadgo(t, "utf-8 with spaces", "тестовый контракт.go", "тестовый контракт с ошибкой.go") - t.Run("loadgo, check calling flags", func(t *testing.T) { - srcAllowNotify := `package kek - import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - func Main() int { - runtime.Log("Hello, world!") - return 1 - } -` + prepareLoadgoSrc := func(t *testing.T, srcAllowNotify string) string { filename := filepath.Join(tmpDir, "vmtestcontract.go") require.NoError(t, os.WriteFile(filename, []byte(srcAllowNotify), os.ModePerm)) filename = "'" + filename + "'" @@ -359,6 +409,17 @@ require ( replace github.com/nspcc-dev/neo-go/pkg/interop => ` + filepath.Join(wd, "../../pkg/interop") + ` go 1.17`) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm)) + return filename + } + t.Run("loadgo, check calling flags", func(t *testing.T) { + srcAllowNotify := `package kek + import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + func Main() int { + runtime.Log("Hello, world!") + return 1 + } +` + filename := prepareLoadgoSrc(t, srcAllowNotify) e := newTestVMCLI(t) e.runProg(t, @@ -367,6 +428,41 @@ go 1.17`) e.checkNextLine(t, "READY: loaded \\d* instructions") e.checkStack(t, 1) }) + t.Run("loadgo, check signers", func(t *testing.T) { + srcCheckWitness := `package kek + import ( + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/util" + ) + func Main() bool { + var owner = util.FromAddress("` + ownerAddress + `") + return runtime.CheckWitness(owner) + } +` + filename := prepareLoadgoSrc(t, srcCheckWitness) + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename+" "+ownerAddress, // owner:DefaultScope => true + "run main", + "loadgo "+filename+" "+ownerAddress+":None", // owner:None => false + "run main", + "loadgo "+filename+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true + "run main", + "loadgo "+filename+" 0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true + "run main", + "loadgo "+filename+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false + "run main") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) + }) t.Run("loadnef", func(t *testing.T) { config.Version = "0.92.0-test" @@ -425,6 +521,9 @@ func TestRunWithDifferentArguments(t *testing.T) { } func GetString(arg string) string { return arg + } + func GetArr(arg []interface{}) []interface{}{ + return arg }` tmpDir := t.TempDir() @@ -446,6 +545,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") @@ -477,6 +577,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) { @@ -986,3 +1093,109 @@ func TestDumpChanges(t *testing.T) { e.checkChange(t, expected[1]) e.checkChange(t, expected[2]) } + +func TestLoadtx(t *testing.T) { + e := newTestVMClIWithState(t) + + b, err := e.cli.chain.GetBlock(e.cli.chain.GetHeaderHash(2)) // Block #2 contains transaction that puts (1,1) pair to storage contract. + require.NoError(t, err) + require.Equal(t, 1, len(b.Transactions)) + tx := b.Transactions[0] + + tmp := filepath.Join(t.TempDir(), "tx.json") + require.NoError(t, paramcontext.InitAndSave(netmode.UnitTestNet, tx, nil, tmp)) + + e.runProg(t, + "loadtx "+tx.Hash().StringLE(), // hash LE + "run", + "loadtx 0x"+tx.Hash().StringLE(), // hash LE with 0x prefix + "run", + "loadtx '"+tmp+"'", // Tx from parameter context file. + "run", + "loadtx", // missing argument + "exit", + ) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, 1) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, 1) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, 1) + e.checkError(t, errors.New("missing argument: ")) +} + +func TestLoaddeployed(t *testing.T) { + e := newTestVMClIWithState(t) + + h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go + require.NoError(t, err) + ownerAddress := "NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB" // owner of examples/runtime/runtime.go (taken from deployed contract with ID=2) + ownerAcc, err := address.StringToUint160(ownerAddress) + require.NoError(t, err) + sideAcc := util.Uint160{1, 2, 3} + + e.runProg(t, + "loaddeployed "+h.StringLE(), // hash LE + "run get 1", + "loaddeployed 0x"+h.StringLE(), // hash LE with 0x prefix + "run get 1", + "loaddeployed 1", // contract ID + "run get 1", + "loaddeployed --historic 2 1", // historic state, check that hash is properly set + "run get 1", + // Check signers parsing: + "loaddeployed 2 "+ownerAddress, // check witness (owner:DefautScope => true) + "run checkWitness", + "loaddeployed 2 "+ownerAddress+":None", // check witness (owner:None => false) + "run checkWitness", + "loaddeployed 2 "+ownerAddress+":CalledByEntry", // check witness (owner:CalledByEntry => true) + "run checkWitness", + "loaddeployed 2 "+ownerAcc.StringLE()+":CalledByEntry", // check witness (ownerLE:CalledByEntry => true) + "run checkWitness", + "loaddeployed 2 0x"+ownerAcc.StringLE()+":CalledByEntry", // check witness (owner0xLE:CalledByEntry => true) + "run checkWitness", + "loaddeployed 2 "+sideAcc.StringLE()+":Global", // check witness (sideLE:Global => false) + "run checkWitness", + "loaddeployed", // missing argument + "exit", + ) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, []byte{2}) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, []byte{2}) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, []byte{2}) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, []byte{1}) + // Check signers parsing: + e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner:DefaultScope + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner:None + e.checkStack(t, false) + e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner:CalledByEntry + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of ownerLE:CalledByEntry + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner0xLE:CalledByEntry + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner0xLE:CalledByEntry + e.checkStack(t, false) + e.checkError(t, errors.New("contract hash, address or ID is mandatory argument")) +} + +func TestJump(t *testing.T) { + buf := io.NewBufBinWriter() + emit.Opcodes(buf.BinWriter, opcode.PUSH1, opcode.PUSH2, opcode.ABORT) // some garbage + jmpTo := buf.Len() + emit.Opcodes(buf.BinWriter, opcode.PUSH4, opcode.PUSH5, opcode.ADD) // useful script + e := newTestVMCLI(t) + e.runProg(t, + "loadhex "+hex.EncodeToString(buf.Bytes()), + "jump "+strconv.Itoa(jmpTo), + "run", + ) + + e.checkNextLine(t, "READY: loaded 6 instructions") + e.checkNextLine(t, fmt.Sprintf("jumped to instruction %d", jmpTo)) + e.checkStack(t, 9) +} diff --git a/pkg/smartcontract/context/context.go b/pkg/smartcontract/context/context.go index 13d2927f7..2974e2675 100644 --- a/pkg/smartcontract/context/context.go +++ b/pkg/smartcontract/context/context.go @@ -62,6 +62,8 @@ func NewParameterContext(typ string, network netmode.Magic, verif crypto.Verifia } } +// GetCompleteTransaction clears transaction witnesses (if any) and refills them with +// signatures from the parameter context. func (c *ParameterContext) GetCompleteTransaction() (*transaction.Transaction, error) { tx, ok := c.Verifiable.(*transaction.Transaction) if !ok { diff --git a/pkg/smartcontract/param_type.go b/pkg/smartcontract/param_type.go index 59d44ab04..c6d640ae5 100644 --- a/pkg/smartcontract/param_type.go +++ b/pkg/smartcontract/param_type.go @@ -327,6 +327,8 @@ func adjustValToType(typ ParamType, val string) (interface{}, error) { return pub.Bytes(), nil case StringType: return val, nil + case AnyType: + return nil, nil default: return nil, errors.New("unsupported parameter type") } @@ -347,6 +349,10 @@ func inferParamType(val string) ParamType { return IntegerType } + if val == "nil" { + return AnyType + } + if val == "true" || val == "false" { return BoolType } diff --git a/pkg/smartcontract/param_type_test.go b/pkg/smartcontract/param_type_test.go index 24ffa69fe..af643b042 100644 --- a/pkg/smartcontract/param_type_test.go +++ b/pkg/smartcontract/param_type_test.go @@ -155,6 +155,9 @@ func TestInferParamType(t *testing.T) { }, { in: "dead", out: ByteArrayType, + }, { + in: "nil", + out: AnyType, }} for _, inout := range inouts { out := inferParamType(inout.in) @@ -323,6 +326,10 @@ func TestAdjustValToType(t *testing.T) { typ: InteropInterfaceType, val: "", err: true, + }, { + typ: AnyType, + val: "nil", + out: nil, }} for _, inout := range inouts { 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}, }, } diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 1e4f88df7..3a348d530 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -262,7 +262,7 @@ func (v *VM) LoadFileWithFlags(path string, f callflag.CallFlag) error { if err != nil { return err } - v.Load(nef.Script) + v.LoadWithFlags(nef.Script, f) return nil }