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"
|
"bytes"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"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). " +
|
"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." +
|
"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). " +
|
"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]`,
|
UsageText: `storage <hash-or-address-or-id> [<prefix>] [--backwards] [--diff]`,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
|
@ -286,7 +288,7 @@ Example:
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: diffFlagFullName + ",d",
|
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]
|
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.
|
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).
|
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.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
> storage 0x0000000009070e030d0f0e020d0c06050e030c02 030e --backwards --diff`,
|
> storage 0x0000000009070e030d0f0e020d0c06050e030c02 030e --backwards --diff`,
|
||||||
Action: handleStorage,
|
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
|
var completer *readline.PrefixCompleter
|
||||||
|
@ -907,37 +930,15 @@ func handleEnv(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStorage(c *cli.Context) error {
|
func handleStorage(c *cli.Context) error {
|
||||||
if !c.Args().Present() {
|
id, prefix, err := getDumpArgs(c)
|
||||||
return errors.New("contract hash, address or ID is mandatory argument")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
hashOrID := c.Args().Get(0)
|
|
||||||
var (
|
var (
|
||||||
id int32
|
|
||||||
ic = getInteropContextFromContext(c.App)
|
|
||||||
prefix []byte
|
|
||||||
backwards bool
|
backwards bool
|
||||||
seekDepth int
|
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) {
|
if c.Bool(backwardsFlagFullName) {
|
||||||
backwards = true
|
backwards = true
|
||||||
}
|
}
|
||||||
|
@ -955,6 +956,79 @@ func handleStorage(c *cli.Context) error {
|
||||||
return nil
|
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) {
|
func dumpEvents(app *cli.App) (string, error) {
|
||||||
ic := getInteropContextFromContext(app)
|
ic := getInteropContextFromContext(app)
|
||||||
if len(ic.Notifications) == 0 {
|
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/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
"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/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/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
"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{}) {
|
func (e *executor) checkSlot(t *testing.T, items ...interface{}) {
|
||||||
d := json.NewDecoder(e.out)
|
d := json.NewDecoder(e.out)
|
||||||
var actual interface{}
|
var actual interface{}
|
||||||
|
@ -910,3 +925,56 @@ func TestDumpStorageDiff(t *testing.T) {
|
||||||
e.checkStorage(t, append(expected, diff)...)
|
e.checkStorage(t, append(expected, diff)...)
|
||||||
e.checkStorage(t, 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