fix(fast-inbox): insert the block's L1-to-L2 messages before executing its txs (AztecProtocol/aztec-packages#25323) - #216
Open
spalladino wants to merge 1 commit into
Conversation
…g its txs (AztecProtocol/aztec-packages#25323) CheckpointBuilder.buildBlock ran the public processor on a world-state fork that did not yet contain the block's own streaming L1-to-L2 messages; they were appended afterwards inside LightweightCheckpointBuilder.addBlock. The prover node appends them to its fork before re-executing, and the block-root circuit pins each tx's l1_to_l2_tree_snapshot to the post-append root, so a public tx consuming a message its own block inserts reverted at proposal time and succeeded at proving time. The differing tx effects make the prover throw a block header mismatch on an already-attested block, and the epoch cannot be proven. buildBlock now appends the block's messages to the fork right after ForkCheckpoint.new and before processor.process, so the AVM reads the same post-append tree the prover and the circuits use. Appending inside the fork checkpoint means a failed block rolls the leaves back with the tx effects. LightweightCheckpointBuilder.addBlock splits into sealBlock (caller already applied the state updates) and applyEffectsAndSealBlock (the builder inserts tx effects and messages, then seals). Either way the messages are accumulated into the checkpoint's message list, so inboxRollingHash is unchanged.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
spalladino
added this pull request to stack #217
September 15, 2026 22:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CheckpointBuilder.buildBlockran the public processor on a world-state fork that did not yet contain theblock's own streaming L1→L2 messages. The messages were appended afterwards, inside
LightweightCheckpointBuilder.addBlock, once the block was being sealed.The proving path does it the other way round.
prover-node/src/job/checkpoint-prover.tscreateForkappendsthe block's message leaves to its fork before re-executing the block's txs, and the block-root circuit pins
each tx's
l1_to_l2_tree_snapshotto the post-append root.So the two orderings disagree for exactly one kind of transaction: a public call that consumes a message its own
block inserts.
buildBlock)checkpoint-prover)The tx effects therefore differ between the block that was proposed and the block that is re-executed, the prover
throws
Block header mismatchon a block that has already been attested and published, and the epoch cannot beproven and gets pruned. Proposer and validators both go through
buildBlock, so they agree with each other andsign; nothing catches it before the prover. Any user can trigger it by sending an L1 message and consuming it
promptly enough to land in the inserting block.
The fix
Two parts.
1.
buildBlockappends the block's messages to the fork before executing its txs — right afterForkCheckpoint.newand beforeprocessor.process, so the AVM reads the same post-append tree the prover andthe circuits already use. Because the circuits pin the post-append root, only the proposer/validator side has to
move: the prover, the orchestrator and the circuits are untouched.
Appending inside the fork checkpoint is what makes the retry safe. A block that fails
(
InsufficientValidTxsError, or a throw out of the processor) rolls its message leaves back together with its txeffects, so the retry in the next sub-slot does not double-insert.
2.
LightweightCheckpointBuilder.addBlocksplits into two public methods over one private implementation.sealBlockis for a caller that has already applied the state updates to the fork — that isbuildBlocknow —and
applyEffectsAndSealBlockis for a caller that wants the builder to insert the tx effects and the messagesitself, which is every test and mock. The old
insertTxsEffectsflag and the message append were always toggledtogether, so a single
applyStateUpdatesswitch replaces both. Either way the messages are accumulated into thecheckpoint's message list, so
inboxRollingHashis unchanged.What does not change
Block bodies, headers, leaf indices and the checkpoint
inboxRollingHashare identical before and after — theleaves land compactly at the same indices, and the header's
l1ToL2MessageTreesnapshot is read at the samepoint. The only thing that moves is the tree the AVM reads during execution. No circuit, L1, p2p or
serialization change.
Port provenance
This is a port of
AztecProtocol/aztec-packages#25323 (6271d5277c). That PR never merged tonext— itmerged into its stack parent branch — so the node port plan recorded its content as baseline and skipped it.
Three rungs of this stack independently hit the resulting revert and logged it as an open item rather than
fixing it; this rung is that fix.
Red / green
New regression cases in
validator-client/src/checkpoint_builder.test.ts, underbuildBlock with streaming L1-to-L2 messages (real world state). They run a realNativeWorldStateServicefork and a realLightweightCheckpointBuilder, because the position of the messagesrelative to tx execution is invisible with a mocked fork.
On the base branch (
spl/fi-n10-docs-corrections), before the source change:The L1→L2 tree is empty while the txs execute. The other three cases — the two rollback cases and the
empty-list case — pass on the base branch by construction: they pin the "append inside the
ForkCheckpoint"requirement so the fix cannot be implemented by appending outside it.
After the fix:
lightweight_checkpoint_builder.test.tsgainssealBlock reuses leaves already in the fork and produces the same block as applyEffectsAndSealBlock: same header, same tree size, and the sameinboxRollingHashas thedefault path. It cannot be run red, since
sealBlockdoes not exist before the source change.Also green:
proposal_handler.test.ts(79),checkpoint_proposal_job.test.ts(87), the prover-clientlightsuite (13 including the bench test),
yarn build,yarn lint.Where the e2e coverage lives
Upstream #25323 added an e2e case,
consumes a message in the same block that inserts it, which this repo neverhad. It does not need porting: PR #189 already added the same assertion at
end-to-end/src/single-node/cross-chain/streaming_inbox.test.ts:349,which has been expected-red precisely because this fix was missing. Since this rung sits below #189, that
assertion is the e2e coverage and should go green once this lands. No duplicate case was added.
Stack position
Inserted between #186 (
spl/fi-n10-docs-corrections) and #187 (spl/fi-n11-inbox-bot). #187, #189 and #190were rebased onto it with their content unchanged.
Fixes A-2041
Fixes A-2042