mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-21 23:29:38 +00:00
vm: implement OnExecHook
Refs #3415 This commit introduces a small new change that implements the Hooks API and more specifically the OnExecHook. This feature can be used to implement test coverage collection, tracing, breakpoints, and etc. To be more specific, this commit: 1. adds a new `hooks` field to the `VM` (this field contains the OnExecHook function) 2. sets the default value of this hook to be a NOP function 3. adds the `VM.SetOnExecHook` method Signed-off-by: Furetur <furetur@gmail.com>
This commit is contained in:
parent
228052360e
commit
f5794e91a2
2 changed files with 72 additions and 0 deletions
27
pkg/vm/vm.go
27
pkg/vm/vm.go
|
@ -63,6 +63,15 @@ const (
|
|||
// SyscallHandler is a type for syscall handler.
|
||||
type SyscallHandler = func(*VM, uint32) error
|
||||
|
||||
// OnExecHook is a type for a callback that is invoked
|
||||
// before each instruction is executed.
|
||||
type OnExecHook = func(scriptHash util.Uint160, offset int, opcode opcode.Opcode)
|
||||
|
||||
// A struct that contains all VM hooks.
|
||||
type hooks struct {
|
||||
onExec OnExecHook
|
||||
}
|
||||
|
||||
// VM represents the virtual machine.
|
||||
type VM struct {
|
||||
state vmstate.State
|
||||
|
@ -90,6 +99,9 @@ type VM struct {
|
|||
|
||||
// invTree is a top-level invocation tree (if enabled).
|
||||
invTree *invocations.Tree
|
||||
|
||||
// All registered hooks.
|
||||
hooks hooks
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -116,6 +128,16 @@ func NewWithTrigger(t trigger.Type) *VM {
|
|||
return vm
|
||||
}
|
||||
|
||||
// SetOnExecHook sets the value of OnExecHook which
|
||||
// will be invoked for each executed instruction.
|
||||
// This function panics if the VM has been started.
|
||||
func (v *VM) SetOnExecHook(hook OnExecHook) {
|
||||
if v.state != vmstate.None {
|
||||
panic("Cannot set onExec hook of a started VM")
|
||||
}
|
||||
v.hooks.onExec = hook
|
||||
}
|
||||
|
||||
// SetPriceGetter registers the given PriceGetterFunc in v.
|
||||
// f accepts vm's Context, current instruction and instruction parameter.
|
||||
func (v *VM) SetPriceGetter(f func(opcode.Opcode, []byte) int64) {
|
||||
|
@ -472,7 +494,12 @@ func (v *VM) Step() error {
|
|||
|
||||
// step executes one instruction in the given context.
|
||||
func (v *VM) step(ctx *Context) error {
|
||||
ip := ctx.nextip
|
||||
scriptHash := v.GetCurrentScriptHash()
|
||||
op, param, err := ctx.Next()
|
||||
if v.hooks.onExec != nil {
|
||||
v.hooks.onExec(scriptHash, ip, op)
|
||||
}
|
||||
if err != nil {
|
||||
v.state = vmstate.Fault
|
||||
return newError(ctx.ip, op, err)
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||
"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/emit"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
|
@ -2776,6 +2777,50 @@ func TestUninitializedSyscallHandler(t *testing.T) {
|
|||
assert.Equal(t, true, v.HasFailed())
|
||||
}
|
||||
|
||||
func TestCannotSetOnExecHookOfStartedVm(t *testing.T) {
|
||||
prog := makeProgram(opcode.NOP)
|
||||
v := load(prog)
|
||||
runVM(t, v)
|
||||
require.Panics(t, func() {
|
||||
v.SetOnExecHook(func(scriptHash util.Uint160, offset int, opcode opcode.Opcode) {})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnExecHookGivesValidTrace(t *testing.T) {
|
||||
prog := makeProgram(opcode.NOP, opcode.NOP, opcode.NOP)
|
||||
expectedOffsets := []int{0, 1, 2, 3}
|
||||
expectedOpcodes := []opcode.Opcode{opcode.NOP, opcode.NOP, opcode.NOP, opcode.RET}
|
||||
|
||||
actualScriptHashes, actualOffsets, actualOpcodes := runWithTrace(t, prog)
|
||||
|
||||
require.Equal(t, expectedOffsets, actualOffsets, "Invalid offsets")
|
||||
require.Equal(t, expectedOpcodes, actualOpcodes, "Invalid opcodes")
|
||||
|
||||
t.Run("Validate collected script hashes", func(t *testing.T) {
|
||||
scriptHash := actualScriptHashes[0]
|
||||
expectedScriptHashes := []util.Uint160{scriptHash, scriptHash, scriptHash, scriptHash}
|
||||
require.Equal(t, expectedScriptHashes, actualScriptHashes)
|
||||
})
|
||||
}
|
||||
|
||||
func runWithTrace(t *testing.T, prog []byte) ([]util.Uint160, []int, []opcode.Opcode) {
|
||||
v := load(prog)
|
||||
|
||||
scriptHashes := make([]util.Uint160, 0)
|
||||
offsets := make([]int, 0)
|
||||
opcodes := make([]opcode.Opcode, 0)
|
||||
|
||||
onExec := func(scriptHash util.Uint160, offset int, opcode opcode.Opcode) {
|
||||
scriptHashes = append(scriptHashes, scriptHash)
|
||||
offsets = append(offsets, offset)
|
||||
opcodes = append(opcodes, opcode)
|
||||
}
|
||||
v.SetOnExecHook(onExec)
|
||||
runVM(t, v)
|
||||
|
||||
return scriptHashes, offsets, opcodes
|
||||
}
|
||||
|
||||
func makeProgram(opcodes ...opcode.Opcode) []byte {
|
||||
prog := make([]byte, len(opcodes)+1) // RET
|
||||
for i := 0; i < len(opcodes); i++ {
|
||||
|
|
Loading…
Reference in a new issue