Skip to content

feat(typewriter): debug assertions for revm journals and settlement state - #77

Closed
kyscott18 wants to merge 1 commit into
mainfrom
debug-assertions
Closed

kyscott18 wants to merge 1 commit into
mainfrom
debug-assertions

Conversation

@kyscott18

Copy link
Copy Markdown
Collaborator

Summary

Adds internal assertions behind globalThis.DEBUG. The simulation runs with them on, and the production build strips them out. They check invariants that the simulation's end-of-run validation can't see.

  • revm journal accounting. revm holds a journal for each accepted mutation that is not finalized, and for nothing else. This is checked after each watch message, under the speculative-state lock. The EVM is wrapped (debug only) to record the journals revm holds.
  • Settlement post-state equals speculative state. After simulate succeeds, every slot the settlement writes must equal the value in speculative state. In other words, what execute callers and state readers were shown is what lands onchain.

Bug fix found by the journal assertion

enqueueMutation replays each ForceInclusionQueued log in revm. That creates a journal, and the journal was never pruned, so every enqueue leaked a journal (pre- and post-images) for the life of the process. The enqueue is already onchain and a settlement never rewinds it, so the journal is now pruned right away.

With the fix removed, seed 0x4f4ff5f9… fails with AssertionError: revm holds journal 137, which belongs to no mutation.

Supporting changes

  • typewriter-evm: simulate now reports slot_writes. They are collected in run_two_pass for every successful pass, not only in execute. A test covers the new field.
  • packages/typewriter/bunfig.toml: [define] "globalThis.DEBUG" = "true" applies to bun test and the simulation. The simulation exits if DEBUG isn't on.
  • New build script: bun build to dist/ with --define globalThis.DEBUG=false --minify-syntax --sourcemap=linked. Each if (globalThis.DEBUG) block is removed along with its arguments. I checked dist/index.js and none of the assertion code is left; only two empty, unused Set/Map allocations remain.
  • globalThis.DEBUG is used rather than a bare DEBUG, so code that imports src/ without the define (e.g. apps) reads undefined and runs with the checks off, rather than throwing a ReferenceError.

Test plan

  • bun run typecheck, biome, bun run test (typewriter), bun test --timeout 30000 (typewriter-evm)
  • 12 random simulation seeds pass with the assertions on
  • With the enqueue prune removed, the journal assertion fails the simulation
  • The settlement-state assertion has not been seen to fire. I didn't find a natural way to break that invariant on purpose.

🤖 Generated with Claude Code

…nt state

Adds `globalThis.DEBUG` assertions that the simulation runs and the
production build strips:

- revm holds a journal for each accepted mutation that is not finalized, and
  for nothing else. It caught a leak: `enqueueMutation` never pruned the
  journal of the `enqueue` it replays, so each ForceInclusionQueued log kept
  a journal for the life of the process. The enqueue is already onchain, so
  it is now pruned right away.
- the settlement transaction, simulated from the state before its mutations,
  writes the values that accepting them left in speculative state. `simulate`
  now reports slot writes so the runtime can compare.

`bunfig.toml` defines DEBUG for tests and the simulation, which refuses to
run without it. The new `build` script bundles to dist/ with DEBUG=false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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