diff --git a/VERSION b/VERSION index e06193879..9b0025a78 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.39.2 +0.40.0 diff --git a/pkg/vm/README.md b/pkg/vm/README.md index 76b440f33..46e21b560 100644 --- a/pkg/vm/README.md +++ b/pkg/vm/README.md @@ -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. diff --git a/pkg/vm/smartcontract/runtime/runtime.go b/pkg/vm/api/runtime/runtime.go similarity index 88% rename from pkg/vm/smartcontract/runtime/runtime.go rename to pkg/vm/api/runtime/runtime.go index cd3312840..95d1f8c1f 100644 --- a/pkg/vm/smartcontract/runtime/runtime.go +++ b/pkg/vm/api/runtime/runtime.go @@ -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 { diff --git a/pkg/vm/api/storage/storage.go b/pkg/vm/api/storage/storage.go new file mode 100644 index 000000000..73c09e9aa --- /dev/null +++ b/pkg/vm/api/storage/storage.go @@ -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) {} diff --git a/pkg/vm/smartcontract/types/block.go b/pkg/vm/api/types/block.go similarity index 100% rename from pkg/vm/smartcontract/types/block.go rename to pkg/vm/api/types/block.go diff --git a/pkg/vm/api/util/util.go b/pkg/vm/api/util/util.go new file mode 100644 index 000000000..207dcc1d6 --- /dev/null +++ b/pkg/vm/api/util/util.go @@ -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{}) {} diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 782adf5ed..8eb3ea0f5 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -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 := 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 diff --git a/pkg/vm/compiler/README.md b/pkg/vm/compiler/README.md index fac47a5a7..1f4959887 100644 --- a/pkg/vm/compiler/README.md +++ b/pkg/vm/compiler/README.md @@ -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) diff --git a/pkg/vm/compiler/analysis.go b/pkg/vm/compiler/analysis.go index 85a267e61..bf0150aa5 100644 --- a/pkg/vm/compiler/analysis.go +++ b/pkg/vm/compiler/analysis.go @@ -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{} diff --git a/pkg/vm/compiler/codegen.go b/pkg/vm/compiler/codegen.go index 49b0e91c2..c7e2dee80 100644 --- a/pkg/vm/compiler/codegen.go +++ b/pkg/vm/compiler/codegen.go @@ -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) { diff --git a/pkg/vm/compiler/tests/constant_test.go b/pkg/vm/compiler/tests/constant_test.go deleted file mode 100644 index 1bca94d64..000000000 --- a/pkg/vm/compiler/tests/constant_test.go +++ /dev/null @@ -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", - }, -} diff --git a/pkg/vm/compiler/tests/for_test.go b/pkg/vm/compiler/tests/for_test.go deleted file mode 100644 index eb85a3a3f..000000000 --- a/pkg/vm/compiler/tests/for_test.go +++ /dev/null @@ -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", - }, -} diff --git a/pkg/vm/compiler/tests/function_call_test.go b/pkg/vm/compiler/tests/function_call_test.go deleted file mode 100644 index 66cdbca30..000000000 --- a/pkg/vm/compiler/tests/function_call_test.go +++ /dev/null @@ -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", - }, -} diff --git a/pkg/vm/compiler/tests/runtime_test.go b/pkg/vm/compiler/tests/runtime_test.go deleted file mode 100644 index 498122270..000000000 --- a/pkg/vm/compiler/tests/runtime_test.go +++ /dev/null @@ -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", - }, -} diff --git a/pkg/vm/compiler/tests/storage_test.go b/pkg/vm/compiler/tests/storage_test.go deleted file mode 100644 index d0d30d5bf..000000000 --- a/pkg/vm/compiler/tests/storage_test.go +++ /dev/null @@ -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", - }, -} diff --git a/pkg/vm/script_builder.go b/pkg/vm/emit.go similarity index 100% rename from pkg/vm/script_builder.go rename to pkg/vm/emit.go diff --git a/pkg/vm/script_builder_test.go b/pkg/vm/emit_test.go similarity index 100% rename from pkg/vm/script_builder_test.go rename to pkg/vm/emit_test.go diff --git a/pkg/vm/interop.go b/pkg/vm/interop.go index 9da8714c5..e59d4ac78 100644 --- a/pkg/vm/interop.go +++ b/pkg/vm/interop.go @@ -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 } diff --git a/pkg/vm/opcode.go b/pkg/vm/opcode.go index f220ccbc6..74dc75571 100644 --- a/pkg/vm/opcode.go +++ b/pkg/vm/opcode.go @@ -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 diff --git a/pkg/vm/opcode_string.go b/pkg/vm/opcode_string.go index 5d118ec0b..84b34c5e3 100644 --- a/pkg/vm/opcode_string.go +++ b/pkg/vm/opcode_string.go @@ -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 { diff --git a/pkg/vm/smartcontract/storage/storage.go b/pkg/vm/smartcontract/storage/storage.go deleted file mode 100644 index f6bfdcf0a..000000000 --- a/pkg/vm/smartcontract/storage/storage.go +++ /dev/null @@ -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 } diff --git a/pkg/vm/syscall.go b/pkg/vm/syscall.go index e3416dd3b..2e9c30260 100644 --- a/pkg/vm/syscall.go +++ b/pkg/vm/syscall.go @@ -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", diff --git a/pkg/vm/tests/contant_test.go b/pkg/vm/tests/contant_test.go new file mode 100644 index 000000000..be6a2a796 --- /dev/null +++ b/pkg/vm/tests/contant_test.go @@ -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")) +} diff --git a/pkg/vm/tests/for_test.go b/pkg/vm/tests/for_test.go index 6151d28be..ed6d26266 100644 --- a/pkg/vm/tests/for_test.go +++ b/pkg/vm/tests/for_test.go @@ -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 diff --git a/pkg/vm/tests/function_call_test.go b/pkg/vm/tests/function_call_test.go new file mode 100644 index 000000000..442d703b4 --- /dev/null +++ b/pkg/vm/tests/function_call_test.go @@ -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)) +} diff --git a/pkg/vm/tests/syscall_test.go b/pkg/vm/tests/syscall_test.go new file mode 100644 index 000000000..db28cb628 --- /dev/null +++ b/pkg/vm/tests/syscall_test.go @@ -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")) +} diff --git a/pkg/vm/tests/vm_test.go b/pkg/vm/tests/vm_test.go index f533578a6..66b2f8e59 100644 --- a/pkg/vm/tests/vm_test.go +++ b/pkg/vm/tests/vm_test.go @@ -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) +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 87be7f2ef..4bb132132 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -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)) } diff --git a/pkg/vm/opcode_test.go b/pkg/vm/vm_test.go similarity index 87% rename from pkg/vm/opcode_test.go rename to pkg/vm/vm_test.go index 2640b987c..58957ad9e 100644 --- a/pkg/vm/opcode_test.go +++ b/pkg/vm/vm_test.go @@ -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