Merge pull request #2723 from nspcc-dev/smart-vm-cli

vm: make VM CLI state-dependant
This commit is contained in:
Roman Khimov 2022-10-07 21:21:11 +07:00 committed by GitHub
commit 1426b54fd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1278 additions and 353 deletions

View file

@ -0,0 +1,28 @@
package options
import "go.uber.org/zap/zapcore"
// FilteringCore is custom implementation of zapcore.Core that allows to filter
// log entries using custom filtering function.
type FilteringCore struct {
zapcore.Core
filter FilterFunc
}
// FilterFunc is the filter function that is called to check whether the given
// entry together with the associated fields is to be written to a core or not.
type FilterFunc func(zapcore.Entry) bool
// NewFilteringCore returns a core middleware that uses the given filter function
// to decide whether to log this message or not.
func NewFilteringCore(next zapcore.Core, filter FilterFunc) zapcore.Core {
return &FilteringCore{next, filter}
}
// Check implements zapcore.Core interface and performs log entries filtering.
func (c *FilteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.filter(e) {
return c.Core.Check(e, ce)
}
return ce
}

View file

@ -6,15 +6,23 @@ package options
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"runtime"
"strconv"
"time"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// DefaultTimeout is the default timeout used for RPC requests.
@ -52,6 +60,18 @@ var Historic = cli.StringFlag{
Usage: "Use historic state (height, block hash or state root hash)",
}
// Config is a flag for commands that use node configuration.
var Config = cli.StringFlag{
Name: "config-path",
Usage: "path to directory with configuration files",
}
// Debug is a flag for commands that allow node in debug mode usage.
var Debug = cli.BoolFlag{
Name: "debug, d",
Usage: "enable debug logging (LOTS of output)",
}
var errNoEndpoint = errors.New("no RPC endpoint specified, use option '--" + RPCEndpointFlag + "' or '-r'")
var errInvalidHistoric = errors.New("invalid 'historic' parameter, neither a block number, nor a block/state hash")
@ -128,3 +148,95 @@ func GetRPCWithInvoker(gctx context.Context, ctx *cli.Context, signers []transac
}
return c, inv, err
}
// GetConfigFromContext looks at the path and the mode flags in the given config and
// returns an appropriate config.
func GetConfigFromContext(ctx *cli.Context) (config.Config, error) {
configPath := "./config"
if argCp := ctx.String("config-path"); argCp != "" {
configPath = argCp
}
return config.Load(configPath, GetNetwork(ctx))
}
var (
// _winfileSinkRegistered denotes whether zap has registered
// user-supplied factory for all sinks with `winfile`-prefixed scheme.
_winfileSinkRegistered bool
_winfileSinkCloser func() error
)
// HandleLoggingParams reads logging parameters.
// If a user selected debug level -- function enables it.
// If logPath is configured -- function creates a dir and a file for logging.
// If logPath is configured on Windows -- function returns closer to be
// able to close sink for the opened log output file.
func HandleLoggingParams(debug bool, cfg config.ApplicationConfiguration) (*zap.Logger, func() error, error) {
level := zapcore.InfoLevel
if debug {
level = zapcore.DebugLevel
}
cc := zap.NewProductionConfig()
cc.DisableCaller = true
cc.DisableStacktrace = true
cc.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
cc.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
cc.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
cc.Encoding = "console"
cc.Level = zap.NewAtomicLevelAt(level)
cc.Sampling = nil
if logPath := cfg.LogPath; logPath != "" {
if err := io.MakeDirForFile(logPath, "logger"); err != nil {
return nil, nil, err
}
if runtime.GOOS == "windows" {
if !_winfileSinkRegistered {
// See https://github.com/uber-go/zap/issues/621.
err := zap.RegisterSink("winfile", func(u *url.URL) (zap.Sink, error) {
if u.User != nil {
return nil, fmt.Errorf("user and password not allowed with file URLs: got %v", u)
}
if u.Fragment != "" {
return nil, fmt.Errorf("fragments not allowed with file URLs: got %v", u)
}
if u.RawQuery != "" {
return nil, fmt.Errorf("query parameters not allowed with file URLs: got %v", u)
}
// Error messages are better if we check hostname and port separately.
if u.Port() != "" {
return nil, fmt.Errorf("ports not allowed with file URLs: got %v", u)
}
if hn := u.Hostname(); hn != "" && hn != "localhost" {
return nil, fmt.Errorf("file URLs must leave host empty or use localhost: got %v", u)
}
switch u.Path {
case "stdout":
return os.Stdout, nil
case "stderr":
return os.Stderr, nil
}
f, err := os.OpenFile(u.Path[1:], // Remove leading slash left after url.Parse.
os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
_winfileSinkCloser = func() error {
_winfileSinkCloser = nil
return f.Close()
}
return f, err
})
if err != nil {
return nil, nil, fmt.Errorf("failed to register windows-specific sinc: %w", err)
}
_winfileSinkRegistered = true
}
logPath = "winfile:///" + logPath
}
cc.OutputPaths = []string{logPath}
}
log, err := cc.Build()
return log, _winfileSinkCloser, err
}

View file

@ -8,6 +8,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/testcli"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
@ -19,7 +20,7 @@ func TestDBRestoreDump(t *testing.T) {
chainPath := filepath.Join(tmpDir, "neogotestchain")
cfg, err := config.LoadFile(filepath.Join("..", "..", "config", "protocol.unit_testnet.yml"))
require.NoError(t, err, "could not load config")
cfg.ApplicationConfiguration.DBConfiguration.Type = "leveldb"
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath
return cfg
}

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/cli/server"
"github.com/nspcc-dev/neo-go/internal/testcli"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
@ -25,7 +26,7 @@ func TestServerStart(t *testing.T) {
saveCfg := func(t *testing.T, f func(cfg *config.Config)) string {
cfg := *ptr
chainPath := filepath.Join(t.TempDir(), "neogotestchain")
cfg.ApplicationConfiguration.DBConfiguration.Type = "leveldb"
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath
f(&cfg)
out, err := yaml.Marshal(cfg)

View file

@ -4,10 +4,8 @@ import (
"context"
"errors"
"fmt"
"net/url"
"os"
"os/signal"
"runtime"
"syscall"
"time"
@ -31,25 +29,15 @@ import (
"github.com/nspcc-dev/neo-go/pkg/services/stateroot"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
// _winfileSinkRegistered denotes whether zap has registered
// user-supplied factory for all sinks with `winfile`-prefixed scheme.
_winfileSinkRegistered bool
_winfileSinkCloser func() error
)
// NewCommands returns 'node' command.
func NewCommands() []cli.Command {
var cfgFlags = []cli.Flag{
cli.StringFlag{Name: "config-path", Usage: "path to directory with configuration files"},
}
cfgFlags := []cli.Flag{options.Config}
cfgFlags = append(cfgFlags, options.Network...)
var cfgWithCountFlags = make([]cli.Flag, len(cfgFlags))
copy(cfgWithCountFlags, cfgFlags)
cfgFlags = append(cfgFlags, cli.BoolFlag{Name: "debug, d", Usage: "enable debug logging (LOTS of output)"})
cfgFlags = append(cfgFlags, options.Debug)
cfgWithCountFlags = append(cfgWithCountFlags,
cli.UintFlag{
@ -128,91 +116,6 @@ func newGraceContext() context.Context {
return ctx
}
// getConfigFromContext looks at the path and the mode flags in the given config and
// returns an appropriate config.
func getConfigFromContext(ctx *cli.Context) (config.Config, error) {
configPath := "./config"
if argCp := ctx.String("config-path"); argCp != "" {
configPath = argCp
}
return config.Load(configPath, options.GetNetwork(ctx))
}
// handleLoggingParams reads logging parameters.
// If a user selected debug level -- function enables it.
// If logPath is configured -- function creates a dir and a file for logging.
// If logPath is configured on Windows -- function returns closer to be
// able to close sink for the opened log output file.
func handleLoggingParams(ctx *cli.Context, cfg config.ApplicationConfiguration) (*zap.Logger, func() error, error) {
level := zapcore.InfoLevel
if ctx.Bool("debug") {
level = zapcore.DebugLevel
}
cc := zap.NewProductionConfig()
cc.DisableCaller = true
cc.DisableStacktrace = true
cc.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
cc.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
cc.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
cc.Encoding = "console"
cc.Level = zap.NewAtomicLevelAt(level)
cc.Sampling = nil
if logPath := cfg.LogPath; logPath != "" {
if err := io.MakeDirForFile(logPath, "logger"); err != nil {
return nil, nil, err
}
if runtime.GOOS == "windows" {
if !_winfileSinkRegistered {
// See https://github.com/uber-go/zap/issues/621.
err := zap.RegisterSink("winfile", func(u *url.URL) (zap.Sink, error) {
if u.User != nil {
return nil, fmt.Errorf("user and password not allowed with file URLs: got %v", u)
}
if u.Fragment != "" {
return nil, fmt.Errorf("fragments not allowed with file URLs: got %v", u)
}
if u.RawQuery != "" {
return nil, fmt.Errorf("query parameters not allowed with file URLs: got %v", u)
}
// Error messages are better if we check hostname and port separately.
if u.Port() != "" {
return nil, fmt.Errorf("ports not allowed with file URLs: got %v", u)
}
if hn := u.Hostname(); hn != "" && hn != "localhost" {
return nil, fmt.Errorf("file URLs must leave host empty or use localhost: got %v", u)
}
switch u.Path {
case "stdout":
return os.Stdout, nil
case "stderr":
return os.Stderr, nil
}
f, err := os.OpenFile(u.Path[1:], // Remove leading slash left after url.Parse.
os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
_winfileSinkCloser = func() error {
_winfileSinkCloser = nil
return f.Close()
}
return f, err
})
if err != nil {
return nil, nil, fmt.Errorf("failed to register windows-specific sinc: %w", err)
}
_winfileSinkRegistered = true
}
logPath = "winfile:///" + logPath
}
cc.OutputPaths = []string{logPath}
}
log, err := cc.Build()
return log, _winfileSinkCloser, err
}
func initBCWithMetrics(cfg config.Config, log *zap.Logger) (*core.Blockchain, *metrics.Service, *metrics.Service, error) {
chain, err := initBlockChain(cfg, log)
if err != nil {
@ -233,11 +136,11 @@ func dumpDB(ctx *cli.Context) error {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
cfg, err := getConfigFromContext(ctx)
cfg, err := options.GetConfigFromContext(ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
log, logCloser, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration)
log, logCloser, err := options.HandleLoggingParams(ctx.Bool("debug"), cfg.ApplicationConfiguration)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -286,11 +189,11 @@ func restoreDB(ctx *cli.Context) error {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
cfg, err := getConfigFromContext(ctx)
cfg, err := options.GetConfigFromContext(ctx)
if err != nil {
return err
}
log, logCloser, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration)
log, logCloser, err := options.HandleLoggingParams(ctx.Bool("debug"), cfg.ApplicationConfiguration)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -472,11 +375,11 @@ func startServer(ctx *cli.Context) error {
return err
}
cfg, err := getConfigFromContext(ctx)
cfg, err := options.GetConfigFromContext(ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
log, logCloser, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration)
log, logCloser, err := options.HandleLoggingParams(ctx.Bool("debug"), cfg.ApplicationConfiguration)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -549,7 +452,7 @@ Main:
cancel()
case sig := <-sigCh:
log.Info("signal received", zap.Stringer("name", sig))
cfgnew, err := getConfigFromContext(ctx)
cfgnew, err := options.GetConfigFromContext(ctx)
if err != nil {
log.Warn("can't reread the config file, signal ignored", zap.Error(err))
break // Continue working.

View file

@ -7,12 +7,14 @@ import (
"path/filepath"
"testing"
"go.uber.org/zap/zapcore"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
@ -32,7 +34,7 @@ func TestGetConfigFromContext(t *testing.T) {
set.String("config-path", "../../config", "")
set.Bool("testnet", true, "")
ctx := cli.NewContext(cli.NewApp(), set, nil)
cfg, err := getConfigFromContext(ctx)
cfg, err := options.GetConfigFromContext(ctx)
require.NoError(t, err)
require.Equal(t, netmode.TestNet, cfg.ProtocolConfiguration.Magic)
}
@ -44,49 +46,42 @@ func TestHandleLoggingParams(t *testing.T) {
t.Run("logdir is a file", func(t *testing.T) {
logfile := filepath.Join(d, "logdir")
require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm))
set := flag.NewFlagSet("flagSet", flag.ExitOnError)
ctx := cli.NewContext(cli.NewApp(), set, nil)
cfg := config.ApplicationConfiguration{
LogPath: filepath.Join(logfile, "file.log"),
}
_, closer, err := handleLoggingParams(ctx, cfg)
_, closer, err := options.HandleLoggingParams(false, cfg)
require.Error(t, err)
require.Nil(t, closer)
})
t.Run("default", func(t *testing.T) {
set := flag.NewFlagSet("flagSet", flag.ExitOnError)
ctx := cli.NewContext(cli.NewApp(), set, nil)
cfg := config.ApplicationConfiguration{
LogPath: testLog,
}
logger, closer, err := handleLoggingParams(ctx, cfg)
logger, closer, err := options.HandleLoggingParams(false, cfg)
require.NoError(t, err)
t.Cleanup(func() {
if closer != nil {
require.NoError(t, closer())
}
})
require.True(t, logger.Core().Enabled(zap.InfoLevel))
require.False(t, logger.Core().Enabled(zap.DebugLevel))
require.True(t, logger.Core().Enabled(zapcore.InfoLevel))
require.False(t, logger.Core().Enabled(zapcore.DebugLevel))
})
t.Run("debug", func(t *testing.T) {
set := flag.NewFlagSet("flagSet", flag.ExitOnError)
set.Bool("debug", true, "")
ctx := cli.NewContext(cli.NewApp(), set, nil)
cfg := config.ApplicationConfiguration{
LogPath: testLog,
}
logger, closer, err := handleLoggingParams(ctx, cfg)
logger, closer, err := options.HandleLoggingParams(true, cfg)
require.NoError(t, err)
t.Cleanup(func() {
if closer != nil {
require.NoError(t, closer())
}
})
require.True(t, logger.Core().Enabled(zap.InfoLevel))
require.True(t, logger.Core().Enabled(zap.DebugLevel))
require.True(t, logger.Core().Enabled(zapcore.InfoLevel))
require.True(t, logger.Core().Enabled(zapcore.DebugLevel))
})
}
@ -101,9 +96,9 @@ func TestInitBCWithMetrics(t *testing.T) {
set.Bool("testnet", true, "")
set.Bool("debug", true, "")
ctx := cli.NewContext(cli.NewApp(), set, nil)
cfg, err := getConfigFromContext(ctx)
cfg, err := options.GetConfigFromContext(ctx)
require.NoError(t, err)
logger, closer, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration)
logger, closer, err := options.HandleLoggingParams(true, cfg.ApplicationConfiguration)
require.NoError(t, err)
t.Cleanup(func() {
if closer != nil {
@ -349,7 +344,7 @@ func TestInitBlockChain(t *testing.T) {
_, err := initBlockChain(config.Config{
ApplicationConfiguration: config.ApplicationConfiguration{
DBConfiguration: dbconfig.DBConfiguration{
Type: "inmemory",
Type: dbconfig.InMemoryDB,
},
},
}, nil)

View file

@ -4,7 +4,7 @@ import (
"fmt"
"github.com/nspcc-dev/neo-go/cli/options"
vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli"
vmcli "github.com/nspcc-dev/neo-go/cli/vm"
"github.com/urfave/cli"
)

View file

@ -1,9 +1,10 @@
package cli
package vm
import (
"bytes"
"crypto/elliptic"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
@ -17,22 +18,34 @@ import (
"github.com/chzyer/readline"
"github.com/kballard/go-shellquote"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options"
"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/storage"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"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/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/util/slice"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
vmKey = "vm"
chainKey = "chain"
chainCfgKey = "chainCfg"
icKey = "ic"
manifestKey = "manifest"
exitFuncKey = "exitFunc"
readlineInstanceKey = "readlineKey"
@ -44,6 +57,20 @@ const (
stringType = "string"
)
// Various flag names.
const (
verboseFlagFullName = "verbose"
historicFlagFullName = "historic"
backwardsFlagFullName = "backwards"
diffFlagFullName = "diff"
)
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.",
}
var commands = []cli.Command{
{
Name: "exit",
@ -100,7 +127,8 @@ var commands = []cli.Command{
Name: "loadnef",
Usage: "Load a NEF-consistent script into the VM",
UsageText: `loadnef <file> <manifest>`,
Description: `loadnef <file> <manifest>
Flags: []cli.Flag{historicFlag},
Description: `loadnef [--historic <height>] <file> <manifest>
both parameters are mandatory, example:
> loadnef /path/to/script.nef /path/to/manifest.json`,
Action: handleLoadNEF,
@ -108,8 +136,9 @@ both parameters are mandatory, example:
{
Name: "loadbase64",
Usage: "Load a base64-encoded script string into the VM",
UsageText: `loadbase64 <string>`,
Description: `loadbase64 <string>
UsageText: `loadbase64 [--historic <height>] <string>`,
Flags: []cli.Flag{historicFlag},
Description: `loadbase64 [--historic <height>] <string>
<string> is mandatory parameter, example:
> loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`,
@ -118,8 +147,9 @@ both parameters are mandatory, example:
{
Name: "loadhex",
Usage: "Load a hex-encoded script string into the VM",
UsageText: `loadhex <string>`,
Description: `loadhex <string>
UsageText: `loadhex [--historic <height>] <string>`,
Flags: []cli.Flag{historicFlag},
Description: `loadhex [--historic <height>] <string>
<string> is mandatory parameter, example:
> loadhex 0c0c48656c6c6f20776f726c6421`,
@ -128,8 +158,9 @@ both parameters are mandatory, example:
{
Name: "loadgo",
Usage: "Compile and load a Go file with the manifest into the VM",
UsageText: `loadgo <file>`,
Description: `loadgo <file>
UsageText: `loadgo [--historic <height>] <file>`,
Flags: []cli.Flag{historicFlag},
Description: `loadgo [--historic <height>] <file>
<file> is mandatory parameter, example:
> loadgo /path/to/file.go`,
@ -137,7 +168,8 @@ both parameters are mandatory, example:
},
{
Name: "reset",
Usage: "Unload compiled script from the VM",
Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state",
Flags: []cli.Flag{historicFlag},
Action: handleReset,
},
{
@ -218,6 +250,84 @@ example:
Description: "Dump opcodes of the current loaded program",
Action: handleOps,
},
{
Name: "events",
Usage: "Dump events emitted by the current loaded program",
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 + ",v",
Usage: "Print the whole blockchain node configuration.",
},
},
Description: `env [-v]
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. " +
"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.",
UsageText: `storage <hash-or-address-or-id> [<prefix>] [--backwards] [--diff]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: backwardsFlagFullName + ",b",
Usage: "Backwards traversal direction",
},
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, use 'changes' command for that.",
},
},
Description: `storage <hash-or-address-or-id> <prefix> [--backwards] [--diff]
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. " +
"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
@ -243,28 +353,22 @@ var (
ErrInvalidParameter = errors.New("can't parse argument")
)
// VMCLI object for interacting with the VM.
type VMCLI struct {
vm *vm.VM
// CLI object for interacting with the VM.
type CLI struct {
chain *core.Blockchain
shell *cli.App
}
// New returns a new VMCLI object.
func New() *VMCLI {
return NewWithConfig(true, os.Exit, &readline.Config{
Prompt: "\033[32mNEO-GO-VM >\033[0m ", // green prompt ^^
})
}
// NewWithConfig returns new VMCLI instance using provided config.
func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VMCLI {
// 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 {
panic(err)
return nil, fmt.Errorf("failed to create readline instance: %w", err)
}
ctl := cli.NewApp()
ctl.Name = "VM CLI"
@ -284,20 +388,59 @@ func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VM
ctl.Commands = commands
vmcli := VMCLI{
vm: vm.New(),
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.NewExitError(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.ProtocolConfiguration, fLog)
if err != nil {
return nil, cli.NewExitError(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.NewExitError(fmt.Errorf("failed to create test VM: %w", err), 1)
}
vmcli := CLI{
chain: chain,
shell: ctl,
}
vmcli.shell.Metadata = map[string]interface{}{
vmKey: vmcli.vm,
chainKey: chain,
chainCfgKey: cfg,
icKey: ic,
manifestKey: new(manifest.Manifest),
exitFuncKey: onExit,
exitFuncKey: exitF,
readlineInstanceKey: l,
printLogoKey: printLogotype,
}
changePrompt(vmcli.shell)
return &vmcli
return &vmcli, nil
}
func getExitFuncFromContext(app *cli.App) func(int) {
@ -309,12 +452,19 @@ func getReadlineInstanceFromContext(app *cli.App) *readline.Instance {
}
func getVMFromContext(app *cli.App) *vm.VM {
return app.Metadata[vmKey].(*vm.VM)
return getInteropContextFromContext(app).VM
}
func setVMInContext(app *cli.App, v *vm.VM) {
old := getVMFromContext(app)
*old = *v
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 getManifestFromContext(app *cli.App) *manifest.Manifest {
@ -325,9 +475,12 @@ func getPrintLogoFromContext(app *cli.App) bool {
return app.Metadata[printLogoKey].(bool)
}
func setInteropContextInContext(app *cli.App, ic *interop.Context) {
app.Metadata[icKey] = ic
}
func setManifestInContext(app *cli.App, m *manifest.Manifest) {
old := getManifestFromContext(app)
*old = *m
app.Metadata[manifestKey] = m
}
func checkVMIsReady(app *cli.App) bool {
@ -340,6 +493,7 @@ func checkVMIsReady(app *cli.App) bool {
}
func handleExit(c *cli.Context) error {
finalizeInteropContext(c.App)
l := getReadlineInstanceFromContext(c.App)
_ = l.Close()
exit := getExitFuncFromContext(c.App)
@ -418,7 +572,22 @@ func handleSlots(c *cli.Context) error {
return nil
}
// prepareVM retrieves --historic flag from context (if set) and resets app state
// (to the specified historic height if given).
func prepareVM(c *cli.Context) error {
if c.IsSet(historicFlagFullName) {
height := c.Int(historicFlagFullName)
return resetState(c.App, uint32(height))
}
return resetState(c.App)
}
func handleLoadNEF(c *cli.Context) error {
err := prepareVM(c)
if err != nil {
return err
}
v := getVMFromContext(c.App)
args := c.Args()
if len(args) < 2 {
@ -438,6 +607,10 @@ func handleLoadNEF(c *cli.Context) error {
}
func handleLoadBase64(c *cli.Context) error {
err := prepareVM(c)
if err != nil {
return err
}
v := getVMFromContext(c.App)
args := c.Args()
if len(args) < 1 {
@ -454,6 +627,10 @@ func handleLoadBase64(c *cli.Context) error {
}
func handleLoadHex(c *cli.Context) error {
err := prepareVM(c)
if err != nil {
return err
}
v := getVMFromContext(c.App)
args := c.Args()
if len(args) < 1 {
@ -470,6 +647,10 @@ func handleLoadHex(c *cli.Context) error {
}
func handleLoadGo(c *cli.Context) error {
err := prepareVM(c)
if err != nil {
return err
}
v := getVMFromContext(c.App)
args := c.Args()
if len(args) < 1 {
@ -496,11 +677,61 @@ func handleLoadGo(c *cli.Context) error {
}
func handleReset(c *cli.Context) error {
setVMInContext(c.App, vm.New())
err := prepareVM(c)
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.
func resetInteropContext(app *cli.App, height ...uint32) error {
finalizeInteropContext(app)
bc := getChainFromContext(app)
var (
newIc *interop.Context
err error
)
if len(height) != 0 {
newIc, err = bc.GetTestHistoricVM(trigger.Application, nil, height[0]+1)
if err != nil {
return fmt.Errorf("failed to create historic VM for height %d: %w", height[0], err)
}
} else {
newIc, err = bc.GetTestVM(trigger.Application, nil, nil)
if err != nil {
return fmt.Errorf("failed to create VM: %w", err)
}
}
setInteropContextInContext(app, newIc)
return nil
}
// resetManifest removes manifest from app context.
func resetManifest(app *cli.App) {
setManifestInContext(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, height ...uint32) error {
err := resetInteropContext(app, height...)
if err != nil {
return err
}
resetManifest(app)
return nil
}
func getManifestFromFile(name string) (*manifest.Manifest, error) {
bs, err := os.ReadFile(name)
if err != nil {
@ -531,6 +762,9 @@ func handleRun(c *cli.Context) error {
return err
}
if runCurrent {
if m == nil {
return fmt.Errorf("manifest is not loaded; either use 'run' command to run loaded script from the start or use 'loadgo' and 'loadnef' commands to provide manifest")
}
md := m.ABI.GetMethod(args[0], len(params))
if md == nil {
return fmt.Errorf("%w: method not found", ErrInvalidParameter)
@ -563,12 +797,17 @@ func runVMWithHandling(c *cli.Context) {
writeErr(c.App.ErrWriter, err)
}
var message string
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() {
@ -578,6 +817,16 @@ func runVMWithHandling(c *cli.Context) {
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)
}
@ -670,8 +919,147 @@ func changePrompt(app *cli.App) {
}
}
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) {
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.ParseInt(hashOrID, 10, 32)
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 {
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 *VMCLI) Run() error {
func (c *CLI) Run() error {
if getPrintLogoFromContext(c.shell) {
printLogo(c.shell.Writer)
}

View file

@ -1,4 +1,4 @@
package cli
package vm
import (
"bytes"
@ -15,12 +15,20 @@ import (
"time"
"github.com/chzyer/readline"
"github.com/nspcc-dev/neo-go/internal/basicchain"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"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"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
@ -54,7 +62,7 @@ func (r *readCloser) WriteString(s string) {
type executor struct {
in *readCloser
out *bytes.Buffer
cli *VMCLI
cli *CLI
ch chan struct{}
exit atomic.Bool
}
@ -64,12 +72,27 @@ func newTestVMCLI(t *testing.T) *executor {
}
func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor {
return newTestVMCLIWithLogoAndCustomConfig(t, printLogo, nil)
}
func newTestVMCLIWithLogoAndCustomConfig(t *testing.T, printLogo bool, cfg *config.Config) *executor {
e := &executor{
in: &readCloser{Buffer: *bytes.NewBuffer(nil)},
out: bytes.NewBuffer(nil),
ch: make(chan struct{}),
}
e.cli = NewWithConfig(printLogo,
var c config.Config
if cfg == nil {
configPath := "../../config/protocol.unit_testnet.single.yml"
var err error
c, err = config.LoadFile(configPath)
require.NoError(t, err, "could not load chain config")
c.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB
} else {
c = *cfg
}
var err error
e.cli, err = NewWithConfig(printLogo,
func(int) { e.exit.Store(true) },
&readline.Config{
Prompt: "",
@ -79,10 +102,40 @@ func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor {
FuncIsTerminal: func() bool {
return false
},
})
}, c)
require.NoError(t, err)
return e
}
func newTestVMClIWithState(t *testing.T) *executor {
// Firstly create a DB with chain, save and close it.
path := t.TempDir()
opts := dbconfig.LevelDBOptions{
DataDirectoryPath: path,
}
store, err := storage.NewLevelDBStore(opts)
require.NoError(t, err)
customConfig := func(c *config.ProtocolConfiguration) {
c.StateRootInHeader = true // Need for P2PStateExchangeExtensions check.
c.P2PSigExtensions = true // Need for basic chain initializer.
}
bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, store)
require.NoError(t, err)
go bc.Run()
e := neotest.NewExecutor(t, bc, validators, committee)
basicchain.InitSimple(t, "../../", e)
bc.Close()
// After that create CLI backed by created chain.
configPath := "../../config/protocol.unit_testnet.yml"
cfg, err := config.LoadFile(configPath)
require.NoError(t, err)
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions = opts
cfg.ProtocolConfiguration.StateRootInHeader = true
return newTestVMCLIWithLogoAndCustomConfig(t, false, &cfg)
}
func (e *executor) runProg(t *testing.T, commands ...string) {
e.runProgWithTimeout(t, 4*time.Second, commands...)
}
@ -107,6 +160,12 @@ func (e *executor) checkNextLine(t *testing.T, expected string) {
require.Regexp(t, expected, line)
}
func (e *executor) checkNextLineExact(t *testing.T, expected string) {
line, err := e.out.ReadString('\n')
require.NoError(t, err)
require.Equal(t, expected, line)
}
func (e *executor) checkError(t *testing.T, expectedErr error) {
line, err := e.out.ReadString('\n')
require.NoError(t, err)
@ -139,6 +198,50 @@ func (e *executor) checkStack(t *testing.T, items ...interface{}) {
require.NoError(t, err)
}
func (e *executor) checkEvents(t *testing.T, isKeywordExpected bool, events ...state.NotificationEvent) {
if isKeywordExpected {
e.checkNextLine(t, "Events:")
}
d := json.NewDecoder(e.out)
var actual interface{}
require.NoError(t, d.Decode(&actual))
rawActual, err := json.Marshal(actual)
require.NoError(t, err)
rawExpected, err := json.Marshal(events)
require.NoError(t, err)
require.JSONEq(t, string(rawExpected), string(rawActual))
// Decoder has it's own buffer, we need to return unread part to the output.
outRemain := e.out.String()
e.out.Reset()
_, err = gio.Copy(e.out, d.Buffered())
require.NoError(t, err)
e.out.WriteString(outRemain)
_, err = e.out.ReadString('\n')
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)))
}
}
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{}
@ -251,7 +354,7 @@ go 1.17`)
require (
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0
)
replace github.com/nspcc-dev/neo-go/pkg/interop => ` + filepath.Join(wd, "../../interop") + `
replace github.com/nspcc-dev/neo-go/pkg/interop => ` + filepath.Join(wd, "../../pkg/interop") + `
go 1.17`)
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm))
@ -662,3 +765,216 @@ func TestReset(t *testing.T) {
e.checkNextLine(t, "")
e.checkError(t, fmt.Errorf("VM is not ready: no program loaded"))
}
func TestRunWithState(t *testing.T) {
e := newTestVMClIWithState(t)
// Ensure that state is properly loaded and on-chain contract can be called.
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, 3)
e.runProg(t,
"loadhex "+hex.EncodeToString(script.Bytes()),
"run")
e.checkNextLine(t, "READY: loaded 37 instructions")
e.checkStack(t, 3)
}
func TestRunWithHistoricState(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, "get", callflag.All, 1)
b := script.Bytes()
e.runProg(t,
"loadhex "+hex.EncodeToString(b), // normal invocation
"run",
"loadhex --historic 3 "+hex.EncodeToString(b), // historic invocation, old value should be retrieved
"run",
"loadhex --historic 0 "+hex.EncodeToString(b), // historic invocation, contract is not deployed yet
"run",
)
e.checkNextLine(t, "READY: loaded 36 instructions")
e.checkStack(t, []byte{2})
e.checkNextLine(t, "READY: loaded 36 instructions")
e.checkStack(t, []byte{1})
e.checkNextLine(t, "READY: loaded 36 instructions")
e.checkNextLineExact(t, "Error: at instruction 31 (SYSCALL): failed to invoke syscall 1381727586: called contract a00e3c2643a08a452d8b0bdd31849ae11a17c445 not found: key not found\n")
}
func TestEvents(t *testing.T) {
e := newTestVMClIWithState(t)
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(2) // examples/runtime/runtime.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "notify", callflag.All, []interface{}{true, 5})
e.runProg(t,
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"events")
expectedEvent := state.NotificationEvent{
ScriptHash: h,
Name: "Event",
Item: stackitem.NewArray([]stackitem.Item{
stackitem.NewArray([]stackitem.Item{
stackitem.Make(true),
stackitem.Make(5),
}),
}),
}
e.checkNextLine(t, "READY: loaded 44 instructions")
e.checkStack(t, stackitem.Null{})
e.checkEvents(t, true, expectedEvent) // automatically printed after `run` command
e.checkEvents(t, false, expectedEvent) // printed after `events` command
}
func TestEnv(t *testing.T) {
t.Run("default setup", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t, "env")
e.checkNextLine(t, "Chain height: 0")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 0\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: inmemory")
})
t.Run("setup with state", func(t *testing.T) {
e := newTestVMClIWithState(t)
e.runProg(t, "env")
e.checkNextLine(t, "Chain height: 5")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 5\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: leveldb")
})
t.Run("setup with historic state", func(t *testing.T) {
e := newTestVMClIWithState(t)
e.runProg(t, "loadbase64 --historic 3 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.PUSH1)}),
"env")
e.checkNextLine(t, "READY: loaded 1 instructions")
e.checkNextLine(t, "Chain height: 5")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 3\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: leveldb")
})
t.Run("verbose", func(t *testing.T) {
e := newTestVMClIWithState(t)
e.runProg(t, "env -v")
e.checkNextLine(t, "Chain height: 5")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 5\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: leveldb")
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])
}
func TestDumpStorageDiff(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, 3)
expected := []storage.KeyValue{
{Key: []byte{1}, Value: []byte{2}},
{Key: []byte{2}, Value: []byte{2}},
}
diff := storage.KeyValue{Key: []byte{3}, Value: []byte{3}}
e.runProg(t,
"storage 1",
"storage 1 --diff",
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"storage 1",
"storage 1 --diff",
)
e.checkStorage(t, expected...)
// no script is executed => no diff
e.checkNextLine(t, "READY: loaded 37 instructions")
e.checkStack(t, 3)
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])
}

View file

@ -1,23 +1,25 @@
package vm
import (
"fmt"
"os"
"github.com/chzyer/readline"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/urfave/cli"
)
// NewCommands returns 'vm' command.
func NewCommands() []cli.Command {
cfgFlags := []cli.Flag{options.Config}
cfgFlags = append(cfgFlags, options.Network...)
return []cli.Command{{
Name: "vm",
Usage: "start the virtual machine",
Action: startVMPrompt,
Flags: []cli.Flag{
cli.BoolFlag{Name: "debug, d"},
},
Flags: cfgFlags,
}}
}
@ -25,6 +27,22 @@ func startVMPrompt(ctx *cli.Context) error {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
p := vmcli.NewWithConfig(true, os.Exit, &readline.Config{})
cfg, err := options.GetConfigFromContext(ctx)
if err != nil {
return cli.NewExitError(err, 1)
}
if ctx.NumFlags() == 0 {
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB
}
if cfg.ApplicationConfiguration.DBConfiguration.Type != dbconfig.InMemoryDB {
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.ReadOnly = true
cfg.ApplicationConfiguration.DBConfiguration.BoltDBOptions.ReadOnly = true
}
p, err := NewWithConfig(true, os.Exit, &readline.Config{}, cfg)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create VM CLI: %w", err), 1)
}
return p.Run()
}

View file

@ -47,14 +47,20 @@ DBConfiguration:
Type: leveldb
LevelDBOptions:
DataDirectoryPath: /chains/privnet
ReadOnly: false
BoltDBOptions:
FilePath: ./chains/privnet.bolt
ReadOnly: false
```
where:
- `Type` is the database type (string value). Supported types: `levelDB` and
`boltDB`.
- `LevelDBOptions` are settings for LevelDB.
- `BoltDBOptions` configures BoltDB.
- `Type` is the database type (string value). Supported types: `leveldb`, `boltdb` and
`inmemory` (not recommended for production usage).
- `LevelDBOptions` are settings for LevelDB. Includes the DB files path and ReadOnly mode toggle.
If ReadOnly mode is on, then an error will be returned on attempt to connect to unexisting or empty
database. Database doesn't allow changes in this mode, a warning will be logged on DB persist attempts.
- `BoltDBOptions` configures BoltDB. Includes the DB files path and ReadOnly mode toggle. If ReadOnly
mode is on, then an error will be returned on attempt to connect with unexisting or empty database.
Database doesn't allow changes in this mode, a warning will be logged on DB persist attempts.
Only options for the specified database type will be used.

View file

@ -23,6 +23,39 @@ import (
const neoAmount = 99999000
// InitSimple initializes chain with simple contracts from 'examples' folder.
// It's not as complicated as chain got after Init and may be used for tests where
// chain with a small amount of data is needed and for historical functionality testing.
// Needs a path to the root directory.
func InitSimple(t *testing.T, rootpath string, e *neotest.Executor) {
// examplesPrefix is a prefix of the example smart-contracts.
var examplesPrefix = filepath.Join(rootpath, "examples")
deployExample := func(t *testing.T, name string) util.Uint160 {
_, h := newDeployTx(t, e, e.Validator,
filepath.Join(examplesPrefix, name, name+".go"),
filepath.Join(examplesPrefix, name, name+".yml"),
true)
return h
}
// Block #1: deploy storage contract (examples/storage/storage.go).
storageHash := deployExample(t, "storage")
storageValidatorInvoker := e.ValidatorInvoker(storageHash)
// Block #2: put (1, 1) kv pair.
storageValidatorInvoker.Invoke(t, 1, "put", 1, 1)
// Block #3: put (2, 2) kv pair.
storageValidatorInvoker.Invoke(t, 2, "put", 2, 2)
// Block #4: update (1, 1) -> (1, 2).
storageValidatorInvoker.Invoke(t, 1, "put", 1, 2)
// Block #5: deploy runtime contract (examples/runtime/runtime.go).
_ = deployExample(t, "runtime")
}
// Init pushes some predefined set of transactions into the given chain, it needs a path to
// the root project directory.
func Init(t *testing.T, rootpath string, e *neotest.Executor) {

View file

@ -298,7 +298,7 @@ func (chain *FakeChain) GetStorageItem(id int32, key []byte) state.StorageItem {
}
// GetTestVM implements the Blockchainer interface.
func (chain *FakeChain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context {
func (chain *FakeChain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) {
panic("TODO")
}

View file

@ -55,6 +55,7 @@ const (
defaultMaxBlockSystemFee = 900000000000
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
defaultMaxTransactionsPerBlock = 512
defaultSecondsPerBlock = 15
// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
HeaderVerificationGasLimit = 3_00000000 // 3 GAS
defaultStateSyncInterval = 40000
@ -245,6 +246,11 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
log.Info("MaxTransactionsPerBlock is not set or wrong, using default value",
zap.Uint16("MaxTransactionsPerBlock", cfg.MaxTransactionsPerBlock))
}
if cfg.SecondsPerBlock == 0 {
cfg.SecondsPerBlock = defaultSecondsPerBlock
log.Info("SecondsPerBlock is not set or wrong, using default value",
zap.Int("SecondsPerBlock", cfg.SecondsPerBlock))
}
if cfg.MaxValidUntilBlockIncrement == 0 {
const secondsPerDay = int(24 * time.Hour / time.Second)
@ -2235,19 +2241,28 @@ func (bc *Blockchain) GetEnrollments() ([]state.Validator, error) {
}
// GetTestVM returns an interop context with VM set up for a test run.
func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context {
func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) {
if b == nil {
var err error
h := bc.BlockHeight() + 1
b, err = bc.getFakeNextBlock(h)
if err != nil {
return nil, fmt.Errorf("failed to create fake block for height %d: %w", h, err)
}
}
systemInterop := bc.newInteropContext(t, bc.dao, b, tx)
_ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready.
return systemInterop
return systemInterop, nil
}
// GetTestHistoricVM returns an interop context with VM set up for a test run.
func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) {
func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, nextBlockHeight uint32) (*interop.Context, error) {
if bc.config.KeepOnlyLatestState {
return nil, errors.New("only latest state is supported")
}
if b == nil {
return nil, errors.New("block is mandatory to produce test historic VM")
b, err := bc.getFakeNextBlock(nextBlockHeight)
if err != nil {
return nil, fmt.Errorf("failed to create fake block for height %d: %w", nextBlockHeight, err)
}
var mode = mpt.ModeAll
if bc.config.RemoveUntraceableBlocks {
@ -2278,6 +2293,18 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact
return systemInterop, nil
}
// getFakeNextBlock returns fake block with the specified index and pre-filled Timestamp field.
func (bc *Blockchain) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) {
b := block.New(bc.config.StateRootInHeader)
b.Index = nextBlockHeight
hdr, err := bc.GetHeader(bc.GetHeaderHash(int(nextBlockHeight - 1)))
if err != nil {
return nil, err
}
b.Timestamp = hdr.Timestamp + uint64(bc.config.SecondsPerBlock*int(time.Second/time.Millisecond))
return b, nil
}
// Various witness verification errors.
var (
ErrWitnessHashMismatch = errors.New("witness hash mismatch")

View file

@ -32,7 +32,8 @@ var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal",
func TestGetCallFlags(t *testing.T) {
bc, _ := chain.NewSingle(t)
ic := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
ic.VM.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All)
require.NoError(t, contract.GetCallFlags(ic))
@ -41,7 +42,8 @@ func TestGetCallFlags(t *testing.T) {
func TestCall(t *testing.T) {
bc, _ := chain.NewSingle(t)
ic := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test
require.NoError(t, native.PutContractState(ic.DAO, cs))

View file

@ -19,6 +19,9 @@ const (
MaxEventNameLen = 32
// MaxNotificationSize is the maximum length of a runtime log message.
MaxNotificationSize = 1024
// SystemRuntimeLogMessage represents log entry message used for output
// of the System.Runtime.Log syscall.
SystemRuntimeLogMessage = "runtime log"
)
// GetExecutingScriptHash returns executing script hash.
@ -112,7 +115,7 @@ func Log(ic *interop.Context) error {
if ic.Tx != nil {
txHash = ic.Tx.Hash().StringLE()
}
ic.Log.Info("runtime log",
ic.Log.Info(SystemRuntimeLogMessage,
zap.String("tx", txHash),
zap.String("script", ic.VM.GetCurrentScriptHash().StringLE()),
zap.String("msg", state))

View file

@ -65,7 +65,8 @@ func getSharpTestGenesis(t *testing.T) *block.Block {
func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) {
chain, _ := chain.NewSingle(t)
ic := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
ic, err := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
v := ic.SpawnVM()
return v, ic, chain
}
@ -523,7 +524,8 @@ func TestGetRandomCompatibility(t *testing.T) {
b := getSharpTestGenesis(t)
tx := getSharpTestTx(util.Uint160{})
ic := bc.GetTestVM(trigger.Application, tx, b)
ic, err := bc.GetTestVM(trigger.Application, tx, b)
require.NoError(t, err)
ic.Network = 860833102 // Old mainnet magic used by C# tests.
ic.VM = vm.New()
@ -550,7 +552,8 @@ func TestNotify(t *testing.T) {
caller := random.Uint160()
newIC := func(name string, args interface{}) *interop.Context {
_, _, bc, cs := getDeployedInternal(t)
ic := bc.GetTestVM(trigger.Application, nil, nil)
ic, err := bc.GetTestVM(trigger.Application, nil, nil)
require.NoError(t, err)
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, callflag.NoneFlag, true, 0, -1, nil)
ic.VM.Estack().PushVal(args)
ic.VM.Estack().PushVal(name)

View file

@ -300,7 +300,8 @@ func TestFind(t *testing.T) {
func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) {
chain, _ := chain.NewSingle(t)
ic := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
ic, err := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
v := ic.SpawnVM()
return v, ic, chain
}

View file

@ -74,7 +74,8 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
require.NotNil(t, md)
t.Run("fail, bad current script hash", func(t *testing.T) {
ic := bc.GetTestVM(trigger.Application, nil, nil)
ic, err := bc.GetTestVM(trigger.Application, nil, nil)
require.NoError(t, err)
v := ic.SpawnVM()
fakeH := util.Uint160{1, 2, 3}
v.LoadScriptWithHash(clState.NEF.Script, fakeH, callflag.All)
@ -83,7 +84,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
v.Context().Jump(md.Offset)
// Bad current script hash
err := v.Run()
err = v.Run()
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s (version 0) not found", fakeH.StringLE())), err.Error())
})
@ -104,7 +105,8 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
})
eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad)
ic := bcBad.GetTestVM(trigger.Application, nil, nil)
ic, err := bcBad.GetTestVM(trigger.Application, nil, nil)
require.NoError(t, err)
v := ic.SpawnVM()
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
input := []byte{1, 2, 3, 4}
@ -112,13 +114,14 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
v.Context().Jump(md.Offset)
// It's prohibited to call natives before NativeUpdateHistory[0] height.
err := v.Run()
err = v.Run()
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "native contract CryptoLib is active after height = 1"))
// Add new block => CryptoLib should be active now.
eBad.AddNewBlock(t)
ic = bcBad.GetTestVM(trigger.Application, nil, nil)
ic, err = bcBad.GetTestVM(trigger.Application, nil, nil)
require.NoError(t, err)
v = ic.SpawnVM()
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
v.Estack().PushVal(input)
@ -130,7 +133,8 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
})
t.Run("success", func(t *testing.T) {
ic := bc.GetTestVM(trigger.Application, nil, nil)
ic, err := bc.GetTestVM(trigger.Application, nil, nil)
require.NoError(t, err)
v := ic.SpawnVM()
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All)
input := []byte{1, 2, 3, 4}

View file

@ -288,7 +288,7 @@ func TestManagement_StartFromHeight(t *testing.T) {
// Create database to be able to start another chain from the same height later.
ldbDir := t.TempDir()
dbConfig := dbconfig.DBConfiguration{
Type: "leveldb",
Type: dbconfig.LevelDB,
LevelDBOptions: dbconfig.LevelDBOptions{
DataDirectoryPath: ldbDir,
},

View file

@ -2,6 +2,7 @@ package storage
import (
"bytes"
"errors"
"fmt"
"os"
@ -22,23 +23,41 @@ type BoltDBStore struct {
// NewBoltDBStore returns a new ready to use BoltDB storage with created bucket.
func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) {
var opts *bbolt.Options // should be exposed via BoltDBOptions if anything needed
cp := *bbolt.DefaultOptions // Do not change bbolt's global variable.
opts := &cp
fileMode := os.FileMode(0600) // should be exposed via BoltDBOptions if anything needed
fileName := cfg.FilePath
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
return nil, err
if cfg.ReadOnly {
opts.ReadOnly = true
} else {
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
return nil, err
}
}
db, err := bbolt.Open(fileName, fileMode, opts)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to open BoltDB instance: %w", err)
}
if opts.ReadOnly {
err = db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket(Bucket)
if b == nil {
return errors.New("root bucket does not exist")
}
return nil
})
} else {
err = db.Update(func(tx *bbolt.Tx) error {
_, err = tx.CreateBucketIfNotExists(Bucket)
if err != nil {
return fmt.Errorf("could not create root bucket: %w", err)
}
return nil
})
}
if err != nil {
return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err)
}
err = db.Update(func(tx *bbolt.Tx) error {
_, err = tx.CreateBucketIfNotExists(Bucket)
if err != nil {
return fmt.Errorf("could not create root bucket: %w", err)
}
return nil
})
return &BoltDBStore{db: db}, nil
}

View file

@ -1,11 +1,15 @@
package storage
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
)
func newBoltStoreForTesting(t testing.TB) Store {
@ -15,3 +19,42 @@ func newBoltStoreForTesting(t testing.TB) Store {
require.NoError(t, err)
return boltDBStore
}
func TestROBoltDB(t *testing.T) {
d := t.TempDir()
testFileName := filepath.Join(d, "test_ro_bolt_db")
cfg := dbconfig.BoltDBOptions{
FilePath: testFileName,
ReadOnly: true,
}
// If DB doesn't exist, then error should be returned.
_, err := NewBoltDBStore(cfg)
require.Error(t, err)
// Create the DB and try to open it in RO mode.
cfg.ReadOnly = false
store, err := NewBoltDBStore(cfg)
require.NoError(t, err)
require.NoError(t, store.Close())
cfg.ReadOnly = true
store, err = NewBoltDBStore(cfg)
require.NoError(t, err)
// Changes must be prohibited.
putErr := store.PutChangeSet(map[string][]byte{"one": []byte("one")}, nil)
require.ErrorIs(t, putErr, bbolt.ErrDatabaseReadOnly)
require.NoError(t, store.Close())
// Create the DB without buckets and try to open it in RO mode, an error is expected.
fileMode := os.FileMode(0600)
cfg.FilePath = filepath.Join(d, "clean_ro_bolt_db")
require.NoError(t, io.MakeDirForFile(cfg.FilePath, "BoltDB"))
db, err := bbolt.Open(cfg.FilePath, fileMode, nil)
require.NoError(t, err)
require.NoError(t, db.Close())
_, err = NewBoltDBStore(cfg)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "root bucket does not exist"))
}

View file

@ -13,9 +13,11 @@ type (
// LevelDBOptions configuration for LevelDB.
LevelDBOptions struct {
DataDirectoryPath string `yaml:"DataDirectoryPath"`
ReadOnly bool `yaml:"ReadOnly"`
}
// BoltDBOptions configuration for BoltDB.
BoltDBOptions struct {
FilePath string `yaml:"FilePath"`
ReadOnly bool `yaml:"ReadOnly"`
}
)

View file

@ -0,0 +1,11 @@
package dbconfig
// Available storage types.
const (
// BoltDB represents Bolt DB storage name.
BoltDB = "boltdb"
// LevelDB represents Level DB storage name.
LevelDB = "leveldb"
// InMemoryDB represents in-memory storage name.
InMemoryDB = "inmemory"
)

View file

@ -2,6 +2,7 @@ package storage
import (
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/syndtr/goleveldb/leveldb"
@ -21,11 +22,14 @@ type LevelDBStore struct {
// initialize the database found at the given path.
func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) {
var opts = new(opt.Options) // should be exposed via LevelDBOptions if anything needed
if cfg.ReadOnly {
opts.ReadOnly = true
opts.ErrorIfMissing = true
}
opts.Filter = filter.NewBloomFilter(10)
db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to open LevelDB instance: %w", err)
}
return &LevelDBStore{

View file

@ -5,17 +5,41 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
"github.com/syndtr/goleveldb/leveldb"
)
func newLevelDBForTesting(t testing.TB) Store {
ldbDir := t.TempDir()
dbConfig := dbconfig.DBConfiguration{
Type: "leveldb",
LevelDBOptions: dbconfig.LevelDBOptions{
DataDirectoryPath: ldbDir,
},
opts := dbconfig.LevelDBOptions{
DataDirectoryPath: ldbDir,
}
newLevelStore, err := NewLevelDBStore(dbConfig.LevelDBOptions)
newLevelStore, err := NewLevelDBStore(opts)
require.Nil(t, err, "NewLevelDBStore error")
return newLevelStore
}
func TestROLevelDB(t *testing.T) {
ldbDir := t.TempDir()
opts := dbconfig.LevelDBOptions{
DataDirectoryPath: ldbDir,
ReadOnly: true,
}
// If DB doesn't exist, then error should be returned.
_, err := NewLevelDBStore(opts)
require.Error(t, err)
// Create the DB and try to open it in RO mode.
opts.ReadOnly = false
store, err := NewLevelDBStore(opts)
require.NoError(t, err)
require.NoError(t, store.Close())
opts.ReadOnly = true
store, err = NewLevelDBStore(opts)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, store.Close()) })
// Changes must be prohibited.
putErr := store.PutChangeSet(map[string][]byte{"one": []byte("one")}, nil)
require.ErrorIs(t, putErr, leveldb.ErrReadOnly)
}

View file

@ -301,7 +301,12 @@ func performSeek(ctx context.Context, ps Store, memRes []KeyValueExists, rng See
}
}
}
ps.Seek(rng, mergeFunc)
if rng.SearchDepth == 0 || rng.SearchDepth > 1 {
if rng.SearchDepth > 1 {
rng.SearchDepth--
}
ps.Seek(rng, mergeFunc)
}
if !done && haveMem {
loop:

View file

@ -59,6 +59,11 @@ type SeekRange struct {
// whether seeking should be performed in a descending way.
// Backwards can be safely combined with Prefix and Start.
Backwards bool
// SearchDepth is the depth of Seek operation, denotes the number of cached
// DAO layers to perform search. Use 1 to fetch the latest changes from upper
// in-memory layer of cached DAO. Default 0 value denotes searching through
// the whole set of cached layers.
SearchDepth int
}
// ErrKeyNotFound is an error returned by Store implementations
@ -114,11 +119,11 @@ func NewStore(cfg dbconfig.DBConfiguration) (Store, error) {
var store Store
var err error
switch cfg.Type {
case "leveldb":
case dbconfig.LevelDB:
store, err = NewLevelDBStore(cfg.LevelDBOptions)
case "inmemory":
case dbconfig.InMemoryDB:
store = NewMemoryStore()
case "boltdb":
case dbconfig.BoltDB:
store, err = NewBoltDBStore(cfg.BoltDBOptions)
default:
return nil, fmt.Errorf("unknown storage: %s", cfg.Type)

View file

@ -0,0 +1,29 @@
package storage
import (
"path/filepath"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
)
func TestStorageNames(t *testing.T) {
tmp := t.TempDir()
cfg := dbconfig.DBConfiguration{
LevelDBOptions: dbconfig.LevelDBOptions{
DataDirectoryPath: filepath.Join(tmp, "level"),
},
BoltDBOptions: dbconfig.BoltDBOptions{
FilePath: filepath.Join(tmp, "bolt"),
},
}
for _, name := range []string{dbconfig.BoltDB, dbconfig.LevelDB, dbconfig.InMemoryDB} {
t.Run(name, func(t *testing.T) {
cfg.Type = name
s, err := NewStore(cfg)
require.NoError(t, err)
require.NoError(t, s.Close())
})
}
}

View file

@ -374,7 +374,8 @@ func TestInvoke(bc *core.Blockchain, tx *transaction.Transaction) (*vm.VM, error
// `GetTestVM` as well as `Run` can use a transaction hash which will set a cached value.
// This is unwanted behavior, so we explicitly copy the transaction to perform execution.
ttx := *tx
ic := bc.GetTestVM(trigger.Application, &ttx, b)
ic, _ := bc.GetTestVM(trigger.Application, &ttx, b)
defer ic.Finalize()
ic.VM.LoadWithFlags(tx.Script, callflag.All)

View file

@ -51,11 +51,14 @@ func (e *Executor) ValidatorInvoker(h util.Uint160) *ContractInvoker {
func (c *ContractInvoker) TestInvoke(t testing.TB, method string, args ...interface{}) (*vm.Stack, error) {
tx := c.PrepareInvokeNoSign(t, method, args...)
b := c.NewUnsignedBlock(t, tx)
ic := c.Chain.GetTestVM(trigger.Application, tx, b)
ic, err := c.Chain.GetTestVM(trigger.Application, tx, b)
if err != nil {
return nil, err
}
t.Cleanup(ic.Finalize)
ic.VM.LoadWithFlags(tx.Script, callflag.All)
err := ic.VM.Run()
err = ic.VM.Run()
return ic.VM.Estack(), err
}

View file

@ -29,7 +29,7 @@ type (
GetBaseExecFee() int64
GetConfig() config.ProtocolConfiguration
GetMaxVerificationGAS() int64
GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context
GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error)
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
}

View file

@ -3,6 +3,7 @@ package oracle
import (
"encoding/hex"
"errors"
"fmt"
gio "io"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
@ -107,7 +108,10 @@ func (o *Oracle) CreateResponseTx(gasForResponse int64, vub uint32, resp *transa
size := io.GetVarSize(tx)
tx.Scripts = append(tx.Scripts, transaction.Witness{VerificationScript: oracleSignContract})
gasConsumed, ok := o.testVerify(tx)
gasConsumed, ok, err := o.testVerify(tx)
if err != nil {
return nil, fmt.Errorf("failed to prepare `verify` invocation: %w", err)
}
if !ok {
return nil, errors.New("can't verify transaction")
}
@ -131,18 +135,21 @@ func (o *Oracle) CreateResponseTx(gasForResponse int64, vub uint32, resp *transa
return tx, nil
}
func (o *Oracle) testVerify(tx *transaction.Transaction) (int64, bool) {
func (o *Oracle) testVerify(tx *transaction.Transaction) (int64, bool, error) {
// (*Blockchain).GetTestVM calls Hash() method of the provided transaction; once being called, this
// method caches transaction hash, but tx building is not yet completed and hash will be changed.
// So, make a copy of the tx to avoid wrong hash caching.
cp := *tx
ic := o.Chain.GetTestVM(trigger.Verification, &cp, nil)
ic, err := o.Chain.GetTestVM(trigger.Verification, &cp, nil)
if err != nil {
return 0, false, fmt.Errorf("failed to create test VM: %w", err)
}
ic.VM.GasLimit = o.Chain.GetMaxVerificationGAS()
ic.VM.LoadScriptWithHash(o.oracleScript, o.oracleHash, callflag.ReadOnly)
ic.VM.Context().Jump(o.verifyOffset)
ok := isVerifyOk(ic)
return ic.VM.GasConsumed(), ok
return ic.VM.GasConsumed(), ok, nil
}
func isVerifyOk(ic *interop.Context) bool {

View file

@ -1181,7 +1181,8 @@ func TestCreateNEP17TransferTx(t *testing.T) {
require.NoError(t, err)
require.NoError(t, acc.SignTx(testchain.Network(), tx))
require.NoError(t, chain.VerifyTx(tx))
ic := chain.GetTestVM(trigger.Application, tx, nil)
ic, err := chain.GetTestVM(trigger.Application, tx, nil)
require.NoError(t, err)
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
require.NoError(t, ic.VM.Run())
})
@ -1195,7 +1196,8 @@ func TestCreateNEP17TransferTx(t *testing.T) {
})
require.NoError(t, err)
require.NoError(t, chain.VerifyTx(tx))
ic := chain.GetTestVM(trigger.Application, tx, nil)
ic, err := chain.GetTestVM(trigger.Application, tx, nil)
require.NoError(t, err)
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
require.NoError(t, ic.VM.Run())
require.Equal(t, 2, len(ic.Notifications))
@ -1228,7 +1230,8 @@ func TestCreateNEP17TransferTx(t *testing.T) {
require.NoError(t, err)
require.NoError(t, acc.SignTx(testchain.Network(), tx))
require.NoError(t, chain.VerifyTx(tx))
ic := chain.GetTestVM(trigger.Application, tx, nil)
ic, err := chain.GetTestVM(trigger.Application, tx, nil)
require.NoError(t, err)
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
require.NoError(t, ic.VM.Run())
})

View file

@ -89,8 +89,8 @@ type (
GetNotaryServiceFeePerKey() int64
GetStateModule() core.StateRoot
GetStorageItem(id int32, key []byte) state.StorageItem
GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error)
GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context
GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, nextBlockHeight uint32) (*interop.Context, error)
GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error)
GetTokenLastUpdated(acc util.Uint160) (map[int32]uint32, error)
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
GetValidators() ([]*keys.PublicKey, error)
@ -1065,11 +1065,10 @@ func (s *Server) invokeReadOnlyMulti(bw *io.BufBinWriter, h util.Uint160, method
}
script := bw.Bytes()
tx := &transaction.Transaction{Script: script}
b, err := s.getFakeNextBlock(s.chain.BlockHeight() + 1)
ic, err := s.chain.GetTestVM(trigger.Application, tx, nil)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("faile to prepare test VM: %w", err)
}
ic := s.chain.GetTestVM(trigger.Application, tx, b)
ic.VM.GasLimit = core.HeaderVerificationGasLimit
ic.VM.LoadScriptWithFlags(script, callflag.All)
err = ic.VM.Run()
@ -1832,7 +1831,7 @@ func (s *Server) invokeFunction(reqParams params.Params) (interface{}, *neorpc.E
// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call.
func (s *Server) invokeFunctionHistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
b, respErr := s.getHistoricParams(reqParams)
nextH, respErr := s.getHistoricParams(reqParams)
if respErr != nil {
return nil, respErr
}
@ -1843,7 +1842,7 @@ func (s *Server) invokeFunctionHistoric(reqParams params.Params) (interface{}, *
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose)
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, &nextH, verbose)
}
func (s *Server) getInvokeFunctionParams(reqParams params.Params) (*transaction.Transaction, bool, *neorpc.Error) {
@ -1899,7 +1898,7 @@ func (s *Server) invokescript(reqParams params.Params) (interface{}, *neorpc.Err
// invokescripthistoric implements the `invokescripthistoric` RPC call.
func (s *Server) invokescripthistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
b, respErr := s.getHistoricParams(reqParams)
nextH, respErr := s.getHistoricParams(reqParams)
if respErr != nil {
return nil, respErr
}
@ -1910,7 +1909,7 @@ func (s *Server) invokescripthistoric(reqParams params.Params) (interface{}, *ne
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose)
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, &nextH, verbose)
}
func (s *Server) getInvokeScriptParams(reqParams params.Params) (*transaction.Transaction, bool, *neorpc.Error) {
@ -1953,7 +1952,7 @@ func (s *Server) invokeContractVerify(reqParams params.Params) (interface{}, *ne
// invokeContractVerifyHistoric implements the `invokecontractverifyhistoric` RPC call.
func (s *Server) invokeContractVerifyHistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
b, respErr := s.getHistoricParams(reqParams)
nextH, respErr := s.getHistoricParams(reqParams)
if respErr != nil {
return nil, respErr
}
@ -1964,7 +1963,7 @@ func (s *Server) invokeContractVerifyHistoric(reqParams params.Params) (interfac
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, b, false)
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, &nextH, false)
}
func (s *Server) getInvokeContractVerifyParams(reqParams params.Params) (util.Uint160, *transaction.Transaction, []byte, *neorpc.Error) {
@ -2003,68 +2002,52 @@ func (s *Server) getInvokeContractVerifyParams(reqParams params.Params) (util.Ui
return scriptHash, tx, invocationScript, nil
}
// getHistoricParams checks that historic calls are supported and returns fake block
// with the specified index to perform the historic call. It also checks that
// getHistoricParams checks that historic calls are supported and returns index of
// a fake next block to perform the historic call. It also checks that
// specified stateroot is stored at the specified height for further request
// handling consistency.
func (s *Server) getHistoricParams(reqParams params.Params) (*block.Block, *neorpc.Error) {
func (s *Server) getHistoricParams(reqParams params.Params) (uint32, *neorpc.Error) {
if s.chain.GetConfig().KeepOnlyLatestState {
return nil, neorpc.NewInvalidRequestError(fmt.Sprintf("only latest state is supported: %s", errKeepOnlyLatestState))
return 0, neorpc.NewInvalidRequestError(fmt.Sprintf("only latest state is supported: %s", errKeepOnlyLatestState))
}
if len(reqParams) < 1 {
return nil, neorpc.ErrInvalidParams
return 0, neorpc.ErrInvalidParams
}
height, respErr := s.blockHeightFromParam(reqParams.Value(0))
if respErr != nil {
hash, err := reqParams.Value(0).GetUint256()
if err != nil {
return nil, neorpc.NewInvalidParamsError(fmt.Sprintf("invalid block hash or index or stateroot hash: %s", err))
return 0, neorpc.NewInvalidParamsError(fmt.Sprintf("invalid block hash or index or stateroot hash: %s", err))
}
b, err := s.chain.GetBlock(hash)
if err != nil {
stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash)
if err != nil {
return nil, neorpc.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err))
return 0, neorpc.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err))
}
height = int(stateH)
} else {
height = int(b.Index)
}
}
b, err := s.getFakeNextBlock(uint32(height + 1))
if err != nil {
return nil, neorpc.NewInternalServerError(fmt.Sprintf("can't create fake block for height %d: %s", height+1, err))
if height > math.MaxUint32 {
return 0, neorpc.NewInvalidParamsError("historic height exceeds max uint32 value")
}
return b, nil
return uint32(height) + 1, nil
}
func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) {
// When transferring funds, script execution does no auto GAS claim,
// because it depends on persisting tx height.
// This is why we provide block here.
b := block.New(s.stateRootEnabled)
b.Index = nextBlockHeight
hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(nextBlockHeight - 1)))
if err != nil {
return nil, err
}
b.Timestamp = hdr.Timestamp + uint64(s.chain.GetConfig().SecondsPerBlock*int(time.Second/time.Millisecond))
return b, nil
}
func (s *Server) prepareInvocationContext(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*interop.Context, *neorpc.Error) {
func (s *Server) prepareInvocationContext(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, nextH *uint32, verbose bool) (*interop.Context, *neorpc.Error) {
var (
err error
ic *interop.Context
)
if b == nil {
b, err = s.getFakeNextBlock(s.chain.BlockHeight() + 1)
if nextH == nil {
ic, err = s.chain.GetTestVM(t, tx, nil)
if err != nil {
return nil, neorpc.NewInternalServerError(fmt.Sprintf("can't create fake block: %s", err))
return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to create test VM: %s", err))
}
ic = s.chain.GetTestVM(t, tx, b)
} else {
ic, err = s.chain.GetTestHistoricVM(t, tx, b)
ic, err = s.chain.GetTestHistoricVM(t, tx, *nextH)
if err != nil {
return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to create historic VM: %s", err))
}
@ -2096,8 +2079,8 @@ func (s *Server) prepareInvocationContext(t trigger.Type, script []byte, contrac
// witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *neorpc.Error) {
ic, respErr := s.prepareInvocationContext(t, script, contractScriptHash, tx, b, verbose)
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, nextH *uint32, verbose bool) (*result.Invoke, *neorpc.Error) {
ic, respErr := s.prepareInvocationContext(t, script, contractScriptHash, tx, nextH, verbose)
if respErr != nil {
return nil, respErr
}
@ -2111,16 +2094,12 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
var id uuid.UUID
if sess != nil {
// b == nil only when we're not using MPT-backed storage, therefore
// nextH == nil only when we're not using MPT-backed storage, therefore
// the second attempt won't stop here.
if s.config.SessionBackedByMPT && b == nil {
if s.config.SessionBackedByMPT && nextH == nil {
ic.Finalize()
b, err = s.getFakeNextBlock(ic.Block.Index)
if err != nil {
return nil, neorpc.NewInternalServerError(fmt.Sprintf("unable to prepare block for historic call: %s", err))
}
// Rerun with MPT-backed storage.
return s.runScriptInVM(t, script, contractScriptHash, tx, b, verbose)
return s.runScriptInVM(t, script, contractScriptHash, tx, &ic.Block.Index, verbose)
}
id = uuid.New()
sessionID := id.String()

View file

@ -14,7 +14,7 @@ import (
// where n is the length of publicKeys.
func CreateMultiSigRedeemScript(m int, publicKeys keys.PublicKeys) ([]byte, error) {
if m < 1 {
return nil, fmt.Errorf("param m cannot be smaller or equal to 1 got %d", m)
return nil, fmt.Errorf("param m cannot be smaller than 1, got %d", m)
}
if m > len(publicKeys) {
return nil, fmt.Errorf("length of the signatures (%d) is higher then the number of public keys", m)

View file

@ -1,66 +0,0 @@
package vm
import (
"errors"
"fmt"
"sort"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
)
// interopIDFuncPrice adds an ID to the InteropFuncPrice.
type interopIDFuncPrice struct {
ID uint32
Func func(vm *VM) error
Price int64
RequiredFlags callflag.CallFlag
}
var defaultVMInterops = []interopIDFuncPrice{
{ID: interopnames.ToID([]byte(interopnames.SystemRuntimeLog)),
Func: runtimeLog, Price: 1 << 15, RequiredFlags: callflag.AllowNotify},
{ID: interopnames.ToID([]byte(interopnames.SystemRuntimeNotify)),
Func: runtimeNotify, Price: 1 << 15, RequiredFlags: callflag.AllowNotify},
}
func init() {
sort.Slice(defaultVMInterops, func(i, j int) bool { return defaultVMInterops[i].ID < defaultVMInterops[j].ID })
}
func defaultSyscallHandler(v *VM, id uint32) error {
n := sort.Search(len(defaultVMInterops), func(i int) bool {
return defaultVMInterops[i].ID >= id
})
if n >= len(defaultVMInterops) || defaultVMInterops[n].ID != id {
return errors.New("syscall not found")
}
d := defaultVMInterops[n]
ctxFlag := v.Context().sc.callFlag
if !ctxFlag.Has(d.RequiredFlags) {
return fmt.Errorf("missing call flags: %05b vs %05b", ctxFlag, d.RequiredFlags)
}
return d.Func(v)
}
// runtimeLog handles the syscall "System.Runtime.Log" for printing and logging stuff.
func runtimeLog(vm *VM) error {
msg := vm.Estack().Pop().String()
fmt.Printf("NEO-GO-VM (log) > %s\n", msg)
return nil
}
// runtimeNotify handles the syscall "System.Runtime.Notify" for printing and logging stuff.
func runtimeNotify(vm *VM) error {
name := vm.Estack().Pop().String()
item := vm.Estack().Pop()
fmt.Printf("NEO-GO-VM (notify) > [%s] %s\n", name, item.Value())
return nil
}
// init sorts the global defaultVMInterops value.
func init() {
sort.Slice(defaultVMInterops, func(i, j int) bool {
return defaultVMInterops[i].ID < defaultVMInterops[j].ID
})
}

View file

@ -108,8 +108,6 @@ func NewWithTrigger(t trigger.Type) *VM {
vm := &VM{
state: vmstate.None,
trigger: t,
SyscallHandler: defaultSyscallHandler,
}
initStack(&vm.istack, "invocation", nil)
@ -1458,6 +1456,9 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
case opcode.SYSCALL:
interopID := GetInteropID(parameter)
if v.SyscallHandler == nil {
panic("vm's SyscallHandler is not initialized")
}
err := v.SyscallHandler(v, interopID)
if err != nil {
panic(fmt.Sprintf("failed to invoke syscall %d: %s", interopID, err))

View file

@ -17,6 +17,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
@ -2747,6 +2748,19 @@ func TestRemoveReferrer(t *testing.T) {
assert.Equal(t, 0, int(vm.refs))
}
func TestUninitializedSyscallHandler(t *testing.T) {
v := newTestVM()
v.Reset(trigger.Application) // Reset SyscallHandler.
id := make([]byte, 4)
binary.LittleEndian.PutUint32(id, interopnames.ToID([]byte(interopnames.SystemRuntimeGasLeft)))
script := append([]byte{byte(opcode.SYSCALL)}, id...)
v.LoadScript(script)
err := v.Run()
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "SyscallHandler is not initialized"), err.Error())
assert.Equal(t, true, v.HasFailed())
}
func makeProgram(opcodes ...opcode.Opcode) []byte {
prog := make([]byte, len(opcodes)+1) // RET
for i := 0; i < len(opcodes); i++ {