From 9977606e40c82f17dc6118b3ab5219bb3507dbf3 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 10 Oct 2022 13:59:37 +0300 Subject: [PATCH 01/11] cli: support 'loadtx' VM CLI command --- cli/vm/cli.go | 83 ++++++++++++++++++++++++++++++++++++++-------- cli/vm/cli_test.go | 33 ++++++++++++++++++ 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index ad4f3a184..647e3057d 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -20,6 +20,7 @@ import ( "github.com/kballard/go-shellquote" "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,6 +28,7 @@ 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" @@ -166,6 +168,21 @@ both parameters are mandatory, example: > loadgo /path/to/file.go`, Action: handleLoadGo, }, + { + Name: "loadtx", + Usage: "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.", + UsageText: `loadtx [--historic ] `, + Flags: []cli.Flag{historicFlag}, + Description: `loadtx [--historic ] + +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: "reset", Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state", @@ -574,17 +591,17 @@ 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) + err := prepareVM(c, nil) if err != nil { return err } @@ -607,7 +624,7 @@ func handleLoadNEF(c *cli.Context) error { } func handleLoadBase64(c *cli.Context) error { - err := prepareVM(c) + err := prepareVM(c, nil) if err != nil { return err } @@ -627,7 +644,7 @@ func handleLoadBase64(c *cli.Context) error { } func handleLoadHex(c *cli.Context) error { - err := prepareVM(c) + err := prepareVM(c, nil) if err != nil { return err } @@ -647,7 +664,7 @@ func handleLoadHex(c *cli.Context) error { } func handleLoadGo(c *cli.Context) error { - err := prepareVM(c) + err := prepareVM(c, nil) if err != nil { return err } @@ -676,8 +693,48 @@ func handleLoadGo(c *cli.Context) error { 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) + + v.LoadWithFlags(tx.Script, callflag.All) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c.App) + return nil +} + func handleReset(c *cli.Context) error { - err := prepareVM(c) + err := prepareVM(c, nil) if err != nil { return err } @@ -693,7 +750,7 @@ 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 { +func resetInteropContext(app *cli.App, tx *transaction.Transaction, height ...uint32) error { finalizeInteropContext(app) bc := getChainFromContext(app) var ( @@ -701,12 +758,12 @@ func resetInteropContext(app *cli.App, height ...uint32) error { err error ) if len(height) != 0 { - newIc, err = bc.GetTestHistoricVM(trigger.Application, nil, 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) + newIc, err = bc.GetTestVM(trigger.Application, tx, nil) if err != nil { return fmt.Errorf("failed to create VM: %w", err) } @@ -723,8 +780,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 } diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index 256625e1f..4e90cd1b5 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" gio "io" "os" @@ -15,10 +16,12 @@ import ( "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" @@ -986,3 +989,33 @@ 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: ")) +} From 3fba4e4f17a36e5e410ff9d98b4eaaab9dda783d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 11 Oct 2022 12:22:07 +0300 Subject: [PATCH 02/11] cli: support 'loaddeployed' VM CLI command --- cli/vm/cli.go | 95 ++++++++++++++++++++++++++++++++++++---------- cli/vm/cli_test.go | 29 ++++++++++++++ 2 files changed, 103 insertions(+), 21 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 647e3057d..846dc809d 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -183,6 +183,19 @@ The transaction script will be loaded into VM; the resulting execution context w > loadtx /path/to/file`, Action: handleLoadTx, }, + { + Name: "loaddeployed", + Usage: "Load deployed contract into the VM from chain. If '--historic' flag specified, then the historic contract state (historic script and manifest) will be loaded.", + UsageText: `loaddeployed [--historic ] `, + Flags: []cli.Flag{historicFlag}, + Description: `loaddeployed [--historic ] + +Load deployed contract into the VM from chain. If '--historic' flag specified, then the historic contract state (historic script and manifest) will be loaded. + + is mandatory parameter, example: +> loaddeployed 0x0000000009070e030d0f0e020d0c06050e030c02`, + Action: handleLoadDeployed, + }, { Name: "reset", Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state", @@ -733,6 +746,41 @@ func handleLoadTx(c *cli.Context) error { 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) + } + + v := getVMFromContext(c.App) + v.LoadScriptWithHash(cs.NEF.Script, h, callflag.All) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + setManifestInContext(c.App, &cs.Manifest) + changePrompt(c.App) + return nil +} + func handleReset(c *cli.Context) error { err := prepareVM(c, nil) if err != nil { @@ -1071,29 +1119,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 { @@ -1103,6 +1133,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 { diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index 4e90cd1b5..7186d02cd 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -1019,3 +1019,32 @@ func TestLoadtx(t *testing.T) { 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) + + 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", + "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}) + e.checkError(t, errors.New("contract hash, address or ID is mandatory argument")) +} From 7eb87afab8c6047b9555d0c64cc4b6d326e1f794 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 10 Oct 2022 14:00:26 +0300 Subject: [PATCH 03/11] 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}, }, } From af658bc3e5df907b867a10fbe55cf149de81b16b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 12 Oct 2022 12:07:44 +0300 Subject: [PATCH 04/11] cli: support Null as an argument for invocation-related commands --- cli/cmdargs/parser.go | 6 +++++- pkg/smartcontract/param_type.go | 6 ++++++ pkg/smartcontract/param_type_test.go | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index 3868466a2..13fccaf8a 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -32,7 +32,8 @@ const ( '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. + 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 @@ -60,6 +61,8 @@ const ( 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 @@ -76,6 +79,7 @@ const ( 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' 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 { From 4dbaf2a123ae5e350508587e828629fb3a7460a4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 11 Oct 2022 12:55:51 +0300 Subject: [PATCH 05/11] smartcontract: add comment to GetCompleteTransaction --- pkg/smartcontract/context/context.go | 2 ++ 1 file changed, 2 insertions(+) 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 { From d09a0c18a74c42509b0345c3882ce56768bd86ab Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 11 Oct 2022 14:59:51 +0300 Subject: [PATCH 06/11] cli: unify signers parsing Share signers parsing code between 'contract invokefunction' and 'vm load*' commands, quite a useful thing when it comes to witness checks. --- cli/cmdargs/parser.go | 65 +++++++++-- cli/smartcontract/smart_contract.go | 37 +------ cli/vm/cli.go | 165 ++++++++++++++++++++-------- cli/vm/cli_test.go | 141 ++++++++++++++++++++++-- 4 files changed, 311 insertions(+), 97 deletions(-) diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index 13fccaf8a..b90fa60d7 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -96,25 +96,76 @@ const ( * '[ 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 51cc2d9e6..8a468ed1a 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -211,42 +211,7 @@ func NewCommands() []cli.Command { ` + 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 - 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 e947c3ef4..4aaeab3cf 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -35,6 +35,7 @@ import ( "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" @@ -123,44 +124,57 @@ var commands = []cli.Command{ }, { 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 [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loadnef [--historic ] -both parameters are mandatory, example: + Description: `loadnef [--historic ] [, ...] + + 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: `loadbase64 [--historic ] [, ...] - is mandatory parameter, example: + is mandatory parameter. +` + 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: `loadhex [--historic ] [, ...] - is mandatory parameter, example: + is mandatory parameter. +` + 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: `loadgo [--historic ] [, ...] - is mandatory parameter, example: + is mandatory parameter. +` + cmdargs.SignersParsingDoc + ` + +Example: > loadgo /path/to/file.go`, Action: handleLoadGo, }, @@ -181,14 +195,18 @@ The transaction script will be loaded into VM; the resulting execution context w }, { Name: "loaddeployed", - Usage: "Load deployed contract into the VM from chain. If '--historic' flag specified, then the historic contract state (historic script and manifest) will be loaded.", - UsageText: `loaddeployed [--historic ] `, + Usage: "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.", + UsageText: `loaddeployed [--historic ] [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loaddeployed [--historic ] + Description: `loaddeployed [--historic ] [, ...] -Load deployed contract into the VM from chain. If '--historic' flag specified, then the historic contract state (historic script and manifest) will be loaded. +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, example: + is mandatory parameter. +` + cmdargs.SignersParsingDoc + ` + +Example: > loaddeployed 0x0000000009070e030d0f0e020d0c06050e030c02`, Action: handleLoadDeployed, }, @@ -604,22 +622,35 @@ func prepareVM(c *cli.Context, tx *transaction.Transaction) error { } func handleLoadNEF(c *cli.Context) error { - err := prepareVM(c, nil) - 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) + v.LoadWithFlags(nef.Script, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) setManifestInContext(c.App, m) changePrompt(c.App) @@ -627,11 +658,6 @@ func handleLoadNEF(c *cli.Context) error { } func handleLoadBase64(c *cli.Context) error { - err := prepareVM(c, nil) - if err != nil { - return err - } - v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { return fmt.Errorf("%w: ", ErrMissingParameter) @@ -640,18 +666,33 @@ func handleLoadBase64(c *cli.Context) error { if err != nil { return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } + 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) v.LoadWithFlags(b, callflag.All) 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, nil) - 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) @@ -660,6 +701,18 @@ func handleLoadHex(c *cli.Context) error { if err != nil { return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } + 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) v.LoadWithFlags(b, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) @@ -667,11 +720,6 @@ func handleLoadHex(c *cli.Context) error { } func handleLoadGo(c *cli.Context) error { - err := prepareVM(c, nil) - if err != nil { - return err - } - v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { return fmt.Errorf("%w: ", ErrMissingParameter) @@ -688,8 +736,20 @@ 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) + } + } + err = prepareVM(c, createFakeTransaction(b.Script, signers)) + if err != nil { + return err + } + v := getVMFromContext(c.App) + setManifestInContext(c.App, m) v.LoadWithFlags(b.Script, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) @@ -763,6 +823,17 @@ func handleLoadDeployed(c *cli.Context) error { 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 + } v := getVMFromContext(c.App) v.LoadScriptWithHash(cs.NEF.Script, h, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) @@ -796,11 +867,17 @@ func resetInteropContext(app *cli.App, tx *transaction.Transaction, height ...ui err error ) if len(height) != 0 { + 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 { + 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) diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index de7cf7a04..af1e3efcd 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -281,27 +281,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 @@ -343,14 +395,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 + "'" @@ -363,6 +408,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, @@ -371,6 +427,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" @@ -1037,6 +1128,10 @@ func TestLoaddeployed(t *testing.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 @@ -1047,6 +1142,19 @@ func TestLoaddeployed(t *testing.T) { "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", ) @@ -1058,5 +1166,18 @@ func TestLoaddeployed(t *testing.T) { 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")) } From 7db9258104b3ec64eb9ec4f86f1fa8a95aa68843 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 11 Oct 2022 15:01:54 +0300 Subject: [PATCH 07/11] vm: make LoadFileWithFlags actually load with flags provided --- pkg/vm/vm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From 811e09675f582eddf01d575fda050f046d4bd9e9 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 12 Oct 2022 10:19:47 +0300 Subject: [PATCH 08/11] cli: simplify VM preparation for VM CLI Load script from provided transaction if so. --- cli/vm/cli.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 4aaeab3cf..afd0aa087 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -650,7 +650,6 @@ func handleLoadNEF(c *cli.Context) error { return err } v := getVMFromContext(c.App) - v.LoadWithFlags(nef.Script, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) setManifestInContext(c.App, m) changePrompt(c.App) @@ -678,7 +677,6 @@ func handleLoadBase64(c *cli.Context) error { return err } v := getVMFromContext(c.App) - v.LoadWithFlags(b, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil @@ -713,7 +711,6 @@ func handleLoadHex(c *cli.Context) error { return err } v := getVMFromContext(c.App) - v.LoadWithFlags(b, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil @@ -750,7 +747,6 @@ func handleLoadGo(c *cli.Context) error { } v := getVMFromContext(c.App) setManifestInContext(c.App, m) - v.LoadWithFlags(b.Script, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil @@ -789,8 +785,6 @@ func handleLoadTx(c *cli.Context) error { return err } v := getVMFromContext(c.App) - - v.LoadWithFlags(tx.Script, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil @@ -834,9 +828,10 @@ func handleLoadDeployed(c *cli.Context) error { if err != nil { return err } - v := getVMFromContext(c.App) - v.LoadScriptWithHash(cs.NEF.Script, h, callflag.All) - fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + 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 @@ -858,7 +853,8 @@ func finalizeInteropContext(app *cli.App) { } // resetInteropContext calls finalizer for current interop context and replaces -// it with the newly created one. +// 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) @@ -883,6 +879,9 @@ func resetInteropContext(app *cli.App, tx *transaction.Transaction, height ...ui return fmt.Errorf("failed to create VM: %w", err) } } + if tx != nil { + newIc.VM.LoadWithFlags(tx.Script, callflag.All) + } setInteropContextInContext(app, newIc) return nil From 884428ab935f3f5e21637b0c10815c46eeb962d2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 12 Oct 2022 13:16:19 +0300 Subject: [PATCH 09/11] cli: add 'jump' command Close #2606. --- cli/vm/cli.go | 46 +++++++++++++++++++++++++++++++++++++++------- cli/vm/cli_test.go | 18 ++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index afd0aa087..a83ca6e6f 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -92,6 +92,15 @@ var commands = []cli.Command{ > break 12`, Action: handleBreak, }, + { + Name: "jump", + Usage: "Jump to the specified instruction (absolute IP value)", + UsageText: `jump `, + Description: `jump + is mandatory parameter, example: +> jump 12`, + Action: handleJump, + }, { Name: "estack", Usage: "Show evaluation stack contents", @@ -559,21 +568,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 diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index af1e3efcd..4552f9253 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -11,6 +11,7 @@ import ( "math/big" "os" "path/filepath" + "strconv" "strings" "sync" "testing" @@ -1181,3 +1182,20 @@ func TestLoaddeployed(t *testing.T) { 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) +} From 62197929f4d689dd9bb03733d20b704cc0808e06 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 12 Oct 2022 13:21:37 +0300 Subject: [PATCH 10/11] cli: unify the style of Usage message for VM CLI commands Usage message is shown on common --help command, thus it should be meaningful and short. If user needs more detailed command description, then he can use command-specific help. As a result, current VM help looks pretty simple: ``` NEO-GO-VM > help NAME: VM CLI - Official VM CLI for Neo-Go USAGE: [global options] command [command options] [arguments...] VERSION: 0.99.5-pre-15-g5463ec41 COMMANDS: exit Exit the VM prompt ip Show current instruction break Place a breakpoint jump Jump to the specified instruction (absolute IP value) estack Show evaluation stack contents istack Show invocation stack contents sslot Show static slot contents lslot Show local slot contents aslot Show arguments slot contents loadnef Load a NEF-consistent script into the VM optionally attaching to it provided signers with scopes loadbase64 Load a base64-encoded script string into the VM optionally attaching to it provided signers with scopes loadhex Load a hex-encoded script string into the VM optionally attaching to it provided signers with scopes loadgo Compile and load a Go file with the manifest into the VM optionally attaching to it provided signers with scopes loadtx Load transaction into the VM from chain or from parameter context file loaddeployed Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes reset Unload compiled script from the VM and reset context to proper (possibly, historic) state parse Parse provided argument and convert it into other possible formats run Execute the current loaded script cont Continue execution of the current loaded script step Step (n) instruction in the program stepinto Stepinto instruction to take in the debugger stepout Stepout instruction to take in the debugger stepover Stepover instruction to take in the debugger ops Dump opcodes of the current loaded program events Dump events emitted by the current loaded program env Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration) storage Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation changes Dump storage changes as is at the current stage of loaded script invocation help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help --version, -v print the version ``` --- cli/vm/cli.go | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index a83ca6e6f..1f218fcda 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -188,9 +188,8 @@ Example: Action: handleLoadGo, }, { - Name: "loadtx", - Usage: "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.", + Name: "loadtx", + Usage: "Load transaction into the VM from chain or from parameter context file", UsageText: `loadtx [--historic ] `, Flags: []cli.Flag{historicFlag}, Description: `loadtx [--historic ] @@ -204,7 +203,7 @@ The transaction script will be loaded into VM; the resulting execution context w }, { Name: "loaddeployed", - Usage: "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.", + 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: `loaddeployed [--historic ] [, ...] @@ -322,14 +321,8 @@ Example: 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{ @@ -356,12 +349,8 @@ 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 [ []] From 8b91428db8b6e589e8f9288ff6a031594fcc7af1 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 12 Oct 2022 13:40:36 +0300 Subject: [PATCH 11/11] cli: unify documentation of VM CLI commands 1. UsageText shows the command usage rule. Fixed/added where needed. 2. Description shows the command description, huh. It is shown right after UsageText, so there's no need to repeat the command usage rule. If Description contains Example, then it should be printed on a new line. --- cli/vm/cli.go | 130 ++++++++++++++++++++++++++------------------------ 1 file changed, 68 insertions(+), 62 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 1f218fcda..27f01a7f6 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -74,21 +74,24 @@ 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, }, @@ -96,49 +99,54 @@ var commands = []cli.Command{ Name: "jump", Usage: "Jump to the specified instruction (absolute IP value)", UsageText: `jump `, - Description: `jump - is mandatory parameter, example: + 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 optionally attaching to it provided signers with scopes", - UsageText: `loadnef [, ...]`, + UsageText: `loadnef [--historic ] [, ...]`, Flags: []cli.Flag{historicFlag}, - Description: `loadnef [--historic ] [, ...] + Description: ` and parameters are mandatory. - and parameters are mandatory. ` + cmdargs.SignersParsingDoc + ` Example: @@ -150,9 +158,8 @@ Example: 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. ` + cmdargs.SignersParsingDoc + ` Example: @@ -164,9 +171,8 @@ Example: 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. ` + cmdargs.SignersParsingDoc + ` Example: @@ -178,9 +184,8 @@ Example: 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. ` + cmdargs.SignersParsingDoc + ` Example: @@ -192,12 +197,12 @@ Example: Usage: "Load transaction into the VM from chain or from parameter context file", UsageText: `loadtx [--historic ] `, Flags: []cli.Flag{historicFlag}, - Description: `loadtx [--historic ] - -Load transaction into the VM from chain or from parameter context file. + 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: + is mandatory parameter. + +Example: > loadtx /path/to/file`, Action: handleLoadTx, }, @@ -206,12 +211,11 @@ The transaction script will be loaded into VM; the resulting execution context w 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: `loaddeployed [--historic ] [, ...] - -Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes. + 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: @@ -219,28 +223,26 @@ Example: Action: handleLoadDeployed, }, { - Name: "reset", - Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state", - Flags: []cli.Flag{historicFlag}, - Action: handleReset, + 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 using the same rules as for 'contract testinvokefunction' command: @@ -248,57 +250,67 @@ and converted to other formats. Strings are escaped and output in quotes.`, ` + cmdargs.ParamsParsingDoc + ` Example: -> run put string:"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, }, @@ -312,9 +324,7 @@ 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`, @@ -334,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. @@ -352,9 +360,7 @@ Example: 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.