Merge pull request #565 from nspcc-dev/hashed-interop-callback

vm/core: add ID support for SYSCALL, redo interop registration
This commit is contained in:
Roman Khimov 2019-12-19 15:02:53 +03:00 committed by GitHub
commit 26ea4799c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 384 additions and 247 deletions

View file

@ -1,13 +1,63 @@
package vm
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"sort"
)
// InteropFunc allows to hook into the VM.
type InteropFunc func(vm *VM) error
// InteropFuncPrice represents an interop function with a price.
type InteropFuncPrice struct {
Func InteropFunc
Price int
}
// interopIDFuncPrice adds an ID to the InteropFuncPrice.
type interopIDFuncPrice struct {
ID uint32
InteropFuncPrice
}
// InteropGetterFunc is a function that returns an interop function-price
// structure by the given interop ID.
type InteropGetterFunc func(uint32) *InteropFuncPrice
var defaultVMInterops = []interopIDFuncPrice{
{InteropNameToID([]byte("Neo.Runtime.Log")),
InteropFuncPrice{runtimeLog, 1}},
{InteropNameToID([]byte("Neo.Runtime.Notify")),
InteropFuncPrice{runtimeNotify, 1}},
{InteropNameToID([]byte("Neo.Runtime.Serialize")),
InteropFuncPrice{RuntimeSerialize, 1}},
{InteropNameToID([]byte("System.Runtime.Serialize")),
InteropFuncPrice{RuntimeSerialize, 1}},
{InteropNameToID([]byte("Neo.Runtime.Deserialize")),
InteropFuncPrice{RuntimeDeserialize, 1}},
{InteropNameToID([]byte("System.Runtime.Deserialize")),
InteropFuncPrice{RuntimeDeserialize, 1}},
}
func getDefaultVMInterop(id uint32) *InteropFuncPrice {
n := sort.Search(len(defaultVMInterops), func(i int) bool {
return defaultVMInterops[i].ID >= id
})
if n < len(defaultVMInterops) && defaultVMInterops[n].ID == id {
return &defaultVMInterops[n].InteropFuncPrice
}
return nil
}
// InteropNameToID returns an identificator of the method based on its name.
func InteropNameToID(name []byte) uint32 {
h := sha256.Sum256(name)
return binary.LittleEndian.Uint32(h[:4])
}
// runtimeLog handles the syscall "Neo.Runtime.Log" for printing and logging stuff.
func runtimeLog(vm *VM) error {
item := vm.Estack().Pop()
@ -50,3 +100,10 @@ func RuntimeDeserialize(vm *VM) error {
return nil
}
// init sorts the global defaultVMInterops value.
func init() {
sort.Slice(defaultVMInterops, func(i, j int) bool {
return defaultVMInterops[i].ID < defaultVMInterops[j].ID
})
}

View file

@ -108,6 +108,18 @@ func TestUT(t *testing.T) {
require.Equal(t, true, testsRan, "neo-vm tests should be available (check submodules)")
}
func getTestingInterop(id uint32) *InteropFuncPrice {
// FIXME in NEO 3.0 it is []byte{0x77, 0x77, 0x77, 0x77} https://github.com/nspcc-dev/neo-go/issues/477
if id == InteropNameToID([]byte("Test.ExecutionEngine.GetScriptContainer")) ||
id == InteropNameToID([]byte("System.ExecutionEngine.GetScriptContainer")) {
return &InteropFuncPrice{InteropFunc(func(v *VM) error {
v.estack.Push(&Element{value: (*InteropItem)(nil)})
return nil
}), 0}
}
return nil
}
func testFile(t *testing.T, filename string) {
data, err := ioutil.ReadFile(filename)
if err != nil {
@ -134,16 +146,7 @@ func testFile(t *testing.T, filename string) {
// FIXME remove when NEO 3.0 https://github.com/nspcc-dev/neo-go/issues/477
vm.getScript = getScript(test.ScriptTable)
// FIXME in NEO 3.0 it is []byte{0x77, 0x77, 0x77, 0x77} https://github.com/nspcc-dev/neo-go/issues/477
vm.RegisterInteropFunc("Test.ExecutionEngine.GetScriptContainer", InteropFunc(func(v *VM) error {
v.estack.Push(&Element{value: (*InteropItem)(nil)})
return nil
}), 0)
vm.RegisterInteropFunc("System.ExecutionEngine.GetScriptContainer", InteropFunc(func(v *VM) error {
v.estack.Push(&Element{value: (*InteropItem)(nil)})
return nil
}), 0)
vm.RegisterInteropGetter(getTestingInterop)
for i := range test.Steps {
execStep(t, vm, test.Steps[i])

View file

@ -48,9 +48,7 @@ func vmAndCompile(t *testing.T, src string) *vm.VM {
vm := vm.New()
storePlugin := newStoragePlugin()
vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get, 1)
vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put, 1)
vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext, 1)
vm.RegisterInteropGetter(storePlugin.getInterop)
b, err := compiler.Compile(strings.NewReader(src))
if err != nil {
@ -61,13 +59,28 @@ func vmAndCompile(t *testing.T, src string) *vm.VM {
}
type storagePlugin struct {
mem map[string][]byte
mem map[string][]byte
interops map[uint32]vm.InteropFunc
}
func newStoragePlugin() *storagePlugin {
return &storagePlugin{
mem: make(map[string][]byte),
s := &storagePlugin{
mem: make(map[string][]byte),
interops: make(map[uint32]vm.InteropFunc),
}
s.interops[vm.InteropNameToID([]byte("Neo.Storage.Get"))] = s.Get
s.interops[vm.InteropNameToID([]byte("Neo.Storage.Put"))] = s.Put
s.interops[vm.InteropNameToID([]byte("Neo.Storage.GetContext"))] = s.GetContext
return s
}
func (s *storagePlugin) getInterop(id uint32) *vm.InteropFuncPrice {
f := s.interops[id]
if f != nil {
return &vm.InteropFuncPrice{Func: f, Price: 1}
}
return nil
}
func (s *storagePlugin) Delete(vm *vm.VM) error {

View file

@ -60,8 +60,8 @@ const (
type VM struct {
state State
// registered interop hooks.
interop map[string]InteropFuncPrice
// callbacks to get interops.
getInterop []InteropGetterFunc
// callback to get scripts.
getScript func(util.Uint160) []byte
@ -80,19 +80,13 @@ type VM struct {
keys map[string]*keys.PublicKey
}
// InteropFuncPrice represents an interop function with a price.
type InteropFuncPrice struct {
Func InteropFunc
Price int
}
// New returns a new VM object ready to load .avm bytecode scripts.
func New() *VM {
vm := &VM{
interop: make(map[string]InteropFuncPrice),
getScript: nil,
state: haltState,
istack: NewStack("invocation"),
getInterop: make([]InteropGetterFunc, 0, 3), // 3 functions is typical for our default usage.
getScript: nil,
state: haltState,
istack: NewStack("invocation"),
itemCount: make(map[StackItem]int),
keys: make(map[string]*keys.PublicKey),
@ -101,14 +95,7 @@ func New() *VM {
vm.estack = vm.newItemStack("evaluation")
vm.astack = vm.newItemStack("alt")
// Register native interop hooks.
vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog, 1)
vm.RegisterInteropFunc("Neo.Runtime.Notify", runtimeNotify, 1)
vm.RegisterInteropFunc("Neo.Runtime.Serialize", RuntimeSerialize, 1)
vm.RegisterInteropFunc("System.Runtime.Serialize", RuntimeSerialize, 1)
vm.RegisterInteropFunc("Neo.Runtime.Deserialize", RuntimeDeserialize, 1)
vm.RegisterInteropFunc("System.Runtime.Deserialize", RuntimeDeserialize, 1)
vm.RegisterInteropGetter(getDefaultVMInterop)
return vm
}
@ -120,18 +107,11 @@ func (v *VM) newItemStack(n string) *Stack {
return s
}
// RegisterInteropFunc registers the given InteropFunc to the VM.
func (v *VM) RegisterInteropFunc(name string, f InteropFunc, price int) {
v.interop[name] = InteropFuncPrice{f, price}
}
// RegisterInteropFuncs registers all interop functions passed in a map in
// the VM. Effectively it's a batched version of RegisterInteropFunc.
func (v *VM) RegisterInteropFuncs(interops map[string]InteropFuncPrice) {
// We allow reregistration here.
for name := range interops {
v.interop[name] = interops[name]
}
// RegisterInteropGetter registers the given InteropGetterFunc into VM. There
// can be many interop getters and they're probed in LIFO order wrt their
// registration time.
func (v *VM) RegisterInteropGetter(f InteropGetterFunc) {
v.getInterop = append(v.getInterop, f)
}
// Estack returns the evaluation stack so interop hooks can utilize this.
@ -1095,9 +1075,23 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
}
case opcode.SYSCALL:
ifunc, ok := v.interop[string(parameter)]
if !ok {
panic(fmt.Sprintf("interop hook (%q) not registered", parameter))
var ifunc *InteropFuncPrice
var interopID uint32
if len(parameter) == 4 {
interopID = binary.LittleEndian.Uint32(parameter)
} else {
interopID = InteropNameToID(parameter)
}
// LIFO interpretation of callbacks.
for i := len(v.getInterop) - 1; i >= 0; i-- {
ifunc = v.getInterop[i](interopID)
if ifunc != nil {
break
}
}
if ifunc == nil {
panic(fmt.Sprintf("interop hook (%q/0x%x) not registered", parameter, interopID))
}
if err := ifunc.Func(v); err != nil {
panic(fmt.Sprintf("failed to invoke syscall: %s", err))

View file

@ -16,12 +16,19 @@ import (
"github.com/stretchr/testify/require"
)
func fooInteropGetter(id uint32) *InteropFuncPrice {
if id == InteropNameToID([]byte("foo")) {
return &InteropFuncPrice{func(evm *VM) error {
evm.Estack().PushVal(1)
return nil
}, 1}
}
return nil
}
func TestInteropHook(t *testing.T) {
v := New()
v.RegisterInteropFunc("foo", func(evm *VM) error {
evm.Estack().PushVal(1)
return nil
}, 1)
v.RegisterInteropGetter(fooInteropGetter)
buf := new(bytes.Buffer)
EmitSyscall(buf, "foo")
@ -32,13 +39,27 @@ func TestInteropHook(t *testing.T) {
assert.Equal(t, big.NewInt(1), v.estack.Pop().value.Value())
}
func TestRegisterInterop(t *testing.T) {
func TestInteropHookViaID(t *testing.T) {
v := New()
currRegistered := len(v.interop)
v.RegisterInteropFunc("foo", func(evm *VM) error { return nil }, 1)
assert.Equal(t, currRegistered+1, len(v.interop))
_, ok := v.interop["foo"]
assert.Equal(t, true, ok)
v.RegisterInteropGetter(fooInteropGetter)
buf := new(bytes.Buffer)
fooid := InteropNameToID([]byte("foo"))
var id = make([]byte, 4)
binary.LittleEndian.PutUint32(id, fooid)
_ = EmitSyscall(buf, string(id))
_ = EmitOpcode(buf, opcode.RET)
v.Load(buf.Bytes())
runVM(t, v)
assert.Equal(t, 1, v.estack.Len())
assert.Equal(t, big.NewInt(1), v.estack.Pop().value.Value())
}
func TestRegisterInteropGetter(t *testing.T) {
v := New()
currRegistered := len(v.getInterop)
v.RegisterInteropGetter(fooInteropGetter)
assert.Equal(t, currRegistered+1, len(v.getInterop))
}
func TestBytesToPublicKey(t *testing.T) {