From b807fd9e7fae65a4904e86cebc2d49a7b29a77a2 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 8 Dec 2020 14:11:06 +0300 Subject: [PATCH 1/3] compiler: rename `engine.AppCall()` to `contract.Call()` --- examples/timer/timer.go | 3 +-- pkg/compiler/interop_test.go | 20 ++++++++++---------- pkg/compiler/syscall.go | 4 +--- pkg/interop/contract/contract.go | 8 ++++++++ pkg/interop/engine/engine.go | 16 ---------------- 5 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 pkg/interop/engine/engine.go diff --git a/examples/timer/timer.go b/examples/timer/timer.go index 169506fa0..0b0839df2 100644 --- a/examples/timer/timer.go +++ b/examples/timer/timer.go @@ -3,7 +3,6 @@ package timer import ( "github.com/nspcc-dev/neo-go/pkg/interop/binary" "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/engine" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neo-go/pkg/interop/util" @@ -54,7 +53,7 @@ func Tick() bool { ticksLeft = ticksLeft.(int) - 1 if ticksLeft == 0 { runtime.Log("Fired!") - return engine.AppCall(runtime.GetExecutingScriptHash(), "selfDestroy").(bool) + return contract.Call(runtime.GetExecutingScriptHash(), "selfDestroy").(bool) } storage.Put(ctx, ticksKey, ticksLeft) i := binary.Itoa(ticksLeft.(int), 10) diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index a87bcbfbe..91a5dbb39 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -132,12 +132,12 @@ func TestAppCall(t *testing.T) { t.Run("convert from string constant", func(t *testing.T) { src := ` package foo - import "github.com/nspcc-dev/neo-go/pkg/interop/engine" + import "github.com/nspcc-dev/neo-go/pkg/interop/contract" const scriptHash = ` + fmt.Sprintf("%#v", string(ih.BytesBE())) + ` func Main() []byte { x := []byte{1, 2} y := []byte{3, 4} - result := engine.AppCall([]byte(scriptHash), "append", x, y) + result := contract.Call([]byte(scriptHash), "append", x, y) return result.([]byte) } ` @@ -151,12 +151,12 @@ func TestAppCall(t *testing.T) { t.Run("convert from var", func(t *testing.T) { src := ` package foo - import "github.com/nspcc-dev/neo-go/pkg/interop/engine" + import "github.com/nspcc-dev/neo-go/pkg/interop/contract" func Main() []byte { x := []byte{1, 2} y := []byte{3, 4} var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `) - result := engine.AppCall(addr, "append", x, y) + result := contract.Call(addr, "append", x, y) return result.([]byte) } ` @@ -169,10 +169,10 @@ func TestAppCall(t *testing.T) { t.Run("InitializedGlobals", func(t *testing.T) { src := `package foo - import "github.com/nspcc-dev/neo-go/pkg/interop/engine" + import "github.com/nspcc-dev/neo-go/pkg/interop/contract" func Main() int { var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `) - result := engine.AppCall(addr, "add3", 39) + result := contract.Call(addr, "add3", 39) return result.(int) }` @@ -184,10 +184,10 @@ func TestAppCall(t *testing.T) { t.Run("AliasPackage", func(t *testing.T) { src := `package foo - import ee "github.com/nspcc-dev/neo-go/pkg/interop/engine" + import ee "github.com/nspcc-dev/neo-go/pkg/interop/contract" func Main() int { var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `) - result := ee.AppCall(addr, "add3", 39) + result := ee.Call(addr, "add3", 39) return result.(int) }` v := spawnVM(t, ic, src) @@ -199,11 +199,11 @@ func TestAppCall(t *testing.T) { func getAppCallScript(h string) string { return ` package foo - import "github.com/nspcc-dev/neo-go/pkg/interop/engine" + import "github.com/nspcc-dev/neo-go/pkg/interop/contract" func Main() []byte { x := []byte{1, 2} y := []byte{3, 4} - result := engine.AppCall(` + h + `, "append", x, y) + result := contract.Call(` + h + `, "append", x, y) return result.([]byte) } ` diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index 3ad960b4b..37626157f 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -23,6 +23,7 @@ var syscalls = map[string]map[string]string{ "GetTransactionHeight": interopnames.SystemBlockchainGetTransactionHeight, }, "contract": { + "Call": interopnames.SystemContractCall, "Create": interopnames.SystemContractCreate, "CreateStandardAccount": interopnames.SystemContractCreateStandardAccount, "Destroy": interopnames.SystemContractDestroy, @@ -44,9 +45,6 @@ var syscalls = map[string]map[string]string{ "Next": interopnames.SystemEnumeratorNext, "Value": interopnames.SystemEnumeratorValue, }, - "engine": { - "AppCall": interopnames.SystemContractCall, - }, "iterator": { "Concat": interopnames.SystemIteratorConcat, "Create": interopnames.SystemIteratorCreate, diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index 89862018a..8a6f15639 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -55,3 +55,11 @@ func CreateStandardAccount(pub interop.PublicKey) []byte { func GetCallFlags() int64 { return 0 } + +// Call executes previously deployed blockchain contract with specified hash +// (20 bytes in BE form) using provided arguments. +// It returns whatever this contract returns. This function uses +// `System.Contract.Call` syscall. +func Call(scriptHash interop.Hash160, method string, args ...interface{}) interface{} { + return nil +} diff --git a/pkg/interop/engine/engine.go b/pkg/interop/engine/engine.go deleted file mode 100644 index 353044c15..000000000 --- a/pkg/interop/engine/engine.go +++ /dev/null @@ -1,16 +0,0 @@ -/* -Package engine allows to make contract calls. -It's roughly similar in function to ExecutionEngine class in the Neo .net -framework. -*/ -package engine - -import "github.com/nspcc-dev/neo-go/pkg/interop" - -// AppCall 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. This function uses -// `System.Contract.Call` syscall. -func AppCall(scriptHash interop.Hash160, method string, args ...interface{}) interface{} { - return nil -} From ec58bec8034b3754c2fed670208c9016b547a4fc Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 8 Dec 2020 15:24:01 +0300 Subject: [PATCH 2/3] compiler: fix global constant traversal There can be no global variables, but some global constants. Introduced in 0b44a430. --- pkg/compiler/analysis.go | 25 ++++++++++++++++-------- pkg/compiler/global_test.go | 22 +++++++++++++++------ pkg/compiler/testdata/constonly/const.go | 4 ++++ 3 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 pkg/compiler/testdata/constonly/const.go diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index b1015d3cd..fa3fa5501 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -43,11 +43,13 @@ func (c *codegen) getIdentName(pkg string, name string) string { // Same for `_deploy()` functions (see docs/compiler.md). func (c *codegen) traverseGlobals() (int, int, int) { var hasDefer bool - var n int + var n, nConst int initLocals := -1 deployLocals := -1 c.ForEachFile(func(f *ast.File, _ *types.Package) { - n += countGlobals(f) + nv, nc := countGlobals(f) + n += nv + nConst += nc if initLocals == -1 || deployLocals == -1 || !hasDefer { ast.Inspect(f, func(node ast.Node) bool { switch n := node.(type) { @@ -75,7 +77,7 @@ func (c *codegen) traverseGlobals() (int, int, int) { if hasDefer { n++ } - if n != 0 || initLocals > -1 { + if n+nConst != 0 || initLocals > -1 { if n > 255 { c.prog.BinWriter.Err = errors.New("too many global variables") return 0, initLocals, deployLocals @@ -88,7 +90,7 @@ func (c *codegen) traverseGlobals() (int, int, int) { } seenBefore := false c.ForEachPackage(func(pkg *loader.PackageInfo) { - if n > 0 { + if n+nConst > 0 { for _, f := range pkg.Files { c.fillImportMap(f, pkg.Pkg) c.convertGlobals(f, pkg.Pkg) @@ -116,7 +118,9 @@ func (c *codegen) traverseGlobals() (int, int, int) { // countGlobals counts the global variables in the program to add // them with the stack size of the function. -func countGlobals(f ast.Node) (i int) { +// Second returned argument contains amount of global constants. +func countGlobals(f ast.Node) (int, int) { + var numVar, numConst int ast.Inspect(f, func(node ast.Node) bool { switch n := node.(type) { // Skip all function declarations if we have already encountered `defer`. @@ -125,11 +129,16 @@ func countGlobals(f ast.Node) (i int) { // After skipping all funcDecls we are sure that each value spec // is a global declared variable or constant. case *ast.GenDecl: - if n.Tok == token.VAR { + isVar := n.Tok == token.VAR + if isVar || n.Tok == token.CONST { for _, s := range n.Specs { for _, id := range s.(*ast.ValueSpec).Names { if id.Name != "_" { - i++ + if isVar { + numVar++ + } else { + numConst++ + } } } } @@ -138,7 +147,7 @@ func countGlobals(f ast.Node) (i int) { } return true }) - return + return numVar, numConst } // isExprNil looks if the given expression is a `nil`. diff --git a/pkg/compiler/global_test.go b/pkg/compiler/global_test.go index 434c22290..f58673d07 100644 --- a/pkg/compiler/global_test.go +++ b/pkg/compiler/global_test.go @@ -202,12 +202,22 @@ func TestExportedVariable(t *testing.T) { } func TestExportedConst(t *testing.T) { - src := `package foo - import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/multi" - func Main() int { - return multi.SomeConst - }` - eval(t, src, big.NewInt(42)) + t.Run("with vars", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/multi" + func Main() int { + return multi.SomeConst + }` + eval(t, src, big.NewInt(42)) + }) + t.Run("const only", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/constonly" + func Main() int { + return constonly.Answer + }` + eval(t, src, big.NewInt(42)) + }) } func TestMultipleFuncSameName(t *testing.T) { diff --git a/pkg/compiler/testdata/constonly/const.go b/pkg/compiler/testdata/constonly/const.go new file mode 100644 index 000000000..8066eab5f --- /dev/null +++ b/pkg/compiler/testdata/constonly/const.go @@ -0,0 +1,4 @@ +package constonly + +// Answer is the only thing you will ever need. +const Answer = 42 From 37a855021514eed4b1d34c3bb662b4017abd9fc8 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 8 Dec 2020 15:37:03 +0300 Subject: [PATCH 3/3] 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 +}