mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-29 23:33:37 +00:00
895a0ae624
vm: make slots public
1566 lines
45 KiB
Go
1566 lines
45 KiB
Go
package vm
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/elliptic"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"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"
|
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
"github.com/nspcc-dev/neo-go/pkg/core"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
"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/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"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"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/vm"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
"github.com/urfave/cli/v2"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
const (
|
|
chainKey = "chain"
|
|
chainCfgKey = "chainCfg"
|
|
icKey = "ic"
|
|
contractStateKey = "contractState"
|
|
exitFuncKey = "exitFunc"
|
|
readlineInstanceKey = "readlineKey"
|
|
printLogoKey = "printLogoKey"
|
|
)
|
|
|
|
// Various flag names.
|
|
const (
|
|
verboseFlagFullName = "verbose"
|
|
historicFlagFullName = "historic"
|
|
gasFlagFullName = "gas"
|
|
backwardsFlagFullName = "backwards"
|
|
diffFlagFullName = "diff"
|
|
hashFlagFullName = "hash"
|
|
)
|
|
|
|
var (
|
|
historicFlag = &cli.IntFlag{
|
|
Name: historicFlagFullName,
|
|
Usage: "Height for historic script invocation (for MPT-enabled blockchain configuration with KeepOnlyLatestState setting disabled). " +
|
|
"Assuming that block N-th is specified as an argument, the historic invocation is based on the storage state of height N and fake currently-accepting block with index N+1.",
|
|
}
|
|
gasFlag = &cli.Int64Flag{
|
|
Name: gasFlagFullName,
|
|
Usage: "GAS limit for this execution (integer number, satoshi).",
|
|
}
|
|
hashFlag = &flags.AddressFlag{
|
|
Name: hashFlagFullName,
|
|
Usage: "Smart-contract hash in LE form or address",
|
|
}
|
|
)
|
|
|
|
var commands = []*cli.Command{
|
|
{
|
|
Name: "exit",
|
|
Usage: "Exit the VM prompt",
|
|
UsageText: "exit",
|
|
Description: "Exit the VM prompt.",
|
|
Action: handleExit,
|
|
},
|
|
{
|
|
Name: "ip",
|
|
Usage: "Show current instruction",
|
|
UsageText: "ip",
|
|
Description: "Show current instruction.",
|
|
Action: handleIP,
|
|
},
|
|
{
|
|
Name: "break",
|
|
Usage: "Place a breakpoint",
|
|
UsageText: `break <ip>`,
|
|
Description: `<ip> is mandatory parameter.
|
|
|
|
Example:
|
|
> break 12`,
|
|
Action: handleBreak,
|
|
},
|
|
{
|
|
Name: "delete",
|
|
Usage: "Remove a breakpoint",
|
|
UsageText: `delete <ip>`,
|
|
Description: `<ip> is mandatory parameter.
|
|
|
|
Example:
|
|
> delete 12`,
|
|
Action: handleRemoveBreak,
|
|
},
|
|
{
|
|
Name: "ib",
|
|
Usage: "List breakpoints",
|
|
UsageText: `ib`,
|
|
Description: `List breakpoints.
|
|
|
|
Example:
|
|
> ib`,
|
|
Action: handleListBreak,
|
|
},
|
|
{
|
|
Name: "jump",
|
|
Usage: "Jump to the specified instruction (absolute IP value)",
|
|
UsageText: `jump <ip>`,
|
|
Description: `<ip> is mandatory parameter (absolute IP value).
|
|
|
|
Example:
|
|
> jump 12`,
|
|
Action: handleJump,
|
|
},
|
|
{
|
|
Name: "estack",
|
|
Usage: "Show evaluation stack contents",
|
|
UsageText: "estack",
|
|
Description: "Show evaluation stack contents.",
|
|
Action: handleXStack,
|
|
},
|
|
{
|
|
Name: "istack",
|
|
Usage: "Show invocation stack contents",
|
|
UsageText: "istack",
|
|
Description: "Show invocation stack contents.",
|
|
Action: handleXStack,
|
|
},
|
|
{
|
|
Name: "sslot",
|
|
Usage: "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",
|
|
UsageText: "aslot",
|
|
Description: "Show arguments slot contents.",
|
|
Action: handleSlots,
|
|
},
|
|
{
|
|
Name: "loadnef",
|
|
Usage: "Load a NEF (possibly with a contract hash) into the VM optionally using provided scoped signers in the context",
|
|
UsageText: `loadnef [--historic <height>] [--gas <int>] [--hash <hash-or-address>] <file> [<manifest>] [-- <signer-with-scope>, ...]`,
|
|
Flags: []cli.Flag{historicFlag, gasFlag, hashFlag},
|
|
Description: `<file> parameter is mandatory, <manifest> parameter (if omitted) will
|
|
be guessed from the <file> parameter by replacing '.nef' suffix with '.manifest.json'
|
|
suffix.
|
|
|
|
` + 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 optionally attaching to it provided signers with scopes",
|
|
UsageText: `loadbase64 [--historic <height>] [--gas <int>] <string> [-- <signer-with-scope>, ...]`,
|
|
Flags: []cli.Flag{historicFlag, gasFlag},
|
|
Description: `<string> 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 optionally attaching to it provided signers with scopes",
|
|
UsageText: `loadhex [--historic <height>] [--gas <int>] <string> [-- <signer-with-scope>, ...]`,
|
|
Flags: []cli.Flag{historicFlag, gasFlag},
|
|
Description: `<string> 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 optionally attaching to it provided signers with scopes and setting provided hash",
|
|
UsageText: `loadgo [--historic <height>] [--gas <int>] [--hash <hash-or-address>] <file> [-- <signer-with-scope>, ...]`,
|
|
Flags: []cli.Flag{historicFlag, gasFlag, hashFlag},
|
|
Description: `<file> is mandatory parameter.
|
|
|
|
` + cmdargs.SignersParsingDoc + `
|
|
|
|
Example:
|
|
> loadgo /path/to/file.go`,
|
|
Action: handleLoadGo,
|
|
},
|
|
{
|
|
Name: "loadtx",
|
|
Usage: "Load transaction into the VM from chain or from parameter context file",
|
|
UsageText: `loadtx [--historic <height>] [--gas <int>] <file-or-hash>`,
|
|
Flags: []cli.Flag{historicFlag, gasFlag},
|
|
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. It'll also use transaction's system fee value as GAS limit if
|
|
--gas option is not used.
|
|
|
|
<file-or-hash> is mandatory parameter.
|
|
|
|
Example:
|
|
> loadtx /path/to/file`,
|
|
Action: handleLoadTx,
|
|
},
|
|
{
|
|
Name: "loaddeployed",
|
|
Usage: "Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes",
|
|
UsageText: `loaddeployed [--historic <height>] [--gas <int>] <hash-or-address-or-id> [-- <signer-with-scope>, ...]`,
|
|
Flags: []cli.Flag{historicFlag, gasFlag},
|
|
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.
|
|
|
|
<hash-or-address-or-id> is mandatory parameter.
|
|
|
|
` + cmdargs.SignersParsingDoc + `
|
|
|
|
Example:
|
|
> loaddeployed 0x0000000009070e030d0f0e020d0c06050e030c02`,
|
|
Action: handleLoadDeployed,
|
|
},
|
|
{
|
|
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 <arg>`,
|
|
Description: `<arg> 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: "Usage Execute the current loaded script",
|
|
UsageText: `run [<method> [<parameter>...]]`,
|
|
Description: `<method> is a contract method, specified in manifest. It can be '_' which will push
|
|
parameters onto the stack and execute from the current offset.
|
|
<parameter> is a parameter (can be repeated multiple times) that can be specified
|
|
using the same rules as for 'contract testinvokefunction' command:
|
|
|
|
` + cmdargs.ParamsParsingDoc + `
|
|
|
|
Example:
|
|
> run put int:5 string:some_string_value`,
|
|
Action: handleRun,
|
|
},
|
|
{
|
|
Name: "cont",
|
|
Usage: "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 [<n>]`,
|
|
Description: `<n> 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",
|
|
UsageText: "stepinto",
|
|
Description: `Stepinto instruction to take in the debugger.
|
|
|
|
Example:
|
|
> stepinto`,
|
|
Action: handleStepInto,
|
|
},
|
|
{
|
|
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",
|
|
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,
|
|
},
|
|
{
|
|
Name: "env",
|
|
Usage: "Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration)",
|
|
UsageText: `env [-v]`,
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: verboseFlagFullName,
|
|
Aliases: []string{"v"},
|
|
Usage: "Print the whole blockchain node configuration.",
|
|
},
|
|
},
|
|
Description: `Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration).
|
|
|
|
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",
|
|
UsageText: `storage [--backwards] [--diff] <hash-or-address-or-id> [<prefix>]`,
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: backwardsFlagFullName,
|
|
Aliases: []string{"b"},
|
|
Usage: "Backwards traversal direction",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: diffFlagFullName,
|
|
Aliases: []string{"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, use 'changes' command for that.",
|
|
},
|
|
},
|
|
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.
|
|
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",
|
|
UsageText: `changes [<hash-or-address-or-id> [<prefix>]]`,
|
|
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.
|
|
Resulting values are not sorted.
|
|
|
|
Example:
|
|
> changes 0x0000000009070e030d0f0e020d0c06050e030c02 030e`,
|
|
Action: handleChanges,
|
|
},
|
|
}
|
|
|
|
var completer *readline.PrefixCompleter
|
|
|
|
func init() {
|
|
var pcItems []readline.PrefixCompleterInterface
|
|
for _, c := range commands {
|
|
if !c.Hidden {
|
|
var flagsItems []readline.PrefixCompleterInterface
|
|
for _, f := range c.Flags {
|
|
names := strings.SplitN(f.Names()[0], ", ", 2) // only long name will be offered
|
|
flagsItems = append(flagsItems, readline.PcItem("--"+names[0]))
|
|
}
|
|
pcItems = append(pcItems, readline.PcItem(c.Name, flagsItems...))
|
|
}
|
|
}
|
|
completer = readline.NewPrefixCompleter(pcItems...)
|
|
}
|
|
|
|
// Various errors.
|
|
var (
|
|
ErrMissingParameter = errors.New("missing argument")
|
|
ErrInvalidParameter = errors.New("can't parse argument")
|
|
)
|
|
|
|
// CLI object for interacting with the VM.
|
|
type CLI struct {
|
|
chain *core.Blockchain
|
|
shell *cli.App
|
|
}
|
|
|
|
// NewWithConfig returns new CLI instance using provided config and (optionally)
|
|
// provided node config for state-backed VM.
|
|
func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config, cfg config.Config) (*CLI, error) {
|
|
if c.AutoComplete == nil {
|
|
// Autocomplete commands/flags on TAB.
|
|
c.AutoComplete = completer
|
|
}
|
|
l, err := readline.NewEx(c)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create readline instance: %w", err)
|
|
}
|
|
ctl := cli.NewApp()
|
|
ctl.Name = "VM CLI"
|
|
|
|
// Note: need to set empty `ctl.HelpName` and `ctl.UsageText`, otherwise
|
|
// `filepath.Base(os.Args[0])` will be used which is `neo-go`.
|
|
ctl.HelpName = ""
|
|
ctl.UsageText = ""
|
|
|
|
ctl.Writer = l.Stdout()
|
|
ctl.ErrWriter = l.Stderr()
|
|
ctl.Version = config.Version
|
|
ctl.Usage = "Official VM CLI for NeoGo"
|
|
|
|
// Override default error handler in order not to exit on error.
|
|
ctl.ExitErrHandler = func(context *cli.Context, err error) {}
|
|
|
|
ctl.Commands = commands
|
|
|
|
store, err := storage.NewStore(cfg.ApplicationConfiguration.DBConfiguration)
|
|
if err != nil {
|
|
writeErr(ctl.ErrWriter, fmt.Errorf("failed to open DB, clean in-memory storage will be used: %w", err))
|
|
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB
|
|
store = storage.NewMemoryStore()
|
|
}
|
|
|
|
log, _, logCloser, err := options.HandleLoggingParams(false, cfg.ApplicationConfiguration)
|
|
if err != nil {
|
|
return nil, cli.Exit(fmt.Errorf("failed to init logger: %w", err), 1)
|
|
}
|
|
filter := zap.WrapCore(func(z zapcore.Core) zapcore.Core {
|
|
return options.NewFilteringCore(z, func(entry zapcore.Entry) bool {
|
|
// Log only Runtime.Notify messages.
|
|
return entry.Level == zapcore.InfoLevel && entry.Message == runtime.SystemRuntimeLogMessage
|
|
})
|
|
})
|
|
fLog := log.WithOptions(filter)
|
|
|
|
exitF := func(i int) {
|
|
_ = store.Close()
|
|
if logCloser != nil {
|
|
_ = logCloser()
|
|
}
|
|
onExit(i)
|
|
}
|
|
|
|
chain, err := core.NewBlockchain(store, cfg.Blockchain(), fLog)
|
|
if err != nil {
|
|
return nil, cli.Exit(fmt.Errorf("could not initialize blockchain: %w", err), 1)
|
|
}
|
|
// Do not run chain, we need only state-related functionality from it.
|
|
ic, err := chain.GetTestVM(trigger.Application, nil, nil)
|
|
if err != nil {
|
|
return nil, cli.Exit(fmt.Errorf("failed to create test VM: %w", err), 1)
|
|
}
|
|
|
|
vmcli := CLI{
|
|
chain: chain,
|
|
shell: ctl,
|
|
}
|
|
|
|
vmcli.shell.Metadata = map[string]any{
|
|
chainKey: chain,
|
|
chainCfgKey: cfg,
|
|
icKey: ic,
|
|
contractStateKey: new(state.ContractBase),
|
|
exitFuncKey: exitF,
|
|
readlineInstanceKey: l,
|
|
printLogoKey: printLogotype,
|
|
}
|
|
changePrompt(vmcli.shell)
|
|
return &vmcli, nil
|
|
}
|
|
|
|
func getExitFuncFromContext(app *cli.App) func(int) {
|
|
return app.Metadata[exitFuncKey].(func(int))
|
|
}
|
|
|
|
func getReadlineInstanceFromContext(app *cli.App) *readline.Instance {
|
|
return app.Metadata[readlineInstanceKey].(*readline.Instance)
|
|
}
|
|
|
|
func getVMFromContext(app *cli.App) *vm.VM {
|
|
return getInteropContextFromContext(app).VM
|
|
}
|
|
|
|
func getChainFromContext(app *cli.App) *core.Blockchain {
|
|
return app.Metadata[chainKey].(*core.Blockchain)
|
|
}
|
|
|
|
func getChainConfigFromContext(app *cli.App) config.Config {
|
|
return app.Metadata[chainCfgKey].(config.Config)
|
|
}
|
|
|
|
func getInteropContextFromContext(app *cli.App) *interop.Context {
|
|
return app.Metadata[icKey].(*interop.Context)
|
|
}
|
|
|
|
func getContractStateFromContext(app *cli.App) *state.ContractBase {
|
|
return app.Metadata[contractStateKey].(*state.ContractBase)
|
|
}
|
|
|
|
func getPrintLogoFromContext(app *cli.App) bool {
|
|
return app.Metadata[printLogoKey].(bool)
|
|
}
|
|
|
|
func setInteropContextInContext(app *cli.App, ic *interop.Context) {
|
|
app.Metadata[icKey] = ic
|
|
}
|
|
|
|
func setContractStateInContext(app *cli.App, cs *state.ContractBase) {
|
|
app.Metadata[contractStateKey] = cs
|
|
}
|
|
|
|
func checkVMIsReady(app *cli.App) bool {
|
|
v := getVMFromContext(app)
|
|
if v == nil || !v.Ready() {
|
|
writeErr(app.Writer, errors.New("VM is not ready: no program loaded"))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func handleExit(c *cli.Context) error {
|
|
finalizeInteropContext(c.App)
|
|
l := getReadlineInstanceFromContext(c.App)
|
|
_ = l.Close()
|
|
exit := getExitFuncFromContext(c.App)
|
|
fmt.Fprintln(c.App.Writer, "Bye!")
|
|
exit(0)
|
|
return nil
|
|
}
|
|
|
|
func handleIP(c *cli.Context) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
v := getVMFromContext(c.App)
|
|
ctx := v.Context()
|
|
if ctx.NextIP() < ctx.LenInstr() {
|
|
ip, opcode := v.Context().NextInstr()
|
|
fmt.Fprintf(c.App.Writer, "instruction pointer at %d (%s)\n", ip, opcode)
|
|
} else {
|
|
fmt.Fprintln(c.App.Writer, "execution has finished")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleBreak(c *cli.Context) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
n, err := getInstructionParameter(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
v := getVMFromContext(c.App)
|
|
v.AddBreakPoint(n)
|
|
fmt.Fprintf(c.App.Writer, "breakpoint added at instruction %d\n", n)
|
|
return nil
|
|
}
|
|
|
|
func handleRemoveBreak(c *cli.Context) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
n, err := getInstructionParameter(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
v := getVMFromContext(c.App)
|
|
v.RemoveBreakPoint(n)
|
|
fmt.Fprintf(c.App.Writer, "breakpoint removed at instruction %d\n", n)
|
|
return nil
|
|
}
|
|
|
|
func handleListBreak(c *cli.Context) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
|
|
v := getVMFromContext(c.App)
|
|
for _, bp := range v.Context().BreakPoints() {
|
|
fmt.Fprintf(c.App.Writer, "%d\n", bp)
|
|
}
|
|
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().Slice()
|
|
if len(args) != 1 {
|
|
return 0, fmt.Errorf("%w: <ip>", ErrMissingParameter)
|
|
}
|
|
n, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func handleXStack(c *cli.Context) error {
|
|
v := getVMFromContext(c.App)
|
|
var stackDump string
|
|
switch c.Command.Name {
|
|
case "estack":
|
|
stackDump = v.DumpEStack()
|
|
case "istack":
|
|
stackDump = v.DumpIStack()
|
|
default:
|
|
return errors.New("unknown stack")
|
|
}
|
|
fmt.Fprintln(c.App.Writer, stackDump)
|
|
return nil
|
|
}
|
|
|
|
func handleSlots(c *cli.Context) error {
|
|
v := getVMFromContext(c.App)
|
|
vmCtx := v.Context()
|
|
if vmCtx == nil {
|
|
return errors.New("no program loaded")
|
|
}
|
|
var rawSlot string
|
|
switch c.Command.Name {
|
|
case "sslot":
|
|
rawSlot = dumpSlot(vmCtx.StaticsSlot())
|
|
case "lslot":
|
|
rawSlot = dumpSlot(vmCtx.LocalsSlot())
|
|
case "aslot":
|
|
rawSlot = dumpSlot(vmCtx.ArgumentsSlot())
|
|
default:
|
|
return errors.New("unknown slot")
|
|
}
|
|
fmt.Fprintln(c.App.Writer, rawSlot)
|
|
return nil
|
|
}
|
|
|
|
func dumpSlot(s *vm.Slot) string {
|
|
if s == nil {
|
|
return "[]"
|
|
}
|
|
b, _ := json.MarshalIndent(s, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
// prepareVM retrieves --historic flag from context (if set) and resets app state
|
|
// (to the specified historic height if given).
|
|
func prepareVM(c *cli.Context, tx *transaction.Transaction) error {
|
|
var err error
|
|
if c.IsSet(historicFlagFullName) {
|
|
height := c.Int(historicFlagFullName)
|
|
err = resetState(c.App, tx, uint32(height))
|
|
} else {
|
|
err = resetState(c.App, tx)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c.IsSet(gasFlagFullName) {
|
|
gas := c.Int64(gasFlagFullName)
|
|
v := getVMFromContext(c.App)
|
|
v.GasLimit = gas
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getHashFlag(c *cli.Context) (util.Uint160, error) {
|
|
if !c.IsSet(hashFlagFullName) {
|
|
return util.Uint160{}, nil
|
|
}
|
|
h := c.Generic(hashFlagFullName).(*flags.Address)
|
|
return h.Uint160(), nil
|
|
}
|
|
|
|
func handleLoadNEF(c *cli.Context) error {
|
|
args := c.Args().Slice()
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("%w: <nef> is required", ErrMissingParameter)
|
|
}
|
|
nefFile := args[0]
|
|
var (
|
|
manifestFile string
|
|
signersStartOffset int
|
|
)
|
|
if len(args) == 2 {
|
|
manifestFile = args[1]
|
|
} else if len(args) == 3 {
|
|
if args[1] != cmdargs.CosignersSeparator {
|
|
return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
|
|
}
|
|
signersStartOffset = 2
|
|
} else if len(args) > 3 {
|
|
if args[1] == cmdargs.CosignersSeparator {
|
|
signersStartOffset = 2
|
|
} else {
|
|
manifestFile = args[1]
|
|
if args[2] != cmdargs.CosignersSeparator {
|
|
return fmt.Errorf("%w: `%s` was expected as the third parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[2])
|
|
}
|
|
signersStartOffset = 3
|
|
}
|
|
}
|
|
if len(manifestFile) == 0 {
|
|
manifestFile = strings.TrimSuffix(nefFile, ".nef") + ".manifest.json"
|
|
}
|
|
b, err := os.ReadFile(nefFile)
|
|
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(manifestFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read manifest: %w", err)
|
|
}
|
|
var signers []transaction.Signer
|
|
if signersStartOffset != 0 && len(args) > signersStartOffset {
|
|
signers, err = cmdargs.ParseSigners(args[signersStartOffset:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: failed to parse signers: %w", ErrInvalidParameter, err)
|
|
}
|
|
}
|
|
err = prepareVM(c, createFakeTransaction(nef.Script, signers))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h, err := getHashFlag(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := &state.ContractBase{
|
|
Hash: h,
|
|
NEF: nef,
|
|
Manifest: *m,
|
|
}
|
|
setContractStateInContext(c.App, cs)
|
|
|
|
v := getVMFromContext(c.App)
|
|
fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleLoadBase64(c *cli.Context) error {
|
|
args := c.Args().Slice()
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("%w: <string>", ErrMissingParameter)
|
|
}
|
|
b, err := base64.StdEncoding.DecodeString(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
var signers []transaction.Signer
|
|
if len(args) > 1 {
|
|
if args[1] != cmdargs.CosignersSeparator {
|
|
return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
|
|
}
|
|
if len(args) < 3 {
|
|
return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
|
|
}
|
|
signers, err = cmdargs.ParseSigners(args[2:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
}
|
|
err = prepareVM(c, createFakeTransaction(b, signers))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v := getVMFromContext(c.App)
|
|
fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
func handleLoadHex(c *cli.Context) error {
|
|
args := c.Args().Slice()
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("%w: <string>", ErrMissingParameter)
|
|
}
|
|
b, err := hex.DecodeString(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
var signers []transaction.Signer
|
|
if len(args) > 1 {
|
|
if args[1] != cmdargs.CosignersSeparator {
|
|
return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
|
|
}
|
|
if len(args) < 3 {
|
|
return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
|
|
}
|
|
signers, err = cmdargs.ParseSigners(args[2:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
}
|
|
err = prepareVM(c, createFakeTransaction(b, signers))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v := getVMFromContext(c.App)
|
|
fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleLoadGo(c *cli.Context) error {
|
|
args := c.Args().Slice()
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("%w: <file>", ErrMissingParameter)
|
|
}
|
|
|
|
name := strings.TrimSuffix(args[0], ".go")
|
|
ne, di, err := compiler.CompileWithOptions(args[0], nil, &compiler.Options{Name: name})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to compile: %w", err)
|
|
}
|
|
|
|
// Don't perform checks, just load.
|
|
m, err := di.ConvertToManifest(&compiler.Options{})
|
|
if err != nil {
|
|
return fmt.Errorf("can't create manifest: %w", err)
|
|
}
|
|
var signers []transaction.Signer
|
|
if len(args) > 1 {
|
|
if args[1] != cmdargs.CosignersSeparator {
|
|
return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
|
|
}
|
|
if len(args) < 3 {
|
|
return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
|
|
}
|
|
signers, err = cmdargs.ParseSigners(args[2:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
}
|
|
|
|
err = prepareVM(c, createFakeTransaction(ne.Script, signers))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h, err := getHashFlag(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := &state.ContractBase{
|
|
Hash: h,
|
|
NEF: *ne,
|
|
Manifest: *m,
|
|
}
|
|
setContractStateInContext(c.App, cs)
|
|
|
|
v := getVMFromContext(c.App)
|
|
fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleLoadTx(c *cli.Context) error {
|
|
args := c.Args().Slice()
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("%w: <file-or-hash>", 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)
|
|
if v.GasLimit == -1 {
|
|
v.GasLimit = tx.SystemFee
|
|
}
|
|
fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
changePrompt(c.App)
|
|
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")
|
|
}
|
|
args := c.Args().Slice()
|
|
hashOrID := args[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)
|
|
}
|
|
h, err = native.GetContractScriptHash(ic.DAO, int32(i))
|
|
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)
|
|
}
|
|
|
|
var signers []transaction.Signer
|
|
if len(args) > 1 {
|
|
if args[1] != cmdargs.CosignersSeparator {
|
|
return fmt.Errorf("%w: %s was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
|
|
}
|
|
if len(args) < 3 {
|
|
return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
|
|
}
|
|
signers, err = cmdargs.ParseSigners(args[2:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
}
|
|
err = prepareVM(c, createFakeTransaction(cs.NEF.Script, signers)) // prepare VM one more time for proper IC initialization.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ic = getInteropContextFromContext(c.App) // fetch newly-created IC.
|
|
gasLimit := ic.VM.GasLimit
|
|
ic.ReuseVM(ic.VM) // clear previously loaded program and context.
|
|
ic.VM.GasLimit = gasLimit
|
|
ic.VM.LoadScriptWithHash(cs.NEF.Script, cs.Hash, callflag.All)
|
|
fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", ic.VM.Context().LenInstr())
|
|
setContractStateInContext(c.App, &cs.ContractBase)
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleReset(c *cli.Context) error {
|
|
err := prepareVM(c, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
// finalizeInteropContext calls finalizer for the current interop context.
|
|
func finalizeInteropContext(app *cli.App) {
|
|
ic := getInteropContextFromContext(app)
|
|
ic.Finalize()
|
|
}
|
|
|
|
// resetInteropContext calls finalizer for current interop context and replaces
|
|
// 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)
|
|
var (
|
|
newIc *interop.Context
|
|
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)
|
|
}
|
|
}
|
|
if tx != nil {
|
|
newIc.VM.LoadWithFlags(tx.Script, callflag.All)
|
|
}
|
|
|
|
setInteropContextInContext(app, newIc)
|
|
return nil
|
|
}
|
|
|
|
// resetContractState removes loaded contract state from app context.
|
|
func resetContractState(app *cli.App) {
|
|
setContractStateInContext(app, nil)
|
|
}
|
|
|
|
// 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, tx *transaction.Transaction, height ...uint32) error {
|
|
err := resetInteropContext(app, tx, height...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resetContractState(app)
|
|
return nil
|
|
}
|
|
|
|
func getManifestFromFile(name string) (*manifest.Manifest, error) {
|
|
bs, err := os.ReadFile(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: can't read manifest", ErrInvalidParameter)
|
|
}
|
|
|
|
var m manifest.Manifest
|
|
if err := json.Unmarshal(bs, &m); err != nil {
|
|
return nil, fmt.Errorf("%w: can't unmarshal manifest", ErrInvalidParameter)
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
func handleRun(c *cli.Context) error {
|
|
v := getVMFromContext(c.App)
|
|
cs := getContractStateFromContext(c.App)
|
|
args := c.Args().Slice()
|
|
if len(args) != 0 {
|
|
var (
|
|
params []stackitem.Item
|
|
offset int
|
|
err error
|
|
runCurrent = args[0] != "_"
|
|
hasRet bool
|
|
)
|
|
|
|
_, scParams, err := cmdargs.ParseParams(args[1:], true)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", 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 cs == nil {
|
|
return fmt.Errorf("manifest is not loaded; either use 'run' command to run loaded script from the start or use 'loadgo', 'loadnef' or 'loaddeployed' commands to provide manifest")
|
|
}
|
|
md := cs.Manifest.ABI.GetMethod(args[0], len(params))
|
|
if md == nil {
|
|
return fmt.Errorf("%w: method not found", ErrInvalidParameter)
|
|
}
|
|
hasRet = md.ReturnType != smartcontract.VoidType
|
|
offset = md.Offset
|
|
var initOff = -1
|
|
if initMD := cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil {
|
|
initOff = initMD.Offset
|
|
}
|
|
|
|
// Clear context loaded by 'loadgo', 'loadnef' or 'loaddeployed' to properly handle LoadNEFMethod.
|
|
// At the same time, preserve previously set gas limit and the set of breakpoints.
|
|
ic := getInteropContextFromContext(c.App)
|
|
gasLimit := v.GasLimit
|
|
breaks := v.Context().BreakPoints() // We ensure that there's a context loaded.
|
|
ic.ReuseVM(v)
|
|
v.GasLimit = gasLimit
|
|
v.LoadNEFMethod(&cs.NEF, &cs.Manifest, util.Uint160{}, cs.Hash, callflag.All, hasRet, offset, initOff, nil)
|
|
for _, bp := range breaks {
|
|
v.AddBreakPoint(bp)
|
|
}
|
|
}
|
|
for i := len(params) - 1; i >= 0; i-- {
|
|
v.Estack().PushVal(params[i])
|
|
}
|
|
}
|
|
runVMWithHandling(c)
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
// runVMWithHandling runs VM with handling errors and additional state messages.
|
|
func runVMWithHandling(c *cli.Context) {
|
|
v := getVMFromContext(c.App)
|
|
err := v.Run()
|
|
if err != nil {
|
|
writeErr(c.App.ErrWriter, err)
|
|
}
|
|
|
|
var (
|
|
message string
|
|
dumpNtf bool
|
|
)
|
|
switch {
|
|
case v.HasFailed():
|
|
message = "" // the error will be printed on return
|
|
dumpNtf = true
|
|
case v.HasHalted():
|
|
message = v.DumpEStack()
|
|
dumpNtf = true
|
|
case v.AtBreakpoint():
|
|
ctx := v.Context()
|
|
if ctx.NextIP() < ctx.LenInstr() {
|
|
i, op := ctx.NextInstr()
|
|
message = fmt.Sprintf("at breakpoint %d (%s)", i, op)
|
|
} else {
|
|
message = "execution has finished"
|
|
}
|
|
}
|
|
if dumpNtf {
|
|
var e string
|
|
e, err = dumpEvents(c.App)
|
|
if err == nil && len(e) != 0 {
|
|
if message != "" {
|
|
message += "\n"
|
|
}
|
|
message += "Events:\n" + e
|
|
}
|
|
}
|
|
if message != "" {
|
|
fmt.Fprintln(c.App.Writer, message)
|
|
}
|
|
}
|
|
|
|
func handleCont(c *cli.Context) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
runVMWithHandling(c)
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleStep(c *cli.Context) error {
|
|
var (
|
|
n = 1
|
|
err error
|
|
)
|
|
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
v := getVMFromContext(c.App)
|
|
args := c.Args().Slice()
|
|
if len(args) > 0 {
|
|
n, err = strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
|
|
}
|
|
}
|
|
v.AddBreakPointRel(n)
|
|
runVMWithHandling(c)
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleStepInto(c *cli.Context) error {
|
|
return handleStepType(c, "into")
|
|
}
|
|
|
|
func handleStepOut(c *cli.Context) error {
|
|
return handleStepType(c, "out")
|
|
}
|
|
|
|
func handleStepOver(c *cli.Context) error {
|
|
return handleStepType(c, "over")
|
|
}
|
|
|
|
func handleStepType(c *cli.Context, stepType string) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
v := getVMFromContext(c.App)
|
|
var err error
|
|
switch stepType {
|
|
case "into":
|
|
err = v.StepInto()
|
|
case "out":
|
|
err = v.StepOut()
|
|
case "over":
|
|
err = v.StepOver()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = handleIP(c)
|
|
changePrompt(c.App)
|
|
return nil
|
|
}
|
|
|
|
func handleOps(c *cli.Context) error {
|
|
if !checkVMIsReady(c.App) {
|
|
return nil
|
|
}
|
|
v := getVMFromContext(c.App)
|
|
out := bytes.NewBuffer(nil)
|
|
v.PrintOps(out)
|
|
fmt.Fprintln(c.App.Writer, out.String())
|
|
return nil
|
|
}
|
|
|
|
func changePrompt(app *cli.App) {
|
|
v := getVMFromContext(app)
|
|
l := getReadlineInstanceFromContext(app)
|
|
if v.Ready() && v.Context().NextIP() >= 0 && v.Context().NextIP() < v.Context().LenInstr() {
|
|
l.SetPrompt(fmt.Sprintf("\033[32mNEO-GO-VM %d >\033[0m ", v.Context().NextIP()))
|
|
} else {
|
|
l.SetPrompt("\033[32mNEO-GO-VM >\033[0m ")
|
|
}
|
|
}
|
|
|
|
func handleEvents(c *cli.Context) error {
|
|
e, err := dumpEvents(c.App)
|
|
if err != nil {
|
|
writeErr(c.App.ErrWriter, err)
|
|
return nil
|
|
}
|
|
fmt.Fprintln(c.App.Writer, e)
|
|
return nil
|
|
}
|
|
|
|
func handleEnv(c *cli.Context) error {
|
|
bc := getChainFromContext(c.App)
|
|
cfg := getChainConfigFromContext(c.App)
|
|
ic := getInteropContextFromContext(c.App)
|
|
message := fmt.Sprintf("Chain height: %d\nVM height (may differ from chain height in case of historic call): %d\nNetwork magic: %d\nDB type: %s\n",
|
|
bc.BlockHeight(), ic.BlockHeight(), bc.GetConfig().Magic, cfg.ApplicationConfiguration.DBConfiguration.Type)
|
|
if c.Bool(verboseFlagFullName) {
|
|
cfgBytes, err := json.MarshalIndent(cfg, "", "\t")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal node configuration: %w", err)
|
|
}
|
|
message += "Node config:\n" + string(cfgBytes) + "\n"
|
|
}
|
|
fmt.Fprint(c.App.Writer, message)
|
|
return nil
|
|
}
|
|
|
|
func handleStorage(c *cli.Context) error {
|
|
id, prefix, err := getDumpArgs(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var (
|
|
backwards bool
|
|
seekDepth int
|
|
ic = getInteropContextFromContext(c.App)
|
|
)
|
|
if c.Bool(backwardsFlagFullName) {
|
|
backwards = true
|
|
}
|
|
if c.Bool(diffFlagFullName) {
|
|
seekDepth = 1 // take only upper DAO layer which stores only added or updated items.
|
|
}
|
|
ic.DAO.Seek(id, storage.SeekRange{
|
|
Prefix: prefix,
|
|
Backwards: backwards,
|
|
SearchDepth: seekDepth,
|
|
}, func(k, v []byte) bool {
|
|
fmt.Fprintf(c.App.Writer, "%s: %v\n", hex.EncodeToString(k), hex.EncodeToString(v))
|
|
return true
|
|
})
|
|
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) {
|
|
id, err := getContractID(c)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
var prefix []byte
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
return "", nil
|
|
}
|
|
b, err := json.MarshalIndent(ic.Notifications, "", "\t")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal notifications: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// Run waits for user input from Stdin and executes the passed command.
|
|
func (c *CLI) Run() error {
|
|
if getPrintLogoFromContext(c.shell) {
|
|
printLogo(c.shell.Writer)
|
|
}
|
|
l := getReadlineInstanceFromContext(c.shell)
|
|
for {
|
|
line, err := l.Readline()
|
|
if errors.Is(err, io.EOF) || errors.Is(err, readline.ErrInterrupt) {
|
|
return nil // OK, stop execution.
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read input: %w", err) // Critical error, stop execution.
|
|
}
|
|
|
|
args, err := shellquote.Split(line)
|
|
if err != nil {
|
|
writeErr(c.shell.ErrWriter, fmt.Errorf("failed to parse arguments: %w", err))
|
|
continue // Not a critical error, continue execution.
|
|
}
|
|
|
|
err = c.shell.Run(append([]string{"vm"}, args...))
|
|
if err != nil {
|
|
writeErr(c.shell.ErrWriter, err) // Various command/flags parsing errors and execution errors.
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleParse(c *cli.Context) error {
|
|
res, err := Parse(c.Args().Slice())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(c.App.Writer, res)
|
|
return nil
|
|
}
|
|
|
|
// Parse converts it's argument to other formats.
|
|
func Parse(args []string) (string, error) {
|
|
if len(args) < 1 {
|
|
return "", ErrMissingParameter
|
|
}
|
|
arg := args[0]
|
|
var buf []byte
|
|
if val, err := strconv.ParseInt(arg, 10, 64); err == nil {
|
|
bs := bigint.ToBytes(big.NewInt(val))
|
|
buf = fmt.Appendf(buf, "Integer to Hex\t%s\n", hex.EncodeToString(bs))
|
|
buf = fmt.Appendf(buf, "Integer to Base64\t%s\n", base64.StdEncoding.EncodeToString(bs))
|
|
}
|
|
noX := strings.TrimPrefix(arg, "0x")
|
|
if rawStr, err := hex.DecodeString(noX); err == nil {
|
|
if val, err := util.Uint160DecodeBytesBE(rawStr); err == nil {
|
|
buf = fmt.Appendf(buf, "BE ScriptHash to Address\t%s\n", address.Uint160ToString(val))
|
|
buf = fmt.Appendf(buf, "LE ScriptHash to Address\t%s\n", address.Uint160ToString(val.Reverse()))
|
|
}
|
|
if pub, err := keys.NewPublicKeyFromBytes(rawStr, elliptic.P256()); err == nil {
|
|
sh := pub.GetScriptHash()
|
|
buf = fmt.Appendf(buf, "Public key to BE ScriptHash\t%s\n", sh)
|
|
buf = fmt.Appendf(buf, "Public key to LE ScriptHash\t%s\n", sh.Reverse())
|
|
buf = fmt.Appendf(buf, "Public key to Address\t%s\n", address.Uint160ToString(sh))
|
|
}
|
|
buf = fmt.Appendf(buf, "Hex to String\t%s\n", fmt.Sprintf("%q", string(rawStr)))
|
|
buf = fmt.Appendf(buf, "Hex to Integer\t%s\n", bigint.FromBytes(rawStr))
|
|
var clonedStr = slices.Clone(rawStr)
|
|
slices.Reverse(clonedStr)
|
|
buf = fmt.Appendf(buf, "Swap Endianness\t%s\n", hex.EncodeToString(clonedStr))
|
|
}
|
|
if addr, err := address.StringToUint160(arg); err == nil {
|
|
buf = fmt.Appendf(buf, "Address to BE ScriptHash\t%s\n", addr)
|
|
buf = fmt.Appendf(buf, "Address to LE ScriptHash\t%s\n", addr.Reverse())
|
|
buf = fmt.Appendf(buf, "Address to Base64 (BE)\t%s\n", base64.StdEncoding.EncodeToString(addr.BytesBE()))
|
|
buf = fmt.Appendf(buf, "Address to Base64 (LE)\t%s\n", base64.StdEncoding.EncodeToString(addr.BytesLE()))
|
|
}
|
|
if rawStr, err := base64.StdEncoding.DecodeString(arg); err == nil {
|
|
buf = fmt.Appendf(buf, "Base64 to String\t%s\n", fmt.Sprintf("%q", string(rawStr)))
|
|
buf = fmt.Appendf(buf, "Base64 to BigInteger\t%s\n", bigint.FromBytes(rawStr))
|
|
if u, err := util.Uint160DecodeBytesBE(rawStr); err == nil {
|
|
buf = fmt.Appendf(buf, "Base64 to BE ScriptHash\t%s\n", u.StringBE())
|
|
buf = fmt.Appendf(buf, "Base64 to LE ScriptHash\t%s\n", u.StringLE())
|
|
buf = fmt.Appendf(buf, "Base64 to Address (BE)\t%s\n", address.Uint160ToString(u))
|
|
buf = fmt.Appendf(buf, "Base64 to Address (LE)\t%s\n", address.Uint160ToString(u.Reverse()))
|
|
}
|
|
}
|
|
|
|
buf = fmt.Appendf(buf, "String to Hex\t%s\n", hex.EncodeToString([]byte(arg)))
|
|
buf = fmt.Appendf(buf, "String to Base64\t%s\n", base64.StdEncoding.EncodeToString([]byte(arg)))
|
|
|
|
res := bytes.NewBuffer(nil)
|
|
w := tabwriter.NewWriter(res, 0, 4, 4, '\t', 0)
|
|
if _, err := w.Write(buf); err != nil {
|
|
return "", err
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return "", err
|
|
}
|
|
return res.String(), nil
|
|
}
|
|
|
|
const logo = `
|
|
_ ____________ __________ _ ____ ___
|
|
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /
|
|
/ |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ /
|
|
/ /| / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / / / /
|
|
/_/ |_/_____/\____/ \____/\____/ |___/_/ /_/
|
|
`
|
|
|
|
func printLogo(w io.Writer) {
|
|
fmt.Fprint(w, logo)
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w)
|
|
}
|
|
|
|
func writeErr(w io.Writer, err error) {
|
|
fmt.Fprintf(w, "Error: %s\n", err)
|
|
}
|