vmcli: use manifest for method execution

This commit is contained in:
Evgenii Stratonikov 2020-12-21 14:27:07 +03:00
parent a0c4deb20f
commit 1d4d93b3eb
2 changed files with 140 additions and 54 deletions

View file

@ -4,8 +4,10 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"math/big" "math/big"
"os" "os"
"strconv" "strconv"
@ -16,6 +18,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/compiler" "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/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/manifest"
"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/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
@ -23,13 +26,14 @@ import (
) )
const ( const (
vmKey = "vm" vmKey = "vm"
boolType = "bool" manifestKey = "manifest"
boolFalse = "false" boolType = "bool"
boolTrue = "true" boolFalse = "false"
intType = "int" boolTrue = "true"
stringType = "string" intType = "int"
exitFunc = "exitFunc" stringType = "string"
exitFunc = "exitFunc"
) )
var commands = []*ishell.Cmd{ var commands = []*ishell.Cmd{
@ -74,9 +78,9 @@ var commands = []*ishell.Cmd{
{ {
Name: "loadnef", Name: "loadnef",
Help: "Load a NEF-consistent script into the VM", Help: "Load a NEF-consistent script into the VM",
LongHelp: `Usage: loadnef <file> LongHelp: `Usage: loadnef <file> <manifest>
<file> is mandatory parameter, example: both parameters are mandatory, example:
> loadnef /path/to/script.nef`, > loadnef /path/to/script.nef /path/to/manifest.json`,
Func: handleLoadNEF, Func: handleLoadNEF,
}, },
{ {
@ -97,7 +101,7 @@ var commands = []*ishell.Cmd{
}, },
{ {
Name: "loadgo", 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 <file> LongHelp: `Usage: loadgo <file>
<file> is mandatory parameter, example: <file> is mandatory parameter, example:
> loadgo /path/to/file.go`, > loadgo /path/to/file.go`,
@ -115,10 +119,11 @@ var commands = []*ishell.Cmd{
{ {
Name: "run", Name: "run",
Help: "Execute the current loaded script", Help: "Execute the current loaded script",
LongHelp: `Usage: run [<operation> [<parameter>...]] LongHelp: `Usage: run [<method> [<parameter>...]]
<operation> is an operation name, passed as a first parameter to Main() (and it <method> is a contract method, specified in manifest (and it
can't be 'help' at the moment) can't be 'help' at the moment). It can be '_' which will push parameters
onto the stack and execute from the current offset.
<parameter> is a parameter (can be repeated multiple times) that can be specified <parameter> is a parameter (can be repeated multiple times) that can be specified
as <type>:<value>, where type can be: as <type>:<value>, where type can be:
'` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values '` + 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 boolean values, everything that can be converted to integer is treated as
integer and everything else is treated like a string. 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: Example:
> run put ` + stringType + `:"Something to put"`, > run put ` + stringType + `:"Something to put"`,
Func: handleRun, Func: handleRun,
@ -214,6 +214,7 @@ func NewWithConfig(printLogo bool, onExit func(int), c *readline.Config) *VMCLI
printLogo: printLogo, printLogo: printLogo,
} }
vmcli.shell.Set(vmKey, vmcli.vm) vmcli.shell.Set(vmKey, vmcli.vm)
vmcli.shell.Set(manifestKey, new(manifest.Manifest))
vmcli.shell.Set(exitFunc, onExit) vmcli.shell.Set(exitFunc, onExit)
for _, c := range commands { for _, c := range commands {
vmcli.shell.AddCmd(c) vmcli.shell.AddCmd(c)
@ -226,6 +227,15 @@ func getVMFromContext(c *ishell.Context) *vm.VM {
return c.Get(vmKey).(*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 { func checkVMIsReady(c *ishell.Context) bool {
v := getVMFromContext(c) v := getVMFromContext(c)
if v == nil || !v.Ready() { if v == nil || !v.Ready() {
@ -280,15 +290,21 @@ func handleXStack(c *ishell.Context) {
func handleLoadNEF(c *ishell.Context) { func handleLoadNEF(c *ishell.Context) {
v := getVMFromContext(c) v := getVMFromContext(c)
if len(c.Args) < 1 { if len(c.Args) < 2 {
c.Err(fmt.Errorf("%w: <file>", ErrMissingParameter)) c.Err(fmt.Errorf("%w: <file> <manifest>", ErrMissingParameter))
return return
} }
if err := v.LoadFile(c.Args[0]); err != nil { if err := v.LoadFile(c.Args[0]); err != nil {
c.Err(err) c.Err(err)
} else { return
c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr())
} }
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) changePrompt(c, v)
} }
@ -330,32 +346,71 @@ func handleLoadGo(c *ishell.Context) {
c.Err(fmt.Errorf("%w: <file>", ErrMissingParameter)) c.Err(fmt.Errorf("%w: <file>", ErrMissingParameter))
return return
} }
b, err := compiler.Compile(c.Args[0], nil) b, di, err := compiler.CompileWithDebugInfo(c.Args[0], nil)
if err != nil { if err != nil {
c.Err(err) c.Err(err)
return 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) v.Load(b)
c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr())
changePrompt(c, v) 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) { func handleRun(c *ishell.Context) {
v := getVMFromContext(c) v := getVMFromContext(c)
m := getManifestFromContext(c)
if len(c.Args) != 0 { if len(c.Args) != 0 {
var ( var (
method []byte params []stackitem.Item
params []stackitem.Item offset int
err error 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:]) params, err = parseArgs(c.Args[1:])
if err != nil { if err != nil {
c.Err(err) c.Err(err)
return 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) runVMWithHandling(c, v)
changePrompt(c, v) changePrompt(c, v)

View file

@ -155,9 +155,7 @@ func TestLoad(t *testing.T) {
}) })
src := `package kek src := `package kek
func Main(op string, args []interface{}) int { func Main(op string, a, b int) int {
a := args[0].(int)
b := args[1].(int)
if op == "add" { if op == "add" {
return a + b return a + b
} else { } else {
@ -179,7 +177,7 @@ func TestLoad(t *testing.T) {
"loadgo", "loadgo",
"loadgo "+filenameErr, "loadgo "+filenameErr,
"loadgo "+filename, "loadgo "+filename,
"run add 3 5") "run main add 3 5")
e.checkError(t, ErrMissingParameter) e.checkError(t, ErrMissingParameter)
e.checkNextLine(t, "Error:") e.checkNextLine(t, "Error:")
@ -189,7 +187,7 @@ func TestLoad(t *testing.T) {
t.Run("loadnef", func(t *testing.T) { t.Run("loadnef", func(t *testing.T) {
config.Version = "0.92.0-test" 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) require.NoError(t, err)
nefFile, err := nef.NewFile(script) nefFile, err := nef.NewFile(script)
require.NoError(t, err) require.NoError(t, err)
@ -197,18 +195,29 @@ func TestLoad(t *testing.T) {
rawNef, err := nefFile.Bytes() rawNef, err := nefFile.Bytes()
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, ioutil.WriteFile(filename, rawNef, os.ModePerm)) 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") filenameErr := path.Join(tmpDir, "vmtestcontract_err.nef")
require.NoError(t, ioutil.WriteFile(filenameErr, append([]byte{1, 2, 3, 4}, rawNef...), os.ModePerm)) 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 := newTestVMCLI(t)
e.runProg(t, e.runProg(t,
"loadnef", "loadnef",
"loadnef "+filenameErr, "loadnef "+filenameErr+" "+manifestFile,
"loadnef "+filename, "loadnef "+filename+" "+notExists,
"run add 3 5") "loadnef "+filename+" "+filename,
"loadnef "+filename+" "+manifestFile,
"run main add 3 5")
e.checkError(t, ErrMissingParameter) e.checkError(t, ErrMissingParameter)
e.checkNextLine(t, "Error:") e.checkNextLine(t, "Error:")
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "READY: loaded \\d* instructions") e.checkNextLine(t, "READY: loaded \\d* instructions")
e.checkStack(t, 8) e.checkStack(t, 8)
}) })
@ -216,17 +225,21 @@ func TestLoad(t *testing.T) {
func TestRunWithDifferentArguments(t *testing.T) { func TestRunWithDifferentArguments(t *testing.T) {
src := `package kek src := `package kek
func Main(op string, args []interface{}) interface{} { var a = 1
switch op { func init() {
case "getbool": a += 1
return args[0].(bool) }
case "getint": func InitHasRun() bool {
return args[0].(int) return a == 2
case "getstring": }
return args[0].(string) func Negate(arg bool) bool {
default: return !arg
return nil }
} func GetInt(arg int) int {
return arg
}
func GetString(arg string) string {
return arg
}` }`
filename := path.Join(os.TempDir(), "run_vmtestcontract.go") filename := path.Join(os.TempDir(), "run_vmtestcontract.go")
@ -235,14 +248,23 @@ func TestRunWithDifferentArguments(t *testing.T) {
e := newTestVMCLI(t) e := newTestVMCLI(t)
e.runProg(t, e.runProg(t,
"loadgo "+filename, "run getbool true", "loadgo "+filename, "run notexists",
"loadgo "+filename, "run getbool false", "loadgo "+filename, "run negate false",
"loadgo "+filename, "run getbool bool:invalid", "loadgo "+filename, "run negate true",
"loadgo "+filename, "run getint 123", "loadgo "+filename, "run negate bool:invalid",
"loadgo "+filename, "run getint int:invalid", "loadgo "+filename, "run getInt 123",
"loadgo "+filename, "run getstring validstring", "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.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, true) e.checkStack(t, true)
@ -260,6 +282,15 @@ func TestRunWithDifferentArguments(t *testing.T) {
e.checkNextLine(t, "READY: loaded \\d.* instructions") e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, "validstring") 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) { func TestPrintOps(t *testing.T) {