diff --git a/cli/vm/cli.go b/cli/vm/cli.go index f7f210966..d463ffcdb 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -19,20 +19,27 @@ import ( "github.com/kballard/go-shellquote" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/urfave/cli" + "go.uber.org/zap" ) const ( - vmKey = "vm" + chainKey = "chain" + icKey = "ic" manifestKey = "manifest" exitFuncKey = "exitFunc" readlineInstanceKey = "readlineKey" @@ -245,26 +252,20 @@ var ( // VMCLI object for interacting with the VM. type VMCLI struct { - vm *vm.VM + chain *core.Blockchain shell *cli.App } -// New returns a new VMCLI object. -func New() *VMCLI { - return NewWithConfig(true, os.Exit, &readline.Config{ - Prompt: "\033[32mNEO-GO-VM >\033[0m ", // green prompt ^^ - }) -} - -// NewWithConfig returns new VMCLI instance using provided config. -func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VMCLI { +// NewWithConfig returns new VMCLI instance using provided config and (optionally) +// provided node config for state-backed VM. +func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config, cfg config.Config) (*VMCLI, error) { if c.AutoComplete == nil { // Autocomplete commands/flags on TAB. c.AutoComplete = completer } l, err := readline.NewEx(c) if err != nil { - panic(err) + return nil, fmt.Errorf("failed to create readline instance: %w", err) } ctl := cli.NewApp() ctl.Name = "VM CLI" @@ -284,20 +285,41 @@ func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VM ctl.Commands = commands + store, err := storage.NewStore(cfg.ApplicationConfiguration.DBConfiguration) + if err != nil { + writeErr(ctl.ErrWriter, fmt.Errorf("failed to open DB, clean in-memory storage will be used: %w", err)) + cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB + store = storage.NewMemoryStore() + } + + exitF := func(i int) { + _ = store.Close() + onExit(i) + } + + log := zap.NewNop() + chain, err := core.NewBlockchain(store, cfg.ProtocolConfiguration, log) + 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 := chain.GetTestVM(trigger.Application, nil, nil) + vmcli := VMCLI{ - vm: vm.New(), + chain: chain, shell: ctl, } vmcli.shell.Metadata = map[string]interface{}{ - vmKey: vmcli.vm, + chainKey: chain, + icKey: ic, manifestKey: new(manifest.Manifest), - exitFuncKey: onExit, + exitFuncKey: exitF, readlineInstanceKey: l, printLogoKey: printLogotype, } changePrompt(vmcli.shell) - return &vmcli + return &vmcli, nil } func getExitFuncFromContext(app *cli.App) func(int) { @@ -309,12 +331,15 @@ func getReadlineInstanceFromContext(app *cli.App) *readline.Instance { } func getVMFromContext(app *cli.App) *vm.VM { - return app.Metadata[vmKey].(*vm.VM) + return getInteropContextFromContext(app).VM } -func setVMInContext(app *cli.App, v *vm.VM) { - old := getVMFromContext(app) - *old = *v +func getChainFromContext(app *cli.App) *core.Blockchain { + return app.Metadata[chainKey].(*core.Blockchain) +} + +func getInteropContextFromContext(app *cli.App) *interop.Context { + return app.Metadata[icKey].(*interop.Context) } func getManifestFromContext(app *cli.App) *manifest.Manifest { @@ -325,6 +350,10 @@ func getPrintLogoFromContext(app *cli.App) bool { return app.Metadata[printLogoKey].(bool) } +func setInteropContextInContext(app *cli.App, ic *interop.Context) { + app.Metadata[icKey] = ic +} + func setManifestInContext(app *cli.App, m *manifest.Manifest) { old := getManifestFromContext(app) *old = *m @@ -340,6 +369,7 @@ func checkVMIsReady(app *cli.App) bool { } func handleExit(c *cli.Context) error { + finalizeInteropContext(c.App) l := getReadlineInstanceFromContext(c.App) _ = l.Close() exit := getExitFuncFromContext(c.App) @@ -419,6 +449,7 @@ func handleSlots(c *cli.Context) error { } func handleLoadNEF(c *cli.Context) error { + resetInteropContext(c.App) v := getVMFromContext(c.App) args := c.Args() if len(args) < 2 { @@ -438,6 +469,7 @@ func handleLoadNEF(c *cli.Context) error { } func handleLoadBase64(c *cli.Context) error { + resetInteropContext(c.App) v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { @@ -454,6 +486,7 @@ func handleLoadBase64(c *cli.Context) error { } func handleLoadHex(c *cli.Context) error { + resetInteropContext(c.App) v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { @@ -470,6 +503,7 @@ func handleLoadHex(c *cli.Context) error { } func handleLoadGo(c *cli.Context) error { + resetInteropContext(c.App) v := getVMFromContext(c.App) args := c.Args() if len(args) < 1 { @@ -496,11 +530,26 @@ func handleLoadGo(c *cli.Context) error { } func handleReset(c *cli.Context) error { - setVMInContext(c.App, vm.New()) + resetInteropContext(c.App) changePrompt(c.App) return nil } +// finalizeInteropContext calls finalizer for the current interop context. +func finalizeInteropContext(app *cli.App) { + ic := getInteropContextFromContext(app) + ic.Finalize() +} + +// resetInteropContext calls finalizer for current interop context and replaces +// it with the newly created one. +func resetInteropContext(app *cli.App) { + finalizeInteropContext(app) + bc := getChainFromContext(app) + newIc := bc.GetTestVM(trigger.Application, nil, nil) + setInteropContextInContext(app, newIc) +} + func getManifestFromFile(name string) (*manifest.Manifest, error) { bs, err := os.ReadFile(name) if err != nil { diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index b49e4e274..a84141930 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -15,12 +15,18 @@ import ( "time" "github.com/chzyer/readline" + "github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -64,12 +70,27 @@ func newTestVMCLI(t *testing.T) *executor { } func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor { + return newTestVMCLIWithLogoAndCustomConfig(t, printLogo, nil) +} + +func newTestVMCLIWithLogoAndCustomConfig(t *testing.T, printLogo bool, cfg *config.Config) *executor { e := &executor{ in: &readCloser{Buffer: *bytes.NewBuffer(nil)}, out: bytes.NewBuffer(nil), ch: make(chan struct{}), } - e.cli = NewWithConfig(printLogo, + var c config.Config + if cfg == nil { + configPath := "../../config/protocol.unit_testnet.single.yml" + var err error + c, err = config.LoadFile(configPath) + require.NoError(t, err, "could not load chain config") + c.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB + } else { + c = *cfg + } + var err error + e.cli, err = NewWithConfig(printLogo, func(int) { e.exit.Store(true) }, &readline.Config{ Prompt: "", @@ -79,10 +100,40 @@ func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor { FuncIsTerminal: func() bool { return false }, - }) + }, c) + require.NoError(t, err) return e } +func newTestVMClIWithState(t *testing.T) *executor { + // Firstly create a DB with chain, save and close it. + path := t.TempDir() + opts := dbconfig.LevelDBOptions{ + DataDirectoryPath: path, + } + store, err := storage.NewLevelDBStore(opts) + require.NoError(t, err) + customConfig := func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true // Need for P2PStateExchangeExtensions check. + c.P2PSigExtensions = true // Need for basic chain initializer. + } + bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, store) + require.NoError(t, err) + go bc.Run() + e := neotest.NewExecutor(t, bc, validators, committee) + basicchain.InitSimple(t, "../../", e) + bc.Close() + + // After that create VMCLI backed by created chain. + configPath := "../../config/protocol.unit_testnet.yml" + cfg, err := config.LoadFile(configPath) + require.NoError(t, err) + cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB + cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions = opts + cfg.ProtocolConfiguration.StateRootInHeader = true + return newTestVMCLIWithLogoAndCustomConfig(t, false, &cfg) +} + func (e *executor) runProg(t *testing.T, commands ...string) { e.runProgWithTimeout(t, 4*time.Second, commands...) } @@ -662,3 +713,18 @@ func TestReset(t *testing.T) { e.checkNextLine(t, "") e.checkError(t, fmt.Errorf("VM is not ready: no program loaded")) } + +func TestRunWithState(t *testing.T) { + e := newTestVMClIWithState(t) + + // Ensure that state is properly loaded and on-chain contract can be called. + script := io.NewBufBinWriter() + h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go + require.NoError(t, err) + emit.AppCall(script.BinWriter, h, "put", callflag.All, 3, 3) + e.runProg(t, + "loadhex "+hex.EncodeToString(script.Bytes()), + "run") + e.checkNextLine(t, "READY: loaded 37 instructions") + e.checkStack(t, 3) +} diff --git a/cli/vm/vm.go b/cli/vm/vm.go index a5faf4441..23b9938de 100644 --- a/cli/vm/vm.go +++ b/cli/vm/vm.go @@ -1,20 +1,25 @@ package vm import ( + "fmt" "os" "github.com/chzyer/readline" "github.com/nspcc-dev/neo-go/cli/cmdargs" + "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/urfave/cli" ) // NewCommands returns 'vm' command. func NewCommands() []cli.Command { + cfgFlags := []cli.Flag{options.Config} + cfgFlags = append(cfgFlags, options.Network...) return []cli.Command{{ Name: "vm", Usage: "start the virtual machine", Action: startVMPrompt, - Flags: []cli.Flag{}, + Flags: cfgFlags, }} } @@ -22,6 +27,22 @@ func startVMPrompt(ctx *cli.Context) error { if err := cmdargs.EnsureNone(ctx); err != nil { return err } - p := 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() } diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index 304ff53c8..944008bfa 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -23,6 +23,31 @@ import ( const neoAmount = 99999000 +// InitSimple initializes chain with single contract storing several storage values. +// 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") + + // Block #1: deploy storage contract (examples/storage/storage.go). + _, storageHash := newDeployTx(t, e, e.Validator, + filepath.Join(examplesPrefix, "storage", "storage.go"), + filepath.Join(examplesPrefix, "storage", "storage.yml"), + true) + + // Block #2: put (1, 1) kv pair. + storageValidatorInvoker := e.ValidatorInvoker(storageHash) + 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) +} + // Init pushes some predefined set of transactions into the given chain, it needs a path to // the root project directory. func Init(t *testing.T, rootpath string, e *neotest.Executor) {