From 37a855021514eed4b1d34c3bb662b4017abd9fc8 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 8 Dec 2020 15:37:03 +0300 Subject: [PATCH] compiler: add `contract.CallEx` interop --- pkg/compiler/codegen.go | 9 +++++- pkg/compiler/interop_test.go | 52 +++++++++++++++++++++++++++++--- pkg/compiler/syscall.go | 1 + pkg/compiler/syscall_test.go | 13 ++++++++ pkg/interop/contract/contract.go | 23 ++++++++++++++ 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index a8f2a925e..0a976b73f 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -850,7 +850,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { emit.Opcodes(c.prog.BinWriter, opcode.PACK) numArgs -= varSize - 1 } - c.emitReverse(numArgs) + // CallFlag in CallEx interop should be the last argument + // but this can't be reflected in signature due to varargs. + // It is first in compiler interop though, thus we just need to reverse 1 values less. + if f != nil && isSyscall(f) && f.pkg.Name() == "contract" && f.name == "CallEx" { + c.emitReverse(numArgs - 1) + } else { + c.emitReverse(numArgs) + } } // Check builtin first to avoid nil pointer on funcScope! diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 91a5dbb39..dfdf7e8f6 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/encoding/address" + cinterop "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -85,8 +86,26 @@ func spawnVM(t *testing.T, ic *interop.Context, src string) *vm.VM { } func TestAppCall(t *testing.T) { - srcInner := ` - package foo + srcDeep := `package foo + func Get42() int { + return 42 + }` + barCtr, di, err := compiler.CompileWithDebugInfo("bar.go", strings.NewReader(srcDeep)) + require.NoError(t, err) + mBar, err := di.ConvertToManifest("Bar", nil) + require.NoError(t, err) + + barH := hash.Hash160(barCtr) + ic := interop.NewContext(trigger.Application, nil, dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false), nil, nil, nil, zaptest.NewLogger(t)) + require.NoError(t, ic.DAO.PutContractState(&state.Contract{ + Hash: barH, + Script: barCtr, + Manifest: *mBar, + })) + + srcInner := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/contract" + import "github.com/nspcc-dev/neo-go/pkg/interop" var a int = 3 func Main(a []byte, b []byte) []byte { panic("Main was called") @@ -97,7 +116,11 @@ func TestAppCall(t *testing.T) { func Add3(n int) int { return a + n } - ` + func CallInner() int { + return contract.Call(%s, "get42").(int) + }` + srcInner = fmt.Sprintf(srcInner, + fmt.Sprintf("%#v", cinterop.Hash160(barH.BytesBE()))) inner, di, err := compiler.CompileWithDebugInfo("foo.go", strings.NewReader(srcInner)) require.NoError(t, err) @@ -105,7 +128,6 @@ func TestAppCall(t *testing.T) { require.NoError(t, err) ih := hash.Hash160(inner) - ic := interop.NewContext(trigger.Application, nil, dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false), nil, nil, nil, zaptest.NewLogger(t)) require.NoError(t, ic.DAO.PutContractState(&state.Contract{ Hash: ih, Script: inner, @@ -120,6 +142,19 @@ func TestAppCall(t *testing.T) { assertResult(t, v, []byte{1, 2, 3, 4}) }) + t.Run("callEx, valid", func(t *testing.T) { + src := getCallExScript(fmt.Sprintf("%#v", ih.BytesBE()), "contract.AllowCall") + v := spawnVM(t, ic, src) + require.NoError(t, v.Run()) + + assertResult(t, v, big.NewInt(42)) + }) + t.Run("callEx, missing flags", func(t *testing.T) { + src := getCallExScript(fmt.Sprintf("%#v", ih.BytesBE()), "contract.NoneFlag") + v := spawnVM(t, ic, src) + require.Error(t, v.Run()) + }) + t.Run("missing script", func(t *testing.T) { h := ih h[0] = ^h[0] @@ -209,6 +244,15 @@ func getAppCallScript(h string) string { ` } +func getCallExScript(h string, flags string) string { + return `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/contract" + func Main() int { + result := contract.CallEx(` + flags + `, ` + h + `, "callInner") + return result.(int) + }` +} + func TestBuiltinDoesNotCompile(t *testing.T) { src := `package foo import "github.com/nspcc-dev/neo-go/pkg/interop/util" diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index 37626157f..964564582 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -24,6 +24,7 @@ var syscalls = map[string]map[string]string{ }, "contract": { "Call": interopnames.SystemContractCall, + "CallEx": interopnames.SystemContractCallEx, "Create": interopnames.SystemContractCreate, "CreateStandardAccount": interopnames.SystemContractCreateStandardAccount, "Destroy": interopnames.SystemContractDestroy, diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index 475c94144..c931c246d 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -4,11 +4,24 @@ import ( "math/big" "testing" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Checks that changes in `smartcontract` are reflected in compiler interop package. +func TestCallFlags(t *testing.T) { + require.EqualValues(t, contract.AllowStates, smartcontract.AllowStates) + require.EqualValues(t, contract.AllowModifyStates, smartcontract.AllowModifyStates) + require.EqualValues(t, contract.AllowCall, smartcontract.AllowCall) + require.EqualValues(t, contract.AllowNotify, smartcontract.AllowNotify) + require.EqualValues(t, contract.ReadOnly, smartcontract.ReadOnly) + require.EqualValues(t, contract.All, smartcontract.All) + require.EqualValues(t, contract.NoneFlag, smartcontract.NoneFlag) +} + func TestStoragePutGet(t *testing.T) { src := ` package foo diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index 8a6f15639..bc21470a2 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -14,6 +14,21 @@ type Contract struct { Manifest []byte } +// CallFlag specifies valid call flags. +type CallFlag byte + +// Using `smartcontract` package from compiled contract requires moderate +// compiler refactoring, thus all flags are mirrored here. +const ( + AllowStates CallFlag = 1 << iota + AllowModifyStates + AllowCall + AllowNotify + ReadOnly = AllowStates | AllowCall | AllowNotify + All = ReadOnly | AllowModifyStates + NoneFlag CallFlag = 0 +) + // Create creates a new contract using a set of input parameters: // script contract's bytecode (limited in length by 1M) // manifest contract's manifest (limited in length by 2 KiB) @@ -63,3 +78,11 @@ func GetCallFlags() int64 { func Call(scriptHash interop.Hash160, method string, args ...interface{}) interface{} { return nil } + +// CallEx executes previously deployed blockchain contract with specified hash +// (20 bytes in BE form) using provided arguments and call flags. +// It returns whatever this contract returns. This function uses +// `System.Contract.CallEx` syscall. +func CallEx(f CallFlag, scriptHash interop.Hash160, method string, args ...interface{}) interface{} { + return nil +}