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 ### Debugging
The `neo-go-vm` provides a debugger to inspect your program in-depth. The `neo-go-vm` provides a debugger to inspect your program in-depth.

View file

@ -1,6 +1,6 @@
package runtime 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. // CheckWitness verifies if the invoker is the owner of the contract.
func CheckWitness(hash []byte) bool { func CheckWitness(hash []byte) bool {
@ -21,9 +21,7 @@ func Notify(arg interface{}) int {
} }
// Log intructs the VM to log the given message. // Log intructs the VM to log the given message.
func Log(message string) int { func Log(message string) {}
return 0
}
// Application returns the application trigger type. // Application returns the application trigger type.
func Application() byte { 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" "bufio"
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -51,7 +52,7 @@ type VMCLI struct {
// New returns a new VMCLI object. // New returns a new VMCLI object.
func New() *VMCLI { func New() *VMCLI {
return &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) fmt.Printf("unknown command (%s)\n", cmd)
return 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) fmt.Printf("command (%s) takes at least %d arguments\n", cmd, com.args)
return return
} }
@ -125,7 +126,31 @@ func (c *VMCLI) handleCommand(cmd string, args ...string) {
c.vm.Load(b) c.vm.Load(b)
fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr()) 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() c.vm.Run()
case "step": 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() { func printHelp() {
names := make([]string, len(commands)) names := make([]string, len(commands))
i := 0 i := 0

View file

@ -103,7 +103,7 @@ Will output something like:
```Golang ```Golang
package mycontract 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} 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 package mytoken
import ( import (
"github.com/CityOfZion/neo-go/pkg/vm/smartcontract/runtime" "github.com/CityOfZion/neo-go/pkg/vm/api/runtime"
"github.com/CityOfZion/neo-go/pkg/vm/smartcontract/storage" "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} 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 { func (t Token) AddToCirculation(amount int) bool {
ctx := storage.GetContext() ctx := storage.Context()
inCirc := storage.GetInt(ctx, "in_circ") inCirc := storage.GetInt(ctx, "in_circ")
inCirc += amount inCirc += amount
storage.Put(ctx, "in_circ", inCirc) storage.Put(ctx, "in_circ", inCirc)

View file

@ -13,7 +13,6 @@ import (
var ( var (
// Go language builtin functions. // Go language builtin functions.
builtinFuncs = []string{"len", "append"} builtinFuncs = []string{"len", "append"}
// VM system calls that have no return value. // VM system calls that have no return value.
noRetSyscalls = []string{ noRetSyscalls = []string{
"Notify", "Log", "Put", "Register", "Delete", "Notify", "Log", "Put", "Register", "Delete",
@ -128,6 +127,18 @@ func (f funcUsage) funcUsed(name string) bool {
return ok 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 { func analyzeFuncUsage(pkgs map[*types.Package]*loader.PackageInfo) funcUsage {
usage := funcUsage{} usage := funcUsage{}

View file

@ -118,19 +118,6 @@ func (c *codegen) emitStoreStructField(i int) {
emitOpcode(c.prog, vm.Osetitem) 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. // convertGlobals will traverse the AST and only convert global declarations.
// If we call this in convertFuncDecl then it will load all global variables // If we call this in convertFuncDecl then it will load all global variables
// into the scope of the function. // into the scope of the function.
@ -160,7 +147,7 @@ func (c *codegen) convertFuncDecl(file *ast.File, decl *ast.FuncDecl) {
} }
c.scope = f 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 // All globals copied into the scope of the function need to be added
// to the stack size of the function. // 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) l := c.scope.newLocal(name)
c.emitStoreLocal(l) c.emitStoreLocal(l)
} }
// Load in all the global variables in to the scope of the function.
// If this function is a syscall we will manipulate the return value to 0. // This is not necessary for syscalls.
// All the syscalls are just signatures functions and bring no real return value. if !isSyscall(f.name) {
// 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.
c.convertGlobals(file) 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. // @OPTIMIZE: We could skip these 3 instructions for each return statement.
// To be backwards compatible we will put them them in. // To be backwards compatible we will put them them in.
// See issue #65 (https://github.com/CityOfZion/neo-go/issues/65)
l := c.newLabel() l := c.newLabel()
emitJmp(c.prog, vm.Ojmp, int16(l)) emitJmp(c.prog, vm.Ojmp, int16(l))
c.setLabel(l) c.setLabel(l)
@ -276,9 +266,9 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
ast.Walk(c, n.Results[0]) 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.Ofromaltstack)
emitOpcode(c.prog, vm.Odrop) emitOpcode(c.prog, vm.Odrop) // Cleanup the stack.
emitOpcode(c.prog, vm.Oret) emitOpcode(c.prog, vm.Oret)
return nil 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 // 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/. // (cleanup) 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) { if _, ok := c.scope.voidCalls[n]; ok {
emitOpcode(c.prog, vm.Odrop) emitOpcode(c.prog, vm.Odrop)
} }
return nil return nil
@ -518,6 +508,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
c.setLabel(fend) c.setLabel(fend)
return nil 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 return c
} }
@ -528,7 +526,7 @@ func (c *codegen) convertSyscall(name string) {
log.Fatalf("unknown VM syscall api: %s", name) log.Fatalf("unknown VM syscall api: %s", name)
} }
emitSyscall(c.prog, api) emitSyscall(c.prog, api)
emitOpcode(c.prog, vm.Onop) emitOpcode(c.prog, vm.Onop) // @OPTIMIZE
} }
func (c *codegen) convertBuiltin(name string, expr *ast.CallExpr) { 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 package vm
import "fmt" import (
"fmt"
)
// InteropFunc allows to hook into the VM. // InteropFunc allows to hook into the VM.
type InteropFunc func(vm *VM) error type InteropFunc func(vm *VM) error
// InteropService // runtimeLog will handle the syscall "Neo.Runtime.Log" for printing and logging stuff.
type InteropService struct { func runtimeLog(vm *VM) error {
mapping map[string]InteropFunc item := vm.Estack().Pop()
fmt.Printf("NEO-GO-VM (log) > %s\n", item.value.Value())
return nil
} }
// NewInteropService returns a new InteropService object. // runtimeNotify will handle the syscall "Neo.Runtime.Notify" for printing and logging stuff.
func NewInteropService() *InteropService { func runtimeNotify(vm *VM) error {
return &InteropService{ item := vm.Estack().Pop()
mapping: map[string]InteropFunc{}, fmt.Printf("NEO-GO-VM (notify) > %s\n", item.value.Value())
} return nil
}
// 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)
} }

View file

@ -1,5 +1,7 @@
package vm package vm
//go:generate stringer -type=Opcode
// Opcode is an single operational instruction for the GO NEO virtual machine. // Opcode is an single operational instruction for the GO NEO virtual machine.
type Opcode byte 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 package vm
import "strconv" import "strconv"
const _Opcode_name = "Opush0Opushbytes1Opushbytes75Opushdata1Opushdata2Opushdata4Opushm1Opush1Opush2Opush3Opush4Opush5Opush6Opush7Opush8Opush9Opush10Opush11Opush12Opush13Opush14Opush15Opush16OnopOjmpOjmpifOjmpifnotOcallOretOpcallOsyscallOtailcallOdupfromaltstackOtoaltstackOfromaltstackOxdropOxswapOxtuckOdepthOdropOdupOnipOoverOpickOrollOrotOswapOtuckOcatOsubstrOleftOrightOsizeOinvertOandOorOxorOequalOincOdecOsignOnegateOabsOnotOnzOaddOsubOmulOdivOmodOshlOshrOboolandOboolorOnumequalOnumnotequalOltOgtOlteOgteOminOmaxOwithinOsha1Osha256Ohash160Ohash256OchecksigOcheckmultisigOarraysizeOpackOunpackOpickitemOsetitemOnewarrayOnewstructOappendOreverseOremoveOthrowOthrowifnot" const _Opcode_name = "Opush0Opushbytes1Opushbytes75Opushdata1Opushdata2Opushdata4Opushm1Opush1Opush2Opush3Opush4Opush5Opush6Opush7Opush8Opush9Opush10Opush11Opush12Opush13Opush14Opush15Opush16OnopOjmpOjmpifOjmpifnotOcallOretOappcallOsyscallOtailcallOdupfromaltstackOtoaltstackOfromaltstackOxdropOxswapOxtuckOdepthOdropOdupOnipOoverOpickOrollOrotOswapOtuckOcatOsubstrOleftOrightOsizeOinvertOandOorOxorOequalOincOdecOsignOnegateOabsOnotOnzOaddOsubOmulOdivOmodOshlOshrOboolandOboolorOnumequalOnumnotequalOltOgtOlteOgteOminOmaxOwithinOsha1Osha256Ohash160Ohash256OchecksigOcheckmultisigOarraysizeOpackOunpackOpickitemOsetitemOnewarrayOnewstructOappendOreverseOremoveOthrowOthrowifnot"
var _Opcode_map = map[Opcode]string{ var _Opcode_map = map[Opcode]string{
0: _Opcode_name[0:6], 0: _Opcode_name[0:6],
@ -36,78 +36,78 @@ var _Opcode_map = map[Opcode]string{
100: _Opcode_name[183:192], 100: _Opcode_name[183:192],
101: _Opcode_name[192:197], 101: _Opcode_name[192:197],
102: _Opcode_name[197:201], 102: _Opcode_name[197:201],
103: _Opcode_name[201:207], 103: _Opcode_name[201:209],
104: _Opcode_name[207:215], 104: _Opcode_name[209:217],
105: _Opcode_name[215:224], 105: _Opcode_name[217:226],
106: _Opcode_name[224:240], 106: _Opcode_name[226:242],
107: _Opcode_name[240:251], 107: _Opcode_name[242:253],
108: _Opcode_name[251:264], 108: _Opcode_name[253:266],
109: _Opcode_name[264:270], 109: _Opcode_name[266:272],
114: _Opcode_name[270:276], 114: _Opcode_name[272:278],
115: _Opcode_name[276:282], 115: _Opcode_name[278:284],
116: _Opcode_name[282:288], 116: _Opcode_name[284:290],
117: _Opcode_name[288:293], 117: _Opcode_name[290:295],
118: _Opcode_name[293:297], 118: _Opcode_name[295:299],
119: _Opcode_name[297:301], 119: _Opcode_name[299:303],
120: _Opcode_name[301:306], 120: _Opcode_name[303:308],
121: _Opcode_name[306:311], 121: _Opcode_name[308:313],
122: _Opcode_name[311:316], 122: _Opcode_name[313:318],
123: _Opcode_name[316:320], 123: _Opcode_name[318:322],
124: _Opcode_name[320:325], 124: _Opcode_name[322:327],
125: _Opcode_name[325:330], 125: _Opcode_name[327:332],
126: _Opcode_name[330:334], 126: _Opcode_name[332:336],
127: _Opcode_name[334:341], 127: _Opcode_name[336:343],
128: _Opcode_name[341:346], 128: _Opcode_name[343:348],
129: _Opcode_name[346:352], 129: _Opcode_name[348:354],
130: _Opcode_name[352:357], 130: _Opcode_name[354:359],
131: _Opcode_name[357:364], 131: _Opcode_name[359:366],
132: _Opcode_name[364:368], 132: _Opcode_name[366:370],
133: _Opcode_name[368:371], 133: _Opcode_name[370:373],
134: _Opcode_name[371:375], 134: _Opcode_name[373:377],
135: _Opcode_name[375:381], 135: _Opcode_name[377:383],
139: _Opcode_name[381:385], 139: _Opcode_name[383:387],
140: _Opcode_name[385:389], 140: _Opcode_name[387:391],
141: _Opcode_name[389:394], 141: _Opcode_name[391:396],
143: _Opcode_name[394:401], 143: _Opcode_name[396:403],
144: _Opcode_name[401:405], 144: _Opcode_name[403:407],
145: _Opcode_name[405:409], 145: _Opcode_name[407:411],
146: _Opcode_name[409:412], 146: _Opcode_name[411:414],
147: _Opcode_name[412:416], 147: _Opcode_name[414:418],
148: _Opcode_name[416:420], 148: _Opcode_name[418:422],
149: _Opcode_name[420:424], 149: _Opcode_name[422:426],
150: _Opcode_name[424:428], 150: _Opcode_name[426:430],
151: _Opcode_name[428:432], 151: _Opcode_name[430:434],
152: _Opcode_name[432:436], 152: _Opcode_name[434:438],
153: _Opcode_name[436:440], 153: _Opcode_name[438:442],
154: _Opcode_name[440:448], 154: _Opcode_name[442:450],
155: _Opcode_name[448:455], 155: _Opcode_name[450:457],
156: _Opcode_name[455:464], 156: _Opcode_name[457:466],
158: _Opcode_name[464:476], 158: _Opcode_name[466:478],
159: _Opcode_name[476:479], 159: _Opcode_name[478:481],
160: _Opcode_name[479:482], 160: _Opcode_name[481:484],
161: _Opcode_name[482:486], 161: _Opcode_name[484:488],
162: _Opcode_name[486:490], 162: _Opcode_name[488:492],
163: _Opcode_name[490:494], 163: _Opcode_name[492:496],
164: _Opcode_name[494:498], 164: _Opcode_name[496:500],
165: _Opcode_name[498:505], 165: _Opcode_name[500:507],
167: _Opcode_name[505:510], 167: _Opcode_name[507:512],
168: _Opcode_name[510:517], 168: _Opcode_name[512:519],
169: _Opcode_name[517:525], 169: _Opcode_name[519:527],
170: _Opcode_name[525:533], 170: _Opcode_name[527:535],
172: _Opcode_name[533:542], 172: _Opcode_name[535:544],
174: _Opcode_name[542:556], 174: _Opcode_name[544:558],
192: _Opcode_name[556:566], 192: _Opcode_name[558:568],
193: _Opcode_name[566:571], 193: _Opcode_name[568:573],
194: _Opcode_name[571:578], 194: _Opcode_name[573:580],
195: _Opcode_name[578:587], 195: _Opcode_name[580:589],
196: _Opcode_name[587:595], 196: _Opcode_name[589:597],
197: _Opcode_name[595:604], 197: _Opcode_name[597:606],
198: _Opcode_name[604:614], 198: _Opcode_name[606:616],
200: _Opcode_name[614:621], 200: _Opcode_name[616:623],
201: _Opcode_name[621:629], 201: _Opcode_name[623:631],
202: _Opcode_name[629:636], 202: _Opcode_name[631:638],
240: _Opcode_name[636:642], 240: _Opcode_name[638:644],
241: _Opcode_name[642:653], 241: _Opcode_name[644:655],
} }
func (i Opcode) String() string { 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 // Storage API
"GetContext": "Neo.Storage.GetContext", "GetContext": "Neo.Storage.GetContext",
"Put": "Neo.Storage.Put", "Put": "Neo.Storage.Put",
"GetInt": "Neo.Storage.Get", "Get": "Neo.Storage.Get",
"GetString": "Neo.Storage.Get",
"Delete": "Neo.Storage.Delete", "Delete": "Neo.Storage.Delete",
// Runtime // Runtime API
"GetTrigger": "Neo.Runtime.GetTrigger", "GetTrigger": "Neo.Runtime.GetTrigger",
"CheckWitness": "Neo.Runtime.CheckWitness", "CheckWitness": "Neo.Runtime.CheckWitness",
"GetCurrentBlock": "Neo.Runtime.GetCurrentBlock", "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" "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) { func TestArrayFieldInStruct(t *testing.T) {
src := ` src := `
package foo 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 package vm_test
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
@ -16,19 +17,44 @@ type testCase struct {
} }
func eval(t *testing.T, src string, result interface{}) { 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{}) b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
vm.Load(b) vm.Load(b)
vm.Run() return vm
assert.Equal(t, result, vm.PopResult())
} }
func TestVMAndCompilerCases(t *testing.T) { 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 := []testCase{}
testCases = append(testCases, numericTestCases...) testCases = append(testCases, numericTestCases...)
@ -46,3 +72,38 @@ func TestVMAndCompilerCases(t *testing.T) {
assert.Equal(t, tc.result, vm.PopResult()) 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 { type VM struct {
state State state State
// interop layer. // registered interop hooks.
interop *InteropService interop map[string]InteropFunc
// scripts loaded in memory. // scripts loaded in memory.
scripts map[util.Uint160][]byte scripts map[util.Uint160][]byte
@ -41,12 +41,9 @@ type VM struct {
} }
// New returns a new VM object ready to load .avm bytecode scripts. // New returns a new VM object ready to load .avm bytecode scripts.
func New(svc *InteropService, mode Mode) *VM { func New(mode Mode) *VM {
if svc == nil {
svc = NewInteropService()
}
vm := &VM{ vm := &VM{
interop: svc, interop: make(map[string]InteropFunc),
scripts: make(map[util.Uint160][]byte), scripts: make(map[util.Uint160][]byte),
state: haltState, state: haltState,
istack: NewStack("invocation"), istack: NewStack("invocation"),
@ -56,9 +53,44 @@ func New(svc *InteropService, mode Mode) *VM {
if mode == ModeMute { if mode == ModeMute {
vm.mute = true vm.mute = true
} }
// Register native interop hooks.
vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog)
vm.RegisterInteropFunc("Neo.Runtime.Notify", runtimeNotify)
return vm 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. // PrintOps will print the opcodes of the current loaded program to stdout.
func (v *VM) PrintOps() { func (v *VM) PrintOps() {
prog := v.Context().Program() prog := v.Context().Program()
@ -592,8 +624,11 @@ func (v *VM) execute(ctx *Context, op Opcode) {
case Osyscall: case Osyscall:
api := ctx.readVarBytes() api := ctx.readVarBytes()
err := v.interop.Call(api, v) ifunc, ok := v.interop[string(api)]
if err != nil { 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)) panic(fmt.Sprintf("failed to invoke syscall: %s", err))
} }

View file

@ -11,6 +11,31 @@ import (
"github.com/stretchr/testify/assert" "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) { func TestPushBytes1to75(t *testing.T) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
for i := 1; i <= 75; i++ { for i := 1; i <= 75; i++ {
@ -216,7 +241,7 @@ func makeProgram(opcodes ...Opcode) []byte {
} }
func load(prog []byte) *VM { func load(prog []byte) *VM {
vm := New(nil, ModeMute) vm := New(ModeMute)
vm.mute = true vm.mute = true
vm.istack.PushVal(NewContext(prog)) vm.istack.PushVal(NewContext(prog))
return vm return vm