diff --git a/cli/vm/cli.go b/cli/vm/cli.go index 50e71ea45..9b294ed08 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -28,12 +28,14 @@ import ( "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/native" + "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/transaction" "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" "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/nef" @@ -51,7 +53,7 @@ const ( chainKey = "chain" chainCfgKey = "chainCfg" icKey = "ic" - manifestKey = "manifest" + contractStateKey = "contractState" exitFuncKey = "exitFunc" readlineInstanceKey = "readlineKey" printLogoKey = "printLogoKey" @@ -64,6 +66,7 @@ const ( gasFlagFullName = "gas" backwardsFlagFullName = "backwards" diffFlagFullName = "diff" + hashFlagFullName = "hash" ) var ( @@ -76,6 +79,10 @@ var ( Name: gasFlagFullName, Usage: "GAS limit for this execution (integer number, satoshi).", } + hashFlag = cli.StringFlag{ + Name: hashFlagFullName, + Usage: "Smart-contract hash in LE form or address", + } ) var commands = []cli.Command{ @@ -150,9 +157,9 @@ Example: }, { Name: "loadnef", - Usage: "Load a NEF-consistent script into the VM optionally attaching to it provided signers with scopes", - UsageText: `loadnef [--historic ] [--gas ] [] [-- , ...]`, - Flags: []cli.Flag{historicFlag, gasFlag}, + Usage: "Load a NEF (possibly with a contract hash) into the VM optionally using provided scoped signers in the context", + UsageText: `loadnef [--historic ] [--gas ] [--hash ] [] [-- , ...]`, + Flags: []cli.Flag{historicFlag, gasFlag, hashFlag}, Description: ` parameter is mandatory, parameter (if omitted) will be guessed from the parameter by replacing '.nef' suffix with '.manifest.json' suffix. @@ -191,9 +198,9 @@ Example: }, { Name: "loadgo", - Usage: "Compile and load a Go file with the manifest into the VM optionally attaching to it provided signers with scopes", - UsageText: `loadgo [--historic ] [--gas ] [-- , ...]`, - Flags: []cli.Flag{historicFlag, gasFlag}, + Usage: "Compile and load a Go file with the manifest into the VM optionally attaching to it provided signers with scopes and setting provided hash", + UsageText: `loadgo [--historic ] [--gas ] [--hash ] [-- , ...]`, + Flags: []cli.Flag{historicFlag, gasFlag, hashFlag}, Description: ` is mandatory parameter. ` + cmdargs.SignersParsingDoc + ` @@ -489,7 +496,7 @@ func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config, cfg chainKey: chain, chainCfgKey: cfg, icKey: ic, - manifestKey: new(manifest.Manifest), + contractStateKey: new(state.ContractBase), exitFuncKey: exitF, readlineInstanceKey: l, printLogoKey: printLogotype, @@ -522,8 +529,8 @@ func getInteropContextFromContext(app *cli.App) *interop.Context { return app.Metadata[icKey].(*interop.Context) } -func getManifestFromContext(app *cli.App) *manifest.Manifest { - return app.Metadata[manifestKey].(*manifest.Manifest) +func getContractStateFromContext(app *cli.App) *state.ContractBase { + return app.Metadata[contractStateKey].(*state.ContractBase) } func getPrintLogoFromContext(app *cli.App) bool { @@ -534,8 +541,8 @@ func setInteropContextInContext(app *cli.App, ic *interop.Context) { app.Metadata[icKey] = ic } -func setManifestInContext(app *cli.App, m *manifest.Manifest) { - app.Metadata[manifestKey] = m +func setContractStateInContext(app *cli.App, cs *state.ContractBase) { + app.Metadata[contractStateKey] = cs } func checkVMIsReady(app *cli.App) bool { @@ -671,6 +678,17 @@ func prepareVM(c *cli.Context, tx *transaction.Transaction) error { return nil } +func getHashFlag(c *cli.Context) (util.Uint160, error) { + if !c.IsSet(hashFlagFullName) { + return util.Uint160{}, nil + } + h, err := flags.ParseAddress(c.String(hashFlagFullName)) + if err != nil { + return util.Uint160{}, fmt.Errorf("failed to parse contract hash: %w", err) + } + return h, nil +} + func handleLoadNEF(c *cli.Context) error { args := c.Args() if len(args) < 1 { @@ -725,9 +743,19 @@ func handleLoadNEF(c *cli.Context) error { if err != nil { return err } + h, err := getHashFlag(c) + if err != nil { + return err + } + cs := &state.ContractBase{ + Hash: h, + NEF: nef, + Manifest: *m, + } + setContractStateInContext(c.App, cs) + v := getVMFromContext(c.App) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) - setManifestInContext(c.App, m) changePrompt(c.App) return nil } @@ -811,7 +839,7 @@ func handleLoadGo(c *cli.Context) error { } name := strings.TrimSuffix(args[0], ".go") - b, di, err := compiler.CompileWithOptions(args[0], nil, &compiler.Options{Name: name}) + ne, di, err := compiler.CompileWithOptions(args[0], nil, &compiler.Options{Name: name}) if err != nil { return fmt.Errorf("failed to compile: %w", err) } @@ -835,12 +863,22 @@ func handleLoadGo(c *cli.Context) error { } } - err = prepareVM(c, createFakeTransaction(b.Script, signers)) + err = prepareVM(c, createFakeTransaction(ne.Script, signers)) if err != nil { return err } + h, err := getHashFlag(c) + if err != nil { + return err + } + cs := &state.ContractBase{ + Hash: h, + NEF: *ne, + Manifest: *m, + } + setContractStateInContext(c.App, cs) + v := getVMFromContext(c.App) - setManifestInContext(c.App, m) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c.App) return nil @@ -937,7 +975,7 @@ func handleLoadDeployed(c *cli.Context) error { ic.VM.GasLimit = gasLimit ic.VM.LoadScriptWithHash(cs.NEF.Script, cs.Hash, callflag.All) fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", ic.VM.Context().LenInstr()) - setManifestInContext(c.App, &cs.Manifest) + setContractStateInContext(c.App, &cs.ContractBase) changePrompt(c.App) return nil } @@ -992,9 +1030,9 @@ func resetInteropContext(app *cli.App, tx *transaction.Transaction, height ...ui return nil } -// resetManifest removes manifest from app context. -func resetManifest(app *cli.App) { - setManifestInContext(app, nil) +// resetContractState removes loaded contract state from app context. +func resetContractState(app *cli.App) { + setContractStateInContext(app, nil) } // resetState resets state of the app (clear interop context and manifest) so that it's ready @@ -1004,7 +1042,7 @@ func resetState(app *cli.App, tx *transaction.Transaction, height ...uint32) err if err != nil { return err } - resetManifest(app) + resetContractState(app) return nil } @@ -1023,7 +1061,7 @@ func getManifestFromFile(name string) (*manifest.Manifest, error) { func handleRun(c *cli.Context) error { v := getVMFromContext(c.App) - m := getManifestFromContext(c.App) + cs := getContractStateFromContext(c.App) args := c.Args() if len(args) != 0 { var ( @@ -1031,6 +1069,7 @@ func handleRun(c *cli.Context) error { offset int err error runCurrent = args[0] != "_" + hasRet bool ) _, scParams, err := cmdargs.ParseParams(args[1:], true) @@ -1045,27 +1084,35 @@ func handleRun(c *cli.Context) error { } } if runCurrent { - if m == nil { + if cs == 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 := cs.Manifest.ABI.GetMethod(args[0], len(params)) if md == nil { return fmt.Errorf("%w: method not found", ErrInvalidParameter) } + hasRet = md.ReturnType != smartcontract.VoidType offset = md.Offset + var initOff = -1 + if initMD := cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil { + initOff = initMD.Offset + } + + // Clear context loaded by 'loadgo', 'loadnef' or 'loaddeployed' to properly handle LoadNEFMethod. + // At the same time, preserve previously set gas limit and the set of breakpoints. + ic := getInteropContextFromContext(c.App) + gasLimit := v.GasLimit + breaks := v.Context().BreakPoints() // We ensure that there's a context loaded. + ic.ReuseVM(v) + v.GasLimit = gasLimit + v.LoadNEFMethod(&cs.NEF, util.Uint160{}, cs.Hash, callflag.All, hasRet, offset, initOff, nil) + for _, bp := range breaks { + v.AddBreakPoint(bp) + } } for i := len(params) - 1; i >= 0; i-- { v.Estack().PushVal(params[i]) } - if runCurrent { - if !v.Ready() { - return errors.New("no program loaded") - } - v.Context().Jump(offset) - if initMD := m.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil { - v.Call(initMD.Offset) - } - } } runVMWithHandling(c) changePrompt(c.App) diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index 8cae8d017..65252aa9b 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -281,6 +281,97 @@ func (e *executor) checkSlot(t *testing.T, items ...any) { require.NoError(t, err) } +func TestRun_WithNewVMContextAndBreakpoints(t *testing.T) { + t.Run("contract without init", func(t *testing.T) { + src := `package kek + func Main(a, b int) int { + var c = a + b + return c + 5 + }` + tmpDir := t.TempDir() + filename := prepareLoadgoSrc(t, tmpDir, src) + + e := newTestVMCLI(t) + e.runProgWithTimeout(t, 10*time.Second, + "loadgo "+filename, + "break 8", + "run main 3 5", + "run", + ) + + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkNextLine(t, "breakpoint added at instruction 8") + e.checkNextLine(t, "at breakpoint 8 (PUSH5)*") + e.checkStack(t, 13) + }) + t.Run("contract with init", func(t *testing.T) { + src := `package kek + var I = 5 + func Main(a, b int) int { + var c = a + b + return c + I + }` + + tmpDir := t.TempDir() + filename := prepareLoadgoSrc(t, tmpDir, src) + + e := newTestVMCLI(t) + e.runProgWithTimeout(t, 10*time.Second, + "loadgo "+filename, + "break 10", + "run main 3 5", + "run", + ) + + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkNextLine(t, "breakpoint added at instruction 10") + e.checkNextLine(t, "at breakpoint 10 (ADD)*") + e.checkStack(t, 13) + }) +} + +// prepareLoadgoSrc prepares provided SC source file for loading into VM via `loadgo` command. +func prepareLoadgoSrc(t *testing.T, tmpDir, src string) string { + filename := filepath.Join(tmpDir, "vmtestcontract.go") + require.NoError(t, os.WriteFile(filename, []byte(src), os.ModePerm)) + filename = "'" + filename + "'" + wd, err := os.Getwd() + require.NoError(t, err) + goMod := []byte(`module test.example/kek +require ( + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0 +) +replace github.com/nspcc-dev/neo-go/pkg/interop => ` + filepath.Join(wd, "../../pkg/interop") + ` +go 1.18`) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm)) + return filename +} + +// prepareLoadnefSrc compiles provided SC source and prepares NEF and manifest for loading into VM +// via `loadnef` command. It returns the name of manifest and NEF files ready to be used in CLI +// commands. +func prepareLoadnefSrc(t *testing.T, tmpDir, src string) (string, string) { + config.Version = "0.92.0-test" + + nefFile, di, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) + require.NoError(t, err) + filename := filepath.Join(tmpDir, "vmtestcontract.nef") + rawNef, err := nefFile.Bytes() + require.NoError(t, err) + require.NoError(t, os.WriteFile(filename, rawNef, os.ModePerm)) + m, err := di.ConvertToManifest(&compiler.Options{}) + require.NoError(t, err) + manifestFile := filepath.Join(tmpDir, "vmtestcontract.manifest.json") + rawManifest, err := json.Marshal(m) + require.NoError(t, err) + require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm)) + + manifestFile = "'" + manifestFile + "'" + filename = "'" + filename + "'" + + return manifestFile, filename +} + func TestLoad(t *testing.T) { script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)} @@ -378,10 +469,11 @@ func TestLoad(t *testing.T) { return a * b } }` - tmpDir := t.TempDir() - checkLoadgo := func(t *testing.T, tName, cName, cErrName string) { - t.Run("loadgo "+tName, func(t *testing.T) { + t.Run("loadgo", func(t *testing.T) { + tmpDir := t.TempDir() + + checkLoadgo := func(t *testing.T, cName, cErrName string) { filename := filepath.Join(tmpDir, cName) require.NoError(t, os.WriteFile(filename, []byte(src), os.ModePerm)) filename = "'" + filename + "'" @@ -403,46 +495,34 @@ go 1.18`) e.checkNextLine(t, "Error:") e.checkNextLine(t, "READY: loaded \\d* instructions") e.checkStack(t, 8) + } + + t.Run("simple", func(t *testing.T) { + checkLoadgo(t, "vmtestcontract.go", "vmtestcontract_err.go") + }) + t.Run("utf-8 with spaces", func(t *testing.T) { + checkLoadgo(t, "тестовый контракт.go", "тестовый контракт с ошибкой.go") }) - } - checkLoadgo(t, "simple", "vmtestcontract.go", "vmtestcontract_err.go") - checkLoadgo(t, "utf-8 with spaces", "тестовый контракт.go", "тестовый контракт с ошибкой.go") - - prepareLoadgoSrc := func(t *testing.T, srcAllowNotify string) string { - filename := filepath.Join(tmpDir, "vmtestcontract.go") - require.NoError(t, os.WriteFile(filename, []byte(srcAllowNotify), os.ModePerm)) - filename = "'" + filename + "'" - wd, err := os.Getwd() - require.NoError(t, err) - goMod := []byte(`module test.example/kek -require ( - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0 -) -replace github.com/nspcc-dev/neo-go/pkg/interop => ` + filepath.Join(wd, "../../pkg/interop") + ` -go 1.18`) - require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm)) - return filename - } - t.Run("loadgo, check calling flags", func(t *testing.T) { - srcAllowNotify := `package kek + t.Run("check calling flags", func(t *testing.T) { + srcAllowNotify := `package kek import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" func Main() int { runtime.Log("Hello, world!") return 1 } ` - filename := prepareLoadgoSrc(t, srcAllowNotify) + filename := prepareLoadgoSrc(t, tmpDir, srcAllowNotify) - e := newTestVMCLI(t) - e.runProg(t, - "loadgo "+filename, - "run main") - e.checkNextLine(t, "READY: loaded \\d* instructions") - e.checkStack(t, 1) - }) - t.Run("loadgo, check signers", func(t *testing.T) { - srcCheckWitness := `package kek + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename, + "run main") + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t, 1) + }) + t.Run("check signers", func(t *testing.T) { + srcCheckWitness := `package kek import ( "github.com/nspcc-dev/neo-go/pkg/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/lib/address" @@ -452,90 +532,78 @@ go 1.18`) return runtime.CheckWitness(owner) } ` - filename := prepareLoadgoSrc(t, srcCheckWitness) - t.Run("invalid", func(t *testing.T) { - e := newTestVMCLI(t) - e.runProg(t, - "loadgo "+filename+" "+cmdargs.CosignersSeparator, - "loadgo "+filename+" "+"not-a-separator", - "loadgo "+filename+" "+cmdargs.CosignersSeparator+" not-a-signer", - ) - e.checkError(t, ErrInvalidParameter) - e.checkError(t, ErrInvalidParameter) - e.checkError(t, ErrInvalidParameter) - }) - t.Run("address", func(t *testing.T) { - e := newTestVMCLI(t) - e.runProg(t, - "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+ownerAddress, // owner:DefaultScope => true - "run main", - "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+ownerAddress+":None", // owner:None => false - "run main") - e.checkNextLine(t, "READY: loaded \\d+ instructions") - e.checkStack(t, true) - e.checkNextLine(t, "READY: loaded \\d+ instructions") - e.checkStack(t, false) - }) - t.Run("string LE", func(t *testing.T) { - e := newTestVMCLI(t) - e.runProg(t, - "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true - "run main", - "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+"0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true - "run main") - e.checkNextLine(t, "READY: loaded \\d+ instructions") - e.checkStack(t, true) - e.checkNextLine(t, "READY: loaded \\d+ instructions") - e.checkStack(t, true) - }) - t.Run("nonwitnessed signer", func(t *testing.T) { - e := newTestVMCLI(t) - e.runProg(t, - "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false - "run main") - e.checkNextLine(t, "READY: loaded \\d+ instructions") - e.checkStack(t, false) + filename := prepareLoadgoSrc(t, tmpDir, srcCheckWitness) + t.Run("invalid", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename+" "+cmdargs.CosignersSeparator, + "loadgo "+filename+" "+"not-a-separator", + "loadgo "+filename+" "+cmdargs.CosignersSeparator+" not-a-signer", + ) + e.checkError(t, ErrInvalidParameter) + e.checkError(t, ErrInvalidParameter) + e.checkError(t, ErrInvalidParameter) + }) + t.Run("address", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+ownerAddress, // owner:DefaultScope => true + "run main", + "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+ownerAddress+":None", // owner:None => false + "run main") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) + }) + t.Run("string LE", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true + "run main", + "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+"0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true + "run main") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, true) + }) + t.Run("nonwitnessed signer", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename+" "+cmdargs.CosignersSeparator+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false + "run main") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkStack(t, false) + }) }) }) + t.Run("loadnef", func(t *testing.T) { - config.Version = "0.92.0-test" + tmpDir := t.TempDir() - nefFile, di, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) - require.NoError(t, err) - filename := filepath.Join(tmpDir, "vmtestcontract.nef") - rawNef, err := nefFile.Bytes() - require.NoError(t, err) - require.NoError(t, os.WriteFile(filename, rawNef, os.ModePerm)) - m, err := di.ConvertToManifest(&compiler.Options{}) - require.NoError(t, err) - manifestFile := filepath.Join(tmpDir, "vmtestcontract.manifest.json") - rawManifest, err := json.Marshal(m) - require.NoError(t, err) - require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm)) + manifestFile, nefFile := prepareLoadnefSrc(t, tmpDir, src) filenameErr := filepath.Join(tmpDir, "vmtestcontract_err.nef") - require.NoError(t, os.WriteFile(filenameErr, append([]byte{1, 2, 3, 4}, rawNef...), os.ModePerm)) + require.NoError(t, os.WriteFile(filenameErr, []byte{1, 2, 3, 4}, os.ModePerm)) notExists := filepath.Join(tmpDir, "notexists.json") - - manifestFile = "'" + manifestFile + "'" - filename = "'" + filename + "'" filenameErr = "'" + filenameErr + "'" e := newTestVMCLI(t) e.runProg(t, "loadnef", "loadnef "+filenameErr+" "+manifestFile, - "loadnef "+filename+" "+notExists, - "loadnef "+filename+" "+filename, - "loadnef "+filename+" "+manifestFile, + "loadnef "+nefFile+" "+notExists, + "loadnef "+nefFile+" "+nefFile, + "loadnef "+nefFile+" "+manifestFile, "run main add 3 5", - "loadnef "+filename, + "loadnef "+nefFile, "run main add 3 5", - "loadnef "+filename+" "+cmdargs.CosignersSeparator, - "loadnef "+filename+" "+manifestFile+" "+cmdargs.CosignersSeparator, - "loadnef "+filename+" "+manifestFile+" "+"not-a-separator", - "loadnef "+filename+" "+cmdargs.CosignersSeparator+" "+util.Uint160{1, 2, 3}.StringLE(), + "loadnef "+nefFile+" "+cmdargs.CosignersSeparator, + "loadnef "+nefFile+" "+manifestFile+" "+cmdargs.CosignersSeparator, + "loadnef "+nefFile+" "+manifestFile+" "+"not-a-separator", + "loadnef "+nefFile+" "+cmdargs.CosignersSeparator+" "+util.Uint160{1, 2, 3}.StringLE(), "run main add 3 5", - "loadnef "+filename+" "+manifestFile+" "+cmdargs.CosignersSeparator+" "+util.Uint160{1, 2, 3}.StringLE(), + "loadnef "+nefFile+" "+manifestFile+" "+cmdargs.CosignersSeparator+" "+util.Uint160{1, 2, 3}.StringLE(), "run main add 3 5", ) @@ -557,6 +625,72 @@ go 1.18`) }) } +func TestLoad_RunWithCALLT(t *testing.T) { + // Our smart compiler will generate CALLT instruction for the following StdLib call: + src := `package kek + import "github.com/nspcc-dev/neo-go/pkg/interop/native/std" + func Main() int { + return std.Atoi("123", 10) + }` + + t.Run("loadgo", func(t *testing.T) { + tmp := t.TempDir() + filename := prepareLoadgoSrc(t, tmp, src) + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename, + "run main", + ) + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t, 123) + }) + + t.Run("loadnef", func(t *testing.T) { + tmpDir := t.TempDir() + manifestFile, nefFile := prepareLoadnefSrc(t, tmpDir, src) + + e := newTestVMCLI(t) + e.runProg(t, + "loadnef "+nefFile+" "+manifestFile, + "run main", + ) + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t, 123) + }) + + t.Run("loaddeployed", func(t *testing.T) { + // We'll use `Runtime example` example contract which has a call to native Management + // inside performed via CALLT instruction (`destroy` method). + e := newTestVMClIWithState(t) + var ( + cH util.Uint160 + cName = "Runtime example" + bc = e.cli.chain + ) + for i := int32(1); ; i++ { + h, err := bc.GetContractScriptHash(i) + if err != nil { + break + } + cs := bc.GetContractState(h) + if cs == nil { + break + } + if cs.Manifest.Name == cName { + cH = cs.Hash + break + } + } + require.NotEmpty(t, cH, fmt.Sprintf("failed to locate `%s` example contract with CALLT usage in the simple chain", cName)) + e.runProg(t, + "loaddeployed "+cH.StringLE()+" -- NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB:Global", // the contract's owner got from the contract's code. + "run destroy", + ) + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t) // Nothing on stack, successful execution. + }) +} + func TestRunWithDifferentArguments(t *testing.T) { src := `package kek var a = 1 diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 95d693296..78c0f43ac 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -350,3 +350,10 @@ func DynamicOnUnload(v *VM, ctx *Context, commit bool) error { } return nil } + +// BreakPoints returns the current set of Context's breakpoints. +func (c *Context) BreakPoints() []int { + res := make([]int, len(c.sc.breakPoints)) + copy(res, c.sc.breakPoints) + return res +} diff --git a/pkg/vm/debug_test.go b/pkg/vm/debug_test.go index c30964b82..ea985aa24 100644 --- a/pkg/vm/debug_test.go +++ b/pkg/vm/debug_test.go @@ -4,6 +4,8 @@ import ( "math/big" "testing" + "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/opcode" "github.com/stretchr/testify/require" ) @@ -40,3 +42,20 @@ func TestVM_Debug(t *testing.T) { require.Equal(t, big.NewInt(5), v.estack.Top().Value()) }) } + +func TestContext_BreakPoints(t *testing.T) { + prog := makeProgram(opcode.CALL, 3, opcode.RET, + opcode.PUSH2, opcode.PUSH3, opcode.ADD, opcode.RET) + v := load(prog) + v.AddBreakPoint(3) + v.AddBreakPoint(5) + require.Equal(t, []int{3, 5}, v.Context().BreakPoints()) + + // Preserve the set of breakpoints on Call. + v.Call(3) + require.Equal(t, []int{3, 5}, v.Context().BreakPoints()) + + // New context -> clean breakpoints. + v.loadScriptWithCallingHash(prog, nil, util.Uint160{}, util.Uint160{}, callflag.All, 1, 3, nil) + require.Equal(t, []int{}, v.Context().BreakPoints()) +}