diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 3b7734e21..fdcec3c2d 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -17,6 +17,7 @@ import ( "github.com/chzyer/readline" "github.com/kballard/go-shellquote" + "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core" @@ -54,8 +55,9 @@ const ( // Various flag names. const ( - verboseFlagFullName = "verbose" - historicFlagFullName = "historic" + verboseFlagFullName = "verbose" + historicFlagFullName = "historic" + backwardsFlagFullName = "backwards" ) var historicFlag = cli.IntFlag{ @@ -267,6 +269,32 @@ 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).", + UsageText: `storage [] [--backwards]`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: backwardsFlagFullName + ",b", + Usage: "Backwards traversal direction", + }, + }, + Description: `storage --backwards + +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). + +Example: +> storage 0x0000000009070e030d0f0e020d0c06050e030c02 030e --backwards`, + Action: handleStorage, + }, } var completer *readline.PrefixCompleter @@ -871,6 +899,50 @@ func handleEnv(c *cli.Context) error { return nil } +func handleStorage(c *cli.Context) error { + if !c.Args().Present() { + return errors.New("contract hash, address or ID is mandatory argument") + } + hashOrID := c.Args().Get(0) + var ( + id int32 + ic = getInteropContextFromContext(c.App) + prefix []byte + backwards bool + ) + h, err := flags.ParseAddress(hashOrID) + if err != nil { + i, err := strconv.Atoi(hashOrID) + if err != nil { + return 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 fmt.Errorf("contract %s not found: %w", h.StringLE(), err) + } + id = cs.ID + } + if c.NArg() > 1 { + prefix, err = hex.DecodeString(c.Args().Get(1)) + if err != nil { + return fmt.Errorf("failed to decode prefix from hex: %w", err) + } + } + if c.Bool(backwardsFlagFullName) { + backwards = true + } + ic.DAO.Seek(id, storage.SeekRange{ + Prefix: prefix, + Backwards: backwards, + }, func(k, v []byte) bool { + fmt.Fprintf(c.App.Writer, "%s: %v\n", hex.EncodeToString(k), hex.EncodeToString(v)) + return true + }) + return 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 1dc137cc1..8c19b83b6 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -221,6 +221,12 @@ func (e *executor) checkEvents(t *testing.T, isKeywordExpected bool, events ...s require.NoError(t, err) } +func (e *executor) checkStorage(t *testing.T, kvs ...storage.KeyValue) { + for _, kv := range kvs { + e.checkNextLine(t, fmt.Sprintf("%s: %s", hex.EncodeToString(kv.Key), hex.EncodeToString(kv.Value))) + } +} + func (e *executor) checkSlot(t *testing.T, items ...interface{}) { d := json.NewDecoder(e.out) var actual interface{} @@ -849,3 +855,28 @@ func TestEnv(t *testing.T) { e.checkNextLine(t, "Node config:") // Do not check exact node config. }) } + +func TestDumpStorage(t *testing.T) { + e := newTestVMClIWithState(t) + + h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go + require.NoError(t, err) + expected := []storage.KeyValue{ + {Key: []byte{1}, Value: []byte{2}}, + {Key: []byte{2}, Value: []byte{2}}, + } + e.runProg(t, + "storage "+h.StringLE(), + "storage 0x"+h.StringLE(), + "storage "+address.Uint160ToString(h), + "storage 1", + "storage 1 "+hex.EncodeToString(expected[0].Key), + "storage 1 --backwards", + ) + e.checkStorage(t, expected...) + e.checkStorage(t, expected...) + e.checkStorage(t, expected...) + e.checkStorage(t, expected...) + e.checkStorage(t, storage.KeyValue{Key: nil, Value: []byte{2}}) // empty key because search prefix is trimmed + e.checkStorage(t, expected[1], expected[0]) +}