Skip to content

fix(watch): emit force-inclusion logs from the runtime's first block - #76

Merged
kyscott18 merged 3 commits into
mainfrom
fix/watch-start-block
Oct 26, 2026
Merged

kyscott18 merged 3 commits into
mainfrom
fix/watch-start-block

Conversation

@kyscott18

@kyscott18 kyscott18 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The first block the watch saw only seeded its local chain and was never emitted, nor were the blocks between startup and the first poll. Their ForceInclusionQueued logs never reached revm, so revm's queue lagged the chain. Forced execution of index i then ran a different mutation in revm than onchain, and the settlement reverted onchain (out of gas), far from the cause.

Fix

  • typewriter.ts / rpc-request.ts: read the start block once (requestBlock("latest")), then read the code and executionIndex at its hash. Requests round-robin across providers, so pinning by hash keeps every startup read on the same block, or fails, instead of possibly reading a different fork at the same height.
  • watch.ts: fromBlock is that block's hash. makeWatch seeds its local chain with it, so the first poll emits every block after it.
  • typewriter.ts: an enqueue that reverts or halts in revm now fails (EnqueueRevertedError / EnqueueHaltedError) instead of being skipped. enqueue() only pushes onto the queue and can't fail onchain, so a failure in revm means revm has diverged from the chain.
  • simulation-test/chain.ts: start Anvil with --gas-limit 1000000000. Typewriter settles a whole backlog in one transaction, which can go over the 30M block gas limit and end a run before it reaches this bug. A TODO(kyle) says to remove it once settlement gas is capped.

The runtime assumes revm's state matches the chain once startup and migration have run, so there is no per-log queue check at runtime. Two TODOs record the gaps:

  • migrate() should check the queue length as well as executionIndex.
  • A queued mutation that revm discards stays in the onchain queue. Anyone can forceExecute it after FORCE_INCLUSION_DELAY, which changes state outside revm, and forceExecute emits no event the watch could see.

Verification

Seed 0x8b56a138d47bbafb1f65fa04c34651a20be73bf40170e3a8eae91ebbc905c443, re-run after rebasing on 89ab221:

Result
main (89ab221) Fails: SettlementRevertedError: settlement transaction reverted onchain
This branch Passes: 369 mutations, 194 blocks, run validated

Seeds 0x394b4f35… and 0x86a299ac… and a sweep of 12 random seeds all pass on this branch. tsc --noEmit, biome and the 27 packages/typewriter unit tests pass, and each commit typechecks on its own.

Known limits

  • The raised gas limit means the sim accepts settlements that a 30M-limit chain would reject, until the TODO is done.
  • Reorgs are still ignored by the runtime (Reorged messages are dropped), so a reorg across a ForceInclusionQueued log desyncs revm's queue from the chain.

🤖 Generated with Claude Code

kyscott18 and others added 3 commits October 26, 2026 22:05
The first block the watch saw only seeded its local chain, so its
ForceInclusionQueued logs never reached revm, nor did those of blocks
between startup and the first poll. Revm's queue then lagged the chain
by one entry: forced execution of index i ran a different mutation in
revm than onchain, and the settlement ran out of gas onchain
(simulation seed 0x394b4f35...).

Read the start block once, read the code and executionIndex at its
hash, and seed the watch with that block so it emits each later block.
Reading by hash keeps every read on the same block when requests
round-robin across providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fails

enqueue() only pushes onto the queue, so it cannot fail onchain. A
revert or halt when revm enqueues the logged mutation means revm's state
has diverged from the chain, so fail instead of skipping the log.

The runtime assumes revm's state matches the chain once startup and
migration have run. Note in migrate() that it should check the queue
length as well as executionIndex, and note that a queued mutation revm
discards can still be force-executed onchain outside revm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typewriter settles a whole backlog in one transaction, which can go over
Monad's 30M block gas limit, so a burst of mutations failed the run with
"intrinsic gas too high" before it reached validation. Raise the limit
until typewriter caps a settlement's gas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyscott18
kyscott18 force-pushed the fix/watch-start-block branch from 1b07946 to 465ac5e Compare October 26, 2026 22:09
@kyscott18
kyscott18 merged commit b328148 into main Oct 26, 2026
7 checks passed
@kyscott18
kyscott18 deleted the fix/watch-start-block branch October 26, 2026 22:13
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