Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement System.Callback.* interops #1238

Merged
merged 9 commits into from
Jul 30, 2020
21 changes: 9 additions & 12 deletions pkg/compiler/panic_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler_test

import (
"errors"
"fmt"
"math/big"
"testing"
Expand All @@ -20,7 +21,7 @@ func TestPanic(t *testing.T) {
var logs []string
src := getPanicSource(true, `"execution fault"`)
v := vmAndCompile(t, src)
v.RegisterInteropGetter(logGetter(&logs))
v.SyscallHandler = getLogHandler(&logs)

require.Error(t, v.Run())
require.True(t, v.HasFailed())
Expand All @@ -32,7 +33,7 @@ func TestPanic(t *testing.T) {
var logs []string
src := getPanicSource(true, `nil`)
v := vmAndCompile(t, src)
v.RegisterInteropGetter(logGetter(&logs))
v.SyscallHandler = getLogHandler(&logs)

require.Error(t, v.Run())
require.True(t, v.HasFailed())
Expand All @@ -54,19 +55,15 @@ func getPanicSource(need bool, message string) string {
`, need, message)
}

func logGetter(logs *[]string) vm.InteropGetterFunc {
func getLogHandler(logs *[]string) vm.SyscallHandler {
logID := emit.InteropNameToID([]byte("System.Runtime.Log"))
return func(id uint32) *vm.InteropFuncPrice {
return func(v *vm.VM, id uint32) error {
if id != logID {
return nil
return errors.New("syscall not found")
}

return &vm.InteropFuncPrice{
Func: func(v *vm.VM) error {
msg := string(v.Estack().Pop().Bytes())
*logs = append(*logs, msg)
return nil
},
}
msg := string(v.Estack().Pop().Bytes())
*logs = append(*logs, msg)
return nil
}
}
3 changes: 2 additions & 1 deletion pkg/compiler/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ func TestSHA256(t *testing.T) {
`
v := vmAndCompile(t, src)
ic := &interop.Context{Trigger: trigger.Verification}
v.RegisterInteropGetter(crypto.GetInterop(ic))
crypto.Register(ic)
v.SyscallHandler = ic.SyscallHandler
require.NoError(t, v.Run())
require.True(t, v.Estack().Len() >= 1)

Expand Down
17 changes: 11 additions & 6 deletions pkg/compiler/vm_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler_test

import (
"errors"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -68,7 +69,8 @@ func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) {
vm := vm.New()

storePlugin := newStoragePlugin()
vm.RegisterInteropGetter(storePlugin.getInterop)
vm.GasLimit = -1
vm.SyscallHandler = storePlugin.syscallHandler

b, di, err := compiler.CompileWithDebugInfo(strings.NewReader(src))
require.NoError(t, err)
Expand Down Expand Up @@ -97,14 +99,14 @@ func invokeMethod(t *testing.T, method string, script []byte, v *vm.VM, di *comp

type storagePlugin struct {
mem map[string][]byte
interops map[uint32]vm.InteropFunc
interops map[uint32]func(v *vm.VM) error
events []state.NotificationEvent
}

func newStoragePlugin() *storagePlugin {
s := &storagePlugin{
mem: make(map[string][]byte),
interops: make(map[uint32]vm.InteropFunc),
interops: make(map[uint32]func(v *vm.VM) error),
}
s.interops[emit.InteropNameToID([]byte("System.Storage.Get"))] = s.Get
s.interops[emit.InteropNameToID([]byte("System.Storage.Put"))] = s.Put
Expand All @@ -114,12 +116,15 @@ func newStoragePlugin() *storagePlugin {

}

func (s *storagePlugin) getInterop(id uint32) *vm.InteropFuncPrice {
func (s *storagePlugin) syscallHandler(v *vm.VM, id uint32) error {
f := s.interops[id]
if f != nil {
return &vm.InteropFuncPrice{Func: f, Price: 1}
if !v.AddGas(1) {
return errors.New("insufficient amount of gas")
}
return f(v)
}
return nil
return errors.New("syscall not found")
}

func (s *storagePlugin) Notify(v *vm.VM) error {
Expand Down
7 changes: 4 additions & 3 deletions pkg/consensus/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -222,9 +222,10 @@ func (p *Payload) Verify(scriptHash util.Uint160) bool {
return false
}

v := vm.New()
ic := &interop.Context{Trigger: trigger.Verification, Container: p}
crypto.Register(ic)
v := ic.SpawnVM()
v.GasLimit = payloadGasLimit
v.RegisterInteropGetter(crypto.GetInterop(&interop.Context{Container: p}))
v.LoadScript(verification)
v.LoadScript(p.Witness.InvocationScript)

Expand Down
10 changes: 5 additions & 5 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {

if block.Index > 0 {
systemInterop := bc.newInteropContext(trigger.System, cache, block, nil)
v := SpawnVM(systemInterop)
v.GasLimit = -1
v := systemInterop.SpawnVM()
v.LoadScriptWithFlags(bc.contracts.GetPersistScript(), smartcontract.AllowModifyStates|smartcontract.AllowCall)
v.SetPriceGetter(getPrice)
if err := v.Run(); err != nil {
Expand Down Expand Up @@ -599,7 +598,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
}

systemInterop := bc.newInteropContext(trigger.Application, cache, block, tx)
v := SpawnVM(systemInterop)
v := systemInterop.SpawnVM()
v.LoadScriptWithFlags(tx.Script, smartcontract.All)
v.SetPriceGetter(getPrice)
v.GasLimit = tx.SystemFee
Expand Down Expand Up @@ -1277,7 +1276,7 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([
// GetTestVM returns a VM and a Store setup for a test run of some sort of code.
func (bc *Blockchain) GetTestVM(tx *transaction.Transaction) *vm.VM {
systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, tx)
vm := SpawnVM(systemInterop)
vm := systemInterop.SpawnVM()
vm.SetPriceGetter(getPrice)
return vm
}
Expand Down Expand Up @@ -1310,7 +1309,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
gas = gasPolicy
}

vm := SpawnVM(interopCtx)
vm := interopCtx.SpawnVM()
vm.SetPriceGetter(getPrice)
vm.GasLimit = gas
vm.LoadScriptWithFlags(verification, smartcontract.ReadOnly)
Expand Down Expand Up @@ -1413,6 +1412,7 @@ func (bc *Blockchain) secondsPerBlock() int {

func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block *block.Block, tx *transaction.Transaction) *interop.Context {
ic := interop.NewContext(trigger, bc, d, bc.contracts.Contracts, block, tx, bc.log)
ic.Functions = [][]interop.Function{systemInterops, neoInterops}
switch {
case tx != nil:
ic.Container = tx
Expand Down
8 changes: 0 additions & 8 deletions pkg/core/gas_price.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ import (
const StoragePrice = 100000

// getPrice returns a price for executing op with the provided parameter.
// Some SYSCALLs have variable price depending on their arguments.
func getPrice(v *vm.VM, op opcode.Opcode, parameter []byte) int64 {
if op == opcode.SYSCALL {
fyrchik marked this conversation as resolved.
Show resolved Hide resolved
interopID := vm.GetInteropID(parameter)
ifunc := v.GetInteropByID(interopID)
if ifunc != nil && ifunc.Price > 0 {
return ifunc.Price
}
}
return opcodePrice(op)
}
37 changes: 37 additions & 0 deletions pkg/core/interop/callback/callback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package callback

import (
"errors"

"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// Callback is an interface for arbitrary callbacks.
type Callback interface {
// ArgCount returns expected number of arguments.
ArgCount() int
// LoadContext loads context and arguments on stack.
LoadContext(*vm.VM, []stackitem.Item)
}

// Invoke invokes provided callback.
func Invoke(ic *interop.Context, v *vm.VM) error {
cb := v.Estack().Pop().Interop().Value().(Callback)
args := v.Estack().Pop().Array()
if cb.ArgCount() != len(args) {
return errors.New("invalid argument count")
}
cb.LoadContext(v, args)
switch t := cb.(type) {
case *MethodCallback:
id := emit.InteropNameToID([]byte("System.Contract.Call"))
return ic.SyscallHandler(v, id)
case *SyscallCallback:
return ic.SyscallHandler(v, t.desc.ID)
default:
return nil
}
}
60 changes: 60 additions & 0 deletions pkg/core/interop/callback/method.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package callback

import (
"errors"
"strings"

"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// MethodCallback represents callback for contract method.
type MethodCallback struct {
contract *state.Contract
method *manifest.Method
}

var _ Callback = (*MethodCallback)(nil)

// ArgCount implements Callback interface.
func (s *MethodCallback) ArgCount() int {
return len(s.method.Parameters)
}

// LoadContext implements Callback interface.
func (s *MethodCallback) LoadContext(v *vm.VM, args []stackitem.Item) {
v.Estack().PushVal(args)
v.Estack().PushVal(s.method.Name)
v.Estack().PushVal(s.contract.ScriptHash().BytesBE())
}

// CreateFromMethod creates callback for a contract method.
func CreateFromMethod(ic *interop.Context, v *vm.VM) error {
rawHash := v.Estack().Pop().Bytes()
h, err := util.Uint160DecodeBytesBE(rawHash)
if err != nil {
return err
}
cs, err := ic.DAO.GetContractState(h)
if err != nil {
return errors.New("contract not found")
}
method := string(v.Estack().Pop().Bytes())
if strings.HasPrefix(method, "_") {
return errors.New("invalid method name")
}
currCs, err := ic.DAO.GetContractState(v.GetCurrentScriptHash())
if err == nil && !currCs.Manifest.CanCall(&cs.Manifest, method) {
return errors.New("method call is not allowed")
}
md := cs.Manifest.ABI.GetMethod(method)
v.Estack().PushVal(stackitem.NewInterop(&MethodCallback{
contract: cs,
method: md,
}))
return nil
}
42 changes: 42 additions & 0 deletions pkg/core/interop/callback/pointer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package callback

import (
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// PointerCallback represents callback for a pointer.
type PointerCallback struct {
paramCount int
offset int
context *vm.Context
}

var _ Callback = (*PointerCallback)(nil)

// ArgCount implements Callback interface.
func (p *PointerCallback) ArgCount() int {
return p.paramCount
}

// LoadContext implements Callback interface.
func (p *PointerCallback) LoadContext(v *vm.VM, args []stackitem.Item) {
v.Call(p.context, p.offset)
for i := len(args) - 1; i >= 0; i-- {
v.Estack().PushVal(args[i])
}
}

// Create creates callback using pointer and parameters count.
func Create(_ *interop.Context, v *vm.VM) error {
ctx := v.Estack().Pop().Item().(*vm.Context)
offset := v.Estack().Pop().Item().(*stackitem.Pointer).Position()
count := v.Estack().Pop().BigInt().Int64()
v.Estack().PushVal(stackitem.NewInterop(&PointerCallback{
paramCount: int(count),
offset: offset,
context: ctx,
}))
return nil
}
42 changes: 42 additions & 0 deletions pkg/core/interop/callback/syscall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package callback

import (
"errors"

"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// SyscallCallback represents callback for a syscall.
type SyscallCallback struct {
desc *interop.Function
}

var _ Callback = (*SyscallCallback)(nil)

// ArgCount implements Callback interface.
func (p *SyscallCallback) ArgCount() int {
return p.desc.ParamCount
}

// LoadContext implements Callback interface.
func (p *SyscallCallback) LoadContext(v *vm.VM, args []stackitem.Item) {
for i := len(args) - 1; i >= 0; i-- {
v.Estack().PushVal(args[i])
}
}

// CreateFromSyscall creates callback from syscall.
func CreateFromSyscall(ic *interop.Context, v *vm.VM) error {
id := uint32(v.Estack().Pop().BigInt().Int64())
f := ic.GetFunction(id)
if f == nil {
return errors.New("syscall not found")
}
if f.DisallowCallback {
return errors.New("syscall is not allowed to be used in a callback")
}
v.Estack().PushVal(stackitem.NewInterop(&SyscallCallback{f}))
return nil
}
Loading