vm: allow to provide state for VM CLI

Close #2528.

Also, add new simple testchain as an analogue for basicchain.
This commit is contained in:
Anna Shaleva 2022-10-03 15:05:48 +03:00
parent 0b717b0c22
commit 513821cfff
4 changed files with 186 additions and 25 deletions

View file

@ -19,20 +19,27 @@ import (
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"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/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"
) )
const ( const (
vmKey = "vm" chainKey = "chain"
icKey = "ic"
manifestKey = "manifest" manifestKey = "manifest"
exitFuncKey = "exitFunc" exitFuncKey = "exitFunc"
readlineInstanceKey = "readlineKey" readlineInstanceKey = "readlineKey"
@ -245,26 +252,20 @@ var (
// VMCLI object for interacting with the VM. // VMCLI object for interacting with the VM.
type VMCLI struct { type VMCLI struct {
vm *vm.VM chain *core.Blockchain
shell *cli.App shell *cli.App
} }
// New returns a new VMCLI object. // NewWithConfig returns new VMCLI 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) (*VMCLI, 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 +285,41 @@ func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VM
ctl.Commands = commands 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{ vmcli := VMCLI{
vm: vm.New(), chain: chain,
shell: ctl, shell: ctl,
} }
vmcli.shell.Metadata = map[string]interface{}{ vmcli.shell.Metadata = map[string]interface{}{
vmKey: vmcli.vm, chainKey: chain,
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 +331,15 @@ 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 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,6 +350,10 @@ 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) old := getManifestFromContext(app)
*old = *m *old = *m
@ -340,6 +369,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)
@ -419,6 +449,7 @@ func handleSlots(c *cli.Context) error {
} }
func handleLoadNEF(c *cli.Context) error { func handleLoadNEF(c *cli.Context) error {
resetInteropContext(c.App)
v := getVMFromContext(c.App) v := getVMFromContext(c.App)
args := c.Args() args := c.Args()
if len(args) < 2 { if len(args) < 2 {
@ -438,6 +469,7 @@ func handleLoadNEF(c *cli.Context) error {
} }
func handleLoadBase64(c *cli.Context) error { func handleLoadBase64(c *cli.Context) error {
resetInteropContext(c.App)
v := getVMFromContext(c.App) v := getVMFromContext(c.App)
args := c.Args() args := c.Args()
if len(args) < 1 { if len(args) < 1 {
@ -454,6 +486,7 @@ func handleLoadBase64(c *cli.Context) error {
} }
func handleLoadHex(c *cli.Context) error { func handleLoadHex(c *cli.Context) error {
resetInteropContext(c.App)
v := getVMFromContext(c.App) v := getVMFromContext(c.App)
args := c.Args() args := c.Args()
if len(args) < 1 { if len(args) < 1 {
@ -470,6 +503,7 @@ func handleLoadHex(c *cli.Context) error {
} }
func handleLoadGo(c *cli.Context) error { func handleLoadGo(c *cli.Context) error {
resetInteropContext(c.App)
v := getVMFromContext(c.App) v := getVMFromContext(c.App)
args := c.Args() args := c.Args()
if len(args) < 1 { if len(args) < 1 {
@ -496,11 +530,26 @@ func handleLoadGo(c *cli.Context) error {
} }
func handleReset(c *cli.Context) error { func handleReset(c *cli.Context) error {
setVMInContext(c.App, vm.New()) resetInteropContext(c.App)
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) {
finalizeInteropContext(app)
bc := getChainFromContext(app)
newIc := bc.GetTestVM(trigger.Application, nil, nil)
setInteropContextInContext(app, newIc)
}
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 {

View file

@ -15,12 +15,18 @@ 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/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/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"
@ -64,12 +70,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 +100,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 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) { func (e *executor) runProg(t *testing.T, commands ...string) {
e.runProgWithTimeout(t, 4*time.Second, commands...) e.runProgWithTimeout(t, 4*time.Second, commands...)
} }
@ -662,3 +713,18 @@ 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)
}

View file

@ -1,20 +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"
"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,
}} }}
} }
@ -22,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 := 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()
} }

View file

@ -23,6 +23,31 @@ import (
const neoAmount = 99999000 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 // 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) {