diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 320bac2b4..3b7734e21 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -54,9 +54,16 @@ const ( // Various flag names. const ( - verboseFlagFullName = "verbose" + verboseFlagFullName = "verbose" + historicFlagFullName = "historic" ) +var historicFlag = cli.IntFlag{ + Name: historicFlagFullName, + Usage: "Height for historic script invocation (for MPT-enabled blockchain configuration with KeepOnlyLatestState setting disabled). " + + "Assuming that block N-th is specified as an argument, the historic invocation is based on the storage state of height N and fake currently-accepting block with index N+1.", +} + var commands = []cli.Command{ { Name: "exit", @@ -113,7 +120,8 @@ var commands = []cli.Command{ Name: "loadnef", Usage: "Load a NEF-consistent script into the VM", UsageText: `loadnef `, - Description: `loadnef + Flags: []cli.Flag{historicFlag}, + Description: `loadnef [--historic ] both parameters are mandatory, example: > loadnef /path/to/script.nef /path/to/manifest.json`, Action: handleLoadNEF, @@ -121,8 +129,9 @@ both parameters are mandatory, example: { Name: "loadbase64", Usage: "Load a base64-encoded script string into the VM", - UsageText: `loadbase64 `, - Description: `loadbase64 + UsageText: `loadbase64 [--historic ] `, + Flags: []cli.Flag{historicFlag}, + Description: `loadbase64 [--historic ] is mandatory parameter, example: > loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`, @@ -131,8 +140,9 @@ both parameters are mandatory, example: { Name: "loadhex", Usage: "Load a hex-encoded script string into the VM", - UsageText: `loadhex `, - Description: `loadhex + UsageText: `loadhex [--historic ] `, + Flags: []cli.Flag{historicFlag}, + Description: `loadhex [--historic ] is mandatory parameter, example: > loadhex 0c0c48656c6c6f20776f726c6421`, @@ -141,8 +151,9 @@ both parameters are mandatory, example: { Name: "loadgo", Usage: "Compile and load a Go file with the manifest into the VM", - UsageText: `loadgo `, - Description: `loadgo + UsageText: `loadgo [--historic ] `, + Flags: []cli.Flag{historicFlag}, + Description: `loadgo [--historic ] is mandatory parameter, example: > loadgo /path/to/file.go`, @@ -150,7 +161,8 @@ both parameters are mandatory, example: }, { Name: "reset", - Usage: "Unload compiled script from the VM", + Usage: "Unload compiled script from the VM and reset context to proper (possibly, historic) state", + Flags: []cli.Flag{historicFlag}, Action: handleReset, }, { @@ -485,8 +497,19 @@ func handleSlots(c *cli.Context) error { return nil } +// prepareVM retrieves --historic flag from context (if set) and resets app state +// (to the specified historic height if given). +func prepareVM(c *cli.Context) error { + if c.IsSet(historicFlagFullName) { + height := c.Int(historicFlagFullName) + return resetState(c.App, uint32(height)) + } + + return resetState(c.App) +} + func handleLoadNEF(c *cli.Context) error { - err := resetState(c.App) + err := prepareVM(c) if err != nil { return err } @@ -509,7 +532,7 @@ func handleLoadNEF(c *cli.Context) error { } func handleLoadBase64(c *cli.Context) error { - err := resetState(c.App) + err := prepareVM(c) if err != nil { return err } @@ -529,7 +552,7 @@ func handleLoadBase64(c *cli.Context) error { } func handleLoadHex(c *cli.Context) error { - err := resetState(c.App) + err := prepareVM(c) if err != nil { return err } @@ -549,7 +572,7 @@ func handleLoadHex(c *cli.Context) error { } func handleLoadGo(c *cli.Context) error { - err := resetState(c.App) + err := prepareVM(c) if err != nil { return err } @@ -579,7 +602,7 @@ func handleLoadGo(c *cli.Context) error { } func handleReset(c *cli.Context) error { - err := resetState(c.App) + err := prepareVM(c) if err != nil { return err } @@ -595,13 +618,25 @@ func finalizeInteropContext(app *cli.App) { // resetInteropContext calls finalizer for current interop context and replaces // it with the newly created one. -func resetInteropContext(app *cli.App) error { +func resetInteropContext(app *cli.App, height ...uint32) error { finalizeInteropContext(app) bc := getChainFromContext(app) - newIc, err := bc.GetTestVM(trigger.Application, nil, nil) - if err != nil { - return fmt.Errorf("failed to create test VM: %w", err) + 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 } @@ -613,10 +648,10 @@ func resetManifest(app *cli.App) { // 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) error { - err := resetInteropContext(app) +func resetState(app *cli.App, height ...uint32) error { + err := resetInteropContext(app, height...) if err != nil { - return fmt.Errorf("failed to reset interop context state: %w", err) + return err } resetManifest(app) return nil @@ -822,7 +857,9 @@ func handleEvents(c *cli.Context) error { func handleEnv(c *cli.Context) error { bc := getChainFromContext(c.App) cfg := getChainConfigFromContext(c.App) - message := fmt.Sprintf("Chain height: %d\nNetwork magic: %d\nDB type: %s\n", bc.BlockHeight(), bc.GetConfig().Magic, cfg.ApplicationConfiguration.DBConfiguration.Type) + 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 { diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index 241e54e1a..1dc137cc1 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -760,6 +760,31 @@ func TestRunWithState(t *testing.T) { 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) @@ -792,6 +817,7 @@ func TestEnv(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") }) @@ -799,6 +825,17 @@ func TestEnv(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") }) @@ -806,6 +843,7 @@ func TestEnv(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.