forked from TrueCloudLab/neoneo-go
Merge pull request #621 from nspcc-dev/feature/appcall
compiler: implement AppCall interop
This commit is contained in:
commit
1cfad9ccb8
4 changed files with 178 additions and 2 deletions
|
@ -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,16 @@ 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 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 {
|
func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool {
|
||||||
if len(lit.Elts) == 0 {
|
if len(lit.Elts) == 0 {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -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,20 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
return nil
|
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
|
// 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 +526,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 +657,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 +730,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":
|
||||||
|
|
116
pkg/compiler/interop_test.go
Normal file
116
pkg/compiler/interop_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue