compiler: implement engine.AppCall interop

This commit is contained in:
Evgenii Stratonikov 2020-01-27 10:59:57 +03:00
parent ead0b8ff94
commit 330db36168
4 changed files with 129 additions and 2 deletions

View file

@ -14,7 +14,7 @@ var (
builtinFuncs = []string{ builtinFuncs = []string{
"len", "append", "SHA256", "len", "append", "SHA256",
"SHA1", "Hash256", "Hash160", "SHA1", "Hash256", "Hash160",
"VerifySignature", "VerifySignature", "AppCall",
"FromAddress", "Equals", "FromAddress", "Equals",
} }
) )
@ -180,6 +180,11 @@ func isBuiltin(expr ast.Expr) bool {
return false return false
} }
func isAppCall(expr ast.Expr) bool {
t, ok := expr.(*ast.SelectorExpr)
return ok && t.Sel.Name == "AppCall"
}
func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool { func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool {
if len(lit.Elts) == 0 { if len(lit.Elts) == 0 {
return false return false

View file

@ -2,6 +2,7 @@ package compiler
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"go/ast" "go/ast"
"go/constant" "go/constant"
@ -488,8 +489,16 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
return nil return nil
} }
args := n.Args
isAppCall := isAppCall(n.Fun)
// When using APPCALL, script hash is a part of the instruction so
// script hash should be emitted after APPCALL.
if isAppCall {
args = n.Args[1:]
}
// Handle the arguments // Handle the arguments
for _, arg := range n.Args { for _, arg := range args {
ast.Walk(c, arg) ast.Walk(c, arg)
} }
// Do not swap for builtin functions. // Do not swap for builtin functions.
@ -513,6 +522,16 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
// Use the ident to check, builtins are not in func scopes. // Use the ident to check, builtins are not in func scopes.
// We can be sure builtins are of type *ast.Ident. // We can be sure builtins are of type *ast.Ident.
c.convertBuiltin(n) 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): case isSyscall(f):
c.convertSyscall(f.selector.Name, f.name) c.convertSyscall(f.selector.Name, f.name)
default: default:
@ -634,6 +653,26 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
return c 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) { func (c *codegen) convertSyscall(api, name string) {
api, ok := syscalls[api][name] api, ok := syscalls[api][name]
if !ok { if !ok {
@ -687,6 +726,8 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
emitOpcode(c.prog.BinWriter, opcode.HASH160) emitOpcode(c.prog.BinWriter, opcode.HASH160)
case "VerifySignature": case "VerifySignature":
emitOpcode(c.prog.BinWriter, opcode.VERIFY) emitOpcode(c.prog.BinWriter, opcode.VERIFY)
case "AppCall":
emitOpcode(c.prog.BinWriter, opcode.APPCALL)
case "Equals": case "Equals":
emitOpcode(c.prog.BinWriter, opcode.EQUAL) emitOpcode(c.prog.BinWriter, opcode.EQUAL)
case "FromAddress": case "FromAddress":

View file

@ -0,0 +1,76 @@
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/util"
"github.com/stretchr/testify/require"
)
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)
}
`
}

View file

@ -27,3 +27,8 @@ func GetCallingScriptHash() []byte {
func GetEntryScriptHash() []byte { func GetEntryScriptHash() []byte {
return nil return nil
} }
// AppCall executes script with specified hash using provided arguments.
func AppCall(scriptHash []byte, args []interface{}) interface{} {
return nil
}