From 5cebd4a7a2e3bf8e45a410e2dd6b47aa05dec9c9 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 18 May 2020 12:37:33 +0300 Subject: [PATCH] compiler/engine: add dynamic APPCALL generation, fix #914 Previously we could generate dynamic appcall with a kludge of AppCall([]byte{/* 20 zeroes */, realScriptHash, args...) Now there is a separate function for this. --- pkg/compiler/analysis.go | 2 +- pkg/compiler/codegen.go | 19 ++++++++--- pkg/compiler/interop_test.go | 63 ++++++++++++++++++++++++++++++++++++ pkg/interop/engine/engine.go | 10 ++++++ 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 34134c6d5..bf4430c74 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -16,7 +16,7 @@ var ( "SHA1", "Hash256", "Hash160", "VerifySignature", "AppCall", "FromAddress", "Equals", - "panic", + "panic", "DynAppCall", } ) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index a88f56cf6..c2c312c6b 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1085,14 +1085,23 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { emit.Opcode(c.prog.BinWriter, opcode.HASH160) case "VerifySignature": emit.Opcode(c.prog.BinWriter, opcode.VERIFY) - case "AppCall": - numArgs := len(expr.Args) - 1 + case "AppCall", "DynAppCall": + numArgs := len(expr.Args) + if name == "AppCall" { + numArgs-- + } c.emitReverse(numArgs) emit.Opcode(c.prog.BinWriter, opcode.APPCALL) - buf := c.getByteArray(expr.Args[0]) - if len(buf) != 20 { - c.prog.Err = errors.New("invalid script hash") + var buf []byte + if name == "AppCall" { + buf = c.getByteArray(expr.Args[0]) + if len(buf) != 20 { + c.prog.Err = errors.New("invalid script hash") + } + } else { + // Zeroes for DynAppCall. + buf = make([]byte, 20) } c.prog.WriteBytes(buf) diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 39435c243..e335f5882 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -9,6 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" ) @@ -52,6 +53,19 @@ func TestFromAddress(t *testing.T) { } func TestAppCall(t *testing.T) { + const srcDynApp = ` + package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/engine" + func Main(h []byte) []byte { + x := []byte{1, 2} + y := []byte{3, 4} + result := engine.DynAppCall(h, x, y) + return result.([]byte) + } +` + + var hasDynamicInvoke bool + srcInner := ` package foo func Main(a []byte, b []byte) []byte { @@ -62,14 +76,31 @@ func TestAppCall(t *testing.T) { inner, err := compiler.Compile(strings.NewReader(srcInner)) require.NoError(t, err) + dynapp, err := compiler.Compile(strings.NewReader(srcDynApp)) + require.NoError(t, err) + ih := hash.Hash160(inner) + dh := hash.Hash160(dynapp) getScript := func(u util.Uint160) ([]byte, bool) { if u.Equals(ih) { return inner, true } + if u.Equals(dh) { + return dynapp, hasDynamicInvoke + } return nil, false } + dynEntryScript := ` + package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/engine" + func Main(h []byte) interface{} { + return engine.AppCall(` + fmt.Sprintf("%#v", dh.BytesBE()) + `, h) + } +` + dynentry, err := compiler.Compile(strings.NewReader(dynEntryScript)) + require.NoError(t, err) + t.Run("valid script", func(t *testing.T) { src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) v := vmAndCompile(t, src) @@ -118,6 +149,38 @@ func TestAppCall(t *testing.T) { assertResult(t, v, []byte{1, 2, 3, 4}) }) + + t.Run("dynamic", func(t *testing.T) { + t.Run("valid script", func(t *testing.T) { + hasDynamicInvoke = true + v := vm.New() + v.Load(dynentry) + v.SetScriptGetter(getScript) + v.Estack().PushVal(ih.BytesBE()) + + require.NoError(t, v.Run()) + + assertResult(t, v, []byte{1, 2, 3, 4}) + }) + t.Run("invalid script", func(t *testing.T) { + hasDynamicInvoke = true + v := vm.New() + v.Load(dynentry) + v.SetScriptGetter(getScript) + v.Estack().PushVal([]byte{1}) + + require.Error(t, v.Run()) + }) + t.Run("no dynamic invoke", func(t *testing.T) { + hasDynamicInvoke = false + v := vm.New() + v.Load(dynentry) + v.SetScriptGetter(getScript) + v.Estack().PushVal(ih.BytesBE()) + + require.Error(t, v.Run()) + }) + }) } func getAppCallScript(h string) string { diff --git a/pkg/interop/engine/engine.go b/pkg/interop/engine/engine.go index e4d303067..92ec895f0 100644 --- a/pkg/interop/engine/engine.go +++ b/pkg/interop/engine/engine.go @@ -54,3 +54,13 @@ func GetEntryScriptHash() []byte { func AppCall(scriptHash []byte, args ...interface{}) interface{} { return nil } + +// DynAppCall executes previously deployed blockchain contract with specified +// hash (160 bit in BE form represented as 20-byte slice) using provided +// arguments. It returns whatever this contract returns. It differs from AppCall +// in that you can use it for truly dynamic scriptHash values, but at the same +// time using it requires HasDynamicInvoke property set for a contract doing +// this call. This function uses `APPCALL` opcode. +func DynAppCall(scriptHash []byte, args ...interface{}) interface{} { + return nil +}