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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultTimeout is the default timeout used for RPC requests.
|
// 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)",
|
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 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")
|
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
|
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/internal/testcli"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -19,7 +20,7 @@ func TestDBRestoreDump(t *testing.T) {
|
||||||
chainPath := filepath.Join(tmpDir, "neogotestchain")
|
chainPath := filepath.Join(tmpDir, "neogotestchain")
|
||||||
cfg, err := config.LoadFile(filepath.Join("..", "..", "config", "protocol.unit_testnet.yml"))
|
cfg, err := config.LoadFile(filepath.Join("..", "..", "config", "protocol.unit_testnet.yml"))
|
||||||
require.NoError(t, err, "could not load config")
|
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
|
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/cli/server"
|
"github.com/nspcc-dev/neo-go/cli/server"
|
||||||
"github.com/nspcc-dev/neo-go/internal/testcli"
|
"github.com/nspcc-dev/neo-go/internal/testcli"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -25,7 +26,7 @@ func TestServerStart(t *testing.T) {
|
||||||
saveCfg := func(t *testing.T, f func(cfg *config.Config)) string {
|
saveCfg := func(t *testing.T, f func(cfg *config.Config)) string {
|
||||||
cfg := *ptr
|
cfg := *ptr
|
||||||
chainPath := filepath.Join(t.TempDir(), "neogotestchain")
|
chainPath := filepath.Join(t.TempDir(), "neogotestchain")
|
||||||
cfg.ApplicationConfiguration.DBConfiguration.Type = "leveldb"
|
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
|
||||||
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath
|
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions.DataDirectoryPath = chainPath
|
||||||
f(&cfg)
|
f(&cfg)
|
||||||
out, err := yaml.Marshal(cfg)
|
out, err := yaml.Marshal(cfg)
|
||||||
|
|
|
@ -4,10 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -31,25 +29,15 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/stateroot"
|
"github.com/nspcc-dev/neo-go/pkg/services/stateroot"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"go.uber.org/zap"
|
"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.
|
// NewCommands returns 'node' command.
|
||||||
func NewCommands() []cli.Command {
|
func NewCommands() []cli.Command {
|
||||||
var cfgFlags = []cli.Flag{
|
cfgFlags := []cli.Flag{options.Config}
|
||||||
cli.StringFlag{Name: "config-path", Usage: "path to directory with configuration files"},
|
|
||||||
}
|
|
||||||
cfgFlags = append(cfgFlags, options.Network...)
|
cfgFlags = append(cfgFlags, options.Network...)
|
||||||
var cfgWithCountFlags = make([]cli.Flag, len(cfgFlags))
|
var cfgWithCountFlags = make([]cli.Flag, len(cfgFlags))
|
||||||
copy(cfgWithCountFlags, 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,
|
cfgWithCountFlags = append(cfgWithCountFlags,
|
||||||
cli.UintFlag{
|
cli.UintFlag{
|
||||||
|
@ -128,91 +116,6 @@ func newGraceContext() context.Context {
|
||||||
return ctx
|
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) {
|
func initBCWithMetrics(cfg config.Config, log *zap.Logger) (*core.Blockchain, *metrics.Service, *metrics.Service, error) {
|
||||||
chain, err := initBlockChain(cfg, log)
|
chain, err := initBlockChain(cfg, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -233,11 +136,11 @@ func dumpDB(ctx *cli.Context) error {
|
||||||
if err := cmdargs.EnsureNone(ctx); err != nil {
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cfg, err := getConfigFromContext(ctx)
|
cfg, err := options.GetConfigFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
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 {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -286,11 +189,11 @@ func restoreDB(ctx *cli.Context) error {
|
||||||
if err := cmdargs.EnsureNone(ctx); err != nil {
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cfg, err := getConfigFromContext(ctx)
|
cfg, err := options.GetConfigFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log, logCloser, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration)
|
log, logCloser, err := options.HandleLoggingParams(ctx.Bool("debug"), cfg.ApplicationConfiguration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -472,11 +375,11 @@ func startServer(ctx *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := getConfigFromContext(ctx)
|
cfg, err := options.GetConfigFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
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 {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -549,7 +452,7 @@ Main:
|
||||||
cancel()
|
cancel()
|
||||||
case sig := <-sigCh:
|
case sig := <-sigCh:
|
||||||
log.Info("signal received", zap.Stringer("name", sig))
|
log.Info("signal received", zap.Stringer("name", sig))
|
||||||
cfgnew, err := getConfigFromContext(ctx)
|
cfgnew, err := options.GetConfigFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("can't reread the config file, signal ignored", zap.Error(err))
|
log.Warn("can't reread the config file, signal ignored", zap.Error(err))
|
||||||
break // Continue working.
|
break // Continue working.
|
||||||
|
|
|
@ -7,12 +7,14 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"go.uber.org/zap"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ func TestGetConfigFromContext(t *testing.T) {
|
||||||
set.String("config-path", "../../config", "")
|
set.String("config-path", "../../config", "")
|
||||||
set.Bool("testnet", true, "")
|
set.Bool("testnet", true, "")
|
||||||
ctx := cli.NewContext(cli.NewApp(), set, nil)
|
ctx := cli.NewContext(cli.NewApp(), set, nil)
|
||||||
cfg, err := getConfigFromContext(ctx)
|
cfg, err := options.GetConfigFromContext(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, netmode.TestNet, cfg.ProtocolConfiguration.Magic)
|
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) {
|
t.Run("logdir is a file", func(t *testing.T) {
|
||||||
logfile := filepath.Join(d, "logdir")
|
logfile := filepath.Join(d, "logdir")
|
||||||
require.NoError(t, os.WriteFile(logfile, []byte{1, 2, 3}, os.ModePerm))
|
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{
|
cfg := config.ApplicationConfiguration{
|
||||||
LogPath: filepath.Join(logfile, "file.log"),
|
LogPath: filepath.Join(logfile, "file.log"),
|
||||||
}
|
}
|
||||||
_, closer, err := handleLoggingParams(ctx, cfg)
|
_, closer, err := options.HandleLoggingParams(false, cfg)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, closer)
|
require.Nil(t, closer)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("default", func(t *testing.T) {
|
t.Run("default", func(t *testing.T) {
|
||||||
set := flag.NewFlagSet("flagSet", flag.ExitOnError)
|
|
||||||
ctx := cli.NewContext(cli.NewApp(), set, nil)
|
|
||||||
cfg := config.ApplicationConfiguration{
|
cfg := config.ApplicationConfiguration{
|
||||||
LogPath: testLog,
|
LogPath: testLog,
|
||||||
}
|
}
|
||||||
logger, closer, err := handleLoggingParams(ctx, cfg)
|
logger, closer, err := options.HandleLoggingParams(false, cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
if closer != nil {
|
if closer != nil {
|
||||||
require.NoError(t, closer())
|
require.NoError(t, closer())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
require.True(t, logger.Core().Enabled(zap.InfoLevel))
|
require.True(t, logger.Core().Enabled(zapcore.InfoLevel))
|
||||||
require.False(t, logger.Core().Enabled(zap.DebugLevel))
|
require.False(t, logger.Core().Enabled(zapcore.DebugLevel))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("debug", func(t *testing.T) {
|
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{
|
cfg := config.ApplicationConfiguration{
|
||||||
LogPath: testLog,
|
LogPath: testLog,
|
||||||
}
|
}
|
||||||
logger, closer, err := handleLoggingParams(ctx, cfg)
|
logger, closer, err := options.HandleLoggingParams(true, cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
if closer != nil {
|
if closer != nil {
|
||||||
require.NoError(t, closer())
|
require.NoError(t, closer())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
require.True(t, logger.Core().Enabled(zap.InfoLevel))
|
require.True(t, logger.Core().Enabled(zapcore.InfoLevel))
|
||||||
require.True(t, logger.Core().Enabled(zap.DebugLevel))
|
require.True(t, logger.Core().Enabled(zapcore.DebugLevel))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,9 +96,9 @@ func TestInitBCWithMetrics(t *testing.T) {
|
||||||
set.Bool("testnet", true, "")
|
set.Bool("testnet", true, "")
|
||||||
set.Bool("debug", true, "")
|
set.Bool("debug", true, "")
|
||||||
ctx := cli.NewContext(cli.NewApp(), set, nil)
|
ctx := cli.NewContext(cli.NewApp(), set, nil)
|
||||||
cfg, err := getConfigFromContext(ctx)
|
cfg, err := options.GetConfigFromContext(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
logger, closer, err := handleLoggingParams(ctx, cfg.ApplicationConfiguration)
|
logger, closer, err := options.HandleLoggingParams(true, cfg.ApplicationConfiguration)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
if closer != nil {
|
if closer != nil {
|
||||||
|
@ -349,7 +344,7 @@ func TestInitBlockChain(t *testing.T) {
|
||||||
_, err := initBlockChain(config.Config{
|
_, err := initBlockChain(config.Config{
|
||||||
ApplicationConfiguration: config.ApplicationConfiguration{
|
ApplicationConfiguration: config.ApplicationConfiguration{
|
||||||
DBConfiguration: dbconfig.DBConfiguration{
|
DBConfiguration: dbconfig.DBConfiguration{
|
||||||
Type: "inmemory",
|
Type: dbconfig.InMemoryDB,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/options"
|
"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"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package cli
|
package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -17,22 +18,34 @@ import (
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
"github.com/kballard/go-shellquote"
|
"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/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"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/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
"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/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util/slice"
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
vmKey = "vm"
|
chainKey = "chain"
|
||||||
|
chainCfgKey = "chainCfg"
|
||||||
|
icKey = "ic"
|
||||||
manifestKey = "manifest"
|
manifestKey = "manifest"
|
||||||
exitFuncKey = "exitFunc"
|
exitFuncKey = "exitFunc"
|
||||||
readlineInstanceKey = "readlineKey"
|
readlineInstanceKey = "readlineKey"
|
||||||
|
@ -44,6 +57,20 @@ const (
|
||||||
stringType = "string"
|
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{
|
var commands = []cli.Command{
|
||||||
{
|
{
|
||||||
Name: "exit",
|
Name: "exit",
|
||||||
|
@ -100,7 +127,8 @@ var commands = []cli.Command{
|
||||||
Name: "loadnef",
|
Name: "loadnef",
|
||||||
Usage: "Load a NEF-consistent script into the VM",
|
Usage: "Load a NEF-consistent script into the VM",
|
||||||
UsageText: `loadnef <file> <manifest>`,
|
UsageText: `loadnef <file> <manifest>`,
|
||||||
Description: `loadnef <file> <manifest>
|
Flags: []cli.Flag{historicFlag},
|
||||||
|
Description: `loadnef [--historic <height>] <file> <manifest>
|
||||||
both parameters are mandatory, example:
|
both parameters are mandatory, example:
|
||||||
> loadnef /path/to/script.nef /path/to/manifest.json`,
|
> loadnef /path/to/script.nef /path/to/manifest.json`,
|
||||||
Action: handleLoadNEF,
|
Action: handleLoadNEF,
|
||||||
|
@ -108,8 +136,9 @@ both parameters are mandatory, example:
|
||||||
{
|
{
|
||||||
Name: "loadbase64",
|
Name: "loadbase64",
|
||||||
Usage: "Load a base64-encoded script string into the VM",
|
Usage: "Load a base64-encoded script string into the VM",
|
||||||
UsageText: `loadbase64 <string>`,
|
UsageText: `loadbase64 [--historic <height>] <string>`,
|
||||||
Description: `loadbase64 <string>
|
Flags: []cli.Flag{historicFlag},
|
||||||
|
Description: `loadbase64 [--historic <height>] <string>
|
||||||
|
|
||||||
<string> is mandatory parameter, example:
|
<string> is mandatory parameter, example:
|
||||||
> loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`,
|
> loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`,
|
||||||
|
@ -118,8 +147,9 @@ both parameters are mandatory, example:
|
||||||
{
|
{
|
||||||
Name: "loadhex",
|
Name: "loadhex",
|
||||||
Usage: "Load a hex-encoded script string into the VM",
|
Usage: "Load a hex-encoded script string into the VM",
|
||||||
UsageText: `loadhex <string>`,
|
UsageText: `loadhex [--historic <height>] <string>`,
|
||||||
Description: `loadhex <string>
|
Flags: []cli.Flag{historicFlag},
|
||||||
|
Description: `loadhex [--historic <height>] <string>
|
||||||
|
|
||||||
<string> is mandatory parameter, example:
|
<string> is mandatory parameter, example:
|
||||||
> loadhex 0c0c48656c6c6f20776f726c6421`,
|
> loadhex 0c0c48656c6c6f20776f726c6421`,
|
||||||
|
@ -128,8 +158,9 @@ both parameters are mandatory, example:
|
||||||
{
|
{
|
||||||
Name: "loadgo",
|
Name: "loadgo",
|
||||||
Usage: "Compile and load a Go file with the manifest into the VM",
|
Usage: "Compile and load a Go file with the manifest into the VM",
|
||||||
UsageText: `loadgo <file>`,
|
UsageText: `loadgo [--historic <height>] <file>`,
|
||||||
Description: `loadgo <file>
|
Flags: []cli.Flag{historicFlag},
|
||||||
|
Description: `loadgo [--historic <height>] <file>
|
||||||
|
|
||||||
<file> is mandatory parameter, example:
|
<file> is mandatory parameter, example:
|
||||||
> loadgo /path/to/file.go`,
|
> loadgo /path/to/file.go`,
|
||||||
|
@ -137,7 +168,8 @@ both parameters are mandatory, example:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "reset",
|
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,
|
Action: handleReset,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -218,6 +250,84 @@ example:
|
||||||
Description: "Dump opcodes of the current loaded program",
|
Description: "Dump opcodes of the current loaded program",
|
||||||
Action: handleOps,
|
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
|
var completer *readline.PrefixCompleter
|
||||||
|
@ -243,28 +353,22 @@ var (
|
||||||
ErrInvalidParameter = errors.New("can't parse argument")
|
ErrInvalidParameter = errors.New("can't parse argument")
|
||||||
)
|
)
|
||||||
|
|
||||||
// VMCLI object for interacting with the VM.
|
// CLI object for interacting with the VM.
|
||||||
type VMCLI struct {
|
type CLI struct {
|
||||||
vm *vm.VM
|
chain *core.Blockchain
|
||||||
shell *cli.App
|
shell *cli.App
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new VMCLI object.
|
// NewWithConfig returns new CLI instance using provided config and (optionally)
|
||||||
func New() *VMCLI {
|
// provided node config for state-backed VM.
|
||||||
return NewWithConfig(true, os.Exit, &readline.Config{
|
func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config, cfg config.Config) (*CLI, error) {
|
||||||
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 {
|
|
||||||
if c.AutoComplete == nil {
|
if c.AutoComplete == nil {
|
||||||
// Autocomplete commands/flags on TAB.
|
// Autocomplete commands/flags on TAB.
|
||||||
c.AutoComplete = completer
|
c.AutoComplete = completer
|
||||||
}
|
}
|
||||||
l, err := readline.NewEx(c)
|
l, err := readline.NewEx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, fmt.Errorf("failed to create readline instance: %w", err)
|
||||||
}
|
}
|
||||||
ctl := cli.NewApp()
|
ctl := cli.NewApp()
|
||||||
ctl.Name = "VM CLI"
|
ctl.Name = "VM CLI"
|
||||||
|
@ -284,20 +388,59 @@ func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VM
|
||||||
|
|
||||||
ctl.Commands = commands
|
ctl.Commands = commands
|
||||||
|
|
||||||
vmcli := VMCLI{
|
store, err := storage.NewStore(cfg.ApplicationConfiguration.DBConfiguration)
|
||||||
vm: vm.New(),
|
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,
|
shell: ctl,
|
||||||
}
|
}
|
||||||
|
|
||||||
vmcli.shell.Metadata = map[string]interface{}{
|
vmcli.shell.Metadata = map[string]interface{}{
|
||||||
vmKey: vmcli.vm,
|
chainKey: chain,
|
||||||
|
chainCfgKey: cfg,
|
||||||
|
icKey: ic,
|
||||||
manifestKey: new(manifest.Manifest),
|
manifestKey: new(manifest.Manifest),
|
||||||
exitFuncKey: onExit,
|
exitFuncKey: exitF,
|
||||||
readlineInstanceKey: l,
|
readlineInstanceKey: l,
|
||||||
printLogoKey: printLogotype,
|
printLogoKey: printLogotype,
|
||||||
}
|
}
|
||||||
changePrompt(vmcli.shell)
|
changePrompt(vmcli.shell)
|
||||||
return &vmcli
|
return &vmcli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExitFuncFromContext(app *cli.App) func(int) {
|
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 {
|
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) {
|
func getChainFromContext(app *cli.App) *core.Blockchain {
|
||||||
old := getVMFromContext(app)
|
return app.Metadata[chainKey].(*core.Blockchain)
|
||||||
*old = *v
|
}
|
||||||
|
|
||||||
|
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 {
|
func getManifestFromContext(app *cli.App) *manifest.Manifest {
|
||||||
|
@ -325,9 +475,12 @@ func getPrintLogoFromContext(app *cli.App) bool {
|
||||||
return app.Metadata[printLogoKey].(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) {
|
func setManifestInContext(app *cli.App, m *manifest.Manifest) {
|
||||||
old := getManifestFromContext(app)
|
app.Metadata[manifestKey] = m
|
||||||
*old = *m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkVMIsReady(app *cli.App) bool {
|
func checkVMIsReady(app *cli.App) bool {
|
||||||
|
@ -340,6 +493,7 @@ func checkVMIsReady(app *cli.App) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleExit(c *cli.Context) error {
|
func handleExit(c *cli.Context) error {
|
||||||
|
finalizeInteropContext(c.App)
|
||||||
l := getReadlineInstanceFromContext(c.App)
|
l := getReadlineInstanceFromContext(c.App)
|
||||||
_ = l.Close()
|
_ = l.Close()
|
||||||
exit := getExitFuncFromContext(c.App)
|
exit := getExitFuncFromContext(c.App)
|
||||||
|
@ -418,7 +572,22 @@ func handleSlots(c *cli.Context) error {
|
||||||
return nil
|
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 {
|
func handleLoadNEF(c *cli.Context) error {
|
||||||
|
err := prepareVM(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
v := getVMFromContext(c.App)
|
v := getVMFromContext(c.App)
|
||||||
args := c.Args()
|
args := c.Args()
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
|
@ -438,6 +607,10 @@ func handleLoadNEF(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLoadBase64(c *cli.Context) error {
|
func handleLoadBase64(c *cli.Context) error {
|
||||||
|
err := prepareVM(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
v := getVMFromContext(c.App)
|
v := getVMFromContext(c.App)
|
||||||
args := c.Args()
|
args := c.Args()
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
|
@ -454,6 +627,10 @@ func handleLoadBase64(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLoadHex(c *cli.Context) error {
|
func handleLoadHex(c *cli.Context) error {
|
||||||
|
err := prepareVM(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
v := getVMFromContext(c.App)
|
v := getVMFromContext(c.App)
|
||||||
args := c.Args()
|
args := c.Args()
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
|
@ -470,6 +647,10 @@ func handleLoadHex(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLoadGo(c *cli.Context) error {
|
func handleLoadGo(c *cli.Context) error {
|
||||||
|
err := prepareVM(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
v := getVMFromContext(c.App)
|
v := getVMFromContext(c.App)
|
||||||
args := c.Args()
|
args := c.Args()
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
|
@ -496,11 +677,61 @@ func handleLoadGo(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleReset(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)
|
changePrompt(c.App)
|
||||||
return nil
|
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) {
|
func getManifestFromFile(name string) (*manifest.Manifest, error) {
|
||||||
bs, err := os.ReadFile(name)
|
bs, err := os.ReadFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -531,6 +762,9 @@ func handleRun(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if runCurrent {
|
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))
|
md := m.ABI.GetMethod(args[0], len(params))
|
||||||
if md == nil {
|
if md == nil {
|
||||||
return fmt.Errorf("%w: method not found", ErrInvalidParameter)
|
return fmt.Errorf("%w: method not found", ErrInvalidParameter)
|
||||||
|
@ -563,12 +797,17 @@ func runVMWithHandling(c *cli.Context) {
|
||||||
writeErr(c.App.ErrWriter, err)
|
writeErr(c.App.ErrWriter, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var message string
|
var (
|
||||||
|
message string
|
||||||
|
dumpNtf bool
|
||||||
|
)
|
||||||
switch {
|
switch {
|
||||||
case v.HasFailed():
|
case v.HasFailed():
|
||||||
message = "" // the error will be printed on return
|
message = "" // the error will be printed on return
|
||||||
|
dumpNtf = true
|
||||||
case v.HasHalted():
|
case v.HasHalted():
|
||||||
message = v.DumpEStack()
|
message = v.DumpEStack()
|
||||||
|
dumpNtf = true
|
||||||
case v.AtBreakpoint():
|
case v.AtBreakpoint():
|
||||||
ctx := v.Context()
|
ctx := v.Context()
|
||||||
if ctx.NextIP() < ctx.LenInstr() {
|
if ctx.NextIP() < ctx.LenInstr() {
|
||||||
|
@ -578,6 +817,16 @@ func runVMWithHandling(c *cli.Context) {
|
||||||
message = "execution has finished"
|
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 != "" {
|
if message != "" {
|
||||||
fmt.Fprintln(c.App.Writer, 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.
|
// 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) {
|
if getPrintLogoFromContext(c.shell) {
|
||||||
printLogo(c.shell.Writer)
|
printLogo(c.shell.Writer)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package cli
|
package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -15,12 +15,20 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"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/internal/random"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"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/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/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/neotest/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/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
@ -54,7 +62,7 @@ func (r *readCloser) WriteString(s string) {
|
||||||
type executor struct {
|
type executor struct {
|
||||||
in *readCloser
|
in *readCloser
|
||||||
out *bytes.Buffer
|
out *bytes.Buffer
|
||||||
cli *VMCLI
|
cli *CLI
|
||||||
ch chan struct{}
|
ch chan struct{}
|
||||||
exit atomic.Bool
|
exit atomic.Bool
|
||||||
}
|
}
|
||||||
|
@ -64,12 +72,27 @@ func newTestVMCLI(t *testing.T) *executor {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *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{
|
e := &executor{
|
||||||
in: &readCloser{Buffer: *bytes.NewBuffer(nil)},
|
in: &readCloser{Buffer: *bytes.NewBuffer(nil)},
|
||||||
out: bytes.NewBuffer(nil),
|
out: bytes.NewBuffer(nil),
|
||||||
ch: make(chan struct{}),
|
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) },
|
func(int) { e.exit.Store(true) },
|
||||||
&readline.Config{
|
&readline.Config{
|
||||||
Prompt: "",
|
Prompt: "",
|
||||||
|
@ -79,10 +102,40 @@ func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor {
|
||||||
FuncIsTerminal: func() bool {
|
FuncIsTerminal: func() bool {
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
})
|
}, c)
|
||||||
|
require.NoError(t, err)
|
||||||
return e
|
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) {
|
func (e *executor) runProg(t *testing.T, commands ...string) {
|
||||||
e.runProgWithTimeout(t, 4*time.Second, commands...)
|
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)
|
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) {
|
func (e *executor) checkError(t *testing.T, expectedErr error) {
|
||||||
line, err := e.out.ReadString('\n')
|
line, err := e.out.ReadString('\n')
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -139,6 +198,50 @@ func (e *executor) checkStack(t *testing.T, items ...interface{}) {
|
||||||
require.NoError(t, err)
|
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{}) {
|
func (e *executor) checkSlot(t *testing.T, items ...interface{}) {
|
||||||
d := json.NewDecoder(e.out)
|
d := json.NewDecoder(e.out)
|
||||||
var actual interface{}
|
var actual interface{}
|
||||||
|
@ -251,7 +354,7 @@ go 1.17`)
|
||||||
require (
|
require (
|
||||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0
|
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`)
|
go 1.17`)
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm))
|
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.checkNextLine(t, "")
|
||||||
e.checkError(t, fmt.Errorf("VM is not ready: no program loaded"))
|
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
|
package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
"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"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCommands returns 'vm' command.
|
// NewCommands returns 'vm' command.
|
||||||
func NewCommands() []cli.Command {
|
func NewCommands() []cli.Command {
|
||||||
|
cfgFlags := []cli.Flag{options.Config}
|
||||||
|
cfgFlags = append(cfgFlags, options.Network...)
|
||||||
return []cli.Command{{
|
return []cli.Command{{
|
||||||
Name: "vm",
|
Name: "vm",
|
||||||
Usage: "start the virtual machine",
|
Usage: "start the virtual machine",
|
||||||
Action: startVMPrompt,
|
Action: startVMPrompt,
|
||||||
Flags: []cli.Flag{
|
Flags: cfgFlags,
|
||||||
cli.BoolFlag{Name: "debug, d"},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +27,22 @@ func startVMPrompt(ctx *cli.Context) error {
|
||||||
if err := cmdargs.EnsureNone(ctx); err != nil {
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
||||||
return err
|
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()
|
return p.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,14 +47,20 @@ DBConfiguration:
|
||||||
Type: leveldb
|
Type: leveldb
|
||||||
LevelDBOptions:
|
LevelDBOptions:
|
||||||
DataDirectoryPath: /chains/privnet
|
DataDirectoryPath: /chains/privnet
|
||||||
|
ReadOnly: false
|
||||||
BoltDBOptions:
|
BoltDBOptions:
|
||||||
FilePath: ./chains/privnet.bolt
|
FilePath: ./chains/privnet.bolt
|
||||||
|
ReadOnly: false
|
||||||
```
|
```
|
||||||
where:
|
where:
|
||||||
- `Type` is the database type (string value). Supported types: `levelDB` and
|
- `Type` is the database type (string value). Supported types: `leveldb`, `boltdb` and
|
||||||
`boltDB`.
|
`inmemory` (not recommended for production usage).
|
||||||
- `LevelDBOptions` are settings for LevelDB.
|
- `LevelDBOptions` are settings for LevelDB. Includes the DB files path and ReadOnly mode toggle.
|
||||||
- `BoltDBOptions` configures BoltDB.
|
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.
|
Only options for the specified database type will be used.
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,39 @@ import (
|
||||||
|
|
||||||
const neoAmount = 99999000
|
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
|
// Init pushes some predefined set of transactions into the given chain, it needs a path to
|
||||||
// the root project directory.
|
// the root project directory.
|
||||||
func Init(t *testing.T, rootpath string, e *neotest.Executor) {
|
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.
|
// 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")
|
panic("TODO")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ const (
|
||||||
defaultMaxBlockSystemFee = 900000000000
|
defaultMaxBlockSystemFee = 900000000000
|
||||||
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
|
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
|
||||||
defaultMaxTransactionsPerBlock = 512
|
defaultMaxTransactionsPerBlock = 512
|
||||||
|
defaultSecondsPerBlock = 15
|
||||||
// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
|
// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
|
||||||
HeaderVerificationGasLimit = 3_00000000 // 3 GAS
|
HeaderVerificationGasLimit = 3_00000000 // 3 GAS
|
||||||
defaultStateSyncInterval = 40000
|
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",
|
log.Info("MaxTransactionsPerBlock is not set or wrong, using default value",
|
||||||
zap.Uint16("MaxTransactionsPerBlock", cfg.MaxTransactionsPerBlock))
|
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 {
|
if cfg.MaxValidUntilBlockIncrement == 0 {
|
||||||
const secondsPerDay = int(24 * time.Hour / time.Second)
|
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.
|
// 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 := bc.newInteropContext(t, bc.dao, b, tx)
|
||||||
_ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready.
|
_ = 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.
|
// 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 {
|
if bc.config.KeepOnlyLatestState {
|
||||||
return nil, errors.New("only latest state is supported")
|
return nil, errors.New("only latest state is supported")
|
||||||
}
|
}
|
||||||
if b == nil {
|
b, err := bc.getFakeNextBlock(nextBlockHeight)
|
||||||
return nil, errors.New("block is mandatory to produce test historic VM")
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create fake block for height %d: %w", nextBlockHeight, err)
|
||||||
}
|
}
|
||||||
var mode = mpt.ModeAll
|
var mode = mpt.ModeAll
|
||||||
if bc.config.RemoveUntraceableBlocks {
|
if bc.config.RemoveUntraceableBlocks {
|
||||||
|
@ -2278,6 +2293,18 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact
|
||||||
return systemInterop, nil
|
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.
|
// Various witness verification errors.
|
||||||
var (
|
var (
|
||||||
ErrWitnessHashMismatch = errors.New("witness hash mismatch")
|
ErrWitnessHashMismatch = errors.New("witness hash mismatch")
|
||||||
|
|
|
@ -32,7 +32,8 @@ var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal",
|
||||||
|
|
||||||
func TestGetCallFlags(t *testing.T) {
|
func TestGetCallFlags(t *testing.T) {
|
||||||
bc, _ := chain.NewSingle(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)
|
ic.VM.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All)
|
||||||
require.NoError(t, contract.GetCallFlags(ic))
|
require.NoError(t, contract.GetCallFlags(ic))
|
||||||
|
@ -41,7 +42,8 @@ func TestGetCallFlags(t *testing.T) {
|
||||||
|
|
||||||
func TestCall(t *testing.T) {
|
func TestCall(t *testing.T) {
|
||||||
bc, _ := chain.NewSingle(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
|
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))
|
require.NoError(t, native.PutContractState(ic.DAO, cs))
|
||||||
|
|
|
@ -19,6 +19,9 @@ const (
|
||||||
MaxEventNameLen = 32
|
MaxEventNameLen = 32
|
||||||
// MaxNotificationSize is the maximum length of a runtime log message.
|
// MaxNotificationSize is the maximum length of a runtime log message.
|
||||||
MaxNotificationSize = 1024
|
MaxNotificationSize = 1024
|
||||||
|
// SystemRuntimeLogMessage represents log entry message used for output
|
||||||
|
// of the System.Runtime.Log syscall.
|
||||||
|
SystemRuntimeLogMessage = "runtime log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetExecutingScriptHash returns executing script hash.
|
// GetExecutingScriptHash returns executing script hash.
|
||||||
|
@ -112,7 +115,7 @@ func Log(ic *interop.Context) error {
|
||||||
if ic.Tx != nil {
|
if ic.Tx != nil {
|
||||||
txHash = ic.Tx.Hash().StringLE()
|
txHash = ic.Tx.Hash().StringLE()
|
||||||
}
|
}
|
||||||
ic.Log.Info("runtime log",
|
ic.Log.Info(SystemRuntimeLogMessage,
|
||||||
zap.String("tx", txHash),
|
zap.String("tx", txHash),
|
||||||
zap.String("script", ic.VM.GetCurrentScriptHash().StringLE()),
|
zap.String("script", ic.VM.GetCurrentScriptHash().StringLE()),
|
||||||
zap.String("msg", state))
|
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) {
|
func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) {
|
||||||
chain, _ := chain.NewSingle(t)
|
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()
|
v := ic.SpawnVM()
|
||||||
return v, ic, chain
|
return v, ic, chain
|
||||||
}
|
}
|
||||||
|
@ -523,7 +524,8 @@ func TestGetRandomCompatibility(t *testing.T) {
|
||||||
|
|
||||||
b := getSharpTestGenesis(t)
|
b := getSharpTestGenesis(t)
|
||||||
tx := getSharpTestTx(util.Uint160{})
|
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.Network = 860833102 // Old mainnet magic used by C# tests.
|
||||||
|
|
||||||
ic.VM = vm.New()
|
ic.VM = vm.New()
|
||||||
|
@ -550,7 +552,8 @@ func TestNotify(t *testing.T) {
|
||||||
caller := random.Uint160()
|
caller := random.Uint160()
|
||||||
newIC := func(name string, args interface{}) *interop.Context {
|
newIC := func(name string, args interface{}) *interop.Context {
|
||||||
_, _, bc, cs := getDeployedInternal(t)
|
_, _, 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.LoadNEFMethod(&cs.NEF, caller, cs.Hash, callflag.NoneFlag, true, 0, -1, nil)
|
||||||
ic.VM.Estack().PushVal(args)
|
ic.VM.Estack().PushVal(args)
|
||||||
ic.VM.Estack().PushVal(name)
|
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) {
|
func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) {
|
||||||
chain, _ := chain.NewSingle(t)
|
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()
|
v := ic.SpawnVM()
|
||||||
return v, ic, chain
|
return v, ic, chain
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,8 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
|
||||||
require.NotNil(t, md)
|
require.NotNil(t, md)
|
||||||
|
|
||||||
t.Run("fail, bad current script hash", func(t *testing.T) {
|
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()
|
v := ic.SpawnVM()
|
||||||
fakeH := util.Uint160{1, 2, 3}
|
fakeH := util.Uint160{1, 2, 3}
|
||||||
v.LoadScriptWithHash(clState.NEF.Script, fakeH, callflag.All)
|
v.LoadScriptWithHash(clState.NEF.Script, fakeH, callflag.All)
|
||||||
|
@ -83,7 +84,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
|
||||||
v.Context().Jump(md.Offset)
|
v.Context().Jump(md.Offset)
|
||||||
|
|
||||||
// Bad current script hash
|
// Bad current script hash
|
||||||
err := v.Run()
|
err = v.Run()
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s (version 0) not found", fakeH.StringLE())), err.Error())
|
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)
|
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 := ic.SpawnVM()
|
||||||
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
|
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
|
||||||
input := []byte{1, 2, 3, 4}
|
input := []byte{1, 2, 3, 4}
|
||||||
|
@ -112,13 +114,14 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
|
||||||
v.Context().Jump(md.Offset)
|
v.Context().Jump(md.Offset)
|
||||||
|
|
||||||
// It's prohibited to call natives before NativeUpdateHistory[0] height.
|
// It's prohibited to call natives before NativeUpdateHistory[0] height.
|
||||||
err := v.Run()
|
err = v.Run()
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.True(t, strings.Contains(err.Error(), "native contract CryptoLib is active after height = 1"))
|
require.True(t, strings.Contains(err.Error(), "native contract CryptoLib is active after height = 1"))
|
||||||
|
|
||||||
// Add new block => CryptoLib should be active now.
|
// Add new block => CryptoLib should be active now.
|
||||||
eBad.AddNewBlock(t)
|
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 = ic.SpawnVM()
|
||||||
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
|
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
|
||||||
v.Estack().PushVal(input)
|
v.Estack().PushVal(input)
|
||||||
|
@ -130,7 +133,8 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("success", func(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 := ic.SpawnVM()
|
||||||
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All)
|
v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All)
|
||||||
input := []byte{1, 2, 3, 4}
|
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.
|
// Create database to be able to start another chain from the same height later.
|
||||||
ldbDir := t.TempDir()
|
ldbDir := t.TempDir()
|
||||||
dbConfig := dbconfig.DBConfiguration{
|
dbConfig := dbconfig.DBConfiguration{
|
||||||
Type: "leveldb",
|
Type: dbconfig.LevelDB,
|
||||||
LevelDBOptions: dbconfig.LevelDBOptions{
|
LevelDBOptions: dbconfig.LevelDBOptions{
|
||||||
DataDirectoryPath: ldbDir,
|
DataDirectoryPath: ldbDir,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
@ -22,23 +23,41 @@ type BoltDBStore struct {
|
||||||
|
|
||||||
// NewBoltDBStore returns a new ready to use BoltDB storage with created bucket.
|
// NewBoltDBStore returns a new ready to use BoltDB storage with created bucket.
|
||||||
func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) {
|
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
|
fileMode := os.FileMode(0600) // should be exposed via BoltDBOptions if anything needed
|
||||||
fileName := cfg.FilePath
|
fileName := cfg.FilePath
|
||||||
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
|
if cfg.ReadOnly {
|
||||||
return nil, err
|
opts.ReadOnly = true
|
||||||
|
} else {
|
||||||
|
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
db, err := bbolt.Open(fileName, fileMode, opts)
|
db, err := bbolt.Open(fileName, fileMode, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to open BoltDB instance: %w", err)
|
||||||
|
}
|
||||||
|
if opts.ReadOnly {
|
||||||
|
err = db.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket(Bucket)
|
||||||
|
if b == nil {
|
||||||
|
return errors.New("root bucket does not exist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
err = db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
_, err = tx.CreateBucketIfNotExists(Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create root bucket: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err)
|
||||||
}
|
}
|
||||||
err = db.Update(func(tx *bbolt.Tx) error {
|
|
||||||
_, err = tx.CreateBucketIfNotExists(Bucket)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create root bucket: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return &BoltDBStore{db: db}, nil
|
return &BoltDBStore{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newBoltStoreForTesting(t testing.TB) Store {
|
func newBoltStoreForTesting(t testing.TB) Store {
|
||||||
|
@ -15,3 +19,42 @@ func newBoltStoreForTesting(t testing.TB) Store {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return boltDBStore
|
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 configuration for LevelDB.
|
||||||
LevelDBOptions struct {
|
LevelDBOptions struct {
|
||||||
DataDirectoryPath string `yaml:"DataDirectoryPath"`
|
DataDirectoryPath string `yaml:"DataDirectoryPath"`
|
||||||
|
ReadOnly bool `yaml:"ReadOnly"`
|
||||||
}
|
}
|
||||||
// BoltDBOptions configuration for BoltDB.
|
// BoltDBOptions configuration for BoltDB.
|
||||||
BoltDBOptions struct {
|
BoltDBOptions struct {
|
||||||
FilePath string `yaml:"FilePath"`
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
@ -21,11 +22,14 @@ type LevelDBStore struct {
|
||||||
// initialize the database found at the given path.
|
// initialize the database found at the given path.
|
||||||
func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) {
|
func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) {
|
||||||
var opts = new(opt.Options) // should be exposed via LevelDBOptions if anything needed
|
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)
|
opts.Filter = filter.NewBloomFilter(10)
|
||||||
db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts)
|
db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to open LevelDB instance: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LevelDBStore{
|
return &LevelDBStore{
|
||||||
|
|
|
@ -5,17 +5,41 @@ import (
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLevelDBForTesting(t testing.TB) Store {
|
func newLevelDBForTesting(t testing.TB) Store {
|
||||||
ldbDir := t.TempDir()
|
ldbDir := t.TempDir()
|
||||||
dbConfig := dbconfig.DBConfiguration{
|
opts := dbconfig.LevelDBOptions{
|
||||||
Type: "leveldb",
|
DataDirectoryPath: ldbDir,
|
||||||
LevelDBOptions: dbconfig.LevelDBOptions{
|
|
||||||
DataDirectoryPath: ldbDir,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
newLevelStore, err := NewLevelDBStore(dbConfig.LevelDBOptions)
|
newLevelStore, err := NewLevelDBStore(opts)
|
||||||
require.Nil(t, err, "NewLevelDBStore error")
|
require.Nil(t, err, "NewLevelDBStore error")
|
||||||
return newLevelStore
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ps.Seek(rng, mergeFunc)
|
if rng.SearchDepth == 0 || rng.SearchDepth > 1 {
|
||||||
|
if rng.SearchDepth > 1 {
|
||||||
|
rng.SearchDepth--
|
||||||
|
}
|
||||||
|
ps.Seek(rng, mergeFunc)
|
||||||
|
}
|
||||||
|
|
||||||
if !done && haveMem {
|
if !done && haveMem {
|
||||||
loop:
|
loop:
|
||||||
|
|
|
@ -59,6 +59,11 @@ type SeekRange struct {
|
||||||
// whether seeking should be performed in a descending way.
|
// whether seeking should be performed in a descending way.
|
||||||
// Backwards can be safely combined with Prefix and Start.
|
// Backwards can be safely combined with Prefix and Start.
|
||||||
Backwards bool
|
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
|
// ErrKeyNotFound is an error returned by Store implementations
|
||||||
|
@ -114,11 +119,11 @@ func NewStore(cfg dbconfig.DBConfiguration) (Store, error) {
|
||||||
var store Store
|
var store Store
|
||||||
var err error
|
var err error
|
||||||
switch cfg.Type {
|
switch cfg.Type {
|
||||||
case "leveldb":
|
case dbconfig.LevelDB:
|
||||||
store, err = NewLevelDBStore(cfg.LevelDBOptions)
|
store, err = NewLevelDBStore(cfg.LevelDBOptions)
|
||||||
case "inmemory":
|
case dbconfig.InMemoryDB:
|
||||||
store = NewMemoryStore()
|
store = NewMemoryStore()
|
||||||
case "boltdb":
|
case dbconfig.BoltDB:
|
||||||
store, err = NewBoltDBStore(cfg.BoltDBOptions)
|
store, err = NewBoltDBStore(cfg.BoltDBOptions)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown storage: %s", cfg.Type)
|
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.
|
// `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.
|
// This is unwanted behavior, so we explicitly copy the transaction to perform execution.
|
||||||
ttx := *tx
|
ttx := *tx
|
||||||
ic := bc.GetTestVM(trigger.Application, &ttx, b)
|
ic, _ := bc.GetTestVM(trigger.Application, &ttx, b)
|
||||||
|
|
||||||
defer ic.Finalize()
|
defer ic.Finalize()
|
||||||
|
|
||||||
ic.VM.LoadWithFlags(tx.Script, callflag.All)
|
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) {
|
func (c *ContractInvoker) TestInvoke(t testing.TB, method string, args ...interface{}) (*vm.Stack, error) {
|
||||||
tx := c.PrepareInvokeNoSign(t, method, args...)
|
tx := c.PrepareInvokeNoSign(t, method, args...)
|
||||||
b := c.NewUnsignedBlock(t, tx)
|
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)
|
t.Cleanup(ic.Finalize)
|
||||||
|
|
||||||
ic.VM.LoadWithFlags(tx.Script, callflag.All)
|
ic.VM.LoadWithFlags(tx.Script, callflag.All)
|
||||||
err := ic.VM.Run()
|
err = ic.VM.Run()
|
||||||
return ic.VM.Estack(), err
|
return ic.VM.Estack(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ type (
|
||||||
GetBaseExecFee() int64
|
GetBaseExecFee() int64
|
||||||
GetConfig() config.ProtocolConfiguration
|
GetConfig() config.ProtocolConfiguration
|
||||||
GetMaxVerificationGAS() int64
|
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)
|
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package oracle
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
gio "io"
|
gio "io"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
"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)
|
size := io.GetVarSize(tx)
|
||||||
tx.Scripts = append(tx.Scripts, transaction.Witness{VerificationScript: oracleSignContract})
|
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 {
|
if !ok {
|
||||||
return nil, errors.New("can't verify transaction")
|
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
|
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
|
// (*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.
|
// 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.
|
// So, make a copy of the tx to avoid wrong hash caching.
|
||||||
cp := *tx
|
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.GasLimit = o.Chain.GetMaxVerificationGAS()
|
||||||
ic.VM.LoadScriptWithHash(o.oracleScript, o.oracleHash, callflag.ReadOnly)
|
ic.VM.LoadScriptWithHash(o.oracleScript, o.oracleHash, callflag.ReadOnly)
|
||||||
ic.VM.Context().Jump(o.verifyOffset)
|
ic.VM.Context().Jump(o.verifyOffset)
|
||||||
|
|
||||||
ok := isVerifyOk(ic)
|
ok := isVerifyOk(ic)
|
||||||
return ic.VM.GasConsumed(), ok
|
return ic.VM.GasConsumed(), ok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isVerifyOk(ic *interop.Context) bool {
|
func isVerifyOk(ic *interop.Context) bool {
|
||||||
|
|
|
@ -1181,7 +1181,8 @@ func TestCreateNEP17TransferTx(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, acc.SignTx(testchain.Network(), tx))
|
require.NoError(t, acc.SignTx(testchain.Network(), tx))
|
||||||
require.NoError(t, chain.VerifyTx(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)
|
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
})
|
})
|
||||||
|
@ -1195,7 +1196,8 @@ func TestCreateNEP17TransferTx(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, chain.VerifyTx(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)
|
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, len(ic.Notifications))
|
require.Equal(t, 2, len(ic.Notifications))
|
||||||
|
@ -1228,7 +1230,8 @@ func TestCreateNEP17TransferTx(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, acc.SignTx(testchain.Network(), tx))
|
require.NoError(t, acc.SignTx(testchain.Network(), tx))
|
||||||
require.NoError(t, chain.VerifyTx(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)
|
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
})
|
})
|
||||||
|
|
|
@ -89,8 +89,8 @@ type (
|
||||||
GetNotaryServiceFeePerKey() int64
|
GetNotaryServiceFeePerKey() int64
|
||||||
GetStateModule() core.StateRoot
|
GetStateModule() core.StateRoot
|
||||||
GetStorageItem(id int32, key []byte) state.StorageItem
|
GetStorageItem(id int32, key []byte) state.StorageItem
|
||||||
GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error)
|
GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, nextBlockHeight uint32) (*interop.Context, error)
|
||||||
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)
|
||||||
GetTokenLastUpdated(acc util.Uint160) (map[int32]uint32, error)
|
GetTokenLastUpdated(acc util.Uint160) (map[int32]uint32, error)
|
||||||
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
|
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
|
||||||
GetValidators() ([]*keys.PublicKey, error)
|
GetValidators() ([]*keys.PublicKey, error)
|
||||||
|
@ -1065,11 +1065,10 @@ func (s *Server) invokeReadOnlyMulti(bw *io.BufBinWriter, h util.Uint160, method
|
||||||
}
|
}
|
||||||
script := bw.Bytes()
|
script := bw.Bytes()
|
||||||
tx := &transaction.Transaction{Script: script}
|
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 {
|
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.GasLimit = core.HeaderVerificationGasLimit
|
||||||
ic.VM.LoadScriptWithFlags(script, callflag.All)
|
ic.VM.LoadScriptWithFlags(script, callflag.All)
|
||||||
err = ic.VM.Run()
|
err = ic.VM.Run()
|
||||||
|
@ -1832,7 +1831,7 @@ func (s *Server) invokeFunction(reqParams params.Params) (interface{}, *neorpc.E
|
||||||
|
|
||||||
// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call.
|
// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call.
|
||||||
func (s *Server) invokeFunctionHistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
|
func (s *Server) invokeFunctionHistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
|
||||||
b, respErr := s.getHistoricParams(reqParams)
|
nextH, respErr := s.getHistoricParams(reqParams)
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
return nil, respErr
|
||||||
}
|
}
|
||||||
|
@ -1843,7 +1842,7 @@ func (s *Server) invokeFunctionHistoric(reqParams params.Params) (interface{}, *
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
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) {
|
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.
|
// invokescripthistoric implements the `invokescripthistoric` RPC call.
|
||||||
func (s *Server) invokescripthistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
|
func (s *Server) invokescripthistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
|
||||||
b, respErr := s.getHistoricParams(reqParams)
|
nextH, respErr := s.getHistoricParams(reqParams)
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
return nil, respErr
|
||||||
}
|
}
|
||||||
|
@ -1910,7 +1909,7 @@ func (s *Server) invokescripthistoric(reqParams params.Params) (interface{}, *ne
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
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) {
|
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.
|
// invokeContractVerifyHistoric implements the `invokecontractverifyhistoric` RPC call.
|
||||||
func (s *Server) invokeContractVerifyHistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
|
func (s *Server) invokeContractVerifyHistoric(reqParams params.Params) (interface{}, *neorpc.Error) {
|
||||||
b, respErr := s.getHistoricParams(reqParams)
|
nextH, respErr := s.getHistoricParams(reqParams)
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
return nil, respErr
|
||||||
}
|
}
|
||||||
|
@ -1964,7 +1963,7 @@ func (s *Server) invokeContractVerifyHistoric(reqParams params.Params) (interfac
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
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) {
|
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
|
return scriptHash, tx, invocationScript, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHistoricParams checks that historic calls are supported and returns fake block
|
// getHistoricParams checks that historic calls are supported and returns index of
|
||||||
// with the specified index to perform the historic call. It also checks that
|
// a fake next block to perform the historic call. It also checks that
|
||||||
// specified stateroot is stored at the specified height for further request
|
// specified stateroot is stored at the specified height for further request
|
||||||
// handling consistency.
|
// 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 {
|
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 {
|
if len(reqParams) < 1 {
|
||||||
return nil, neorpc.ErrInvalidParams
|
return 0, neorpc.ErrInvalidParams
|
||||||
}
|
}
|
||||||
height, respErr := s.blockHeightFromParam(reqParams.Value(0))
|
height, respErr := s.blockHeightFromParam(reqParams.Value(0))
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
hash, err := reqParams.Value(0).GetUint256()
|
hash, err := reqParams.Value(0).GetUint256()
|
||||||
if err != nil {
|
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)
|
b, err := s.chain.GetBlock(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash)
|
stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash)
|
||||||
if err != nil {
|
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)
|
height = int(stateH)
|
||||||
} else {
|
} else {
|
||||||
height = int(b.Index)
|
height = int(b.Index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b, err := s.getFakeNextBlock(uint32(height + 1))
|
if height > math.MaxUint32 {
|
||||||
if err != nil {
|
return 0, neorpc.NewInvalidParamsError("historic height exceeds max uint32 value")
|
||||||
return nil, neorpc.NewInternalServerError(fmt.Sprintf("can't create fake block for height %d: %s", height+1, err))
|
|
||||||
}
|
}
|
||||||
return b, nil
|
return uint32(height) + 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) {
|
func (s *Server) prepareInvocationContext(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, nextH *uint32, verbose bool) (*interop.Context, *neorpc.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) {
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
ic *interop.Context
|
ic *interop.Context
|
||||||
)
|
)
|
||||||
if b == nil {
|
if nextH == nil {
|
||||||
b, err = s.getFakeNextBlock(s.chain.BlockHeight() + 1)
|
ic, err = s.chain.GetTestVM(t, tx, nil)
|
||||||
if err != 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 {
|
} else {
|
||||||
ic, err = s.chain.GetTestHistoricVM(t, tx, b)
|
ic, err = s.chain.GetTestHistoricVM(t, tx, *nextH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to create historic VM: %s", err))
|
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`
|
// witness invocation script in case of `verification` trigger (it pushes `verify`
|
||||||
// arguments on stack before verification). In case of contract verification
|
// arguments on stack before verification). In case of contract verification
|
||||||
// contractScriptHash should be specified.
|
// 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) {
|
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, b, verbose)
|
ic, respErr := s.prepareInvocationContext(t, script, contractScriptHash, tx, nextH, verbose)
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return nil, respErr
|
return nil, respErr
|
||||||
}
|
}
|
||||||
|
@ -2111,16 +2094,12 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
|
|
||||||
if sess != nil {
|
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.
|
// the second attempt won't stop here.
|
||||||
if s.config.SessionBackedByMPT && b == nil {
|
if s.config.SessionBackedByMPT && nextH == nil {
|
||||||
ic.Finalize()
|
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.
|
// 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()
|
id = uuid.New()
|
||||||
sessionID := id.String()
|
sessionID := id.String()
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
// where n is the length of publicKeys.
|
// where n is the length of publicKeys.
|
||||||
func CreateMultiSigRedeemScript(m int, publicKeys keys.PublicKeys) ([]byte, error) {
|
func CreateMultiSigRedeemScript(m int, publicKeys keys.PublicKeys) ([]byte, error) {
|
||||||
if m < 1 {
|
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) {
|
if m > len(publicKeys) {
|
||||||
return nil, fmt.Errorf("length of the signatures (%d) is higher then the number of public keys", m)
|
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{
|
vm := &VM{
|
||||||
state: vmstate.None,
|
state: vmstate.None,
|
||||||
trigger: t,
|
trigger: t,
|
||||||
|
|
||||||
SyscallHandler: defaultSyscallHandler,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initStack(&vm.istack, "invocation", nil)
|
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:
|
case opcode.SYSCALL:
|
||||||
interopID := GetInteropID(parameter)
|
interopID := GetInteropID(parameter)
|
||||||
|
if v.SyscallHandler == nil {
|
||||||
|
panic("vm's SyscallHandler is not initialized")
|
||||||
|
}
|
||||||
err := v.SyscallHandler(v, interopID)
|
err := v.SyscallHandler(v, interopID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("failed to invoke syscall %d: %s", interopID, err))
|
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/encoding/bigint"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"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/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/emit"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"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))
|
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 {
|
func makeProgram(opcodes ...opcode.Opcode) []byte {
|
||||||
prog := make([]byte, len(opcodes)+1) // RET
|
prog := make([]byte, len(opcodes)+1) // RET
|
||||||
for i := 0; i < len(opcodes); i++ {
|
for i := 0; i < len(opcodes); i++ {
|
||||||
|
|
Loading…
Reference in a new issue