diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index c717937d3..4582482bf 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -4,8 +4,10 @@ import ( "bytes" "encoding/base64" "encoding/hex" + "encoding/json" "errors" "fmt" + "io/ioutil" "math/big" "os" "strconv" @@ -16,6 +18,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/compiler" "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/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -23,13 +26,14 @@ import ( ) const ( - vmKey = "vm" - boolType = "bool" - boolFalse = "false" - boolTrue = "true" - intType = "int" - stringType = "string" - exitFunc = "exitFunc" + vmKey = "vm" + manifestKey = "manifest" + boolType = "bool" + boolFalse = "false" + boolTrue = "true" + intType = "int" + stringType = "string" + exitFunc = "exitFunc" ) var commands = []*ishell.Cmd{ @@ -74,9 +78,9 @@ var commands = []*ishell.Cmd{ { Name: "loadnef", Help: "Load a NEF-consistent script into the VM", - LongHelp: `Usage: loadnef - is mandatory parameter, example: -> loadnef /path/to/script.nef`, + LongHelp: `Usage: loadnef +both parameters are mandatory, example: +> loadnef /path/to/script.nef /path/to/manifest.json`, Func: handleLoadNEF, }, { @@ -97,7 +101,7 @@ var commands = []*ishell.Cmd{ }, { Name: "loadgo", - Help: "Compile and load a Go file into the VM", + Help: "Compile and load a Go file with the manifest into the VM", LongHelp: `Usage: loadgo is mandatory parameter, example: > loadgo /path/to/file.go`, @@ -115,10 +119,11 @@ var commands = []*ishell.Cmd{ { Name: "run", Help: "Execute the current loaded script", - LongHelp: `Usage: run [ [...]] + LongHelp: `Usage: run [ [...]] - is an operation name, passed as a first parameter to Main() (and it - can't be 'help' at the moment) + is a contract method, specified in manifest (and it + can't be 'help' at the moment). It can be '_' which will push parameters + onto the stack and execute from the current offset. is a parameter (can be repeated multiple times) that can be specified as :, where type can be: '` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values @@ -130,11 +135,6 @@ var commands = []*ishell.Cmd{ boolean values, everything that can be converted to integer is treated as integer and everything else is treated like a string. -Passing parameters without operation is not supported. Parameters are packed -into array before they're passed to the script, so effectively 'run' only -supports contracts with signatures like this: - func Main(operation string, args []interface{}) interface{} - Example: > run put ` + stringType + `:"Something to put"`, Func: handleRun, @@ -214,6 +214,7 @@ func NewWithConfig(printLogo bool, onExit func(int), c *readline.Config) *VMCLI printLogo: printLogo, } vmcli.shell.Set(vmKey, vmcli.vm) + vmcli.shell.Set(manifestKey, new(manifest.Manifest)) vmcli.shell.Set(exitFunc, onExit) for _, c := range commands { vmcli.shell.AddCmd(c) @@ -226,6 +227,15 @@ func getVMFromContext(c *ishell.Context) *vm.VM { return c.Get(vmKey).(*vm.VM) } +func getManifestFromContext(c *ishell.Context) *manifest.Manifest { + return c.Get(manifestKey).(*manifest.Manifest) +} + +func setManifestInContext(c *ishell.Context, m *manifest.Manifest) { + old := getManifestFromContext(c) + *old = *m +} + func checkVMIsReady(c *ishell.Context) bool { v := getVMFromContext(c) if v == nil || !v.Ready() { @@ -280,15 +290,21 @@ func handleXStack(c *ishell.Context) { func handleLoadNEF(c *ishell.Context) { v := getVMFromContext(c) - if len(c.Args) < 1 { - c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) + if len(c.Args) < 2 { + c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } if err := v.LoadFile(c.Args[0]); err != nil { c.Err(err) - } else { - c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) + return } + m, err := getManifestFromFile(c.Args[1]) + if err != nil { + c.Err(err) + return + } + c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) + setManifestInContext(c, m) changePrompt(c, v) } @@ -330,32 +346,71 @@ func handleLoadGo(c *ishell.Context) { c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } - b, err := compiler.Compile(c.Args[0], nil) + b, di, err := compiler.CompileWithDebugInfo(c.Args[0], nil) if err != nil { c.Err(err) return } + // Don't perform checks, just load. + m, err := di.ConvertToManifest(&compiler.Options{}) + if err != nil { + c.Err(fmt.Errorf("can't create manifest: %w", err)) + return + } + setManifestInContext(c, m) + v.Load(b) c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) changePrompt(c, v) } +func getManifestFromFile(name string) (*manifest.Manifest, error) { + bs, err := ioutil.ReadFile(name) + if err != nil { + return nil, fmt.Errorf("%w: can't read manifest", ErrInvalidParameter) + } + + var m manifest.Manifest + if err := json.Unmarshal(bs, &m); err != nil { + return nil, fmt.Errorf("%w: can't unmarshal manifest", ErrInvalidParameter) + } + return &m, nil +} + func handleRun(c *ishell.Context) { v := getVMFromContext(c) + m := getManifestFromContext(c) if len(c.Args) != 0 { var ( - method []byte - params []stackitem.Item - err error + params []stackitem.Item + offset int + err error + runCurrent = c.Args[0] != "_" ) - method = []byte(c.Args[0]) + + if runCurrent { + md := m.ABI.GetMethod(c.Args[0]) + if md == nil { + c.Err(fmt.Errorf("%w: method not found", ErrInvalidParameter)) + return + } + offset = md.Offset + } params, err = parseArgs(c.Args[1:]) if err != nil { c.Err(err) return } - v.LoadArgs(method, params) + for i := len(params) - 1; i >= 0; i-- { + v.Estack().PushVal(params[i]) + } + if runCurrent { + v.Jump(v.Context(), offset) + if initMD := m.ABI.GetMethod(manifest.MethodInit); initMD != nil { + v.Call(v.Context(), initMD.Offset) + } + } } runVMWithHandling(c, v) changePrompt(c, v) diff --git a/pkg/vm/cli/cli_test.go b/pkg/vm/cli/cli_test.go index d59bcedad..0e89b4e60 100644 --- a/pkg/vm/cli/cli_test.go +++ b/pkg/vm/cli/cli_test.go @@ -155,9 +155,7 @@ func TestLoad(t *testing.T) { }) src := `package kek - func Main(op string, args []interface{}) int { - a := args[0].(int) - b := args[1].(int) + func Main(op string, a, b int) int { if op == "add" { return a + b } else { @@ -179,7 +177,7 @@ func TestLoad(t *testing.T) { "loadgo", "loadgo "+filenameErr, "loadgo "+filename, - "run add 3 5") + "run main add 3 5") e.checkError(t, ErrMissingParameter) e.checkNextLine(t, "Error:") @@ -189,7 +187,7 @@ func TestLoad(t *testing.T) { t.Run("loadnef", func(t *testing.T) { config.Version = "0.92.0-test" - script, err := compiler.Compile("test", strings.NewReader(src)) + script, di, err := compiler.CompileWithDebugInfo("test", strings.NewReader(src)) require.NoError(t, err) nefFile, err := nef.NewFile(script) require.NoError(t, err) @@ -197,18 +195,29 @@ func TestLoad(t *testing.T) { rawNef, err := nefFile.Bytes() require.NoError(t, err) require.NoError(t, ioutil.WriteFile(filename, rawNef, os.ModePerm)) + m, err := di.ConvertToManifest(&compiler.Options{}) + require.NoError(t, err) + manifestFile := path.Join(tmpDir, "vmtestcontract.manifest.json") + rawManifest, err := json.Marshal(m) + require.NoError(t, err) + require.NoError(t, ioutil.WriteFile(manifestFile, rawManifest, os.ModePerm)) filenameErr := path.Join(tmpDir, "vmtestcontract_err.nef") require.NoError(t, ioutil.WriteFile(filenameErr, append([]byte{1, 2, 3, 4}, rawNef...), os.ModePerm)) + notExists := path.Join(tmpDir, "notexists.json") e := newTestVMCLI(t) e.runProg(t, "loadnef", - "loadnef "+filenameErr, - "loadnef "+filename, - "run add 3 5") + "loadnef "+filenameErr+" "+manifestFile, + "loadnef "+filename+" "+notExists, + "loadnef "+filename+" "+filename, + "loadnef "+filename+" "+manifestFile, + "run main add 3 5") e.checkError(t, ErrMissingParameter) e.checkNextLine(t, "Error:") + e.checkNextLine(t, "Error:") + e.checkNextLine(t, "Error:") e.checkNextLine(t, "READY: loaded \\d* instructions") e.checkStack(t, 8) }) @@ -216,17 +225,21 @@ func TestLoad(t *testing.T) { func TestRunWithDifferentArguments(t *testing.T) { src := `package kek - func Main(op string, args []interface{}) interface{} { - switch op { - case "getbool": - return args[0].(bool) - case "getint": - return args[0].(int) - case "getstring": - return args[0].(string) - default: - return nil - } + var a = 1 + func init() { + a += 1 + } + func InitHasRun() bool { + return a == 2 + } + func Negate(arg bool) bool { + return !arg + } + func GetInt(arg int) int { + return arg + } + func GetString(arg string) string { + return arg }` filename := path.Join(os.TempDir(), "run_vmtestcontract.go") @@ -235,14 +248,23 @@ func TestRunWithDifferentArguments(t *testing.T) { e := newTestVMCLI(t) e.runProg(t, - "loadgo "+filename, "run getbool true", - "loadgo "+filename, "run getbool false", - "loadgo "+filename, "run getbool bool:invalid", - "loadgo "+filename, "run getint 123", - "loadgo "+filename, "run getint int:invalid", - "loadgo "+filename, "run getstring validstring", + "loadgo "+filename, "run notexists", + "loadgo "+filename, "run negate false", + "loadgo "+filename, "run negate true", + "loadgo "+filename, "run negate bool:invalid", + "loadgo "+filename, "run getInt 123", + "loadgo "+filename, "run getInt int:invalid", + "loadgo "+filename, "run getString validstring", + "loadgo "+filename, "run initHasRun", + "loadhex "+hex.EncodeToString([]byte{byte(opcode.ADD)}), + "run _ 1 2", + "loadbase64 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.MUL)}), + "run _ 21 2", ) + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkNextLine(t, "Error:") + e.checkNextLine(t, "READY: loaded \\d.* instructions") e.checkStack(t, true) @@ -260,6 +282,15 @@ func TestRunWithDifferentArguments(t *testing.T) { e.checkNextLine(t, "READY: loaded \\d.* instructions") e.checkStack(t, "validstring") + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, true) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, 3) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, 42) } func TestPrintOps(t *testing.T) {