Merge pull request #1598 from nspcc-dev/compiler/callex

Implement `contract.CallEx` in compiler
This commit is contained in:
Roman Khimov 2020-12-10 14:36:11 +03:00 committed by GitHub
commit 982de99cd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 150 additions and 50 deletions

View file

@ -3,7 +3,6 @@ package timer
import ( import (
"github.com/nspcc-dev/neo-go/pkg/interop/binary" "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/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/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util" "github.com/nspcc-dev/neo-go/pkg/interop/util"
@ -54,7 +53,7 @@ func Tick() bool {
ticksLeft = ticksLeft.(int) - 1 ticksLeft = ticksLeft.(int) - 1
if ticksLeft == 0 { if ticksLeft == 0 {
runtime.Log("Fired!") runtime.Log("Fired!")
return engine.AppCall(runtime.GetExecutingScriptHash(), "selfDestroy").(bool) return contract.Call(runtime.GetExecutingScriptHash(), "selfDestroy").(bool)
} }
storage.Put(ctx, ticksKey, ticksLeft) storage.Put(ctx, ticksKey, ticksLeft)
i := binary.Itoa(ticksLeft.(int), 10) i := binary.Itoa(ticksLeft.(int), 10)

View file

@ -43,11 +43,13 @@ func (c *codegen) getIdentName(pkg string, name string) string {
// Same for `_deploy()` functions (see docs/compiler.md). // Same for `_deploy()` functions (see docs/compiler.md).
func (c *codegen) traverseGlobals() (int, int, int) { func (c *codegen) traverseGlobals() (int, int, int) {
var hasDefer bool var hasDefer bool
var n int var n, nConst int
initLocals := -1 initLocals := -1
deployLocals := -1 deployLocals := -1
c.ForEachFile(func(f *ast.File, _ *types.Package) { 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 { if initLocals == -1 || deployLocals == -1 || !hasDefer {
ast.Inspect(f, func(node ast.Node) bool { ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) { switch n := node.(type) {
@ -75,7 +77,7 @@ func (c *codegen) traverseGlobals() (int, int, int) {
if hasDefer { if hasDefer {
n++ n++
} }
if n != 0 || initLocals > -1 { if n+nConst != 0 || initLocals > -1 {
if n > 255 { if n > 255 {
c.prog.BinWriter.Err = errors.New("too many global variables") c.prog.BinWriter.Err = errors.New("too many global variables")
return 0, initLocals, deployLocals return 0, initLocals, deployLocals
@ -88,7 +90,7 @@ func (c *codegen) traverseGlobals() (int, int, int) {
} }
seenBefore := false seenBefore := false
c.ForEachPackage(func(pkg *loader.PackageInfo) { c.ForEachPackage(func(pkg *loader.PackageInfo) {
if n > 0 { if n+nConst > 0 {
for _, f := range pkg.Files { for _, f := range pkg.Files {
c.fillImportMap(f, pkg.Pkg) c.fillImportMap(f, pkg.Pkg)
c.convertGlobals(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 // countGlobals counts the global variables in the program to add
// them with the stack size of the function. // 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 { ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) { switch n := node.(type) {
// Skip all function declarations if we have already encountered `defer`. // 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 // After skipping all funcDecls we are sure that each value spec
// is a global declared variable or constant. // is a global declared variable or constant.
case *ast.GenDecl: 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 _, s := range n.Specs {
for _, id := range s.(*ast.ValueSpec).Names { for _, id := range s.(*ast.ValueSpec).Names {
if id.Name != "_" { if id.Name != "_" {
i++ if isVar {
numVar++
} else {
numConst++
}
} }
} }
} }
@ -138,7 +147,7 @@ func countGlobals(f ast.Node) (i int) {
} }
return true return true
}) })
return return numVar, numConst
} }
// isExprNil looks if the given expression is a `nil`. // isExprNil looks if the given expression is a `nil`.

View file

@ -850,7 +850,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
emit.Opcodes(c.prog.BinWriter, opcode.PACK) emit.Opcodes(c.prog.BinWriter, opcode.PACK)
numArgs -= varSize - 1 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! // Check builtin first to avoid nil pointer on funcScope!

View file

@ -202,12 +202,22 @@ func TestExportedVariable(t *testing.T) {
} }
func TestExportedConst(t *testing.T) { func TestExportedConst(t *testing.T) {
src := `package foo t.Run("with vars", func(t *testing.T) {
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/multi" src := `package foo
func Main() int { import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/multi"
return multi.SomeConst func Main() int {
}` return multi.SomeConst
eval(t, src, big.NewInt(42)) }`
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) { func TestMultipleFuncSameName(t *testing.T) {

View file

@ -15,6 +15,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/storage" "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/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "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"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/vm" "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) { func TestAppCall(t *testing.T) {
srcInner := ` srcDeep := `package foo
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 var a int = 3
func Main(a []byte, b []byte) []byte { func Main(a []byte, b []byte) []byte {
panic("Main was called") panic("Main was called")
@ -97,7 +116,11 @@ func TestAppCall(t *testing.T) {
func Add3(n int) int { func Add3(n int) int {
return a + n 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)) inner, di, err := compiler.CompileWithDebugInfo("foo.go", strings.NewReader(srcInner))
require.NoError(t, err) require.NoError(t, err)
@ -105,7 +128,6 @@ func TestAppCall(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
ih := hash.Hash160(inner) 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{ require.NoError(t, ic.DAO.PutContractState(&state.Contract{
Hash: ih, Hash: ih,
Script: inner, Script: inner,
@ -120,6 +142,19 @@ func TestAppCall(t *testing.T) {
assertResult(t, v, []byte{1, 2, 3, 4}) 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) { t.Run("missing script", func(t *testing.T) {
h := ih h := ih
h[0] = ^h[0] h[0] = ^h[0]
@ -132,12 +167,12 @@ func TestAppCall(t *testing.T) {
t.Run("convert from string constant", func(t *testing.T) { t.Run("convert from string constant", func(t *testing.T) {
src := ` src := `
package foo 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())) + ` const scriptHash = ` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `
func Main() []byte { func Main() []byte {
x := []byte{1, 2} x := []byte{1, 2}
y := []byte{3, 4} y := []byte{3, 4}
result := engine.AppCall([]byte(scriptHash), "append", x, y) result := contract.Call([]byte(scriptHash), "append", x, y)
return result.([]byte) return result.([]byte)
} }
` `
@ -151,12 +186,12 @@ func TestAppCall(t *testing.T) {
t.Run("convert from var", func(t *testing.T) { t.Run("convert from var", func(t *testing.T) {
src := ` src := `
package foo 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 { func Main() []byte {
x := []byte{1, 2} x := []byte{1, 2}
y := []byte{3, 4} y := []byte{3, 4}
var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `) 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) return result.([]byte)
} }
` `
@ -169,10 +204,10 @@ func TestAppCall(t *testing.T) {
t.Run("InitializedGlobals", func(t *testing.T) { t.Run("InitializedGlobals", func(t *testing.T) {
src := `package foo 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 { func Main() int {
var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `) var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `)
result := engine.AppCall(addr, "add3", 39) result := contract.Call(addr, "add3", 39)
return result.(int) return result.(int)
}` }`
@ -184,10 +219,10 @@ func TestAppCall(t *testing.T) {
t.Run("AliasPackage", func(t *testing.T) { t.Run("AliasPackage", func(t *testing.T) {
src := `package foo 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 { func Main() int {
var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `) var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `)
result := ee.AppCall(addr, "add3", 39) result := ee.Call(addr, "add3", 39)
return result.(int) return result.(int)
}` }`
v := spawnVM(t, ic, src) v := spawnVM(t, ic, src)
@ -199,16 +234,25 @@ func TestAppCall(t *testing.T) {
func getAppCallScript(h string) string { func getAppCallScript(h string) string {
return ` return `
package foo 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 { func Main() []byte {
x := []byte{1, 2} x := []byte{1, 2}
y := []byte{3, 4} y := []byte{3, 4}
result := engine.AppCall(` + h + `, "append", x, y) result := contract.Call(` + h + `, "append", x, y)
return result.([]byte) return result.([]byte)
} }
` `
} }
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) { func TestBuiltinDoesNotCompile(t *testing.T) {
src := `package foo src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/interop/util" import "github.com/nspcc-dev/neo-go/pkg/interop/util"

View file

@ -23,6 +23,8 @@ var syscalls = map[string]map[string]string{
"GetTransactionHeight": interopnames.SystemBlockchainGetTransactionHeight, "GetTransactionHeight": interopnames.SystemBlockchainGetTransactionHeight,
}, },
"contract": { "contract": {
"Call": interopnames.SystemContractCall,
"CallEx": interopnames.SystemContractCallEx,
"Create": interopnames.SystemContractCreate, "Create": interopnames.SystemContractCreate,
"CreateStandardAccount": interopnames.SystemContractCreateStandardAccount, "CreateStandardAccount": interopnames.SystemContractCreateStandardAccount,
"Destroy": interopnames.SystemContractDestroy, "Destroy": interopnames.SystemContractDestroy,
@ -44,9 +46,6 @@ var syscalls = map[string]map[string]string{
"Next": interopnames.SystemEnumeratorNext, "Next": interopnames.SystemEnumeratorNext,
"Value": interopnames.SystemEnumeratorValue, "Value": interopnames.SystemEnumeratorValue,
}, },
"engine": {
"AppCall": interopnames.SystemContractCall,
},
"iterator": { "iterator": {
"Concat": interopnames.SystemIteratorConcat, "Concat": interopnames.SystemIteratorConcat,
"Create": interopnames.SystemIteratorCreate, "Create": interopnames.SystemIteratorCreate,

View file

@ -4,11 +4,24 @@ import (
"math/big" "math/big"
"testing" "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/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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) { func TestStoragePutGet(t *testing.T) {
src := ` src := `
package foo package foo

View file

@ -0,0 +1,4 @@
package constonly
// Answer is the only thing you will ever need.
const Answer = 42

View file

@ -14,6 +14,21 @@ type Contract struct {
Manifest []byte 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: // Create creates a new contract using a set of input parameters:
// script contract's bytecode (limited in length by 1M) // script contract's bytecode (limited in length by 1M)
// manifest contract's manifest (limited in length by 2 KiB) // manifest contract's manifest (limited in length by 2 KiB)
@ -55,3 +70,19 @@ func CreateStandardAccount(pub interop.PublicKey) []byte {
func GetCallFlags() int64 { func GetCallFlags() int64 {
return 0 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
}
// 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
}

View file

@ -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
}