cli: add 'changes' command for VM CLI

This commit is contained in:
Anna Shaleva 2022-10-05 15:06:20 +03:00
parent cac4f6a4a6
commit b3c8192d2e
2 changed files with 170 additions and 28 deletions

View file

@ -4,6 +4,7 @@ import (
"bytes"
"crypto/elliptic"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
@ -277,7 +278,8 @@ Example:
"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).",
"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.",
UsageText: `storage <hash-or-address-or-id> [<prefix>] [--backwards] [--diff]`,
Flags: []cli.Flag{
cli.BoolFlag{
@ -286,7 +288,7 @@ Example:
},
cli.BoolFlag{
Name: diffFlagFullName + ",d",
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.",
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 <hash-or-address-or-id> <prefix> [--backwards] [--diff]
@ -297,11 +299,32 @@ Hex-encoded storage items prefix may be specified (empty by default to return th
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.
Example:
> storage 0x0000000009070e030d0f0e020d0c06050e030c02 030e --backwards --diff`,
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.",
UsageText: `changes [<hash-or-address-or-id> [<prefix>]]`,
Description: `changes [<hash-or-address-or-id> [<prefix>]]
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.
Example:
> changes 0x0000000009070e030d0f0e020d0c06050e030c02 030e`,
Action: handleChanges,
},
}
var completer *readline.PrefixCompleter
@ -907,37 +930,15 @@ func handleEnv(c *cli.Context) error {
}
func handleStorage(c *cli.Context) error {
if !c.Args().Present() {
return errors.New("contract hash, address or ID is mandatory argument")
id, prefix, err := getDumpArgs(c)
if err != nil {
return err
}
hashOrID := c.Args().Get(0)
var (
id int32
ic = getInteropContextFromContext(c.App)
prefix []byte
backwards bool
seekDepth int
ic = getInteropContextFromContext(c.App)
)
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
}
@ -955,6 +956,79 @@ func handleStorage(c *cli.Context) error {
return nil
}
func handleChanges(c *cli.Context) error {
var (
expectedID int32
prefix []byte
err error
hasAgs = c.Args().Present()
)
if hasAgs {
expectedID, prefix, err = getDumpArgs(c)
if err != nil {
return err
}
}
ic := getInteropContextFromContext(c.App)
b := ic.DAO.GetBatch()
if b == nil {
return nil
}
ops := storage.BatchToOperations(b)
var notFirst bool
for _, op := range ops {
id := int32(binary.LittleEndian.Uint32(op.Key))
if hasAgs && (expectedID != id || (len(prefix) != 0 && !bytes.HasPrefix(op.Key[4:], prefix))) {
continue
}
var message string
if notFirst {
message += "\n"
}
message += fmt.Sprintf("Contract ID: %d\nState: %s\nKey: %s\n", id, op.State, hex.EncodeToString(op.Key[4:]))
if op.Value != nil {
message += fmt.Sprintf("Value: %s\n", hex.EncodeToString(op.Value))
}
fmt.Fprint(c.App.Writer, message)
notFirst = true
}
return nil
}
// 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)
if err != nil {
i, err := strconv.Atoi(hashOrID)
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
}
if c.NArg() > 1 {
prefix, err = hex.DecodeString(c.Args().Get(1))
if err != nil {
return 0, nil, fmt.Errorf("failed to decode prefix from hex: %w", err)
}
}
return id, prefix, nil
}
func dumpEvents(app *cli.App) (string, error) {
ic := getInteropContextFromContext(app)
if len(ic.Notifications) == 0 {

View file

@ -23,6 +23,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/state"
"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/storage/dboper"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest"
@ -227,6 +228,20 @@ func (e *executor) checkStorage(t *testing.T, kvs ...storage.KeyValue) {
}
}
type storageChange struct {
ContractID int32
dboper.Operation
}
func (e *executor) checkChange(t *testing.T, c storageChange) {
e.checkNextLine(t, fmt.Sprintf("Contract ID: %d", c.ContractID))
e.checkNextLine(t, fmt.Sprintf("State: %s", c.State))
e.checkNextLine(t, fmt.Sprintf("Key: %s", hex.EncodeToString(c.Key)))
if c.Value != nil {
e.checkNextLine(t, fmt.Sprintf("Value: %s", hex.EncodeToString(c.Value)))
}
}
func (e *executor) checkSlot(t *testing.T, items ...interface{}) {
d := json.NewDecoder(e.out)
var actual interface{}
@ -910,3 +925,56 @@ func TestDumpStorageDiff(t *testing.T) {
e.checkStorage(t, append(expected, diff)...)
e.checkStorage(t, diff)
}
func TestDumpChanges(t *testing.T) {
e := newTestVMClIWithState(t)
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "put", callflag.All, 3, 4) // add
emit.AppCall(script.BinWriter, h, "delete", callflag.All, 1) // remove
emit.AppCall(script.BinWriter, h, "put", callflag.All, 2, 5) // update
expected := []storageChange{
{
ContractID: 1,
Operation: dboper.Operation{
State: "Deleted",
Key: []byte{1},
},
},
{
ContractID: 1,
Operation: dboper.Operation{
State: "Changed",
Key: []byte{2},
Value: []byte{5},
},
},
{
ContractID: 1,
Operation: dboper.Operation{
State: "Added",
Key: []byte{3},
Value: []byte{4},
},
},
}
e.runProg(t,
"changes",
"changes 1",
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"changes 1 "+hex.EncodeToString([]byte{1}),
"changes 1 "+hex.EncodeToString([]byte{2}),
"changes 1 "+hex.EncodeToString([]byte{3}),
)
// no script is executed => no diff
e.checkNextLine(t, "READY: loaded 113 instructions")
e.checkStack(t, 3, true, 2)
e.checkChange(t, expected[0])
e.checkChange(t, expected[1])
e.checkChange(t, expected[2])
}