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.
This commit is contained in:
Roman Khimov 2020-05-18 12:37:33 +03:00
parent 78b2387640
commit 5cebd4a7a2
4 changed files with 88 additions and 6 deletions

View file

@ -16,7 +16,7 @@ var (
"SHA1", "Hash256", "Hash160", "SHA1", "Hash256", "Hash160",
"VerifySignature", "AppCall", "VerifySignature", "AppCall",
"FromAddress", "Equals", "FromAddress", "Equals",
"panic", "panic", "DynAppCall",
} }
) )

View file

@ -1085,14 +1085,23 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
emit.Opcode(c.prog.BinWriter, opcode.HASH160) emit.Opcode(c.prog.BinWriter, opcode.HASH160)
case "VerifySignature": case "VerifySignature":
emit.Opcode(c.prog.BinWriter, opcode.VERIFY) emit.Opcode(c.prog.BinWriter, opcode.VERIFY)
case "AppCall": case "AppCall", "DynAppCall":
numArgs := len(expr.Args) - 1 numArgs := len(expr.Args)
if name == "AppCall" {
numArgs--
}
c.emitReverse(numArgs) c.emitReverse(numArgs)
emit.Opcode(c.prog.BinWriter, opcode.APPCALL) emit.Opcode(c.prog.BinWriter, opcode.APPCALL)
buf := c.getByteArray(expr.Args[0]) var buf []byte
if len(buf) != 20 { if name == "AppCall" {
c.prog.Err = errors.New("invalid script hash") 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) c.prog.WriteBytes(buf)

View file

@ -9,6 +9,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -52,6 +53,19 @@ func TestFromAddress(t *testing.T) {
} }
func TestAppCall(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 := ` srcInner := `
package foo package foo
func Main(a []byte, b []byte) []byte { func Main(a []byte, b []byte) []byte {
@ -62,14 +76,31 @@ func TestAppCall(t *testing.T) {
inner, err := compiler.Compile(strings.NewReader(srcInner)) inner, err := compiler.Compile(strings.NewReader(srcInner))
require.NoError(t, err) require.NoError(t, err)
dynapp, err := compiler.Compile(strings.NewReader(srcDynApp))
require.NoError(t, err)
ih := hash.Hash160(inner) ih := hash.Hash160(inner)
dh := hash.Hash160(dynapp)
getScript := func(u util.Uint160) ([]byte, bool) { getScript := func(u util.Uint160) ([]byte, bool) {
if u.Equals(ih) { if u.Equals(ih) {
return inner, true return inner, true
} }
if u.Equals(dh) {
return dynapp, hasDynamicInvoke
}
return nil, false 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) { t.Run("valid script", func(t *testing.T) {
src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE()))
v := vmAndCompile(t, src) v := vmAndCompile(t, src)
@ -118,6 +149,38 @@ func TestAppCall(t *testing.T) {
assertResult(t, v, []byte{1, 2, 3, 4}) 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 { func getAppCallScript(h string) string {

View file

@ -54,3 +54,13 @@ func GetEntryScriptHash() []byte {
func AppCall(scriptHash []byte, args ...interface{}) interface{} { func AppCall(scriptHash []byte, args ...interface{}) interface{} {
return nil 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
}