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
This commit is contained in:
Furetur 2024-04-23 11:26:51 +03:00
parent 0ae5e7ea83
commit 5d7fd0a72a

View file

@ -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
// for each executed instruction
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,10 @@ type VM struct {
// invTree is a top-level invocation tree (if enabled).
invTree *invocations.Tree
// All registered hooks.
// Each hook should never be nil.
hooks hooks
}
var (
@ -99,6 +112,10 @@ var (
bigTwo = big.NewInt(2)
)
var defaultHooks = hooks{
onExec: func(scriptHash util.Uint160, offset int, opcode opcode.Opcode) {},
}
// New returns a new VM object ready to load AVM bytecode scripts.
func New() *VM {
return NewWithTrigger(trigger.Application)
@ -109,6 +126,7 @@ func NewWithTrigger(t trigger.Type) *VM {
vm := &VM{
state: vmstate.None,
trigger: t,
hooks: defaultHooks,
}
vm.istack = make([]*Context, 0, 8) // Most of invocations use one-two contracts, but they're likely to have internal calls.
@ -116,6 +134,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 +500,9 @@ func (v *VM) Step() error {
// step executes one instruction in the given context.
func (v *VM) step(ctx *Context) error {
instruction_offset := v.Context().nextip
op, param, err := ctx.Next()
v.hooks.onExec(v.GetCurrentScriptHash(), instruction_offset, op)
if err != nil {
v.state = vmstate.Fault
return newError(ctx.ip, op, err)