Skip to content

Commit

Permalink
vm: implement OnExecHook
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Furetur committed Jul 2, 2024
1 parent 2280523 commit a7883c7
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 0 deletions.
11 changes: 11 additions & 0 deletions pkg/network/payload/mptdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,15 @@ func TestMPTData_EncodeDecodeBinary(t *testing.T) {
}
testserdes.EncodeDecodeBinary(t, d, new(MPTData))
})

t.Run("exceeds MaxArraySize", func(t *testing.T) {
// `bytes` encodes a byte array of the following shape:
// [1][0xffffffffffffffff]byte { ... }.
// The test fails because 0xffffffffffffffff exceeds the maximum allowed array size.
bytes := []byte{ // Nodes: [?][?]byte.
0x1, // Nodes: [1][?]byte.
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // Nodes: [1][0xffffffffffffffff]byte.
}
require.Error(t, testserdes.DecodeBinary(bytes, new(MPTData)))
})
}
28 changes: 28 additions & 0 deletions pkg/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -116,6 +129,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) {
Expand Down Expand Up @@ -472,7 +495,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)
Expand Down
45 changes: 45 additions & 0 deletions pkg/vm/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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++ {
Expand Down

0 comments on commit a7883c7

Please sign in to comment.