cli: add 'changes' command for VM CLI
This commit is contained in:
parent
cac4f6a4a6
commit
b3c8192d2e
2 changed files with 170 additions and 28 deletions
130
cli/vm/cli.go
130
cli/vm/cli.go
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue