From ae7687422ce6f47c950aa333868ba999b7a1cefb Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 12:53:09 +0300 Subject: [PATCH 1/9] vm: clear state in Load() Make VM usable after the first run. --- pkg/vm/vm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 122e4062b..40fe95952 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -196,10 +196,11 @@ func (v *VM) LoadFile(path string) error { // Load initializes the VM with the program given. func (v *VM) Load(prog []byte) { - // clear all stacks, it could be a reload. + // Clear all stacks and state, it could be a reload. v.istack.Clear() v.estack.Clear() v.astack.Clear() + v.state = noneState v.LoadScript(prog) } From e319c6c6381196d09e20862a955343bd0dc37233 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 12:56:44 +0300 Subject: [PATCH 2/9] cli: move avm inspect from vm to contract command Make inspect work with avms by default and with go files if told so. In the end this makes our CLI interface more consistent and usable. Drop useless CompileAndInspect() compiler method along the way. --- cli/smartcontract/smart_contract.go | 25 ++++++++++++++++++---- cli/vm/vm.go | 32 ----------------------------- pkg/vm/compiler/compiler.go | 18 ---------------- 3 files changed, 21 insertions(+), 54 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 86bc1af41..9328a8f4d 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -6,14 +6,15 @@ import ( "context" "encoding/hex" "encoding/json" - "errors" "fmt" "io/ioutil" "os" "path/filepath" "github.com/CityOfZion/neo-go/pkg/rpc" + "github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm/compiler" + "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -91,6 +92,10 @@ func NewCommands() []cli.Command { Usage: "creates a user readable dump of the program instructions", Action: inspect, Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "compile, c", + Usage: "compile input file (it should be go code then)", + }, cli.StringFlag{ Name: "in, i", Usage: "input file of the program", @@ -251,12 +256,24 @@ func parseContractDetails() ContractDetails { } func inspect(ctx *cli.Context) error { - src := ctx.String("in") - if len(src) == 0 { + in := ctx.String("in") + compile := ctx.Bool("compile") + if len(in) == 0 { return cli.NewExitError(errNoInput, 1) } - if err := compiler.CompileAndInspect(src); err != nil { + b, err := ioutil.ReadFile(in) + if err != nil { return cli.NewExitError(err, 1) } + if compile { + b, err = compiler.Compile(bytes.NewReader(b), &compiler.Options{}) + if err != nil { + return cli.NewExitError(errors.Wrap(err, "failed to compile"), 1) + } + } + v := vm.New() + v.LoadScript(b) + v.PrintOps() + return nil } diff --git a/cli/vm/vm.go b/cli/vm/vm.go index 6e0876723..3dab28638 100644 --- a/cli/vm/vm.go +++ b/cli/vm/vm.go @@ -1,10 +1,6 @@ package vm import ( - "errors" - "io/ioutil" - - "github.com/CityOfZion/neo-go/pkg/vm" vmcli "github.com/CityOfZion/neo-go/pkg/vm/cli" "github.com/urfave/cli" ) @@ -18,19 +14,6 @@ func NewCommands() []cli.Command { Flags: []cli.Flag{ cli.BoolFlag{Name: "debug, d"}, }, - Subcommands: []cli.Command{ - { - Name: "inspect", - Usage: "dump instructions of the avm file given", - Action: inspect, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "in, i", - Usage: "input file of the program (AVM)", - }, - }, - }, - }, }} } @@ -38,18 +21,3 @@ func startVMPrompt(ctx *cli.Context) error { p := vmcli.New() return p.Run() } - -func inspect(ctx *cli.Context) error { - avm := ctx.String("in") - if len(avm) == 0 { - return cli.NewExitError(errors.New("no input file given"), 1) - } - b, err := ioutil.ReadFile(avm) - if err != nil { - return cli.NewExitError(err, 1) - } - v := vm.New() - v.LoadScript(b) - v.PrintOps() - return nil -} diff --git a/pkg/vm/compiler/compiler.go b/pkg/vm/compiler/compiler.go index 7a264650f..7973efa8a 100644 --- a/pkg/vm/compiler/compiler.go +++ b/pkg/vm/compiler/compiler.go @@ -14,7 +14,6 @@ import ( "os" "strings" - "github.com/CityOfZion/neo-go/pkg/vm" "golang.org/x/tools/go/loader" ) @@ -96,23 +95,6 @@ func CompileAndSave(src string, o *Options) error { return ioutil.WriteFile(out, b, os.ModePerm) } -// CompileAndInspect compiles the program and dumps the opcode in a user friendly format. -func CompileAndInspect(src string) error { - b, err := ioutil.ReadFile(src) - if err != nil { - return err - } - b, err = Compile(bytes.NewReader(b), &Options{}) - if err != nil { - return err - } - - v := vm.New() - v.LoadScript(b) - v.PrintOps() - return nil -} - func gopath() string { gopath := os.Getenv("GOPATH") if len(gopath) == 0 { From 579aa31dddc006cd7eec517b95199b5468bf01e3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 13:02:54 +0300 Subject: [PATCH 3/9] compiler: drop useless options parameter to Compile() It's not used in any way there. --- cli/smartcontract/smart_contract.go | 2 +- pkg/vm/cli/cli.go | 2 +- pkg/vm/compiler/compiler.go | 4 ++-- pkg/vm/compiler/compiler_test.go | 6 +----- pkg/vm/tests/vm_test.go | 2 +- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 9328a8f4d..24a1373f0 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -266,7 +266,7 @@ func inspect(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if compile { - b, err = compiler.Compile(bytes.NewReader(b), &compiler.Options{}) + b, err = compiler.Compile(bytes.NewReader(b)) if err != nil { return cli.NewExitError(errors.Wrap(err, "failed to compile"), 1) } diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 3083e0b6e..e4e2115ba 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -259,7 +259,7 @@ func handleLoadGo(c *ishell.Context) { c.Err(err) return } - b, err := compiler.Compile(bytes.NewReader(fb), &compiler.Options{}) + b, err := compiler.Compile(bytes.NewReader(fb)) if err != nil { c.Err(err) return diff --git a/pkg/vm/compiler/compiler.go b/pkg/vm/compiler/compiler.go index 7973efa8a..5ead0d34a 100644 --- a/pkg/vm/compiler/compiler.go +++ b/pkg/vm/compiler/compiler.go @@ -37,7 +37,7 @@ type buildInfo struct { } // Compile compiles a Go program into bytecode that can run on the NEO virtual machine. -func Compile(r io.Reader, o *Options) ([]byte, error) { +func Compile(r io.Reader) ([]byte, error) { conf := loader.Config{ParserMode: parser.ParseComments} f, err := conf.ParseFile("", r) if err != nil { @@ -84,7 +84,7 @@ func CompileAndSave(src string, o *Options) error { if err != nil { return err } - b, err = Compile(bytes.NewReader(b), o) + b, err = Compile(bytes.NewReader(b)) if err != nil { return fmt.Errorf("error while trying to compile smart contract file: %v", err) } diff --git a/pkg/vm/compiler/compiler_test.go b/pkg/vm/compiler/compiler_test.go index 1ab46d1ae..5ddd11824 100644 --- a/pkg/vm/compiler/compiler_test.go +++ b/pkg/vm/compiler/compiler_test.go @@ -44,14 +44,10 @@ func filterFilename(infos []os.FileInfo) string { } func compileFile(src string) error { - o := compiler.Options{ - Outfile: "tmp/contract.avm", - } - file, err := os.Open(src) if err != nil { return err } - _, err = compiler.Compile(file, &o) + _, err = compiler.Compile(file) return err } diff --git a/pkg/vm/tests/vm_test.go b/pkg/vm/tests/vm_test.go index a65357e67..deb18eaa7 100644 --- a/pkg/vm/tests/vm_test.go +++ b/pkg/vm/tests/vm_test.go @@ -52,7 +52,7 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put, 1) vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext, 1) - b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{}) + b, err := compiler.Compile(strings.NewReader(src)) if err != nil { t.Fatal(err) } From 2b3fb14a4a47d7faaced94d866cbca7706fba4dd Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 16:32:49 +0300 Subject: [PATCH 4/9] smartcontract: allow RPC endpoint to be specified, fix #363 We already have support for running scripts inside VM CLI, so here we can just make RPC endpoint configurable and be done with it. It also allows to use local RPC endpoint if there is a need to. --- cli/smartcontract/smart_contract.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 24a1373f0..05b3a6004 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -19,6 +19,7 @@ import ( ) var ( + errNoEndpoint = errors.New("no RPC endpoint specified, use option '--endpoint' or '-e'") errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") errFileExist = errors.New("A file with given smart-contract name already exists") @@ -66,6 +67,10 @@ func NewCommands() []cli.Command { Usage: "Test an invocation of a smart contract on the blockchain", Action: testInvoke, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "endpoint, e", + Usage: "RPC endpoint address (like 'http://seed4.ngd.network:20332')", + }, cli.StringFlag{ Name: "in, i", Usage: "Input location of the avm file that needs to be invoked", @@ -168,16 +173,16 @@ func testInvoke(ctx *cli.Context) error { if len(src) == 0 { return cli.NewExitError(errNoInput, 1) } + endpoint := ctx.String("endpoint") + if len(endpoint) == 0 { + return cli.NewExitError(errNoEndpoint, 1) + } b, err := ioutil.ReadFile(src) if err != nil { return cli.NewExitError(err, 1) } - // For now we will hardcode the endpoint. - // On the long term the internal VM will run the script. - // TODO: remove RPC dependency, hardcoded node. - endpoint := "http://node1.ams2.bridgeprotocol.io:10332" client, err := rpc.NewClient(context.TODO(), endpoint, rpc.ClientOptions{}) if err != nil { return cli.NewExitError(err, 1) From 3d6cf3a64717a4284b82cc5f082bb41cfcfd3d2e Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 16:48:14 +0300 Subject: [PATCH 5/9] rpc: drop useless cases from server Let them be handled by `default` statement returning a proper error instead of plaintext message. --- pkg/rpc/server.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index f3bf7d344..7f48848e3 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -185,10 +185,6 @@ Methods: results = peers - case "getblocksysfee", "getcontractstate", "getrawmempool", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript": - - results = "TODO" - case "validateaddress": param, err := reqParams.Value(0) if err != nil { From a568740f567022098581a07133bd8ff274411b3e Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 17:10:50 +0300 Subject: [PATCH 6/9] core: deduplicate a part of VM setup into spawnVMWithInterops() --- pkg/core/blockchain.go | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f29aaaeda..88b670ab0 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -431,19 +431,9 @@ func (bc *Blockchain) storeBlock(block *Block) error { contracts[contract.ScriptHash()] = contract case *transaction.InvocationTX: - vm := vm.New() - vm.SetCheckedHash(tx.VerificationHash().Bytes()) - vm.SetScriptGetter(func(hash util.Uint160) []byte { - cs := bc.GetContractState(hash) - if cs == nil { - return nil - } - - return cs.Script - }) systemInterop := newInteropContext(0x10, bc, tmpStore, block, tx) - vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) - vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) + vm := bc.spawnVMWithInterops(systemInterop) + vm.SetCheckedHash(tx.VerificationHash().Bytes()) vm.LoadScript(t.Script) err := vm.Run() if !vm.HasFailed() { @@ -1105,6 +1095,22 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([ } +// spawnVMWithInterops returns a VM with script getter and interop functions set +// up for current blockchain. +func (bc *Blockchain) spawnVMWithInterops(interopCtx *interopContext) *vm.VM { + vm := vm.New() + vm.SetScriptGetter(func(hash util.Uint160) []byte { + cs := bc.GetContractState(hash) + if cs == nil { + return nil + } + return cs.Script + }) + vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap()) + vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) + return vm +} + // verifyHashAgainstScript verifies given hash against the given witness. func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error { verification := witness.VerificationScript @@ -1122,17 +1128,8 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa } } - vm := vm.New() + vm := bc.spawnVMWithInterops(interopCtx) vm.SetCheckedHash(checkedHash.Bytes()) - vm.SetScriptGetter(func(hash util.Uint160) []byte { - cs := bc.GetContractState(hash) - if cs == nil { - return nil - } - return cs.Script - }) - vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap()) - vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) vm.LoadScript(verification) vm.LoadScript(witness.InvocationScript) err := vm.Run() From 47f66dfbf369728ad911e3730fff9cbb19e03f50 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 18:26:04 +0300 Subject: [PATCH 7/9] vm: add State() method to get a state description --- pkg/vm/vm.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 40fe95952..435127399 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -248,6 +248,11 @@ func (v *VM) Stack(n string) string { return buildStackOutput(s) } +// State returns string representation of the state for the VM. +func (v *VM) State() string { + return v.state.String() +} + // Ready returns true if the VM ready to execute the loaded program. // Will return false if no program is loaded. func (v *VM) Ready() bool { From 94776b8a1fa47ce32a5a8f3ed8900d45955ca066 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 18:26:59 +0300 Subject: [PATCH 8/9] vm: add MarshalJSON to the Stack To easily dump it in a known format. --- pkg/vm/output.go | 7 +++++-- pkg/vm/stack.go | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/vm/output.go b/pkg/vm/output.go index 4cad005d8..14e83103b 100644 --- a/pkg/vm/output.go +++ b/pkg/vm/output.go @@ -9,7 +9,7 @@ type stackItem struct { Type string `json:"type"` } -func buildStackOutput(s *Stack) string { +func stackToArray(s *Stack) []stackItem { items := make([]stackItem, 0, s.Len()) s.Iter(func(e *Element) { items = append(items, stackItem{ @@ -17,7 +17,10 @@ func buildStackOutput(s *Stack) string { Type: e.value.String(), }) }) + return items +} - b, _ := json.MarshalIndent(items, "", " ") +func buildStackOutput(s *Stack) string { + b, _ := json.MarshalIndent(stackToArray(s), "", " ") return string(b) } diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index f295d1e59..13a6c0f75 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -1,6 +1,7 @@ package vm import ( + "encoding/json" "fmt" "math/big" @@ -334,3 +335,8 @@ func (s *Stack) popSigElements() ([][]byte, error) { } return elems, nil } + +// MarshalJSON implements JSON marshalling interface. +func (s *Stack) MarshalJSON() ([]byte, error) { + return json.Marshal(stackToArray(s)) +} From ebc1ba4f38a5782c0ce27230111f54592cc6b8c3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 29 Oct 2019 18:31:39 +0300 Subject: [PATCH 9/9] rpc/core: implement invokescript method, fix #348 Extend Blockchainer with one more method to spawn a VM for test runs and use it to run scripts. Gas consumption is not counted or limited in any way at the moment (see #424). --- pkg/core/blockchain.go | 8 ++++++++ pkg/core/blockchainer.go | 3 +++ pkg/network/helper_test.go | 5 +++++ pkg/rpc/server.go | 25 +++++++++++++++++++++++++ pkg/rpc/wrappers/invoke_result.go | 13 +++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 pkg/rpc/wrappers/invoke_result.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 88b670ab0..f3168aadf 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1111,6 +1111,14 @@ func (bc *Blockchain) spawnVMWithInterops(interopCtx *interopContext) *vm.VM { return vm } +// GetTestVM returns a VM and a Store setup for a test run of some sort of code. +func (bc *Blockchain) GetTestVM() (*vm.VM, storage.Store) { + tmpStore := storage.NewMemCachedStore(bc.store) + systemInterop := newInteropContext(0x10, bc, tmpStore, nil, nil) + vm := bc.spawnVMWithInterops(systemInterop) + return vm, tmpStore +} + // verifyHashAgainstScript verifies given hash against the given witness. func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error { verification := witness.VerificationScript diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 1409a2c40..1ce3247e6 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -2,8 +2,10 @@ package core import ( "github.com/CityOfZion/neo-go/config" + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" ) // Blockchainer is an interface that abstract the implementation @@ -27,6 +29,7 @@ type Blockchainer interface { GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) + GetTestVM() (*vm.VM, storage.Store) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetUnspentCoinState(util.Uint256) *UnspentCoinState References(t *transaction.Transaction) map[transaction.Input]*transaction.Output diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index dbb55f607..2ad71205d 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -9,9 +9,11 @@ import ( "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/network/payload" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" ) type testChain struct { @@ -78,6 +80,9 @@ func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([] func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem { panic("TODO") } +func (chain testChain) GetTestVM() (*vm.VM, storage.Store) { + panic("TODO") +} func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*core.StorageItem, error) { panic("TODO") } diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 7f48848e3..fdcff9da2 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -227,6 +227,9 @@ Methods: case "getrawtransaction": results, resultsErr = s.getrawtransaction(reqParams) + case "invokescript": + results, resultsErr = s.invokescript(reqParams) + case "sendrawtransaction": results, resultsErr = s.sendrawtransaction(reqParams) @@ -280,6 +283,28 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { return results, resultsErr } +// invokescript implements the `invokescript` RPC call. +func (s *Server) invokescript(reqParams Params) (interface{}, error) { + hexScript, err := reqParams.ValueWithType(0, "string") + if err != nil { + return nil, err + } + script, err := hex.DecodeString(hexScript.StringVal) + if err != nil { + return nil, err + } + vm, _ := s.chain.GetTestVM() + vm.LoadScript(script) + _ = vm.Run() + result := &wrappers.InvokeResult{ + State: vm.State(), + GasConsumed: "0.1", + Script: hexScript.StringVal, + Stack: vm.Estack(), + } + return result, nil +} + func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) { var resultsErr error var results interface{} diff --git a/pkg/rpc/wrappers/invoke_result.go b/pkg/rpc/wrappers/invoke_result.go new file mode 100644 index 000000000..4cc790bf5 --- /dev/null +++ b/pkg/rpc/wrappers/invoke_result.go @@ -0,0 +1,13 @@ +package wrappers + +import ( + "github.com/CityOfZion/neo-go/pkg/vm" +) + +// InvokeResult is used as a wrapper to represent an invokation result. +type InvokeResult struct { + State string `json:"state"` + GasConsumed string `json:"gas_consumed"` + Script string `json:"script"` + Stack *vm.Stack +}