diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index da05ad2f6..c1b01e2ee 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -14,7 +14,7 @@ var ( builtinFuncs = []string{ "len", "append", "SHA256", "SHA1", "Hash256", "Hash160", - "VerifySignature", + "VerifySignature", "AppCall", "FromAddress", "Equals", } ) @@ -180,6 +180,16 @@ func isBuiltin(expr ast.Expr) bool { return false } +func isAppCall(expr ast.Expr) bool { + t, ok := expr.(*ast.SelectorExpr) + return ok && t.Sel.Name == "AppCall" +} + +func isFromAddress(expr ast.Expr) bool { + t, ok := expr.(*ast.SelectorExpr) + return ok && t.Sel.Name == "FromAddress" +} + func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool { if len(lit.Elts) == 0 { return false diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 1d5e144eb..1023bd142 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -2,6 +2,7 @@ package compiler import ( "encoding/binary" + "errors" "fmt" "go/ast" "go/constant" @@ -488,8 +489,20 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil } + args := n.Args + isAppCall := isAppCall(n.Fun) + isFromAddress := isFromAddress(n.Fun) + // There are 2 special cases: + // 1. When using APPCALL, script hash is a part of the instruction so + // script hash should be emitted after APPCALL. + // 2. With FromAddress, parameter conversion is happening at compile-time + // so there is no need to push parameters on stack and perform an actual call + if isAppCall || isFromAddress { + args = n.Args[1:] + } + // Handle the arguments - for _, arg := range n.Args { + for _, arg := range args { ast.Walk(c, arg) } // Do not swap for builtin functions. @@ -513,6 +526,16 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { // Use the ident to check, builtins are not in func scopes. // We can be sure builtins are of type *ast.Ident. c.convertBuiltin(n) + + if isAppCall { + buf := c.getByteArray(n.Args[0]) + if len(buf) != 20 { + c.prog.Err = errors.New("invalid script hash") + return nil + } + + c.prog.WriteBytes(buf) + } case isSyscall(f): c.convertSyscall(f.selector.Name, f.name) default: @@ -634,6 +657,26 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return c } +// getByteArray returns byte array value from constant expr. +// Only literals are supported. +func (c *codegen) getByteArray(expr ast.Expr) []byte { + switch t := expr.(type) { + case *ast.CompositeLit: + if !isByteArray(t, c.typeInfo) { + return nil + } + buf := make([]byte, len(t.Elts)) + for i := 0; i < len(t.Elts); i++ { + t := c.typeInfo.Types[t.Elts[i]] + val, _ := constant.Int64Val(t.Value) + buf[i] = byte(val) + } + return buf + default: + return nil + } +} + func (c *codegen) convertSyscall(api, name string) { api, ok := syscalls[api][name] if !ok { @@ -687,6 +730,8 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { emitOpcode(c.prog.BinWriter, opcode.HASH160) case "VerifySignature": emitOpcode(c.prog.BinWriter, opcode.VERIFY) + case "AppCall": + emitOpcode(c.prog.BinWriter, opcode.APPCALL) case "Equals": emitOpcode(c.prog.BinWriter, opcode.EQUAL) case "FromAddress": diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go new file mode 100644 index 000000000..f1e0e1748 --- /dev/null +++ b/pkg/compiler/interop_test.go @@ -0,0 +1,116 @@ +package compiler_test + +import ( + "fmt" + "math/big" + "strings" + "testing" + + "github.com/CityOfZion/neo-go/pkg/compiler" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/encoding/address" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestFromAddress(t *testing.T) { + as1 := "Aej1fe4mUgou48Zzup5j8sPrE3973cJ5oz" + addr1, err := address.StringToUint160(as1) + require.NoError(t, err) + + as2 := "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y" + addr2, err := address.StringToUint160(as2) + require.NoError(t, err) + + t.Run("append 2 addresses", func(t *testing.T) { + src := ` + package foo + import "github.com/CityOfZion/neo-go/pkg/interop/util" + func Main() []byte { + addr1 := util.FromAddress("` + as1 + `") + addr2 := util.FromAddress("` + as2 + `") + sum := append(addr1, addr2...) + return sum + } + ` + + eval(t, src, append(addr1.BytesBE(), addr2.BytesBE()...)) + }) + + t.Run("append 2 addresses inline", func(t *testing.T) { + src := ` + package foo + import "github.com/CityOfZion/neo-go/pkg/interop/util" + func Main() []byte { + addr1 := util.FromAddress("` + as1 + `") + sum := append(addr1, util.FromAddress("` + as2 + `")...) + return sum + } + ` + + eval(t, src, append(addr1.BytesBE(), addr2.BytesBE()...)) + }) +} + +func TestAppCall(t *testing.T) { + srcInner := ` + package foo + func Main(args []interface{}) int { + a := args[0].(int) + b := args[1].(int) + return a + b + } + ` + + inner, err := compiler.Compile(strings.NewReader(srcInner)) + require.NoError(t, err) + + ih := hash.Hash160(inner) + getScript := func(u util.Uint160) []byte { + if u.Equals(ih) { + return inner + } + return nil + } + + t.Run("valid script", func(t *testing.T) { + src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) + v := vmAndCompile(t, src) + v.SetScriptGetter(getScript) + + require.NoError(t, v.Run()) + + assertResult(t, v, big.NewInt(42)) + }) + + t.Run("missing script", func(t *testing.T) { + h := ih + h[0] = ^h[0] + + src := getAppCallScript(fmt.Sprintf("%#v", h.BytesBE())) + v := vmAndCompile(t, src) + v.SetScriptGetter(getScript) + + require.Error(t, v.Run()) + }) + + t.Run("invalid script address", func(t *testing.T) { + src := getAppCallScript("[]byte{1, 2, 3}") + + _, err := compiler.Compile(strings.NewReader(src)) + require.Error(t, err) + }) +} + +func getAppCallScript(h string) string { + return ` + package foo + import "github.com/CityOfZion/neo-go/pkg/interop/engine" + func Main() int { + x := 13 + y := 29 + result := engine.AppCall(` + h + `, []interface{}{x, y}) + return result.(int) + } + ` +} diff --git a/pkg/interop/engine/engine.go b/pkg/interop/engine/engine.go index 057d42728..3230d4a09 100644 --- a/pkg/interop/engine/engine.go +++ b/pkg/interop/engine/engine.go @@ -27,3 +27,8 @@ func GetCallingScriptHash() []byte { func GetEntryScriptHash() []byte { return nil } + +// AppCall executes script with specified hash using provided arguments. +func AppCall(scriptHash []byte, args []interface{}) interface{} { + return nil +}