Skip to content

feat(fast-inbox): node integration and the consensus flip (part 2/3) - #25037

Open
spalladino wants to merge 11 commits into
spl/fast-inbox-1-circuits-l1from
spl/fast-inbox-2-node-flip
Open

feat(fast-inbox): node integration and the consensus flip (part 2/3)#25037
spalladino wants to merge 11 commits into
spl/fast-inbox-1-circuits-l1from
spl/fast-inbox-2-node-flip

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Part 2 of 3 of the Fast Inbox (AZIP-22) stack — node integration and the consensus flip.

Stacked on #25036 (circuits + L1); until that merges, this diff includes it too.

Previously 7 separate PRs (#24784, #24785, #24786, #24787, #24788, #24789, #24790). Those are now closed and consolidated here as one squashed commit per issue, each carrying its original PR description in the commit body.

What it contains

Node integration — lands pre-flip, either degenerate or behind the streamingInbox flag defaulting off: the archiver mirrors the Inbox rolling-hash buckets and validates chain integrity; world-state accepts per-block message bundles (legacy flow calls it degenerately, bit-identical trees) with per-block unwind; block proposals carry an Inbox bucket reference so validators can derive the bundle themselves; the sequencer selects buckets per block mirroring the L1 predicate; validators run the AZIP's four acceptance checks (bucket known, moves forward, old enough, within caps) plus the last-block censorship check.

The flip — the coordinated cutover, small by construction because everything it switches on already exists and is tested below: Rollup.propose validates streaming inbox consumption against per-checkpoint temp-log records (genesis {0,0,0}); ProposeArgs gains an unsigned bucketHint; MAX_L1_TO_L2_MSGS_PER_BLOCK drops 1024 → 256; every block carries its own L1-to-L2 tree root in the blob; the prover slices a checkpoint's messages per block at compact indices; bundle padding and num_real_msgs are dropped from the circuits; and the streamingInbox flag is removed, making streaming the only path.

e2e — mid-checkpoint message inclusion within the same checkpoint, slot-denominated latency bound, message-only blocks, send-then-consume, plus prover-orchestrator multi-block message-slice coverage.

Commits (bottom → top)

  1. feat(fast-inbox): archiver syncs Inbox buckets (A-1379) — was feat(fast-inbox): archiver syncs Inbox buckets (A-1379) #24784
  2. feat(fast-inbox): world-state per-block message insertion (A-1380) — was feat(fast-inbox): world-state per-block message insertion (A-1380) #24785
  3. feat(fast-inbox): block proposals carry a bucket reference (A-1381) — was feat(fast-inbox): block proposals carry a bucket reference (A-1381) #24786
  4. feat(fast-inbox): sequencer streaming message selection, flag off (A-1382) — was feat(fast-inbox): sequencer streaming message selection, flag off (A-1382) #24787
  5. feat(fast-inbox): validator streaming acceptance conditions, flag off (A-1383) — was feat(fast-inbox): validator streaming acceptance conditions, flag off (A-1383) #24788
  6. feat(fast-inbox): flip consensus to streaming inbox (A-1384) — was feat(fast-inbox): flip consensus to streaming inbox (A-1384) #24789the flip
  7. test(fast-inbox): streaming inbox e2e latency and mid-checkpoint inclusion (A-1385) — was test(fast-inbox): streaming inbox e2e latency and mid-checkpoint inclusion (A-1385) #24790

Merge notes

  • A-1431 is a merge gate on the flip commit. The domain-separation decision must resolve before the flip lands. If it adopts a separator, the rolling-hash link-encoding change must be inserted below the flip.
  • Consensus-format changes (all release-boundary, fresh networks only): per-block message cap 256; the per-block blob encoding always carries the L1-to-L2 root; the block-proposal p2p wire format drops its zeroed inHash.
  • Store versions: ARCHIVER_DB_VERSION and the p2p attestation store are bumped across the stack; no migrations — nodes resync.
  • Accepted residuals: unconsumed-bucket overwrite protection (A-1390), L1-reorg bucket invalidation (A-1389), and the validator L1-sync race (A-1393) land in the hardening milestone. A failed final-block build can leave a checkpoint below the censorship floor, costing that slot; to be tightened with the A-1392 boundary matrix.
  • Known first-run flakes: if streaming_inbox.test.ts tests 1–2 (mid-checkpoint inclusion, latency bound) flake on early CI runs, add tight error_regex entries to .test_patterns.yml for the specific assertion rather than broad skips. None are pre-added.

This was referenced Jul 28, 2026
@spalladino
spalladino force-pushed the spl/fast-inbox-1-circuits-l1 branch from e31be72 to 4a406e6 Compare July 28, 2026 22:57
@spalladino
spalladino force-pushed the spl/fast-inbox-2-node-flip branch from ccf6887 to 3b2f02a Compare July 28, 2026 22:57
@spalladino
spalladino force-pushed the spl/fast-inbox-1-circuits-l1 branch from 4a406e6 to 896df6e Compare July 28, 2026 23:10
@spalladino
spalladino force-pushed the spl/fast-inbox-2-node-flip branch 4 times, most recently from f3ba4bb to 9f55393 Compare July 29, 2026 22:42
@spalladino
spalladino force-pushed the spl/fast-inbox-1-circuits-l1 branch from 896df6e to 7977123 Compare July 29, 2026 22:42
Part of the Fast Inbox stack (AZIP-22). Makes the archiver track the L1 Inbox rolling-hash buckets introduced in A-1377, validate the full-width consensus rolling-hash chain, and expose the bucket queries the sequencer/validator will consume (A-1382/A-1383). Purely additive: the legacy per-checkpoint `getL1ToL2Messages` path is unchanged.

## What's added

- `InboxMessage` gains `inboxRollingHash: Fr`, `bucketSeq`, and `bucketTimestamp` (the L1 block timestamp that keys the bucket). `mapLogInboxMessage` propagates them; the ethereum `MessageSent` log now also carries the emitting L1 block's timestamp.
- `MessageStore` persists a per-bucket snapshot (`{ inboxRollingHash, totalMsgCount, timestamp, msgCount, lastMessageIndex }`) keyed by bucket sequence, plus a timestamp→sequence index for at-or-before lookups. Snapshots are written as messages are inserted and rewound (deleted / boundary-bucket recomputed) on reorg removal, all inside the existing kv-store transactions.
- `addL1ToL2Messages` now validates the full-width consensus rolling hash (`updateInboxRollingHash`) alongside the legacy 128-bit keccak chain. Both run until the streaming inbox flips on (A-1388 removes the legacy one).
- Three queries on `L1ToL2MessageSource` (implemented by the archiver, supported by the mock, exposed over the archiver RPC schema): `getLatestInboxBucketAtOrBefore(timestamp)`, `getInboxBucket(seq)`, and `getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive)`. New `InboxBucket` type + zod schema in stdlib messaging.

## Notes

- No migration: `ARCHIVER_DB_VERSION` is bumped 7 → 8, so nodes resync their L1→L2 messages from L1. Archiver message stores are rebuildable from L1, so this is acceptable on this release line.
- Folds in A-1344: `InboxMessage.l1BlockNumber` is widened from UInt32 to UInt64 (the serialization was being rewritten anyway).
- Field naming: the full-width hash is `inboxRollingHash: Fr` to match the `updateInboxRollingHash` mirror, `CheckpointHeader.inboxRollingHash`, and the decoded `MessageSent` event — the value is a truncated-sha256-to-field element. (The plan's provisional `consensusRollingHash: Buffer32` predates the implemented event.) The legacy `rollingHash: Buffer16` stays until A-1388.

## Out of scope

- Reorg-driven bucket invalidation (a reorg that replays the identical message leaves but re-buckets them across different L1 blocks changes neither the 128-bit nor the full-width leaf-based hash, so `localStateMatches` cannot detect it, and the allowed message-reinsertion path leaves its bucket snapshot stale). This is handled by A-1389, and bucket consumption is gated off by the `streamingInbox` flag pre-flip.
- Sequencer/validator consumption (A-1382/A-1383), proposal bucket-reference types (A-1381), and removing the legacy 128-bit hash (A-1388).

## Testing

- `message_store.test.ts`: bucket snapshotting (multi-message, cross-batch, rollover), full-width chain-mismatch detection, reorg rewinding, and the three queries incl. boundary cases.
- `archiver-sync.test.ts`: end-to-end sync now exercises the full-width chain validation and asserts the bucket queries against the synced state (the fake L1 state mirrors the on-chain bucket assignment).
- `stdlib` archiver interface RPC round-trip tests for the three new methods.


Replaces #24784.
Part of the Fast Inbox stack (AZIP-22, FI-10 / A-1380). Base: \`spl/a-1379-archiver-buckets\`.

## What this changes

- \`NativeWorldStateService.handleL2BlockAndMessages\` no longer throws when a non-first block carries an L1-to-L2 message bundle. Any block may now transition the L1-to-L2 message tree.
- First-in-checkpoint bundles are still padded to \`NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP\` (so the legacy call shape produces bit-identical trees); non-first bundles are appended exactly as given (the post-flip compact append the circuits perform). Padding stays in this method as a transitional concern and moves to the caller at the flip.
- The legacy "non-first blocks carry no messages" rule moves from a throw inside world-state to a transitional assertion owned by the synchronizer call site (\`handleL2Block\`). The synchronizer's per-checkpoint message fetch is unchanged pre-flip.
- \`MerkleTreeAdminDatabase.handleL2BlockAndMessages\` JSDoc updated to document the per-block semantics.

## C++ investigation outcome (no barretenberg change needed)

Investigated the native sync/unwind paths under \`barretenberg/cpp/src/barretenberg/world_state/\`. Per-block message insertion and per-block unwind already work with **no C++ change**:

- The L1-to-L2 message tree is the same append-only \`FrTree\`/\`FrStore\` as the note-hash tree, which already varies its leaf count per block and unwinds per block.
- \`commit_block\` increments the tree's block height and writes a per-block \`BlockPayload\` (\`{size, root}\`) on **every** \`sync_block\`, even when zero leaves are appended, so the message tree's per-block history stays in lockstep with the other trees.
- \`unwind_block\` reverts exactly one block's appended leaves from that stored per-block metadata, generic over leaf count. C++ has no concept of an L2 checkpoint or "first block" — that is a TS-only invariant — so a mid-checkpoint block that appended message leaves unwinds correctly.

The TS-side pending-chain rollback (\`handleChainPruned\` -> \`unwindBlocks\`) just unwinds to a block number and carries no checkpoint assumption about the message tree either.

## Tests

- Every-block equivalence: a legacy-shaped multi-block checkpoint leaves the message tree byte-identical (root + size) on every non-first block, while the archive advances per block — proving the degenerate path is a no-op at every block, not just at checkpoint ends.
- Per-block appends: a checkpoint whose blocks advance the message tree by exactly their (unpadded) bundle size on non-first blocks; forks opened mid-chain see exactly the bundles up to that block.
- Per-block unwind: unwinding a message-carrying non-first block reverts exactly its appended leaves back to the pre-block state, and re-syncing afterwards reconverges.
- Synchronizer guard: a non-first block carrying messages is rejected by the call-site assertion.

world-state type-checks clean and all world-state unit tests pass (76 across the two modified suites). The full \`yarn build\` still fails only in the pre-existing stale-artifact packages (noir-protocol-circuits-types, simulator, prover-client), unrelated to this change.

## Review follow-up

Codex (routine review) confirmed the C++ no-change conclusion and the pre-flip synchronizer soundness, and flagged that the TXE world-state caller (`txe/src/state_machine/synchronizer.ts`) padded the bundle unconditionally. TXE mines one block per checkpoint (always the first block), so this was a no-op in practice, but the second commit makes its padding conditional on `indexWithinCheckpoint === 0` to keep the caller aligned with the new per-block semantics.


Replaces #24785.
Adds an optional `InboxBucketRef` (`bucketSeq`, `bucketTimestamp`, `inboxRollingHash`) to `BlockProposal` and to the last block of `CheckpointProposal` (AZIP-22 Fast Inbox), so validators can derive the consumed L1-to-L2 message bundle from their own Inbox view instead of trusting a proposer-supplied list. Plumbing only for now — the sequencer passes `undefined` pre-flip (populated in FI-12) and the legacy validation path ignores it (validated in FI-13).

- Signed, not an unsigned lookup aid: the reference lives inside the signed payload (`getPayloadToSign`), so a relay cannot strip or inject it without breaking signature recovery. The checkpoint header's `inboxRollingHash` remains the consensus commitment; a wrong reference can only miss the Inbox lookup. This discharges the P2-2 residual from the A-1371 design (signedness + republish path).
- Optional-tail wire encoding, no topic-version bump: an unset proposal serializes byte-identically to the legacy format, and decoders treat EOF after the existing fields as "no reference", so mixed-version gossip keeps working. Gossip topic versions derive from deployment identity, not node software version, so mixed decoders are a hard requirement.
- Wire-compat evidence: golden fixtures captured from the pre-change bytes pin (a) unset serialization is byte-identical, (b) legacy buffers decode as no-reference, for both proposal types.
- `CheckpointProposal` enforces at construction that a set last-block reference's rolling hash equals the checkpoint header's `inboxRollingHash`, mirroring the existing `inHash` cross-check.
- Audited every `BlockProposal.fromBuffer` / `CheckpointProposal.fromBuffer` decode site in `p2p/src` and `message_validator.ts`: none assume a strict buffer length, so the appended tail is tolerated.

Tests: `InboxBucketRef` round-trip/equals/schema; block + checkpoint proposal round-trips with the reference set (with and without signed txs); byte-identity vs golden fixtures when unset; legacy-buffer tolerance; signature coverage (recovery over a set reference, and recovery breaks when the reference is tampered with or injected); payload hashes differ set-vs-unset; checkpoint header-consistency check throws on mismatch.

Replaces #24786.
…1382)

Implements A-1382 (FI-12): sequencer streaming Inbox message selection behind a `streamingInbox` flag, default off. Part of the Fast Inbox stack (AZIP-22).

## Flag semantics

- One shared env var `STREAMING_INBOX` -> `streamingInbox: boolean` (default false), defined once in `stdlib`'s `sharedSequencerConfigMappings` so the sequencer reads it now and the validator (A-1383) maps the same env. Flag off is byte-identical: the new paths are gated at checkpoint start and per block.
- With the flag on, checkpoints are expected to fail L1 submission (on-chain `propose` still enforces the legacy per-checkpoint consumption until the flip, A-1384). The streaming path is exercised with unit/mock tests only.

## Selection policy (mirrors `ProposeLib.validateInboxConsumption`)

New pure selector `sequencer-client/src/sequencer/inbox_bucket_selector.ts`, run per block:

- Pick the newest lag-eligible bucket (`getLatestInboxBucketAtOrBefore(now - INBOX_LAG_SECONDS)`); on the checkpoint's final block, also consider the cutoff bucket and take the newer, so consumption reaches the censorship floor.
- Walk back from the candidate to the newest bucket that fits both the per-block cap (`bucket.totalMsgCount - parent.totalMsgCount`) and the per-checkpoint cap (`bucket.totalMsgCount - checkpointStartTotalMsgCount`). If even the first forward bucket overshoots the per-checkpoint cap, consume nothing — matching L1's cap-escape (`next.totalMsgCount - parentTotal > MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`).
- The inclusive `<=` comparisons make a bucket exactly `INBOX_LAG_SECONDS` old eligible and a bucket exactly at the cutoff mandatory, matching L1's strict `next.timestamp > cutoff`.

Cutoff is computed as `getTimestampForSlot(slot - 1) - INBOX_LAG_SECONDS`, matching `ProposeLib` exactly (not the consensus timetable's `getBuildFrameStart`, which subtracts an extra ethereum-slot). Boundary vectors pinned against A-1371 resolution section 13.

## Cutoff floor

On the checkpoint's final block (including the block that reaches the per-checkpoint block cap) the cutoff is a consumption floor, so the checkpoint's own header would pass the L1 censorship assert post-flip.

## Builder interface change

- `ICheckpointBlockBuilder.buildBlock` gains an optional per-block `l1ToL2Messages` bundle; `ICheckpointsBuilder.startCheckpoint` gains `insertMessagesPerBlock`. Threaded through `FullNodeCheckpointsBuilder`/`CheckpointBuilder` and the lightweight builder (first-in-checkpoint bundle padded, non-first compact; the inHash/rolling hash recompute over the accumulated logical messages).
- An optional signed bucket reference is threaded onto block proposals (`Validator.createBlockProposal` -> `ValidationService` -> `BlockProposal.createProposalFromSigner`).

## Constant

`INBOX_LAG_SECONDS = 12` added to `constants.nr` and regenerated into the TS constants (`constants.gen.ts`). Not emitted to `ConstantsGen.sol` (Solidity whitelist); `ProposeLib` keeps its local copy pending consolidation (A-1434).

## Open item / to verify on CI

- Non-genesis cross-checkpoint parent-bucket sourcing is not wired: there is no by-rolling-hash archiver lookup and legacy parents do not sit on a bucket boundary, so only the genesis base case is resolved and non-genesis throws (safely caught -> skipped proposal). This is a flip-time concern.
- Local build/typecheck of `sequencer-client`, `validator-client`, `prover-client` is blocked by the known stale-artifact/`@aztec/bb-avm-sim` breakage, so CI is the typecheck of record for the job/builder/proposal edits. The pure selector unit tests and the stdlib config changes were verified locally.

Replaces #24787.
… (A-1383)

Implements A-1383: the validator's streaming-Inbox acceptance conditions (AZIP-22 Fast Inbox), behind the shared `streamingInbox` flag (default off). Flag off ⇒ byte-identical behavior. Stacked on #24787 (A-1382).

## The four block-proposal checks (`validator-client/src/streaming_inbox_checks.ts`, wired in `proposal_handler.handleBlockProposal`)
- **Exists**: the referenced bucket resolves in the node's own Inbox view and its consensus rolling hash matches the reference. An unknown bucket is an immediate reject (`bucket_unknown`); the bounded-wait soft path is A-1393. The reference is trusted only as a `bucketSeq` lookup hint — timestamp/counts are read from the locally resolved bucket.
- **Moves forward**: the bucket's cumulative total is at least the parent block's (equal ⇒ empty bundle).
- **Not too new**: the bucket is at least `INBOX_LAG_SECONDS` old at validation time (`dateProvider.now()`, inclusive boundary).
- **Caps**: the per-block count and the running per-checkpoint total fit `MAX_L1_TO_L2_MSGS_PER_BLOCK` / `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`.

The derived bundle is fed into per-block re-execution (`insertMessagesPerBlock` threaded through `openCheckpoint` → `reexecuteTransactions`); the existing state-ref comparison after re-execution stays the final arbiter.

## Last-block censorship check (`validateCheckpointProposal`)
Before attesting, the minimum-consumption rule runs via the shared `isInboxConsumptionSufficient` predicate: the first unconsumed bucket must be absent, past the cutoff, or a cap-escape. A mandatory unconsumed bucket rejects the checkpoint (no attestation, `inbox_consumption_insufficient`).

## Shared-predicate extraction (`refactor` commit)
The cutoff formula and the minimum-consumption/cap-escape rule moved to `stdlib/messaging/inbox_consumption`, so the sequencer's bucket selection and the validator share one source of truth. The sequencer's `selectStreamingBundle` now calls `getInboxCutoffTimestamp` instead of inlining it — behavior unchanged (selector suite still green).

## Corrected cutoff anchor
The cutoff is `getTimestampForSlot(slot - 1) - INBOX_LAG_SECONDS` (mirroring `ProposeLib.validateInboxConsumption`), **not** `getBuildFrameStart`. The A-1371 §13 cross-layer vectors are pinned in `inbox_consumption.test.ts`.

## Parent-ref derivation
Parent/last-consumed buckets are resolved from L1-to-L2 tree leaf counts (compact post-flip indexing == bucket cumulative total) via a new `getInboxBucketByTotalMsgCount` archiver lookup. A leaf count that does not land on a bucket boundary (a pre-flip padded parent) is rejected/deferred; the flip (A-1384) closes that gap.

## CI must typecheck what can't be verified locally
`tsc -b` cannot complete locally due to pre-existing `noir-protocol-circuits-types` breakage (missing artifacts / stale generated types), which cascades to `archiver`/`validator-client`. stdlib builds clean; no errors in the changed files outside the broken zone. Runnable unit suites are green: `streaming_inbox_checks` (14), `inbox_consumption` (10), `proposal_handler` (34, incl. flag-off unchanged + new streaming/censorship tests), `message_store` (35), stdlib `archiver` interface (RPC schema). CI is the typecheck of record for the unbuildable zone.

Out of scope: bounded-wait (A-1393), the flip (A-1384), reorg rejection (A-1389), the full streaming checkpoint rebuild (A-1385).

Replaces #24788.
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`.

- **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.

- `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.

- `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.

- **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).

- 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`).

- **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.

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.

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.

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 #24793).

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`).

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`).

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/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.

- 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.

Replaces #24789.
…usion (A-1385)

Test-only coverage for the Fast Inbox (AZIP-22) streaming L1→L2 message path (issue A-1385, FI-15) — the behaviours the legacy suite could not express, because pre-flip every message entered at the first block of the *next* checkpoint.

Stacked on the node/flip phase: #24784 (A-1379) → #24785 (A-1380) → #24786 (A-1381) → #24787 (A-1382) → #24788 (A-1383) → #24789 (A-1384). Base is `spl/a-1384-flip-streaming-inbox`.

New `single-node/cross-chain/streaming_inbox.test.ts`, on the proven `CrossChainMessagingTest` fixture (production pipelining sequencer) with a widened slot (36s, 6s blocks → up to ~4 blocks per checkpoint) and `minTxsPerBlock: 0`:

- **Mid-checkpoint inclusion** — times a send so the message ages past `INBOX_LAG_SECONDS` partway through a checkpoint's build, then asserts the inserting block has `indexWithinCheckpoint > 0` and that the immediately preceding block did not yet carry the message. Retries with fresh messages if a message happens to age exactly at a checkpoint boundary.
- **Latency bound** — asserts, slot-denominated, that `includingBlockTimestamp − messageL1Timestamp ≤ INBOX_LAG_SECONDS + 2·slotDuration` (derived from the deployed constants, not hardcoded), and that it is positive. Wall-clock latency is logged only (A-1178 style), never asserted.
- **Message-only block** — on a drained pool, asserts the block that consumes the message carries zero tx effects (the FI-05 zero-tx / non-empty-bundle shape) and that the chain keeps proving past it.
- **Send-then-consume on the streaming path** — consumes a streaming-inserted message with a public tx by its compact leaf index and asserts a second consume reverts (double-spend protection); the insert/consume block relationship is logged.

New `checkpoint-sub-tree-orchestrator` test for a checkpoint whose L1→L2 messages span multiple blocks (a non-first block carries a bundle). Asserts per-block start/end L1→L2 tree-snapshot continuity and slice partitioning (no gap/overlap; block slice = `[prevBlockLeafCount, blockLeafCount)`) and the `isFirstBlock` flag. Adds `TestContext.makeCheckpointWithMessagesPerBlock` to distribute a bundle across a checkpoint's blocks (the single-block-per-checkpoint `makeCheckpoint` puts every message in the first block).

- The e2e suite and the `prover-client` orchestrator test **could not be run locally** — the local build is broken in `noir-protocol-circuits-types`/`simulator`/`prover-client` from stale circuit artifacts, and `end-to-end` sits downstream. CI validates both. Every helper, fixture, and API call is grounded on an existing green suite (`l1_to_l2.test.ts`, `cross_chain_public_message.test.ts`) and verified against the current interfaces. No packages that build locally (stdlib, blob-lib, ethereum, foundation) are touched.
- Reviewed by codex (gpt-5.6-terra): it caught an incorrect message-sponge-continuity assumption in the prover test (non-first block roots inherit the checkpoint-wide sponge, not the prior block's end sponge); those sponge assertions were removed, leaving the correct L1→L2 tree-snapshot partitioning checks.

The multi-block-slice orchestrator test now also asserts per-block message-sponge continuity (start empty, each block absorbs exactly its slice, last end equals the InboxParity sponge) and turns its last block into a zero-tx message-only block, exercising the msgs-only block-root wiring fixed in #24789. A stray `async` in the e2e suite was dropped (lint).

Replaces #24790.
…-1539)

The checkpoint root circuit caps how many blocks a checkpoint may contain, but L1 cannot enforce that cap when the
checkpoint is proposed: `ProposedHeader` carries no block count, only `blockHeadersHash`. An over-cap checkpoint would
therefore be accepted on L1 and then be unprovable, forcing an epoch reorg — so validators have to reject it before
attesting, the same enforcement pattern the streaming Inbox uses for `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`.

The checkpoint-proposal check already existed but only fired for an operator-configured `maxBlocksPerCheckpoint`. It
now always applies the protocol cap, with the operator limit only able to tighten it.

Test: an over-cap checkpoint proposal is rejected with no operator limit configured.
@spalladino
spalladino force-pushed the spl/fast-inbox-2-node-flip branch from c8dfef0 to 2842270 Compare July 30, 2026 05:06
…e block cap (A-1384)

The flip drops `MAX_L1_TO_L2_MSGS_PER_BLOCK` from 1024 to 256, so every rollup sample input at or
above the block root carries a stale ABI once it is regenerated on the circuits branch, where the
constant is still 1024. CI failed with

    The value passed for parameter `inputs.message_bundle.messages` does not match the specified
    type: Type Array { length: 256, typ: Field } is expected to have length 256 but value
    Vec([...]) has length 1024

on `rollup-block-root`, `rollup-block-root-no-txs` and `rollup-block-root-single-tx`. The
regenerated bundles also drop `num_real_msgs`, which this branch's `L1ToL2MessageBundle` no longer
has.

Regenerated through the canonical path — `AZTEC_GENERATE_TEST_DATA=1 yarn workspace
@aztec/prover-client test src/test/regenerate_rollup_sample_inputs.test.ts` — against circuits and
VKs built from source at this head. Verified with `nargo execute` on all eleven crates in the CI
list: every one solves its witness.
…lightweight builder (A-1384)

The flip removed `LightweightCheckpointBuilder.addBlock`'s "empty blocks are only allowed as the
first block in a checkpoint" guard, because a zero-tx block carrying only an Inbox bundle is a live
shape once messages are inserted per block. The negative test asserting the old guard was left
behind and failed with `Received promise resolved instead of rejected`.

Replaces it with the positive case it became: a second block with no txs and a two-message bundle
builds, reports no tx effects, advances the L1-to-L2 tree by its own bundle, and lands in the
completed checkpoint. Reinstating the guard or dropping the per-block insert both turn it red again.
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