forked from TrueCloudLab/neoneo-go
Merge pull request #2723 from nspcc-dev/smart-vm-cli
vm: make VM CLI state-dependant
This commit is contained in:
commit
1426b54fd7
39 changed files with 1278 additions and 353 deletions
28
cli/options/filtering_core.go
Normal file
28
cli/options/filtering_core.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
}
|
28
cli/vm/vm.go
28
cli/vm/vm.go
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
@ -22,16 +23,30 @@ 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 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 {
|
||||
|
@ -39,6 +54,10 @@ func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) {
|
|||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err)
|
||||
}
|
||||
|
||||
return &BoltDBStore{db: db}, nil
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
)
|
||||
|
|
11
pkg/core/storage/dbconfig/store_type.go
Normal file
11
pkg/core/storage/dbconfig/store_type.go
Normal 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"
|
||||
)
|
|
@ -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{
|
||||
|
|
|
@ -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{
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -301,7 +301,12 @@ func performSeek(ctx context.Context, ps Store, memRes []KeyValueExists, rng See
|
|||
}
|
||||
}
|
||||
}
|
||||
if rng.SearchDepth == 0 || rng.SearchDepth > 1 {
|
||||
if rng.SearchDepth > 1 {
|
||||
rng.SearchDepth--
|
||||
}
|
||||
ps.Seek(rng, mergeFunc)
|
||||
}
|
||||
|
||||
if !done && haveMem {
|
||||
loop:
|
||||
|
|
|
@ -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)
|
||||
|
|
29
pkg/core/storage/store_type_test.go
Normal file
29
pkg/core/storage/store_type_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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++ {
|
||||
|
|
Loading…
Reference in a new issue