Optimisations and API changes for smart contracts (#67)

* support VM to pass method and arguments to a script.

* added support for type assertions in smartcontracts.

* added native vm support for print.

* moved VM API packages to vm -> API

* reverted the native Print opcode in favor of runtime.Log

* added support for registering custom interop hooks in the VM.

* Updated README

* Updated compiler with @OPTIMIZE tags

* Moved more tests to VM package.

* optimized and refactored compiler and vm API

* updated README with new smartcontract apis

* bumped version
This commit is contained in:
Anthony De Meulemeester 2018-04-10 11:45:31 +02:00 committed by GitHub
parent b2021c126e
commit 4bd5b2812e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 655 additions and 494 deletions

View file

@ -1 +1 @@
0.39.2
0.40.0

View file

@ -111,6 +111,59 @@ NEO-GO-VM > run
```
### Running programs with arguments
You can invoke smart contracts with arguments. Take the following ***roll the dice*** smartcontract as example.
```
package rollthedice
import "github.com/CityOfZion/neo-go/pkg/vm/api/runtime"
func Main(method string, args []interface{}) int {
if method == "rollDice" {
// args parameter is always of type []interface, hence we need to
// cast it to an int.
rollDice(args[0].(int))
}
return 0
}
func rollDice(number int) {
if number == 0 {
runtime.Log("you rolled 0, better luck next time!")
}
if number == 1 {
runtime.Log("you rolled 1, still better then 0!")
}
if number == 2 {
runtime.Log("you rolled 2, coming closer..")
}
if number == 3 {
runtime.Log("Sweet you rolled 3. This dice has only 3 sides o_O")
}
}
```
To invoke this contract we need to specify both the method and the arguments.
The first parameter (called method or operation) is always of type string. Notice that arguments can have different types, to make the VM aware of the type we need to specify it when calling `run`:
```
NEO-GO-VM > run rollDice int:1
```
> The method is always of type string, hence we don't need to specify the type.
To add more then 1 argument:
```
NEO-GO-VM > run someMethod int:1 int:2 string:foo string:bar
```
Current supported types:
- `int (int:1 int:100)`
- `string (string:foo string:this is a string)`
### Debugging
The `neo-go-vm` provides a debugger to inspect your program in-depth.

View file

@ -1,6 +1,6 @@
package runtime
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/types"
import "github.com/CityOfZion/neo-go/pkg/vm/api/types"
// CheckWitness verifies if the invoker is the owner of the contract.
func CheckWitness(hash []byte) bool {
@ -21,9 +21,7 @@ func Notify(arg interface{}) int {
}
// Log intructs the VM to log the given message.
func Log(message string) int {
return 0
}
func Log(message string) {}
// Application returns the application trigger type.
func Application() byte {

View file

@ -0,0 +1,13 @@
package storage
// Context ..
func Context() interface{} { return 0 }
// Put stores a value in to the storage.
func Put(ctx interface{}, key string, value interface{}) {}
// Get returns the value from the storage.
func Get(ctx interface{}, key string) interface{} { return 0 }
// Delete removes a stored key value pair.
func Delete(ctx interface{}, key string) {}

8
pkg/vm/api/util/util.go Normal file
View file

@ -0,0 +1,8 @@
package util
// Package util provides utility functions that can be used in smart contracts.
// These functions are just signatures and provide not internal logic.
// Only the compiler knows how to convert them to bytecode.
// Print is a VM helper function to print/log data.
func Print(v interface{}) {}

View file

@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"os"
@ -51,7 +52,7 @@ type VMCLI struct {
// New returns a new VMCLI object.
func New() *VMCLI {
return &VMCLI{
vm: vm.New(nil, 0),
vm: vm.New(0),
}
}
@ -61,7 +62,7 @@ func (c *VMCLI) handleCommand(cmd string, args ...string) {
fmt.Printf("unknown command (%s)\n", cmd)
return
}
if len(args) < com.args {
if (len(args) < com.args || len(args) > com.args) && cmd != "run" {
fmt.Printf("command (%s) takes at least %d arguments\n", cmd, com.args)
return
}
@ -125,7 +126,31 @@ func (c *VMCLI) handleCommand(cmd string, args ...string) {
c.vm.Load(b)
fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr())
case "run", "cont":
case "run":
var (
method []byte
params []vm.StackItem
err error
start int
)
if len(args) == 0 {
c.vm.Run()
} else {
if isMethodArg(args[0]) {
method = []byte(args[0])
start = 1
}
params, err = parseArgs(args[start:])
if err != nil {
fmt.Println(err)
return
}
}
c.vm.LoadArgs(method, params)
c.vm.Run()
case "cont":
c.vm.Run()
case "step":
@ -172,6 +197,36 @@ func (c *VMCLI) Run() error {
}
}
func isMethodArg(s string) bool {
return len(strings.Split(s, ":")) == 1
}
func parseArgs(args []string) ([]vm.StackItem, error) {
items := make([]vm.StackItem, len(args))
for i, arg := range args {
typeAndVal := strings.Split(arg, ":")
if len(typeAndVal) < 2 {
return nil, errors.New("arguments need to be specified as <typ:val>")
}
typ := typeAndVal[0]
value := typeAndVal[1]
switch typ {
case "int":
val, err := strconv.Atoi(value)
if err != nil {
return nil, err
}
items[i] = vm.NewBigIntegerItem(val)
case "string":
items[i] = vm.NewByteArrayItem([]byte(value))
}
}
return items, nil
}
func printHelp() {
names := make([]string, len(commands))
i := 0

View file

@ -103,7 +103,7 @@ Will output something like:
```Golang
package mycontract
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
import "github.com/CityOfZion/neo-go/pkg/vm/api/runtime"
var owner = []byte{0xaf, 0x12, 0xa8, 0x68, 0x7b, 0x14, 0x94, 0x8b, 0xc4, 0xa0, 0x08, 0x12, 0x8a, 0x55, 0x0a, 0x63, 0x69, 0x5b, 0xc1, 0xa5}
@ -125,8 +125,8 @@ func Main() bool {
package mytoken
import (
"github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
"github.com/CityOfZion/neo-go/pkg/vm/smartcontract/storage"
"github.com/CityOfZion/neo-go/pkg/vm/api/runtime"
"github.com/CityOfZion/neo-go/pkg/vm/api/storage"
)
var owner = []byte{0xaf, 0x12, 0xa8, 0x68, 0x7b, 0x14, 0x94, 0x8b, 0xc4, 0xa0, 0x08, 0x12, 0x8a, 0x55, 0x0a, 0x63, 0x69, 0x5b, 0xc1, 0xa5}
@ -139,7 +139,7 @@ type Token struct {
}
func (t Token) AddToCirculation(amount int) bool {
ctx := storage.GetContext()
ctx := storage.Context()
inCirc := storage.GetInt(ctx, "in_circ")
inCirc += amount
storage.Put(ctx, "in_circ", inCirc)

View file

@ -13,7 +13,6 @@ import (
var (
// Go language builtin functions.
builtinFuncs = []string{"len", "append"}
// VM system calls that have no return value.
noRetSyscalls = []string{
"Notify", "Log", "Put", "Register", "Delete",
@ -128,6 +127,18 @@ func (f funcUsage) funcUsed(name string) bool {
return ok
}
// hasReturnStmt look if the given FuncDecl has a return statement.
func hasReturnStmt(decl *ast.FuncDecl) (b bool) {
ast.Inspect(decl, func(node ast.Node) bool {
if _, ok := node.(*ast.ReturnStmt); ok {
b = true
return false
}
return true
})
return
}
func analyzeFuncUsage(pkgs map[*types.Package]*loader.PackageInfo) funcUsage {
usage := funcUsage{}

View file

@ -118,19 +118,6 @@ func (c *codegen) emitStoreStructField(i int) {
emitOpcode(c.prog, vm.Osetitem)
}
func (c *codegen) emitSyscallReturn() {
emitOpcode(c.prog, vm.Ojmp)
emitOpcode(c.prog, vm.Opcode(0x03))
emitOpcode(c.prog, vm.Opush0)
emitInt(c.prog, int64(0))
emitOpcode(c.prog, vm.Onop)
emitOpcode(c.prog, vm.Ofromaltstack)
emitOpcode(c.prog, vm.Odrop)
emitOpcode(c.prog, vm.Oret)
}
// convertGlobals will traverse the AST and only convert global declarations.
// If we call this in convertFuncDecl then it will load all global variables
// into the scope of the function.
@ -160,7 +147,7 @@ func (c *codegen) convertFuncDecl(file *ast.File, decl *ast.FuncDecl) {
}
c.scope = f
ast.Inspect(decl, c.scope.analyzeVoidCalls)
ast.Inspect(decl, c.scope.analyzeVoidCalls) // @OPTIMIZE
// All globals copied into the scope of the function need to be added
// to the stack size of the function.
@ -193,17 +180,19 @@ func (c *codegen) convertFuncDecl(file *ast.File, decl *ast.FuncDecl) {
l := c.scope.newLocal(name)
c.emitStoreLocal(l)
}
// If this function is a syscall we will manipulate the return value to 0.
// All the syscalls are just signatures functions and bring no real return value.
// The return values you will find in the smartcontract package is just for
// satisfying the typechecker and the user experience.
if isSyscall(f.name) {
c.emitSyscallReturn()
} else {
// After loading the arguments we can convert the globals into the scope of the function.
// Load in all the global variables in to the scope of the function.
// This is not necessary for syscalls.
if !isSyscall(f.name) {
c.convertGlobals(file)
ast.Walk(c, decl.Body)
}
ast.Walk(c, decl.Body)
// If this function returs the void (no return stmt) we will cleanup its junk on the stack.
if !hasReturnStmt(decl) {
emitOpcode(c.prog, vm.Ofromaltstack)
emitOpcode(c.prog, vm.Odrop)
emitOpcode(c.prog, vm.Oret)
}
}
@ -268,6 +257,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
// @OPTIMIZE: We could skip these 3 instructions for each return statement.
// To be backwards compatible we will put them them in.
// See issue #65 (https://github.com/CityOfZion/neo-go/issues/65)
l := c.newLabel()
emitJmp(c.prog, vm.Ojmp, int16(l))
c.setLabel(l)
@ -276,9 +266,9 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
ast.Walk(c, n.Results[0])
}
emitOpcode(c.prog, vm.Onop)
emitOpcode(c.prog, vm.Onop) // @OPTIMIZE
emitOpcode(c.prog, vm.Ofromaltstack)
emitOpcode(c.prog, vm.Odrop)
emitOpcode(c.prog, vm.Odrop) // Cleanup the stack.
emitOpcode(c.prog, vm.Oret)
return nil
@ -439,8 +429,8 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
}
// If we are not assigning this function to a variable we need to drop
// the top stack item. It's not a void but you get the point \o/.
if _, ok := c.scope.voidCalls[n]; ok && !isNoRetSyscall(f.name) {
// (cleanup) the top stack item. It's not a void but you get the point \o/.
if _, ok := c.scope.voidCalls[n]; ok {
emitOpcode(c.prog, vm.Odrop)
}
return nil
@ -518,6 +508,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
c.setLabel(fend)
return nil
// We dont really care about assertions for the core logic.
// The only thing we need is to please the compiler type checking.
// For this to work properly, we only need to walk the expression
// not the assertion type.
case *ast.TypeAssertExpr:
ast.Walk(c, n.X)
return nil
}
return c
}
@ -528,7 +526,7 @@ func (c *codegen) convertSyscall(name string) {
log.Fatalf("unknown VM syscall api: %s", name)
}
emitSyscall(c.prog, api)
emitOpcode(c.prog, vm.Onop)
emitOpcode(c.prog, vm.Onop) // @OPTIMIZE
}
func (c *codegen) convertBuiltin(name string, expr *ast.CallExpr) {

View file

@ -1,64 +0,0 @@
package compiler
var constantTestCases = []testCase{
{
"basic constant",
`
package foo
const x = 10
func Main() int {
return x + 2
}
`,
// This ouput wil not be the same als the boa compiler.
// The go compiler will automatically resolve binary expressions
// involving constants.
// x + 2 in this case will be resolved to 12.
"52c56b5a6c766b00527ac46203005c616c7566",
},
{
"shorthand multi const",
`
package foo
const (
z = 3
y = 2
x = 1
)
// should load al 3 constants in the Main.
func Main() int {
return 0
}
`,
"54c56b536c766b00527ac4526c766b51527ac4516c766b52527ac462030000616c7566",
},
{
"globals with function arguments",
`
package foobar
const (
bar = "FOO"
foo = "BAR"
)
func something(x int) string {
if x > 10 {
return bar
}
return foo
}
func Main() string {
trigger := 100
x := something(trigger)
return x
}
`,
"55c56b03464f4f6c766b00527ac4034241526c766b51527ac401646c766b52527ac46c766b52c3616516006c766b53527ac46203006c766b53c3616c756656c56b6c766b00527ac403464f4f6c766b51527ac4034241526c766b52527ac46c766b00c35aa0640f006203006c766b51c3616c75666203006c766b52c3616c7566",
},
}

View file

@ -1,18 +0,0 @@
package compiler
var forTestCases = []testCase{
{
"classic for loop",
`
package foofor
func Main() int {
y := 0
for i := 0; i < 10; i++ {
y += 1;
}
return y
}
`,
"56c56b006a00527ac4006a53527ac4005a7c6548006a52527ac46a52c3c06a54527ac4616a53c36a54c39f6426006a52c36a53c3c36a51527ac46a53c351936a53527ac46a00c351936a00527ac462d5ff6161616a00c36c75665ec56b6a00527ac46a51527ac46a51c36a00c3946a52527ac46a52c3c56a53527ac4006a54527ac46a00c36a55527ac461616a00c36a51c39f6433006a54c36a55c3936a56527ac46a56c36a53c36a54c37bc46a54c351936a54527ac46a55c36a54c3936a00527ac462c8ff6161616a53c36c7566",
},
}

View file

@ -1,120 +0,0 @@
package compiler
var functionCallTestCases = []testCase{
{
"simple function call",
`
package testcase
func Main() int {
x := 10
y := getSomeInteger()
return x + y
}
func getSomeInteger() int {
x := 10
return x
}
`,
"53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b5a6c766b00527ac46203006c766b00c3616c7566",
},
{
"test function call with no assign",
`
package testcase
func Main() int {
getSomeInteger()
getSomeInteger()
return 0
}
func getSomeInteger() int {
return 0
}
`,
"53c56b616511007561650c007562030000616c756651c56b62030000616c7566",
},
{
"multiple function calls",
`
package testcase
func Main() int {
x := 10
y := getSomeInteger()
return x + y
}
func getSomeInteger() int {
x := 10
y := getSomeOtherInt()
return x + y
}
func getSomeOtherInt() int {
x := 8
return x
}
`,
"53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756653c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b586c766b00527ac46203006c766b00c3616c7566",
},
{
"function call with arguments",
`
package testcase
func Main() int {
x := 10
y := getSomeInteger(x)
return y
}
func getSomeInteger(x int) int {
y := 8
return x + y
}
`,
"53c56b5a6c766b00527ac46c766b00c3616516006c766b51527ac46203006c766b51c3616c756653c56b6c766b00527ac4586c766b51527ac46203006c766b00c36c766b51c393616c7566",
},
{
"function call with arguments of interface type",
`
package testcase
func Main() interface{} {
x := getSomeInteger(10)
return x
}
func getSomeInteger(x interface{}) interface{} {
return x
}
`,
"52c56b5a616516006c766b00527ac46203006c766b00c3616c756652c56b6c766b00527ac46203006c766b00c3616c7566",
},
{
"function call with multiple arguments",
`
package testcase
func Main() int {
x := addIntegers(2, 4)
return x
}
func addIntegers(x int, y int) int {
return x + y
}
`,
"52c56b52547c616516006c766b00527ac46203006c766b00c3616c756653c56b6c766b00527ac46c766b51527ac46203006c766b00c36c766b51c393616c7566",
},
{
"test Main arguments",
`
package foo
func Main(operation string, args []interface{}) int {
if operation == "mintTokens" {
return 1
}
return 0
}
`,
"55c56b6c766b00527ac46c766b51527ac46c766b00c30a6d696e74546f6b656e739c640b0062030051616c756662030000616c7566",
},
}

View file

@ -1,99 +0,0 @@
package compiler
var runtimeTestCases = []testCase{
{
"Notify test",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
func Main() bool {
runtime.Notify("hello")
return true
}
`,
"52c56b0568656c6c6f6168124e656f2e52756e74696d652e4e6f746966796162030051616c756652c56b6c766b00527ac462030000616c7566",
},
{
"Log test",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
func Main() bool {
runtime.Log("hello you there!")
return true
}
`,
"52c56b1068656c6c6f20796f752074686572652161680f4e656f2e52756e74696d652e4c6f676162030051616c756652c56b6c766b00527ac462030000616c7566",
},
{
"GetTime test",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
func Main() int {
t := runtime.GetTime()
return t
}
`,
"52c56b6168134e656f2e52756e74696d652e47657454696d65616c766b00527ac46203006c766b00c3616c756651c56b62030000616c7566",
},
{
"GetTrigger test",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
func Main() int {
trigger := runtime.GetTrigger()
if trigger == runtime.Application() {
return 1
}
if trigger == runtime.Verification() {
return 2
}
return 0
}
`,
"56c56b6168164e656f2e52756e74696d652e47657454726967676572616c766b00527ac46c766b00c361652c009c640b0062030051616c75666c766b00c3616523009c640b0062030052616c756662030000616c756651c56b6203000110616c756651c56b6203000100616c756651c56b62030000616c7566",
},
{
"check witness",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
func Main() int {
owner := []byte{0xaf, 0x12, 0xa8, 0x68, 0x7b, 0x14, 0x94, 0x8b, 0xc4, 0xa0, 0x08, 0x12, 0x8a, 0x55, 0x0a, 0x63, 0x69, 0x5b, 0xc1, 0xa5}
isOwner := runtime.CheckWitness(owner)
if isOwner {
return 1
}
return 0
}
`,
"55c56b14af12a8687b14948bc4a008128a550a63695bc1a56c766b00527ac46c766b00c36168184e656f2e52756e74696d652e436865636b5769746e657373616c766b51527ac46c766b51c3640b0062030051616c756662030000616c756652c56b6c766b00527ac462030000616c7566",
},
{
"getCurrentBlock",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime"
func Main() int {
block := runtime.GetCurrentBlock()
runtime.Notify(block)
return 0
}
`,
"53c56b61681b4e656f2e52756e74696d652e47657443757272656e74426c6f636b616c766b00527ac46c766b00c36168124e656f2e52756e74696d652e4e6f746966796162030000616c756651c56b62030000616c756652c56b6c766b00527ac462030000616c7566",
},
}

View file

@ -1,20 +0,0 @@
package compiler
var storageTestCases = []testCase{
{
"interop storage test",
`
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/storage"
func Main() int {
ctx := storage.GetContext()
storage.Put(ctx, "amount", 1000)
amount := storage.GetInt(ctx, "amount")
return amount
}
`,
"54c56b6168164e656f2e53746f726167652e476574436f6e74657874616c766b00527ac46c766b00c306616d6f756e7402e803527261680f4e656f2e53746f726167652e507574616c766b00c306616d6f756e747c61680f4e656f2e53746f726167652e476574616c766b51527ac46203006c766b51c3616c756651c56b62030000616c756654c56b6c766b00527ac46c766b51527ac46c766b52527ac462030000616c756653c56b6c766b00527ac46c766b51527ac462030000616c7566",
},
}

View file

@ -1,32 +1,22 @@
package vm
import "fmt"
import (
"fmt"
)
// InteropFunc allows to hook into the VM.
type InteropFunc func(vm *VM) error
// InteropService
type InteropService struct {
mapping map[string]InteropFunc
// runtimeLog will handle the syscall "Neo.Runtime.Log" for printing and logging stuff.
func runtimeLog(vm *VM) error {
item := vm.Estack().Pop()
fmt.Printf("NEO-GO-VM (log) > %s\n", item.value.Value())
return nil
}
// NewInteropService returns a new InteropService object.
func NewInteropService() *InteropService {
return &InteropService{
mapping: map[string]InteropFunc{},
}
}
// Register any API to the interop service.
func (i *InteropService) Register(api string, fun InteropFunc) {
i.mapping[api] = fun
}
// Call will invoke the service mapped to the given api.
func (i *InteropService) Call(api []byte, vm *VM) error {
fun, ok := i.mapping[string(api)]
if !ok {
return fmt.Errorf("api (%s) not in interop mapping", api)
}
return fun(vm)
// runtimeNotify will handle the syscall "Neo.Runtime.Notify" for printing and logging stuff.
func runtimeNotify(vm *VM) error {
item := vm.Estack().Pop()
fmt.Printf("NEO-GO-VM (notify) > %s\n", item.value.Value())
return nil
}

View file

@ -1,5 +1,7 @@
package vm
//go:generate stringer -type=Opcode
// Opcode is an single operational instruction for the GO NEO virtual machine.
type Opcode byte

View file

@ -1,10 +1,10 @@
// Code generated by "stringer -type=Opcode ./pkg/vm/compiler"; DO NOT EDIT.
// Code generated by "stringer -type=Opcode"; DO NOT EDIT.
package vm
import "strconv"
const _Opcode_name = "Opush0Opushbytes1Opushbytes75Opushdata1Opushdata2Opushdata4Opushm1Opush1Opush2Opush3Opush4Opush5Opush6Opush7Opush8Opush9Opush10Opush11Opush12Opush13Opush14Opush15Opush16OnopOjmpOjmpifOjmpifnotOcallOretOpcallOsyscallOtailcallOdupfromaltstackOtoaltstackOfromaltstackOxdropOxswapOxtuckOdepthOdropOdupOnipOoverOpickOrollOrotOswapOtuckOcatOsubstrOleftOrightOsizeOinvertOandOorOxorOequalOincOdecOsignOnegateOabsOnotOnzOaddOsubOmulOdivOmodOshlOshrOboolandOboolorOnumequalOnumnotequalOltOgtOlteOgteOminOmaxOwithinOsha1Osha256Ohash160Ohash256OchecksigOcheckmultisigOarraysizeOpackOunpackOpickitemOsetitemOnewarrayOnewstructOappendOreverseOremoveOthrowOthrowifnot"
const _Opcode_name = "Opush0Opushbytes1Opushbytes75Opushdata1Opushdata2Opushdata4Opushm1Opush1Opush2Opush3Opush4Opush5Opush6Opush7Opush8Opush9Opush10Opush11Opush12Opush13Opush14Opush15Opush16OnopOjmpOjmpifOjmpifnotOcallOretOappcallOsyscallOtailcallOdupfromaltstackOtoaltstackOfromaltstackOxdropOxswapOxtuckOdepthOdropOdupOnipOoverOpickOrollOrotOswapOtuckOcatOsubstrOleftOrightOsizeOinvertOandOorOxorOequalOincOdecOsignOnegateOabsOnotOnzOaddOsubOmulOdivOmodOshlOshrOboolandOboolorOnumequalOnumnotequalOltOgtOlteOgteOminOmaxOwithinOsha1Osha256Ohash160Ohash256OchecksigOcheckmultisigOarraysizeOpackOunpackOpickitemOsetitemOnewarrayOnewstructOappendOreverseOremoveOthrowOthrowifnot"
var _Opcode_map = map[Opcode]string{
0: _Opcode_name[0:6],
@ -36,78 +36,78 @@ var _Opcode_map = map[Opcode]string{
100: _Opcode_name[183:192],
101: _Opcode_name[192:197],
102: _Opcode_name[197:201],
103: _Opcode_name[201:207],
104: _Opcode_name[207:215],
105: _Opcode_name[215:224],
106: _Opcode_name[224:240],
107: _Opcode_name[240:251],
108: _Opcode_name[251:264],
109: _Opcode_name[264:270],
114: _Opcode_name[270:276],
115: _Opcode_name[276:282],
116: _Opcode_name[282:288],
117: _Opcode_name[288:293],
118: _Opcode_name[293:297],
119: _Opcode_name[297:301],
120: _Opcode_name[301:306],
121: _Opcode_name[306:311],
122: _Opcode_name[311:316],
123: _Opcode_name[316:320],
124: _Opcode_name[320:325],
125: _Opcode_name[325:330],
126: _Opcode_name[330:334],
127: _Opcode_name[334:341],
128: _Opcode_name[341:346],
129: _Opcode_name[346:352],
130: _Opcode_name[352:357],
131: _Opcode_name[357:364],
132: _Opcode_name[364:368],
133: _Opcode_name[368:371],
134: _Opcode_name[371:375],
135: _Opcode_name[375:381],
139: _Opcode_name[381:385],
140: _Opcode_name[385:389],
141: _Opcode_name[389:394],
143: _Opcode_name[394:401],
144: _Opcode_name[401:405],
145: _Opcode_name[405:409],
146: _Opcode_name[409:412],
147: _Opcode_name[412:416],
148: _Opcode_name[416:420],
149: _Opcode_name[420:424],
150: _Opcode_name[424:428],
151: _Opcode_name[428:432],
152: _Opcode_name[432:436],
153: _Opcode_name[436:440],
154: _Opcode_name[440:448],
155: _Opcode_name[448:455],
156: _Opcode_name[455:464],
158: _Opcode_name[464:476],
159: _Opcode_name[476:479],
160: _Opcode_name[479:482],
161: _Opcode_name[482:486],
162: _Opcode_name[486:490],
163: _Opcode_name[490:494],
164: _Opcode_name[494:498],
165: _Opcode_name[498:505],
167: _Opcode_name[505:510],
168: _Opcode_name[510:517],
169: _Opcode_name[517:525],
170: _Opcode_name[525:533],
172: _Opcode_name[533:542],
174: _Opcode_name[542:556],
192: _Opcode_name[556:566],
193: _Opcode_name[566:571],
194: _Opcode_name[571:578],
195: _Opcode_name[578:587],
196: _Opcode_name[587:595],
197: _Opcode_name[595:604],
198: _Opcode_name[604:614],
200: _Opcode_name[614:621],
201: _Opcode_name[621:629],
202: _Opcode_name[629:636],
240: _Opcode_name[636:642],
241: _Opcode_name[642:653],
103: _Opcode_name[201:209],
104: _Opcode_name[209:217],
105: _Opcode_name[217:226],
106: _Opcode_name[226:242],
107: _Opcode_name[242:253],
108: _Opcode_name[253:266],
109: _Opcode_name[266:272],
114: _Opcode_name[272:278],
115: _Opcode_name[278:284],
116: _Opcode_name[284:290],
117: _Opcode_name[290:295],
118: _Opcode_name[295:299],
119: _Opcode_name[299:303],
120: _Opcode_name[303:308],
121: _Opcode_name[308:313],
122: _Opcode_name[313:318],
123: _Opcode_name[318:322],
124: _Opcode_name[322:327],
125: _Opcode_name[327:332],
126: _Opcode_name[332:336],
127: _Opcode_name[336:343],
128: _Opcode_name[343:348],
129: _Opcode_name[348:354],
130: _Opcode_name[354:359],
131: _Opcode_name[359:366],
132: _Opcode_name[366:370],
133: _Opcode_name[370:373],
134: _Opcode_name[373:377],
135: _Opcode_name[377:383],
139: _Opcode_name[383:387],
140: _Opcode_name[387:391],
141: _Opcode_name[391:396],
143: _Opcode_name[396:403],
144: _Opcode_name[403:407],
145: _Opcode_name[407:411],
146: _Opcode_name[411:414],
147: _Opcode_name[414:418],
148: _Opcode_name[418:422],
149: _Opcode_name[422:426],
150: _Opcode_name[426:430],
151: _Opcode_name[430:434],
152: _Opcode_name[434:438],
153: _Opcode_name[438:442],
154: _Opcode_name[442:450],
155: _Opcode_name[450:457],
156: _Opcode_name[457:466],
158: _Opcode_name[466:478],
159: _Opcode_name[478:481],
160: _Opcode_name[481:484],
161: _Opcode_name[484:488],
162: _Opcode_name[488:492],
163: _Opcode_name[492:496],
164: _Opcode_name[496:500],
165: _Opcode_name[500:507],
167: _Opcode_name[507:512],
168: _Opcode_name[512:519],
169: _Opcode_name[519:527],
170: _Opcode_name[527:535],
172: _Opcode_name[535:544],
174: _Opcode_name[544:558],
192: _Opcode_name[558:568],
193: _Opcode_name[568:573],
194: _Opcode_name[573:580],
195: _Opcode_name[580:589],
196: _Opcode_name[589:597],
197: _Opcode_name[597:606],
198: _Opcode_name[606:616],
200: _Opcode_name[616:623],
201: _Opcode_name[623:631],
202: _Opcode_name[631:638],
240: _Opcode_name[638:644],
241: _Opcode_name[644:655],
}
func (i Opcode) String() string {

View file

@ -1,16 +0,0 @@
package storage
// GetContext ..
func GetContext() interface{} { return 0 }
// Put stores a value in to the storage.
func Put(ctx interface{}, key interface{}, value interface{}) int { return 0 }
// GetInt returns the value as an integer.
func GetInt(ctx interface{}, key interface{}) int { return 0 }
// GetString returns the value as an string.
func GetString(ctx interface{}, key interface{}) string { return "" }
// Delete removes a stored key value pair.
func Delete(ctx interface{}, key interface{}) int { return 0 }

View file

@ -6,11 +6,10 @@ var Syscalls = map[string]string{
// Storage API
"GetContext": "Neo.Storage.GetContext",
"Put": "Neo.Storage.Put",
"GetInt": "Neo.Storage.Get",
"GetString": "Neo.Storage.Get",
"Get": "Neo.Storage.Get",
"Delete": "Neo.Storage.Delete",
// Runtime
// Runtime API
"GetTrigger": "Neo.Runtime.GetTrigger",
"CheckWitness": "Neo.Runtime.CheckWitness",
"GetCurrentBlock": "Neo.Runtime.GetCurrentBlock",

View file

@ -0,0 +1,63 @@
package vm_test
import (
"math/big"
"testing"
)
func TestBasicConstant(t *testing.T) {
src := `
package foo
const x = 10
func Main() int {
return x + 2
}
`
eval(t, src, big.NewInt(12))
}
func TestShortHandMultiConst(t *testing.T) {
src := `
package foo
const (
z = 3
y = 2
x = 1
)
// should load al 3 constants in the Main.
func Main() int {
return x + z + y
}
`
eval(t, src, big.NewInt(6))
}
func TestGlobalsWithFunctionParams(t *testing.T) {
src := `
package foobar
const (
// complex he o_O
bar = "FOO"
foo = "BAR"
)
func something(x int) string {
if x > 10 {
return bar
}
return foo
}
func Main() string {
trigger := 100
x := something(trigger)
return x
}
`
eval(t, src, []byte("FOO"))
}

View file

@ -7,6 +7,47 @@ import (
"github.com/CityOfZion/neo-go/pkg/vm"
)
func TestEntryPointWithMethod(t *testing.T) {
src := `
package foo
func Main(op string) int {
if op == "a" {
return 1
}
return 0
}
`
evalWithArgs(t, src, []byte("a"), nil, big.NewInt(1))
}
func TestEntryPointWithArgs(t *testing.T) {
src := `
package foo
func Main(args []interface{}) int {
return 2 + args[1].(int)
}
`
args := []vm.StackItem{vm.NewBigIntegerItem(0), vm.NewBigIntegerItem(1)}
evalWithArgs(t, src, nil, args, big.NewInt(3))
}
func TestEntryPointWithMethodAndArgs(t *testing.T) {
src := `
package foo
func Main(method string, args []interface{}) int {
if method == "foobar" {
return 2 + args[1].(int)
}
return 0
}
`
args := []vm.StackItem{vm.NewBigIntegerItem(0), vm.NewBigIntegerItem(1)}
evalWithArgs(t, src, []byte("foobar"), args, big.NewInt(3))
}
func TestArrayFieldInStruct(t *testing.T) {
src := `
package foo

View file

@ -0,0 +1,124 @@
package vm_test
import (
"math/big"
"testing"
)
func TestSimpleFunctionCall(t *testing.T) {
src := `
package testcase
func Main() int {
x := 10
y := getSomeInteger()
return x + y
}
func getSomeInteger() int {
x := 10
return x
}
`
eval(t, src, big.NewInt(20))
}
func TestNotAssignedFunctionCall(t *testing.T) {
src := `
package testcase
func Main() int {
getSomeInteger()
getSomeInteger()
return 0
}
func getSomeInteger() int {
return 0
}
`
eval(t, src, big.NewInt(0))
}
func TestMultipleFunctionCalls(t *testing.T) {
src := `
package testcase
func Main() int {
x := 10
y := getSomeInteger()
return x + y
}
func getSomeInteger() int {
x := 10
y := getSomeOtherInt()
return x + y
}
func getSomeOtherInt() int {
x := 8
return x
}
`
eval(t, src, big.NewInt(28))
}
func TestFunctionCallWithArgs(t *testing.T) {
src := `
package testcase
func Main() int {
x := 10
y := getSomeInteger(x)
return y
}
func getSomeInteger(x int) int {
y := 8
return x + y
}
`
eval(t, src, big.NewInt(18))
}
func TestFunctionCallWithInterfaceType(t *testing.T) {
src := `
package testcase
func Main() interface{} {
x := getSomeInteger(10)
return x
}
func getSomeInteger(x interface{}) interface{} {
return x
}
`
eval(t, src, big.NewInt(10))
}
func TestFunctionCallMultiArg(t *testing.T) {
src := `
package testcase
func Main() int {
x := addIntegers(2, 4)
return x
}
func addIntegers(x int, y int) int {
return x + y
}
`
eval(t, src, big.NewInt(6))
}
func TestFunctionWithVoidReturn(t *testing.T) {
src := `
package testcase
func Main() int {
x := 2
getSomeInteger()
y := 4
return x + y
}
func getSomeInteger() { }
`
eval(t, src, big.NewInt(6))
}

View file

@ -0,0 +1,22 @@
package vm_test
import (
"testing"
)
func TestStoragePutGet(t *testing.T) {
src := `
package foo
import "github.com/CityOfZion/neo-go/pkg/vm/api/storage"
func Main() string {
ctx := storage.Context()
key := "token"
storage.Put(ctx, key, "foo")
x := storage.Get(ctx, key)
return x.(string)
}
`
eval(t, src, []byte("foo"))
}

View file

@ -1,6 +1,7 @@
package vm_test
import (
"fmt"
"strings"
"testing"
@ -16,19 +17,44 @@ type testCase struct {
}
func eval(t *testing.T, src string, result interface{}) {
vm := vm.New(nil, vm.ModeMute)
vm := vmAndCompile(t, src)
vm.Run()
assertResult(t, vm, result)
}
func evalWithArgs(t *testing.T, src string, op []byte, args []vm.StackItem, result interface{}) {
vm := vmAndCompile(t, src)
vm.LoadArgs(op, args)
vm.Run()
assertResult(t, vm, result)
}
func assertResult(t *testing.T, vm *vm.VM, result interface{}) {
assert.Equal(t, result, vm.PopResult())
assert.Equal(t, 0, vm.Astack().Len())
assert.Equal(t, 0, vm.Istack().Len())
}
func vmAndCompile(t *testing.T, src string) *vm.VM {
vm := vm.New(vm.ModeMute)
storePlugin := newStoragePlugin()
vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get)
vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put)
b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{})
if err != nil {
t.Fatal(err)
}
vm.Load(b)
vm.Run()
assert.Equal(t, result, vm.PopResult())
return vm
}
func TestVMAndCompilerCases(t *testing.T) {
vm := vm.New(nil, vm.ModeMute)
vm := vm.New(vm.ModeMute)
storePlugin := newStoragePlugin()
vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get)
testCases := []testCase{}
testCases = append(testCases, numericTestCases...)
@ -46,3 +72,38 @@ func TestVMAndCompilerCases(t *testing.T) {
assert.Equal(t, tc.result, vm.PopResult())
}
}
type storagePlugin struct {
mem map[string][]byte
}
func newStoragePlugin() *storagePlugin {
return &storagePlugin{
mem: make(map[string][]byte),
}
}
func (s *storagePlugin) Delete(vm *vm.VM) error {
vm.Estack().Pop()
key := vm.Estack().Pop().Bytes()
delete(s.mem, string(key))
return nil
}
func (s *storagePlugin) Put(vm *vm.VM) error {
vm.Estack().Pop()
key := vm.Estack().Pop().Bytes()
value := vm.Estack().Pop().Bytes()
s.mem[string(key)] = value
return nil
}
func (s *storagePlugin) Get(vm *vm.VM) error {
vm.Estack().Pop()
item := vm.Estack().Pop().Bytes()
if val, ok := s.mem[string(item)]; ok {
vm.Estack().PushVal(val)
return nil
}
return fmt.Errorf("could not find %+v", item)
}

View file

@ -26,8 +26,8 @@ var (
type VM struct {
state State
// interop layer.
interop *InteropService
// registered interop hooks.
interop map[string]InteropFunc
// scripts loaded in memory.
scripts map[util.Uint160][]byte
@ -41,12 +41,9 @@ type VM struct {
}
// New returns a new VM object ready to load .avm bytecode scripts.
func New(svc *InteropService, mode Mode) *VM {
if svc == nil {
svc = NewInteropService()
}
func New(mode Mode) *VM {
vm := &VM{
interop: svc,
interop: make(map[string]InteropFunc),
scripts: make(map[util.Uint160][]byte),
state: haltState,
istack: NewStack("invocation"),
@ -56,9 +53,44 @@ func New(svc *InteropService, mode Mode) *VM {
if mode == ModeMute {
vm.mute = true
}
// Register native interop hooks.
vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog)
vm.RegisterInteropFunc("Neo.Runtime.Notify", runtimeNotify)
return vm
}
// RegisterInteropFunc will register the given InteropFunc to the VM.
func (v *VM) RegisterInteropFunc(name string, f InteropFunc) {
v.interop[name] = f
}
// Estack will return the evalutation stack so interop hooks can utilize this.
func (v *VM) Estack() *Stack {
return v.estack
}
// Astack will return the alt stack so interop hooks can utilize this.
func (v *VM) Astack() *Stack {
return v.astack
}
// Istack will return the invocation stack so interop hooks can utilize this.
func (v *VM) Istack() *Stack {
return v.istack
}
// LoadArgs will load in the arguments used in the Mian entry point.
func (v *VM) LoadArgs(method []byte, args []StackItem) {
if len(args) > 0 {
v.estack.PushVal(args)
}
if method != nil {
v.estack.PushVal([]byte(method))
}
}
// PrintOps will print the opcodes of the current loaded program to stdout.
func (v *VM) PrintOps() {
prog := v.Context().Program()
@ -592,8 +624,11 @@ func (v *VM) execute(ctx *Context, op Opcode) {
case Osyscall:
api := ctx.readVarBytes()
err := v.interop.Call(api, v)
if err != nil {
ifunc, ok := v.interop[string(api)]
if !ok {
panic(fmt.Sprintf("interop hook (%s) not registered", api))
}
if err := ifunc(v); err != nil {
panic(fmt.Sprintf("failed to invoke syscall: %s", err))
}

View file

@ -11,6 +11,31 @@ import (
"github.com/stretchr/testify/assert"
)
func TestInteropHook(t *testing.T) {
v := New(ModeMute)
v.RegisterInteropFunc("foo", func(evm *VM) error {
evm.Estack().PushVal(1)
return nil
})
buf := new(bytes.Buffer)
EmitSyscall(buf, "foo")
EmitOpcode(buf, Oret)
v.Load(buf.Bytes())
v.Run()
assert.Equal(t, 1, v.estack.Len())
assert.Equal(t, big.NewInt(1), v.estack.Pop().value.Value())
}
func TestRegisterInterop(t *testing.T) {
v := New(ModeMute)
currRegistered := len(v.interop)
v.RegisterInteropFunc("foo", func(evm *VM) error { return nil })
assert.Equal(t, currRegistered+1, len(v.interop))
_, ok := v.interop["foo"]
assert.Equal(t, true, ok)
}
func TestPushBytes1to75(t *testing.T) {
buf := new(bytes.Buffer)
for i := 1; i <= 75; i++ {
@ -216,7 +241,7 @@ func makeProgram(opcodes ...Opcode) []byte {
}
func load(prog []byte) *VM {
vm := New(nil, ModeMute)
vm := New(ModeMute)
vm.mute = true
vm.istack.PushVal(NewContext(prog))
return vm