Skip to content

feat(evm): record executions with an inspector - #375

Merged
jxom merged 3 commits into
jxom/evm2-wasmfrom
jxom/evm2-inspect
Aug 5, 2026
Merged

feat(evm): record executions with an inspector#375
jxom merged 3 commits into
jxom/evm2-wasmfrom
jxom/evm2-inspect

Conversation

@jxom

@jxom jxom commented Aug 5, 2026

Copy link
Copy Markdown
Member

Adds Evm.setInspector and Evm.clearInspector, which record what an execution did and report it as a trace on the result, plus Inspector.tree and Inspector.steps to read it. The adapter records the hooks itself rather than calling into JavaScript per opcode, since evm2's Inspector resolves at compile time.

Stacked on #366.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ox Error Error Aug 5, 2026 8:14am

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/ox@375

commit: ff84ca5

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8f43952b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wasm/evm2/src/trace.rs
/// Refusing whole records rather than trimming one keeps every record in the
/// stream complete, so a reader never meets a half-written one.
fn fits(&mut self, length: usize) -> bool {
if self.stream.len() + length <= self.options.limit as usize {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop recording after the first truncation

When a record does not fit, this only sets truncated and leaves the current stream length unchanged, so later smaller records can still be appended. With inputs such as a large call/create payload or large return data, the trace then contains holes rather than a prefix of the execution, and Inspector.tree can pair later end events with the wrong open frame. Once the limit is hit, fits should keep returning false for the rest of that execution.

Useful? React with 👍 / 👎.

Comment thread wasm/evm2/src/trace.rs Outdated
self.u16(interp.message().depth);
self.u64(interp.gas().remaining());
self.u32(if self.options.memory { interp.memory().len() as u32 } else { 0 });
self.u8(stack as u8);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a wider stack count in step records

When stack: true, valid EVM executions can have more than 255 stack items (the limit is 1024), but this casts the count to u8 while still writing every stack word. The TypeScript decoder then consumes only the wrapped count and interprets the remaining stack bytes as later event tags or an early END, so a successful transaction can return a corrupt or silently incomplete trace. Encode the count with a wider field or cap the number of words written to the encoded count.

Useful? React with 👍 / 👎.

Comment thread wasm/evm2/src/lib.rs
Ok(result) => {
let mut writer = Writer::new();
write_result(&mut writer, &result);
write_trace(&mut writer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset traces between async retry attempts

For an async EVM, driver.until replays the transaction after a pending database read, but the collector is drained only on the successful path here. If an abandoned attempt recorded hooks before returning PENDING, those partial events remain in the collector and get prepended to the final retry's trace, producing duplicate or stale events; reset/drain the collector at the start of each attempt or when returning PENDING (the same pattern applies to transact).

Useful? React with 👍 / 👎.

Comment thread wasm/evm2/src/trace.rs Outdated
_interp: &mut Interpreter<'_, '_, BaseEvmTypes>,
message: &mut Message<BaseEvmTypes>,
) -> Option<MessageResult<BaseEvmTypes>> {
if self.fits(1 + 71 + 4 + message.input.len()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count the value word in call/create trace limits

This fit check budgets a call/create message as 76 + input bytes, but message() writes the tag plus kind/depth/gas, three addresses, a 32-byte value, the input length, and input (108 + input). With a user-supplied limit, many small calls can grow the trace well past the advertised byte cap before truncation is reported, defeating the bound that protects memory. Include the actual encoded length in both call and create checks.

Useful? React with 👍 / 👎.

Comment thread src/evm/Evm.ts Outdated
evm: Evm<boolean>,
options: Inspector.Options = {},
): void {
evm['~engine'].setInspector({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize inspector changes on async EVMs

For async databases, every operation that touches the engine is funneled through attempt/Driver.serialize, but this direct call bypasses that queue. If a callTx or transact promise is paused on a pending read, setInspector or clearInspector can mutate the collector before the operation replays, so the in-flight execution is traced with the new settings (or not traced) rather than the settings in effect when it was started; make these setters Awaitable and serialize them like setBlock.

Useful? React with 👍 / 👎.

Comment thread src/evm/Inspector.ts
Comment on lines +168 to +170
(frame.selfdestructs as Frame['selfdestructs'][number][]).push({
target: event.target,
value: event.value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the self-destructed contract in frame trees

For any SELFDESTRUCT, the raw event includes both contract (the account being destroyed) and target (the beneficiary), but the tree view drops contract and exposes only the beneficiary/value. Consumers using Inspector.tree therefore cannot tell which account was removed, and guessing from the frame is wrong for contexts such as delegated execution; include event.contract in the frame's selfdestruct entry.

Useful? React with 👍 / 👎.

@jxom
jxom merged commit a3fa593 into jxom/evm2-wasm Aug 5, 2026
14 of 15 checks passed
@jxom
jxom deleted the jxom/evm2-inspect branch August 5, 2026 10:18
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