forked from TrueCloudLab/neoneo-go
vmcli: use manifest for method execution
This commit is contained in:
parent
a0c4deb20f
commit
1d4d93b3eb
2 changed files with 140 additions and 54 deletions
|
@ -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"
|
||||||
|
@ -24,6 +27,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
vmKey = "vm"
|
vmKey = "vm"
|
||||||
|
manifestKey = "manifest"
|
||||||
boolType = "bool"
|
boolType = "bool"
|
||||||
boolFalse = "false"
|
boolFalse = "false"
|
||||||
boolTrue = "true"
|
boolTrue = "true"
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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":
|
|
||||||
return args[0].(int)
|
|
||||||
case "getstring":
|
|
||||||
return args[0].(string)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
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")
|
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) {
|
||||||
|
|
Loading…
Reference in a new issue