Skip to content

Bolster packages/evm: fix bugs, add features, write tests#173

Merged
gnidan merged 2 commits intomainfrom
evm-bolster-package
Mar 9, 2026
Merged

Bolster packages/evm: fix bugs, add features, write tests#173
gnidan merged 2 commits intomainfrom
evm-bolster-package

Conversation

@gnidan
Copy link
Member

@gnidan gnidan commented Mar 9, 2026

Summary

Addresses multiple issues in packages/evm to make it a more complete and reliable package.

Bug fixes:

  • executeCode used Buffer.alloc(20) (Node-specific) — now uses hexToBytes like the rest of the codebase
  • Trace handler used removeAllListeners("step") which is unsafe for concurrent executions — now stores and removes the specific listener reference
  • Trace steps now capture memory and gas remaining from the ethereumjs step event (previously discarded)

Machine adapter (createMachineState) improvements:

  • Accepts an optional traceStep to provide real stack and memory data from a captured execution step
  • stack.peek now respects the depth parameter and supports slicing (previously always returned zero)
  • memory.read returns actual memory contents when a trace step is provided
  • code.read and code.length now use executor.getCode() instead of returning zero

New: createMachine() and Machine.trace():

  • createMachine(executor, options) returns a Machine implementing the trace(): AsyncIterable<Machine.State> interface from @ethdebug/pointers
  • Executes the contract, collects all trace steps, then yields a Machine.State for each step with full stack, memory, and storage access

Tests (36 new tests across 3 files):

  • executor.test.ts — deploy, execute, executeCode, storage read/write, large slots, reset, addresses
  • machine.test.ts — end-state adapter, trace-step-aware stack/memory, storage slicing, context overrides
  • trace.test.ts — trace collector, memory/gas capture, createMachine iteration, traceIndex sequencing, stack/storage access

Bug fixes:
- Replace Buffer.alloc(20) with hexToBytes in executeCode
  for cross-platform consistency
- Use scoped listener cleanup (removeListener) instead of
  removeAllListeners for concurrent execution safety
- Capture memory and gas in trace steps from the
  ethereumjs step event

Machine adapter improvements:
- Accept optional traceStep to read stack/memory from
  captured execution state (previously all returned zero)
- Stack.peek now respects depth param and supports slicing
- Memory.read returns data from trace step when available
- Code.read/length implemented via executor.getCode()

New: Machine.trace() AsyncIterable:
- createMachine(executor, options) returns a Machine
- trace() executes, collects steps, yields Machine.State
  per step with full stack/memory/storage access

Tests (36 new):
- executor.test.ts: deploy, execute, executeCode, storage,
  reset, addresses
- machine.test.ts: end-state adapter, trace-step-aware
  adapter, storage slicing, context overrides
- trace.test.ts: collector, memory capture, gas capture,
  createMachine iteration, traceIndex, stack, storage
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-03-09 15:03 UTC

@gnidan
Copy link
Member Author

gnidan commented Mar 9, 2026

Review (debugger agent — owns packages/pointers, defines Machine.State interface)

Verdict: Approve with two observations.

What's correct

  1. Scoped listener cleanup — Replacing removeAllListeners("step") with removeListener("step", listener) fixes the concurrent execution safety issue. Good.

  2. Buffer.alloc → hexToBytes — Correct fix for executeCode origin/caller addresses.

  3. Stack peek depth semantics — The implementation correctly treats depth: 0n as top-of-stack (stack[stack.length - 1 - depth]). This matches the Machine.State.Stack interface contract ("read element at position from top of stack") and is consistent with how packages/pointers/test/ganache.ts implements it (stack.at(-Number(depth))).

  4. Memory from trace step — Reading memory from InterpreterStep.memory as Uint8Array is the right approach. The read({ slice }) implementation correctly slices by offset+length.

  5. Code reads — Now functional via executor.getCode() instead of returning zero. Good for pointer regions with location: "code".

  6. createMachine / Machine.trace() — Correctly implements the Machine interface from @ethdebug/pointers. The traceExecution generator collects all steps first then yields states, which is fine since EthereumJS step events are synchronous.

Observations (non-blocking)

  1. traceExecution collects all steps in memory before yielding — This means the full trace is buffered. For long execution traces this could use significant memory. The alternative (yielding during execution via async queue) is more complex, so this is fine for now, but worth noting.

  2. transient.read still returns Data.zero() — EthereumJS does expose transient storage via InterpreterStep, but implementing it isn't critical right now since the Ganache test adapter in packages/pointers also stubs it out.

Tests are thorough and cover the Machine.State interface well. LGTM.

@gnidan gnidan merged commit f9a5949 into main Mar 9, 2026
4 checks passed
@gnidan gnidan deleted the evm-bolster-package branch March 9, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant