Skip to content

feat(fast-inbox): flip consensus to streaming inbox (A-1384) - #24789

Closed
spalladino wants to merge 43 commits into
spl/a-1383-validator-streamingfrom
spl/a-1384-flip-streaming-inbox
Closed

feat(fast-inbox): flip consensus to streaming inbox (A-1384)#24789
spalladino wants to merge 43 commits into
spl/a-1383-validator-streamingfrom
spl/a-1384-flip-streaming-inbox

Conversation

@spalladino

@spalladino spalladino commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Coordinated cutover of the Fast Inbox (AZIP-22) project (issue A-1384, FI-14). Flips the authoritative path across L1, circuits, and the node from the legacy inHash/frontier-tree consumption to the streaming rolling-hash Inbox.

Stacked on the node phase: #24784 (A-1379) -> #24785 (A-1380) -> #24786 (A-1381) -> #24787 (A-1382) -> #24788 (A-1383). Base is spl/a-1383-validator-streaming.

Merge gate

  • A-1431 (domain-separation decision) must resolve before this merges. It is deliberately deferred and nothing is adopted here. If it adopts a separator, the rolling-hash link-encoding change must be inserted below this PR in the stack.

L1 (l1-contracts)

  • ProposeLib.propose calls validateInboxConsumption against the parent checkpoint's consumed position (read from the parent temp-log record) instead of inbox.consume() + the Rollup__InvalidInHash check; the returned consumed total is stored in the new record.
  • ProposeArgs gains an unsigned bucketHint calldata field (out of the attested payload digest).
  • TempCheckpointLog / CompressedTempCheckpointLog carry {inboxRollingHash, inboxMsgTotal, inboxConsumedBucket}; genesis is {0,0,0}.
  • EpochProofLib.getEpochProofPublicInputs anchors the rolling-hash chain start to the record of checkpoint start - 1 (Rollup__InvalidPreviousInboxRollingHash).
  • Inbox.sendL2Message returns and emits the compact cumulative message index.

Circuits (noir-projects)

  • MAX_L1_TO_L2_MSGS_PER_BLOCK 1024 -> 256.
  • L1ToL2MessageBundle drops num_real_msgs; a single num_msgs drives the compact tree append and the message-sponge absorb.
  • SpongeBlob::absorb_block_end_data absorbs the L1-to-L2 tree root for every block.

Node (yarn-project)

  • Streaming is the only path: the streamingInbox flag is removed across foundation/stdlib config + the sequencer/validator plumbing.
  • Sequencer: sources the parent bucket from the fork's L1-to-L2 leaf count + getInboxBucketByTotalMsgCount (cross-checkpoint); feeds inHash zero; relaxes waitForMinTxs for message-only blocks; threads the consumed bucket seq as the propose bucketHint. Automine mirrors the selection.
  • Validator: per-block acceptance + checkpoint re-execution always run; the checkpoint rebuild derives the consumed message list from the buckets between the parent checkpoint's position and the last block.
  • World state: appendL1ToL2MessagesToTree / handleL2BlockAndMessages append real leaves at compact indices; the synchronizer derives each block's bundle from its leaf-index range.
  • Blob / constants / bundle: per-block blob root, MAX_L1_TO_L2_MSGS_PER_BLOCK = 256, L1ToL2MessageBundle drops numRealMsgs; provable per-checkpoint ceiling 2457 -> 2234.
  • Archiver / TXE: the dead inHash cross-check is removed; TXE appends unpadded compact leaves.
  • L1 ABI: @aztec/l1-artifacts regenerated for the new ProposeArgs (gitignored generated output; the ethereum package builds against it).

Verification

  • L1 forge test: full propose/inbox suite green (ProposeInboxConsumption, Inbox, InboxBuckets, Rollup incl. a new anchoring negative, RollupFieldRange, FeeRollup, ValidatorSelection, escape-hatch, tmnt207/419).
  • Circuits nargo test: parity, block_root structure, msgs_only green on touched crates.
  • Node jest (runnable locally): validator proposal_handler (34) + streaming checks (14), sequencer inbox selector (9), world-state native (56), blob-lib + stdlib. tsc for the broken-build-zone packages (prover-client, sequencer-client, validator-client, etc.) is CI's job of record.
  • Codex (gpt-5.6-terra) reviewed both the L1/circuits layer and the node wiring: no consensus-critical issue; bucket cursor/range (fromExclusive, toInclusive] and pipelining-parent sourcing verified correct.

VK regen and Prover.toml sample-input regen ride CI (local bb write_vk OOMs on rollup_root).

Node prover + archiver keying (completed in this PR)

  • Prover per-block message split: the checkpoint's messages are sliced per block from the blocks' L1-to-L2 leaf-count ranges; each block root appends its own real slice at compact indices with per-block start/end snapshots and a full-height frontier hint. startCheckpoint no longer inserts messages up front.
  • Checkpoint keying from records: getL1ToL2MessageCheckpoint binary-searches the block records for the first block whose L1-to-L2 leaf count exceeds the message index (portal claim helpers depend on this); the prover-node derives the checkpoint's consumed messages from the Inbox buckets.

Codex (gpt-5.6-terra) reviewed the prover/keying rework: no consensus defect; slice partitioning, bucket-range agreement, fork ordering, frontier height, and the checkpoint boundary rule all verified.

Left for FI-18 cleanup

The message_store InboxLeaf index-formula methods (smallestIndexForCheckpoint etc.) have no correctness-critical live callers post-flip; the public-simulator's next-checkpoint fetch degrades safely without them (simulates without the not-yet-consumed messages). Deleted in FI-18.

Accepted residual risk

A-1390 (unconsumed-bucket overwrite protection) stays post-flip: a consumption backlog older than the Inbox ring can overwrite unconsumed buckets and halt proposals until an upgrade. Acceptable for non-production lines.

Review follow-up (phase-2 final review)

A final review pass appended fix(fast-inbox): thread per-block message sponges and wire the msgs-only block root in the prover:

  • The prover supplied the checkpoint-wide message sponge as every non-first block root's start sponge, which the block-merge/checkpoint-root continuity asserts reject whenever a non-first block carries messages. The sponge is now threaded per block.
  • A zero-tx non-first block (the message-only shape this PR lets the sequencer produce) was rejected by BlockProvingState and the lightweight builder, and the orchestrator never selected the msgs-only block root. Both now accept it and route it through BlockRootMsgsOnlyRollupPrivateInputs.
  • The required bucketHint parameter added to enqueueProposeCheckpoint here is now passed by the publisher unit/integration tests and the e2e synching test (the integration suite's consumption model is reworked for streaming semantics in chore(fast-inbox): delete legacy node message paths (A-1388) #24793).

Bucket-hint carry on final-block failure (CI fix)

The cross_chain_messages e2e ("builds multiple blocks per slot with L1 to L2 messages") caught a product regression: when a checkpoint's first block is message-only (consuming a bucket) and the final block fails to build, the bucketHint on the propose payload was reconstructed from the last successfully-built block's streaming cursor and lost the already-consumed bucket, so L1 validateInboxConsumption rejected the checkpoint. CheckpointProposalBroadcast now carries bucketHint explicitly, sourced from the streaming state's lastBucketRef.bucketSeq at both propose sites (fisherman and main), so the consumed bucket survives a final-block build failure. A checkpoint_proposal_job.test.ts regression case drives message-only-first-block + final-block-failure and asserts the enqueued hint is the consumed bucket (RED reverting the fix: Expected 2n, Received 0n).

L1→L2 readiness gate on the PXE sync tip (CI fix)

The shared cross-chain e2e helper (message_test_helpers.ts) evaluated isL1ToL2MessageReady against the default 'latest' tip, while these suites anchor the PXE to syncChainTip: 'checkpointed'. Pre-flip every message entered at the first block of the next checkpoint, so 'latest' readiness coincided with checkpointed availability; post-flip per-block insertion lets the proposed chain reach a message's consume-checkpoint a full checkpoint before it is published on L1, so readiness flipped true while the PXE had not yet synced the message and the private consume simulated against a missing membership witness (No L1 to L2 message found). This deterministically failed l1_to_l2.test.ts from this branch up. The helper now gates on the configured PXE sync tip via a new CrossChainMessagingTest.pxeSyncChainTip getter (defaulting to 'latest', so suites that do not pin a tip keep their semantics). Test-support only; box-verified green on all three cross-chain suites (l1_to_l2, streaming_inbox, l1_to_l2_inbox_drift).

Per-block L1→L2 message tree root in archiver reconstruction (consensus fix)

The archiver's checkpoint reconstruction (retrievedToPublishedCheckpoint in data_retrieval.ts) read the L1→L2 message tree root once from the checkpoint's first block and applied it to every block — a stale pre-flip per-checkpoint assumption. Post-flip the blob carries a per-block root (any block in a checkpoint can insert messages). A follower node (prover / sync-only) that rebuilds a non-first message-inserting block therefore got the first block's root, so its world-state synchronizer inserted the correct message, recomputed the real root, and mismatched the reconstructed header (block state does not match world state) — forking the node from the sequencer on the first message-consuming block. In production this would fork every prover/follower on the first cross-chain-consuming block; it surfaced here as cross_chain_public_message and token_bridge failing (only follower nodes reconstruct via the archiver, which is why proposal validation — building from each validator's own world-state fork — never caught it). Reconstruction now uses each block's own l1ToL2MessageRoot from the blob. A data_retrieval.test.ts unit case asserts each reconstructed block's tree root matches its own blob root (RED: non-first blocks got the first block's root); both e2es green at a-1384.

World-state mock Inbox buckets (CI fix, pass-8)

world-state/src/test/integration.test.ts drives the streaming synchronizer through MockPrefilledArchiver, but setPrefilled only seeded the legacy per-checkpoint message map and never registered Inbox buckets. Post-flip the synchronizer reconstructs each block's consumed L1→L2 bundle from buckets (getInboxBucketByTotalMsgCount), which returned undefined against the empty bucket map, so every block synced an empty bundle and the reconstructed state diverged from the header (block state does not match world state, then a 600s timeout). setPrefilled now registers a genesis sentinel plus one bucket per message-carrying checkpoint (cumulative totalMsgCount = the block's post-insertion leaf count), rebuilt from the full prefilled chain so reorg re-prefills stay aligned. Box: 12/12 (reorg 32s→2.4s), verified at a-1384 and top.

Leftover sweep (post-review)

  • Removed the inert isFirstBlock parameter from the blob-data test fixtures (makeBlockEndBlobData / makeBlockBlobData and the archiver makeBlockBlobDataFromBody helper). Post-flip every block carries its own L1-to-L2 root, so the flag had no effect on output; the earlier lint fix had only underscore-bound it "for call-site compatibility". The two now-identical blob-data tests are collapsed into one.

@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from a102839 to e73dd44 Compare July 19, 2026 14:05
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch from 6fef625 to 36ac36d Compare July 19, 2026 14:05
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from e73dd44 to ee895d3 Compare July 19, 2026 17:57
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch from 97fce26 to 4d7c269 Compare July 19, 2026 17:57
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from ee895d3 to 8c8328f Compare July 19, 2026 20:48
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch from 73e288c to 4c6d86b Compare July 19, 2026 20:48
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from 8c8328f to 5d841f5 Compare July 20, 2026 15:30
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch 2 times, most recently from 976062a to de56dbe Compare July 20, 2026 17:28
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from 38daa76 to 28efaeb Compare July 20, 2026 21:21
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch from de56dbe to 66aff9d Compare July 20, 2026 21:21
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from 28efaeb to 7b89eb4 Compare July 21, 2026 02:58
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch 2 times, most recently from 1740bc9 to a17dce8 Compare July 21, 2026 03:39
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from 7b89eb4 to 7e29709 Compare July 21, 2026 03:39
@spalladino
spalladino force-pushed the spl/a-1383-validator-streaming branch from 7e29709 to 812d923 Compare July 27, 2026 20:53
@spalladino
spalladino force-pushed the spl/a-1384-flip-streaming-inbox branch from d97760c to 1cc2caa Compare July 27, 2026 20:53
spalladino and others added 5 commits July 28, 2026 09:24
…384)

Switch Rollup.propose from the legacy inbox.consume()/inHash frontier flow to
the streaming-inbox validation (AZIP-22 Fast Inbox):

- ProposeLib.propose calls validateInboxConsumption against the parent
  checkpoint's consumed position, read from the parent temp-log record, and
  stores the returned consumed total in the new record. The Rollup__InvalidInHash
  check and the inbox.consume() call are removed.
- ProposeArgs gains an unsigned bucketHint calldata field (kept out of the
  attested payload digest).
- TempCheckpointLog / CompressedTempCheckpointLog carry the consumption record
  {inboxRollingHash, inboxMsgTotal, inboxConsumedBucket}; genesis is {0,0,0}.
- EpochProofLib.getEpochProofPublicInputs anchors the rolling-hash chain start to
  the record of checkpoint start-1, mirroring previousArchive
  (Rollup__InvalidPreviousInboxRollingHash).
- Inbox.sendL2Message returns and emits the compact cumulative message index.

Frontier trees keep running unread (deleted in cleanup). Propose-path test
harnesses reference the live buckets; inbox index and rolling-hash suites updated
for compact indexing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uits (A-1384)

Flip the block-root circuits to the streaming inbox (AZIP-22 Fast Inbox):

- MAX_L1_TO_L2_MSGS_PER_BLOCK 1024 -> 256 (per-checkpoint cap and the InboxParity
  size ladder are unchanged).
- L1ToL2MessageBundle drops num_real_msgs; a single num_msgs count drives both the
  compact (unpadded) tree append and the message-sponge absorb.
- BlockRollupPublicInputsComposer::with_message_bundle appends and absorbs the real
  num_msgs leaves and drops the dual-count assert.
- SpongeBlob::absorb_block_end_data absorbs the L1-to-L2 tree root for every block
  (the root is now per-block), dropping the is_first_block_in_checkpoint gating.

InboxParity already absorbs the real count (A-1427), so the checkpoint-root sponge
equality holds. VK and Prover.toml sample-input regen ride CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he node (A-1384)

Mirror the circuit blob-format flip in the node (AZIP-22 Fast Inbox):

- Regenerate MAX_L1_TO_L2_MSGS_PER_BLOCK to 256 in @aztec/constants.
- Every block's blob carries the L1-to-L2 message tree root; drop the first-block
  conditional in encode/decode, the producers (L2Block, prover orchestrator), and
  the NUM_FIRST_BLOCK_END_BLOB_FIELDS distinction.
- getNumBlockEndBlobFields loses its parameter; blob capacity accounting spends the
  block-end overhead (now 7 fields) for every block, dropping the provable
  per-checkpoint ceiling from 2457 to 2234.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-1384)

Post-flip the L1-to-L2 message tree grows by real message counts at compact
(unaligned) indices, so StateReference.validate no longer requires its
next-available leaf index to be a multiple of NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP
(AZIP-22 Fast Inbox). The per-tx partial-state checks are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the streamingInbox flag and wire streaming as the sole path (AZIP-22 Fast
Inbox):

- Config: drop streamingInbox from foundation env vars, stdlib sequencer/validator
  config + schemas, and the sequencer/validator config plumbing.
- Sequencer: source the parent checkpoint's consumed bucket from the fork's
  L1-to-L2 tree leaf count + getInboxBucketByTotalMsgCount (cross-checkpoint, not
  genesis-only); feed the header/block inHash zero; relax waitForMinTxs for
  message-only blocks; pass the consumed bucket seq as the propose bucketHint.
  Automine selects its single block's bundle the same way.
- Validator: per-block acceptance and checkpoint re-execution always run; the
  checkpoint rebuild derives the consumed message list from the buckets between the
  parent checkpoint's position and the last block.
- World state: appendL1ToL2MessagesToTree and handleL2BlockAndMessages append real
  leaves at compact indices; the synchronizer derives each block's bundle from its
  leaf-index range.
- Lightweight checkpoint builder feeds the checkpoint inHash zero.

Legacy bulk-fetch/insert paths go dead (deleted in FI-18). Verified via jest:
validator proposal_handler (34) + streaming checks (14), sequencer inbox selector
(9), world-state native (56). tsc rides CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es (A-1384)

The flip made every block carry the l1-to-l2 message tree root, so
makeBlockEndBlobData no longer branches on isFirstBlock. The parameter is
still accepted for call-site compatibility (makeBlockBlobData, the archiver
data-retrieval test) but is now bound to an underscore name to satisfy
no-unused-vars, which was failing lint --check from this branch upwards.
… flip (A-1384)

The flip switched the inbox message index to a compact cumulative counter
(totalMessagesInserted, zero-based), but left the fee-portal and token-portal
forge tests asserting the old lag-based tree-geometry index (lag * SIZE). Update
depositToAztecPublic and TokenPortal expectations to the compact index; the
MessageSent checkpointNumber field and the consume/lag machinery are unchanged at
this branch, so only the index expectations move. Without this, cd l1-contracts &&
forge test failed 4 tests once the lint gate stopped masking it.
…-message-seen works pre-consumption (A-1384)

The flip repointed getL1ToL2MessageCheckpoint to answer from block records (it
now returns undefined until a block consumes the message), silently breaking
waitForL1ToL2MessageSeen's contract that it only confirms the archiver ingested
the message from L1, without requiring the L2 chain to advance. Every caller uses
the two-phase wait-seen-then-advance pattern with tight windows, so the l1-reorgs
messages e2e (and message harnesses) time out waiting for a checkpoint that needs
inbox lag plus a consuming block. Add getL1ToL2MessageIndex to the AztecNode
interface/schema/server (delegating to the existing message source method) and
repoint waitForL1ToL2MessageSeen to it, restoring the pre-consumption lookup over
a remote-capable node.
…hiver (A-1384)

The flip made the Inbox emit a compact zero-based totalMessagesInserted index, but
the archiver message store still validated the legacy per-checkpoint tree-geometry
range, so it rejected every real message with 'Invalid index 0 ... expected value
in range [..)'. Replace the checkpoint range/gap/first-index checks with the
compact contiguous check (index == previous + 1, first == 0), mirroring the
eventual node-cleanup. The legacy 128-bit rolling-hash check, the checkpointNumber
field and the vestigial per-checkpoint getter stay until that cleanup. Move the
test fixtures to compact indices (keeping checkpointNumber/rollingHash) and drop
the store tests that only exercised the geometry getter / removed checks.
…x (A-1384)

The test encoded the pre-flip protocol: it asserted an L1-to-L2 message had no
membership witness during a proposed-chain drift and could not be consumed until
the chain re-reached the message's insertion-time checkpoint. Post-flip messages
carry a compact L1-assigned leaf index and stream into the tree in insertion order,
so there is no per-message assigned checkpoint and the witness legitimately appears
as the chain catches up. Rewrite the scenario to assert the genuinely regression-worthy
post-flip invariants: after the rollup prunes the drifted chain, the message is still
re-consumed on the new chain (bucket and rolling-hash state surviving the L2 reorg) from
both private and public scope, and its witness leaf index still equals the L1-assigned
global index. Drop the witness-absent and cannot-consume-early probes and the fragile
proven-block destructure; retitle and refresh the scenario comments.
…ions (A-1384)

The flip added compact contiguous-index validation to the message store, but the
archiver-sync fixtures still assigned legacy per-checkpoint tree-geometry indices,
so every sync loop rejected them and the suite failed wholesale. Assign the compact
global insertion index in FakeL1State and switch the assertions to insertion-order
reads, keeping the pre-cleanup getState reorg detection and inHash hint intact.
…ator suites (A-1384)

Streaming is now the only consumption path, so the sequencer checkpoint job resolves
the parent Inbox bucket from a world-state fork and every block proposal carries a
bucket reference. Stub getInboxBucketByTotalMsgCount and the fork tree info in the
sequencer unit suites, and attach the consumed bucket reference (plus a non-zero L1
genesis time) in the validator integration suite so proposals pass the streaming
acceptance checks.
…-state synchronizer suite (A-1384)

The flip makes streaming the only consumption path: messages derive per-block from the
Inbox rather than being attached to the first block of a checkpoint, and non-first blocks
may carry messages. The two transitional tests (first-block-only message fetch, non-first-
block rejection) and their now-unused mock/helper no longer reflect the behavior and
fail. Removes them, matching the up-stack cleanup.
… cursor (A-1384)

When a checkpoint builds several blocks in one slot and the final sub-slot block fails to
build after earlier blocks already consumed L1-to-L2 messages, no block is held for
broadcast, so the L1 propose bucket hint fell back to genesis bucket 0 while the checkpoint
header committed to a non-genesis rolling hash — L1 rejects this as Rollup__InvalidInboxRollingHash.
Take the hint from the streaming consumption cursor's final bucket reference, which is always
defined and always matches the bucket the header committed to, and carry it through the
broadcast result. Behavior-preserving on the normal held-block path (block bucket refs are
cumulative, so the held last block's ref already equals the cursor's). Adds a regression test
for the no-held-block checkpoint.
…aming inbox (A-1384)

The flip makes world-state sync reconstruct L1-to-L2 messages per block from the Inbox,
and propose validates the checkpoint's inboxRollingHash against a bucket hint. Back the
blockSource mock with MockL1ToL2MessageSource, register a bucket per block from the real
on-chain currentBucketSeq, chain the previous checkpoint's rolling hash, and derive the
propose bucket hint from the mock. Updates the propose-revert assertion to the streaming
Rollup__InvalidInboxRollingHash selector (a-1384 already validates rolling hash, not inHash).
…A-1384)

The shared cross-chain readiness helper evaluated isL1ToL2MessageReady against the
default 'latest' tip while these suites anchor the PXE to 'checkpointed'. With per-block
inbox insertion the proposed chain reaches a message's consume-checkpoint before that
checkpoint is published, so readiness flipped true a checkpoint early and the private
consume simulated against a PXE that had not yet synced the message. Gate readiness on
the configured PXE sync tip instead.
…rom the blob (A-1384)

Post-flip the checkpoint blob carries a per-block L1-to-L2 message tree root, since any block within a
checkpoint can insert messages. The archiver reconstruction still read the root once from the checkpoint's
first block and applied it to every block, a stale pre-flip per-checkpoint assumption. Follower nodes
(prover / sync-only) that rebuild a non-first message-inserting block via retrievedToPublishedCheckpoint
therefore got the first block's root, so the world-state synchronizer inserted the correct message,
recomputed the real root, and mismatched the reconstructed header (block state does not match world state),
forking the node from the sequencer on the first message-consuming block. Reconstruct each block's tree
root from its own blob data instead.
…r (A-1384)

The world-state integration test drives the streaming synchronizer through MockPrefilledArchiver, but
setPrefilled only seeded the legacy per-checkpoint message map and never registered Inbox buckets. Post-flip
the synchronizer reconstructs each block's consumed L1-to-L2 bundle from buckets via
getInboxBucketByTotalMsgCount, which returned undefined against the empty bucket map, so every block synced
with an empty bundle and its reconstructed state diverged from the committed header (block state does not
match world state). Register a genesis sentinel bucket plus one bucket per message-carrying checkpoint with
cumulative totalMsgCount matching the block's post-insertion leaf count.
…icitly (A-1384)

getEpochProofPublicInputs anchored only the start of the consumed rolling-hash
chain segment; a wrong endInboxRollingHash was caught transitively, surfacing as
a bare Rollup__InvalidProof. Compare it against the hash recorded at propose for
the epoch's last checkpoint and revert with Rollup__InvalidEndInboxRollingHash,
mirroring the endArchive check. Test helpers that submitted a zero end hash now
source it from the proposed header.
…ber slot (A-1384)

inboxMsgTotal and inboxConsumedBucket occupied a storage slot of their own in
CompressedTempCheckpointLog, costing an extra zero-to-nonzero SSTORE on every
propose and a cold SLOAD when reading the parent checkpoint's total. Declare
them next to CompressedSlot slotNumber so all three share one slot (20 of 32
bytes), which propose already touches for the slot-progression check; the
checkpoint record drops from nine slots to eight. Both counts keep their full
uint64 width, and feeHeader stays at its previous slot offset.

The pipelined-simulation state override writes that slot as a whole word, so it
now packs the two counts alongside the slot number instead of silently zeroing
them, and accepts them as override inputs.
…nt range (A-1384)

The prover node resolved the boundary Inbox buckets for a checkpoint's leaf-count
range itself before asking for the messages between them. The message source now
takes the leaf counts directly and reports an unresolvable boundary, so drop the
two-step lookup.
…in the checkpoint builder (A-1384)

Every block now carries its own L1-to-L2 message bundle, so the up-front whole-checkpoint
insertion has no callers left. Delete the `insertMessagesPerBlock` toggle and the checkpoint-wide
`l1ToL2Messages` argument of `startCheckpoint`/`startNewCheckpoint` (which would otherwise be
silently double-counted into the checkpoint's rolling hash), and promote the bundle from
`addBlock`'s options bag to a required argument, since a block always states what it consumes.
`openCheckpoint` keeps the list for the resume path, where it seeds the rolling hash of blocks
already built, and now rejects a non-empty list when starting fresh. Tests that relied on the
up-front path insert the bundle through the checkpoint's first block instead.
… in propose simulation (A-1384)

`ProposeLib.validateInboxConsumption` reads the parent checkpoint's recorded `inboxMsgTotal` and
caps `bucket.totalMsgCount - parentTotal` at the per-checkpoint limit. When the sequencer simulates
a propose against a parent that is proposed but not yet mined, the parent has no temp-checkpoint-log
cell on L1, so that total read as zero and the consumed count came out as the Inbox's entire
history — a spurious `Rollup__TooManyInboxMessagesConsumed` revert once the Inbox has accumulated
more than one checkpoint's cap of messages. Carry the total on `ProposedCheckpointData` (derived by
the archiver from the checkpoint's last block L1-to-L2 leaf count, which is what `propose` records)
and feed it into the pending temp-log override alongside the slot number it shares a word with.
…propose harnesses (A-1384)

Both propose harnesses warped to the checkpoint's timestamp and only then sent their
L1-to-L2 messages, so the checkpoint referenced a bucket still accumulating in the
same L1 block. Propose rejects that reference: the snapshot could still be overwritten
in place. Sending the messages first puts them in an earlier L1 block, which is what
the sequencer does anyway, since it never selects a bucket younger than the inbox lag.
@spalladino

Copy link
Copy Markdown
Contributor Author

Superseded by #25037.

The Fast Inbox stack has been regrouped from 20 per-issue PRs (plus 2 umbrellas) down to 3 area PRs plus an umbrella, and rebased onto merge-train/spartan including #25007 (the noir-projects fnd//labs/ split). This PR's diff is preserved verbatim as a single squashed commit in #25037, and this description is preserved in that commit's message.

New structure: #25036 (circuits + L1) → #25037 (node + flip) → #25038 (cleanup), with #25039 as the full-stack umbrella. The original branch for this PR is left on the remote as a recovery point.

Closing here to cut the rebase and review overhead of maintaining 22 PRs; the work is not abandoned.

@spalladino spalladino closed this Jul 28, 2026
spalladino added a commit that referenced this pull request Jul 28, 2026
Deletes the legacy L1 Inbox path now that `propose` enforces streaming-inbox consumption (AZIP-22 Fast Inbox, FI-16). Stacked on `spl/a-1385-streaming-e2e`; part of the Fast Inbox stack (#24784..#24790 below this one).

## What is deleted

- **Frontier trees**: the `trees` mapping, `forest`, `HEIGHT`/`SIZE`/`EMPTY_ROOT`, the per-message tree insert, `getRoot()`, and the whole `consume()` flow.
- **LAG / inboxLag**: the `LAG` immutable and `_lag`/`_height` constructor args (the Inbox constructor is now `(rollup, feeAsset, version, bucketRingSize)`), `RollupConfigInput.inboxLag` and its `RollupCore` pass-through, and `Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT` from the Inbox construction.
- **Dead event field**: the `MessageSent.checkpointNumber` (former `inProgress`) argument, which became meaningless once `consume()` stopped advancing it.
- **Orphaned errors**: `Inbox__Unauthorized`, `Inbox__MustBuildBeforeConsume`, `Rollup__InvalidInHash`.

## What is re-homed / kept

- The compact message index now reads the current bucket's running total (`totalMsgCount`) instead of a parallel counter; `InboxState.inProgress` and `getInProgress()` are gone.
- The 128-bit `rollingHash`, `InboxState.{rollingHash, totalMessagesInserted}`, `getState()`, and `getTotalMessagesInserted()` are **kept**: the node still consumes them for message sync and L1-reorg detection. Their removal is the node cleanup's job (FI-18).
- `FrontierLib` is **kept** — it still backs the base-parity (`test/Parity.t.sol`) and merkle (`test/merkle/Frontier.t.sol`) tests, which are unrelated to the Inbox (parity-circuit territory, FI-17). The plan's "delete the file" assumption is contradicted by grep.

## TS follow-through

- `ethereum` InboxContract: drop `getLag()`, drop the `MessageSent.checkpointNumber` decode, stop reading `inProgress`; `InboxContractState.treeInProgress` is now optional (no longer tracked on-chain).
- Stop threading `inboxLag` through `queries.getL1ContractsConfig` and the L1 deploy env. The broader `AZTEC_INBOX_LAG` config sweep (`ethereum/src/config.ts`, `foundation`, `stdlib`, ~30 e2e configs) is deferred to FI-18, which explicitly owns it.
- Archiver decode derives the message's checkpoint number from the compact index (the event no longer carries it), keeping the legacy per-checkpoint store shape untouched for FI-18.
- Removed the obsolete `advanceInboxInProgress` cheat code + its inbox-drift tests, and the orphaned archiver `inHash`-mismatch sync test (the cross-check was removed at the flip).

## Gas

Deleting the frontier insert is a large `sendL2Message` win. Whole-test gas (`forge test` on `InboxBuckets.t.sol`), before → after:

| Case | Before | After |
| --- | --- | --- |
| First-ever message | 175,251 | 116,835 |
| First message of a new L1 block | 329,460 | 195,555 |
| Absorb into an existing bucket | 286,673 | 149,968 |
| Rollover mid-block (256 messages) | 18,923,988 | 3,068,480 |

Per-call `sendL2Message` gas after cleanup: first-ever 99,526; first-of-new-block 53,763; existing-bucket absorb 9,273; rollover 53,801.

## Testing

- `forge build` + `forge test` green: 887 passed, 0 failed. This fixes 4 pre-existing baseline failures the flip stranded (`fee_portal`/`TokenPortal` deposit tests using tree-relative indices + the old event shape).
- `@aztec/ethereum` builds clean; `config`/`queries` unit tests green.
- `@aztec/archiver` has no self-owned type errors; `message_store` (36), `archiver-sync` (59), and the decode/struct suites (58) all pass. (Pre-existing `noir-protocol-circuits-types` stale-artifact errors are unaffected.)
- e2e not run locally. The stability gate ("a few days of green e2e/networks before deleting the fallback") applies at merge time for this stacked line, not at PR creation.


## Review follow-up (phase-2 final review)

Deleting the inbox-drift bot test left `bot.test.ts`'s `cheatCodes` variable unused (lint error); a follow-up commit removes it.

## Leftover sweep (post-review)

- Corrected the `PublicInputArgs` natspec in `IRollup.sol`: it still described `previousInboxRollingHash` / `endInboxRollingHash` as deliberately unvalidated "until the Fast Inbox flip", but the flip (#24789, below this branch) added exactly that validation — `EpochProofLib` anchors the start boundary against the propose-time record, and the end boundary is covered transitively by the stored checkpoint header hashes. The comment now describes the implemented behavior.
- The 128-bit rolling hash this PR deliberately kept (see "What is re-homed / kept") is now removed at the node-cleanup PR #24793, where its last TS readers disappear.



Replaces #24791.
spalladino added a commit that referenced this pull request Jul 28, 2026
…387)

FI-17 of the Fast Inbox (AZIP-22) stack. Stacked on #24791 (A-1386); it removes the legacy `inHash` from the checkpoint header now that the consensus flip (#24789, A-1384) has switched validation to the streaming rolling hash. After this PR, `inboxRollingHash` is the checkpoint's only inbox commitment.

## Consensus-format change

- The checkpoint header loses its `inHash` field across all three preimage layers in lockstep: noir `CheckpointHeader` (`checkpoint_header.nr`), TS `CheckpointHeader` (`stdlib/src/rollup/checkpoint_header.ts`), and Solidity `ProposedHeaderLib` (struct + hash packing). The field ordering is otherwise unchanged; the header shrinks by one field (32 bytes: `CHECKPOINT_HEADER_SIZE_IN_BYTES` 380 to 348, `CHECKPOINT_HEADER_LENGTH` 14 to 13).
- The header-hash fixtures were regenerated from `checkpoint_header.test.ts` (the established workflow) and pasted into the noir tests; the p2p golden-byte fixture (`wire_compat_fixtures.ts`) was refreshed for the checkpoint-proposal serialization, which shrinks by exactly the one removed field. The block-proposal wire fixture is unchanged (block-level `inHash` is out of scope, see A-1388).

## Circuits and constants

- Strips the dead unconstrained `in_hash` pass-through from the sized `InboxParity<S>` circuit and `ParityPublicInputs` (noir + TS + the hand-written `noir-protocol-circuits-types` conversion wrappers). The circuit is kept (sized-parity, per FI-06's topology decision), so no VK-tree indices move.
- Retires `NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP` (replaced everywhere by `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`, same value 1024) and the now-dead `NUM_MSGS_PER_BASE_PARITY`, `NUM_BASE_PARITY_PER_ROOT_PARITY`, and `L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH`. Generated constants (`constants.gen.ts`, `ConstantsGen.sol`) were regenerated via `remake-constants`.
- Deletes `FrontierLib.sol` and its tests (`Parity.t.sol`, the `Frontier` harness and merkle test) now that their last users are gone.

## Verification

- Noir: `types::checkpoint_header` (5/5), `rollup_lib::parity` and `checkpoint_root::parity_tests` (15/15), and the checkpoint-root header-assembly path pass locally. The full `checkpoint_root` suite (68 tests) is too slow to run in full locally and rides CI.
- Solidity: `forge test` green across `Rollup`, `RollupFieldRange`, `Inbox`, `InboxBuckets`, `RollupGetters` (76/76), covering header hashing and the regenerated checkpoint fixtures.
- Jest: `checkpoint_header`, the p2p wire-compat suites, and the parity/messaging suites pass (43 tests), confirming the fixture regeneration is byte-consistent.
- VK/artifact/Prover.toml regeneration and the generated Noir ABI types ride CI (local `yarn build` in `noir-protocol-circuits-types` / `simulator` / `prover-client` is expected to fail on stale circuit artifacts until then); e2e and the broken-zone typecheck are the CI-of-record for those.

## Stack

Part of the Fast Inbox stack #24784 through #24791 (umbrella #24774): A-1379 (#24784), A-1380 (#24785), A-1381 (#24786), A-1382 (#24787), A-1383 (#24788), A-1384 flip (#24789), A-1385 (#24790), A-1386 (#24791, base of this PR).

Resolves A-1387.


## l1 publisher inbox consumption via the streaming selector (CI fix, pass-8)

The A-1387 inHash-removal rewrite left `l1_publisher.integration.test.ts` hardcoding `previousInboxRollingHash = Fr.ZERO` and `bucketHint = 0n` while consuming real L1→L2 messages, so message-consuming checkpoints reverted on L1 with `Rollup__InvalidInboxRollingHash` / `UnconsumedInboxMessages` (masked every prior CI round behind earlier failures). `INBOX_LAG_SECONDS` is an L1-time censorship cutoff, not whole checkpoints, so no checkpoint-depth shift register reproduces the aged bucket set. The test now mirrors the real Inbox buckets into its `MockL1ToL2MessageSource` and reuses the production `selectInboxBucketForBlock` (which mirrors `ProposeLib.validateInboxConsumption`) to pick exactly the buckets each checkpoint must consume, deriving the consumed bundle, the propose bucket hint, and the header rolling hash from that one selection. Box: 13/13, verified at a-1387 and a-1388.


Replaces #24792.
spalladino added a commit that referenced this pull request Jul 28, 2026
Node cleanup for the Fast Inbox (AZIP-22) project — removes the legacy L1-to-L2 message paths the flip (#24789) left dead. Sits on the Fast Inbox stack (#24784..#24792); base is `spl/a-1387-circuits-cleanup`.

## What's removed

- **stdlib / p2p**: `computeInHashFromL1ToL2Messages` and the whole `in_hash.ts`; the `inHash` field on `BlockProposal` (constructor, signed payload, wire, and the `createBlockProposal` validator-interface argument); the `CheckpointProposal.getBlockProposal` `inHash` pass-through; the padded per-checkpoint `InboxLeaf` helpers (`smallestIndexForCheckpoint` / `indexRangeForCheckpoint` / `checkpointNumberFromIndex`); the legacy `getL1ToL2Messages(checkpointNumber)` member from the `L1ToL2MessageSource` and archiver RPC interfaces.
- **archiver**: the legacy per-checkpoint `getL1ToL2Messages` flow, the padded per-checkpoint index invariants, the `inboxTreeInProgress` readiness gate + `L1ToL2MessagesNotReadyError`, and the 128-bit keccak rolling hash. `InboxMessage` now carries only the compact global index and the full-width consensus rolling hash (the vacuous derived `checkpointNumber` and the 128-bit `rollingHash` are gone). Reorg detection compares the local consensus rolling hash and total against the Inbox's current rolling-hash bucket (new `getBucket` / `getCurrentBucketSeq` / `getCurrentBucket` wrappers) instead of the 128-bit `getState`.
- **sequencer / validator**: the dead `inHash = Fr.ZERO` threading through the checkpoint proposal job and the validator/validation-service create-proposal path, plus the dead `in_hash_mismatch` validation-failure reason.
- **world-state**: the no-op first-in-checkpoint padding alias and the obsolete non-first-block-empty-bundle transitional test (the production assertion was already removed at the flip).
- **node**: the public-calls simulator's dead next-checkpoint message fetch and its now-unused `l1ToL2MessageSource` dependency.
- **config / env**: `AZTEC_INBOX_LAG` / `inboxLag` from ethereum config, foundation env vars, the network-consensus-config list, the l1-contracts + spartan network defaults, and the e2e option plumbing.
- **docs**: `THREAT_MODEL.md` and the archiver README rewritten from the `inHash == inbox.consume(...)` model to the consensus rolling-hash / bucket model.

## Format / store / wire notes

- **Store version bump**: `ARCHIVER_DB_VERSION` 8 → 9 because `InboxMessage` serialization dropped `rollingHash` + `checkpointNumber` and the `inboxTreeInProgress` singleton is gone. No migration — nodes resync (fresh rollup instance per release line, same no-migration policy as the rest of the stack).
- **p2p wire format**: dropping the (zeroed-since-flip) `inHash` shrinks the block-proposal bytes. Done as a plain removal at a release boundary (fresh networks), matching the A-1381 optional-tail precedent; the golden `wire_compat_fixtures.ts` buffers were regenerated. The checkpoint-proposal fixture is unaffected (it never carried `inHash`).
- **L1 follow-through: done (leftover sweep, below).** The on-chain Inbox 128-bit `messagesRollingHash` accumulation, `InboxState.rollingHash`, and the `MessageSent.rollingHash` event arg are now removed in this PR — this is the earliest branch where it is safe, since the TS reads disappear here. `getState()` / `getTotalMessagesInserted()` are kept: `chain_monitor` and fast node sync still read the message count.

## Testing

Locally green (unit suites that run without `@aztec/bb-avm-sim`): archiver (561), stdlib p2p + interfaces (76), world-state synchronizer + native (74), validator-client unit (38), ethereum config (6). Suites that import `@aztec/bb-avm-sim` (p2p libp2p, sequencer-client, node simulator, validator integration) and full typecheck of the packages downstream of the pinned-VK blocker are CI-validated. No e2e / VK regen locally.


## Review follow-up (phase-2 final review)

Four commits were appended by the final review pass:

- The env sweep removed `AZTEC_INBOX_LAG` from `scripts/network-defaults.json` while both L1 deploy-script test setUps still `readUint` the key, reverting before any test ran; the reads are gone.
- The p2p attestation store version is bumped 2 -> 3: it persists proposal/attestation buffers whose formats changed in this stack (checkpoint header lost `inHash`; block-proposal wire dropped it), and stored checkpoint attestations decode without a tolerant fallback.
- `l1_publisher.integration.test.ts` is reworked for streaming consumption: each checkpoint consumes every message sent while it was built, threading the previous checkpoint's rolling hash and reading the propose `bucketHint` from the Inbox's current bucket (the legacy inboxLag shift register is gone).
- Remaining `inboxLag`/`inHash` references are swept from the spartan environment profiles, the governance-upgrade tutorial, the validator/sequencer READMEs, and stale e2e comments.


## l1 publisher test dissolved into A-1387 (pass-8)

This branch's rewrite of `l1_publisher.integration.test.ts` is superseded by the corrected streaming-selector version landed at A-1387 (#24792): the branch's own message-reconstruction rework commit is dropped and the file is byte-identical to A-1387's here (segment diff for this file is zero). The A-1384 world-state mock bucket registration coexists with this branch's legacy-message-path deletion (the buckets survive the cleanup). First real box verification of this suite at A-1388: l1_publisher 13/13; world-state integration 12/12 at top.

## Leftover sweep (post-review)

- Removed the legacy 128-bit keccak inbox rolling hash from L1: `InboxState.rollingHash`, the `MessageSent` `bytes16 rollingHash` event arg, and its keccak accumulation in `Inbox.sol`, updating the four Solidity test suites that asserted it (`Inbox.t`, `InboxBuckets.t`, `TokenPortal.t`, `depositToAztecPublic.t`) and the three live docs-example inbox ABIs that embedded the old event shape. The node's message sync and L1-reorg detection already run entirely on the full-width consensus rolling hash from the buckets.
- Deleted the dead `InboxLeaf` class from stdlib (this branch had already removed its per-checkpoint index helpers) and fixed a stale `InboxLeaf` mention in the archiver retrieval JSDoc.
- Clarified the checkpoint-builder comments: the up-front whole-checkpoint message insertion (`insertMessagesPerBlock = false`) is only exercised by tests; production always streams messages per block.



Replaces #24793.
spalladino added a commit that referenced this pull request Jul 28, 2026
…neration (A-1435)

Deduplicates how the committed rollup protocol-circuit sample inputs (`noir-projects/noir-protocol-circuits/crates/rollup-*/Prover.toml`) get regenerated. Two paths previously both wrote `rollup-block-root-first`, `rollup-block-root-first-single-tx`, `rollup-checkpoint-root-single-block`, `rollup-checkpoint-merge`, and `rollup-root`, so the committed fixtures drifted depending on which ran last.

- Makes the prover-client `regenerate_rollup_sample_inputs.test.ts` suite the sole owner of every rollup circuit at or above the transaction merge: the block-root variants, block-merge, both checkpoint roots, checkpoint-merge, tx-merge, and root.
- Stops the e2e `full.test.ts` dump from writing the block-root and checkpoint samples it overlapped on. The e2e dump now regenerates only what needs real client-proved transactions the simulated orchestrator cannot produce: the private-kernel circuits and the transaction-base rollups (`rollup-tx-base-private`, `rollup-tx-base-public`).
- Adds a dedicated three-tx scenario to the suite to restore `rollup-tx-merge` coverage, which was dropped when `orchestrator_single_checkpoint.test.ts` was deleted and replaced by this suite. A block with three txs forces one tx-merge before the two-input block root, whereas one- or two-tx blocks feed the block root directly.
- Updates the regen docs (`barretenberg/cpp/CLAUDE.md`, the `update-prover-toml` and `gate-counts` skills, and the `updateProtocolCircuitSampleInputs` JSDoc) to the two-command split and removes the dead `orchestrator_single_checkpoint.test.ts` references.

Ownership after this change (each committed rollup `Prover.toml` has exactly one writer, no overlap):

- prover-client suite (`AZTEC_GENERATE_TEST_DATA=1 yarn workspace @aztec/prover-client test regenerate_rollup_sample_inputs`): the 11 block-root / block-merge / checkpoint / tx-merge / root tomls.
- e2e (`AZTEC_GENERATE_TEST_DATA=1 FAKE_PROOFS=1` full.test): private-kernel circuits plus `rollup-tx-base-private` / `rollup-tx-base-public`.

Part of the Fast Inbox (AZIP-22) cleanup stack, on top of #24784..#24794.

Validation: neither regeneration path runs in this environment (the prover-client build here has stale artifacts and e2e needs a full L1/anvil stack), so the actual fixture regeneration and the downstream `nargo execute` checks are validated on CI. Locally verified statically that the suite's scenario `dump` arrays plus the e2e list exactly cover all 13 committed rollup tomls with no overlap, and that a three-tx block routes through `getTxMergeRollupProof` and captures `rollup-tx-merge` test data.

Post-flip a zero-tx non-first block carrying a bundle is a live shape routed through the msgs-only block root (wired in #24789), so the regen suite gains a per-block-distribution scenario dumping `rollup-block-root-msgs-only`, and the circuit joins the CI `nargo execute` list — the first regen run must create and commit its `Prover.toml`. The documented e2e regen command also moves to the file's real location (`single-node/prover/server/full.test`; the old `e2e_prover/full.test` jest pattern matches nothing).

Replaces #24795.
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