Skip to content

feat(fast-inbox): streaming inbox node integration, flip, and cleanup (umbrella) - #24796

Closed
spalladino wants to merge 184 commits into
merge-train/spartanfrom
spl/a-1435-regen-dedup
Closed

feat(fast-inbox): streaming inbox node integration, flip, and cleanup (umbrella)#24796
spalladino wants to merge 184 commits into
merge-train/spartanfrom
spl/a-1435-regen-dedup

Conversation

@spalladino

@spalladino spalladino commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Umbrella / consolidated PR for phase 2 of the Fast Inbox (AZIP-22) work.

This PR spans the phase-2 stack — from the topmost branch down to merge-train/spartan — so CI runs once over the combined diff and reviewers get a single consolidated view. It sits on top of the phase-1 (circuits + L1) umbrella #24774: until the phase-1 PRs merge, this diff includes them too.

What it contains, at a high level

Phase 2 lands the node integration, the consensus flip, the streaming e2e coverage, and the legacy-path cleanup for the Fast Inbox:

  • Node integration (pre-flip, degenerate or flag-gated off): the archiver mirrors the Inbox rolling-hash buckets and validates the full-width chain; world-state accepts per-block message bundles; block proposals carry a signed Inbox bucket reference; the sequencer selects buckets per block mirroring the L1 predicate; validators run the AZIP's four acceptance checks plus the last-block censorship rule.
  • The flip: 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, and the streamingInbox flag is removed — streaming is the only path.
  • e2e: mid-checkpoint inclusion, slot-denominated latency bound, message-only blocks, send-then-consume, plus a prover-orchestrator multi-block-slice test.
  • Cleanup: the legacy L1 inbox path (frontier trees, consume(), LAG), the checkpoint header's inHash (noir/TS/Solidity in lockstep), the node's legacy per-checkpoint message paths and the 128-bit sync hash (node reads and, via the leftover sweep, the L1 field and event arg too), the AZTEC_INBOX_LAG env sweep, inbox constants emitted from the generator, and a single canonical path for rollup sample-input regeneration.

Contained PRs (stack order, bottom → top)

  1. feat(fast-inbox): archiver syncs Inbox buckets (A-1379) #24784 — archiver syncs Inbox buckets and serves bucket queries (A-1379)
  2. feat(fast-inbox): world-state per-block message insertion (A-1380) #24785 — world-state per-block message insertion (A-1380)
  3. feat(fast-inbox): block proposals carry a bucket reference (A-1381) #24786 — block proposals carry a signed Inbox bucket reference (A-1381)
  4. feat(fast-inbox): sequencer streaming message selection, flag off (A-1382) #24787 — sequencer streaming message selection, flag off (A-1382)
  5. feat(fast-inbox): validator streaming acceptance conditions, flag off (A-1383) #24788 — validator streaming acceptance conditions, flag off (A-1383)
  6. feat(fast-inbox): flip consensus to streaming inbox (A-1384) #24789the flip: consensus cutover to the streaming inbox (A-1384)
  7. test(fast-inbox): streaming inbox e2e latency and mid-checkpoint inclusion (A-1385) #24790 — streaming inbox e2e + prover multi-block-slice coverage (A-1385)
  8. chore(fast-inbox): delete legacy L1 inbox path (A-1386) #24791 — delete the legacy L1 inbox path (A-1386)
  9. chore(fast-inbox): circuits cleanup and checkpoint header format (A-1387) #24792 — circuits cleanup; checkpoint header loses inHash (A-1387)
  10. chore(fast-inbox): delete legacy node message paths (A-1388) #24793 — delete legacy node message paths, 128-bit sync hash, env sweep (A-1388)
  11. chore(fast-inbox): emit inbox lag and checkpoint cap from the constants generator (A-1434) #24794 — emit inbox lag and checkpoint cap from the constants generator (A-1434)
  12. chore(fast-inbox): single canonical path for rollup sample-input regeneration (A-1435) #24795 — single canonical path for rollup sample-input regeneration (A-1435)

Merge notes

  • A-1431 merge gate: the domain-separation decision must resolve before feat(fast-inbox): flip consensus to streaming inbox (A-1384) #24789 (the flip) merges. If it adopts a separator, the rolling-hash link-encoding change must be inserted below the flip in the stack.
  • Regeneration (done): the rollup Prover.toml sample inputs were regenerated on a bb-capable box and committed at the top of the stack (bbfaf57daa), including the new rollup-block-root-msgs-only/Prover.toml — the circuit joined the regen scenarios and the CI nargo execute list in this stack. All regenerated inputs pass nargo execute.
  • Store versions: ARCHIVER_DB_VERSION 7 → 9 across the stack and the p2p attestation store 2 → 3; no migrations — nodes resync (fresh rollup instance per release line).
  • Consensus-format changes: the checkpoint header drops inHash (preimage changes in noir/TS/Solidity lockstep, fixtures regenerated), the block-proposal p2p wire format drops its zeroed inHash, the per-block blob encoding always carries the L1-to-L2 root, and the per-block message cap is 256. All are release-boundary changes for fresh networks.
  • Review fixes: the final review appended fix commits to feat(fast-inbox): flip consensus to streaming inbox (A-1384) #24789 (per-block message-sponge threading, msgs-only block-root wiring, message-only block building) and chore(fast-inbox): delete legacy node message paths (A-1388) #24793 (deploy-script test env reads, attestation store bump, streaming rework of the L1 publisher integration test), with the stack rebased on top.
  • CI hardening: driving the stack green added a min-work rule (a non-empty message bundle satisfies the block threshold), oldest-epoch-first proving-job scheduling, an EpochProofExtLib split keeping RollupOperationsExtLib deployable post-flip, streaming mocks for the validator/prover-node unit suites, a pxe L2 tips snapshot regen for the inHash-less header (read-defaultable, no schema bump), and docs-example Inbox ABIs kept in lockstep with the MessageSent event.
  • Pass-4 product fixes: two consensus-path bugs surfaced by the cross-chain e2e were fixed in their owning branches, not at the top: the ProvingJobResult Zod union was missing a BLOCK_ROOT_MSGS_ONLY_ROLLUP member (feat(fast-inbox): add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375) #24612 / A-1375, where the request type is introduced), which stalled epochs decoding a message-only block-root result; and the propose bucketHint was lost when a checkpoint's final block failed to build after a message-only first block consumed a bucket (feat(fast-inbox): flip consensus to streaming inbox (A-1384) #24789 / A-1384). Both carry unit-level round-trip / RED-GREEN regression tests. The relocated proving-scheduling test (base of the stack) also swapped its parity reference for PUBLIC_VM: no parity name type-checks across the whole stack (INBOX_PARITY exists only at A-1427+, PARITY_BASE only at A-1374/A-1375, since A-1427 collapses PARITY_BASE/PARITY_ROOT into INBOX_PARITY), whereas PUBLIC_VM is stable everywhere and stays lower-priority than PRIVATE_TX_BASE_ROLLUP. Verified with real tsc at A-1374, A-1384, and the top.
  • Readiness-tip fix (A-1384): the shared cross-chain e2e helper evaluated L1→L2 message readiness against the default 'latest' tip while these suites anchor the PXE to 'checkpointed'. Post-flip per-block inbox insertion lets the proposed chain reach a message's consume-checkpoint a checkpoint before it is published, so readiness flipped true while the PXE had not synced the message and the private consume simulated too early — a deterministic failure on l1_to_l2.test.ts from the flip up. The helper now gates on the configured PXE sync tip (CrossChainMessagingTest.pxeSyncChainTip, defaulting to 'latest'). Test-only, unchanged A-1384→top; box-verified green on all three cross-chain suites (l1_to_l2, streaming_inbox, l1_to_l2_inbox_drift).
  • Archiver per-block message-root reconstruction (A-1384, consensus fix): the archiver rebuilt every block's L1→L2 message tree root from the checkpoint's first block (stale pre-flip per-checkpoint assumption), so any follower node (prover / sync-only) reconstructing a non-first message-inserting block got the wrong root and forked from the sequencer (block state does not match world state) on the first cross-chain-consuming block. Post-flip the blob carries a per-block root; reconstruction now uses each block's own. Surfaced as cross_chain_public_message + token_bridge (only followers reconstruct via the archiver, so validation never caught it). A data_retrieval.test.ts unit pins per-block reconstruction; both e2es green at a-1384. A full box sweep of every L1→L2/inbox/cross-chain/proving e2e at a-1384 is now green.
  • Known first-run flakes: if the new streaming_inbox.test.ts tests 1–2 (mid-checkpoint inclusion, latency bound) flake on their first CI runs, add tight error_regex entries to .test_patterns.yml for the specific assertion rather than broad skips. None are pre-added.
  • 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 phase; 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.

Review and land the individual PRs above; this umbrella exists only to consolidate CI and provide a full-stack diff.

  • Pass-8 CI fixes (test-only, both surfaced by fail-fast unmasking): (1) the world-state integration.test.ts mock archiver never registered Inbox buckets, so the post-flip synchronizer reconstructed empty bundles and diverged (block state does not match world state) — fixed at A-1384 by registering genesis + per-checkpoint buckets in MockPrefilledArchiver.setPrefilled; (2) the l1_publisher.integration.test.ts inbox-consumption modeling (hardcoded rolling hash / bucket hint, then a wrong checkpoint-depth shift register) reverted on L1 — fixed at A-1387 by mirroring the real Inbox buckets and reusing the production selectInboxBucketForBlock (INBOX_LAG_SECONDS is an L1-time cutoff). A-1388's own rewrite of that file dissolves into the A-1387 version. Box-verified: world-state 12/12 (a-1384, top), l1_publisher 13/13 (a-1387, a-1388); full tsc green at a-1384/a-1387/a-1388/top.

  • Leftover sweep (post-review of this umbrella diff): five flip/cleanup leftovers found while reviewing this diff were fixed in their owning branches. The legacy 128-bit keccak inbox rolling hash is now fully removed on L1 (InboxState.rollingHash, the MessageSent bytes16 arg, its keccak accumulation, the four Solidity test suites asserting it, and the three live docs-example inbox ABIs) at A-1388 — the earliest branch whose TS no longer reads it. The stale IRollup.sol PublicInputArgs natspec now describes the implemented epoch-proof anchoring (A-1386). The inert isFirstBlock blob-fixture parameter is removed (A-1384). The dead InboxLeaf class is deleted (A-1388). The checkpoint-builder comments now mark up-front whole-checkpoint message insertion as test-only (A-1388).

  • Gas benchmarks refreshed (top of stack): gas_report.json, gas_benchmark.md, and gas_benchmark_results.json regenerated via ./bootstrap.sh gas_report / gas_benchmark at the pinned foundry v1.4.1. Headlines: sendL2Message mean gas 53,020 → 44,796 (−15.5%), Inbox bytecode −19%; propose +~4% mean from the bucketHint calldata and the added inbox-consumption validation. Much of the raw diff is pre-existing baseline staleness (the committed report predated the block→checkpoint rename and the Outbox.getRootData signature change), now corrected. Known quirk for future regens: three Rollup.t.sol tests (testExtraBlobs, testRevertInvalidCoinbase, testRevertInvalidTimestamp) fail only under FORGE_GAS_REPORT=true — a Foundry vm.expectRevert call-depth interaction with the IInbox.getBucket() staticcall in ProposeLib.validateInboxConsumption, invisible to normal CI; a real fix or a --no-match-test entry in bootstrap.sh gas_report is a follow-up.

@spalladino
spalladino force-pushed the spl/a-1435-regen-dedup branch 8 times, most recently from a6dbe2a to bd8896e Compare July 19, 2026 20:48
@spalladino
spalladino requested a review from a team as a code owner July 19, 2026 20:48
@spalladino
spalladino force-pushed the spl/a-1435-regen-dedup branch 13 times, most recently from 7d1d7d3 to 9e12351 Compare July 21, 2026 22:39

@spalladino spalladino left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got as far as sequencer-client (excluded)

// The fee recipient/value below are sourced from the supplied headers, so the header hashes must be validated here
// as well as on the submit path - otherwise an off-chain caller of this getter would assemble public inputs from
// unverified fee fields and only discover the mismatch when the on-chain proof reverts.
verifyHeaders(_start, _end, _headers);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wasn't this needed before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was needed before but missing — a pre-existing gap, not something this stack introduced. Base commit 8b1ea21 (2026-06-18, "protect epoch-proof fees against an unsound verifier") started sourcing fee recipient/value from the caller-supplied headers in this getter without validating them; only the submit path called verifyHeaders. An off-chain caller assembling public inputs would only discover a header mismatch when the on-chain proof reverted, so ada3771 added the call here as a drive-by fix.

Side effect worth fixing: submitEpochRootProof now hashes the headers twice (directly at :112 and again through this getter via verifyEpochRootProof) — work item filed to dedupe with an internal variant that skips validation on the already-verified submit path.

Comment on lines +192 to +198
bytes32 expectedPreviousInboxRollingHash = STFLib.getInboxRollingHash(_start - 1);
require(
expectedPreviousInboxRollingHash == _args.previousInboxRollingHash,
Errors.Rollup__InvalidPreviousInboxRollingHash(
expectedPreviousInboxRollingHash, _args.previousInboxRollingHash
)
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we need to do the same for the endInboxRollingHash?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the end is already pinned to storage transitively: the checkpoint root writes the parity end into the checkpoint header's inboxRollingHash, this getter sources checkpointHeaderHashes from the stored getHeaderHash(...), and the circuit binds end_inbox_rolling_hash to the last checkpoint's header. A wrong _args.endInboxRollingHash therefore produces public inputs that don't match the proof, and verification fails. The start has no such coverage — the previous epoch's header hashes are not among this proof's public inputs — so it needs the explicit anchor, mirroring previousArchive.

An explicit end check (STFLib.getInboxRollingHash(_end)) would be redundant for soundness but would give a clean error instead of Rollup__InvalidProof and mirror the endArchive check. One SLOAD; happy to add it if you want the symmetry.

v.header.inboxRollingHash,
_args.bucketHint,
v.header.slotNumber,
STFLib.getInboxMsgTotal(checkpointNumber - 1)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is requiring yet another SLOAD, right? Any way we can optimize that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — one extra cold SLOAD to read the parent's consumed total. It can be made warm: inboxMsgTotal and inboxConsumedBucket (uint64 each) currently open a fresh slot, but the temp-log slot holding CompressedSlot slotNumber (uint32) has 28 bytes free, and propose already SLOADs the parent's slot for the slot-progression check (STFLib.getSlotNumber(pendingCheckpointNumber) at ProposeLib.sol:372). Packing the counters there makes this read warm (100 gas) and merges the write into the existing slot-number SSTORE, which also drops one of the two extra stores flagged on CheckpointLog. Work item filed.

Comment on lines +55 to +57
bytes32 inboxRollingHash;
uint64 inboxMsgTotal;
uint64 inboxConsumedBucket;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are two extra SSTOREs. How do these compare vs what we were storing on the Inbox?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout today: inboxRollingHash takes one new slot and the two uint64 counters pack into a second new slot, so propose pays 2 extra zero→nonzero SSTOREs. The legacy path paid ~1 warm Inbox slot write at consume() (per-message frontier-tree writes were paid by senders, as the bucket-ring writes are now). Net measured effect on propose is +7,770 avg gas in the bench (195,988 → 203,758), all stack changes included.

Packing the counters next to CompressedSlot slotNumber (see the ProposeLib thread) eliminates one of the two; the rolling-hash slot has to stay — it anchors the epoch proof's start boundary.

Comment on lines -161 to -166
function testFuzzSendAndConsume(
DataStructures.L1ToL2Msg[] memory _messagesFirstBatch,
DataStructures.L1ToL2Msg[] memory _messagesSecondBatch,
uint256 _numTreesToConsumeFirstBatch,
uint256 _numTreesToConsumeSecondBatch
) public {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an equivalent fuzzing test in the new suite? If not, let's add it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No equivalent — InboxBuckets.t.sol is all deterministic, and only the single-message testFuzzInsert survived in Inbox.t.sol. Work item filed: fuzz message batches across multiple L1 blocks with rollover, asserting the rolling-hash chain recomputes from the leaves, cumulative totals stay monotone, the per-bucket cap holds, and snapshot boundaries align with L1 blocks. The consume-side/wraparound interleaving fuzz is already planned in A-1390's harness (tiny test ring), so this one stays send-path-focused.

Comment on lines +72 to +74
// Streaming Inbox (AZIP-22 Fast Inbox): messages are inserted per block via `addBlock`, so `l1ToL2Messages` here
// is empty and the up-front checkpoint-wide insertion is skipped.
insertMessagesPerBlock: boolean = false,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're post-flip now. No need for a flag to control pre vs post flip behaviour. Just write the code for post-flip.

@@ -165,14 +177,15 @@ export class LightweightCheckpointBuilder {
public async addBlock(
globalVariables: GlobalVariables,
txs: ProcessedTx[],
opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference } = {},
opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference; l1ToL2Messages?: Fr[] } = {},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l1ToL2Messages should not be an "opt", it's now a first-class thing we add to each block. Promote it to a proper argument.

Comment on lines +739 to +744
case 'rollup-block-root-first':
return this.prover.getBlockRootFirstRollupProof(rollup.inputs, signal, provingState.epochNumber);
case 'rollup-block-root-first-single-tx':
return this.prover.getBlockRootSingleTxFirstRollupProof(rollup.inputs, signal, provingState.epochNumber);
case 'rollup-block-root-first-empty-tx':
return this.prover.getBlockRootEmptyTxFirstRollupProof(rollup.inputs, signal, provingState.epochNumber);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should've surfaced this during the circuits review (probably should funnel this comment to the circuits agent), but why do we still need the first flavor of block root circuit? Can't we merge it with the others? Isn't the only difference whether it's allowed to be fully empty?

Comment on lines +47 to +53
blockProofs: Promise<{
blockProofOutputs: PublicInputsAndRecursiveProof<
BlockRollupPublicInputs,
typeof NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH
>[];
inboxParityProof: PublicInputsAndRecursiveProof<ParityPublicInputs>;
}>;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should either rename blockProofs, or split this into two different fields, one for blockProofs and one for inboxParityProof.

Comment on lines +379 to +393
private async deriveCheckpointConsumedMessages(previousBlockHeader: BlockHeader, lastBlock: L2Block): Promise<Fr[]> {
const startLeafCount = BigInt(previousBlockHeader.state.l1ToL2MessageTree.nextAvailableLeafIndex);
const endLeafCount = BigInt(lastBlock.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
if (endLeafCount <= startLeafCount) {
return [];
}
const startBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(startLeafCount);
const endBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(endLeafCount);
if (startBucket === undefined || endBucket === undefined) {
throw new Error(
`Cannot resolve consumed messages for checkpoint ending at block ${lastBlock.number} from the Inbox buckets`,
);
}
return this.l1ToL2MessageSource.getL1ToL2MessagesBetweenBuckets(startBucket.seq, endBucket.seq);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Get all message between these indices" (ie between startLeafCount and endLeafCount) should probably be a method of the messageStore, so clients don't need to bother with explicit buckets.

@spalladino
spalladino force-pushed the spl/a-1435-regen-dedup branch from bbfaf57 to c78ad70 Compare July 27, 2026 20:53
Pure library components for the Fast Inbox (AZIP-22), consumed by nothing
yet: new MAX_L1_TO_L2_MSGS_PER_BLOCK / MAX_L1_TO_L2_MSGS_PER_CHECKPOINT
constants, a variable-length frontier-based append to an
AppendOnlyTreeSnapshot at arbitrary (non-aligned) indices, an absorb-only
poseidon message-bundle sponge (L1ToL2MessageSponge), and the rolling
sha256 chain helper (accumulate_inbox_rolling_hash) matching the L1
truncated-to-field policy. No circuit interface changes.
Replaces the per-leaf frontier walk (MaxLeaves x TreeHeight poseidon
hashes) with a level-by-level batched merge: the batch is prepended with
the pending left sibling when it starts as a right child, dangling odd
nodes become the new frontier entries, and remaining nodes are paired
into the next level with lane bounds halving per level. Total cost drops
to ~MaxLeaves + 3 x TreeHeight hashes (1,166 for 1024 leaves in the
height-36 tree vs ~36,900 before). Same signature, semantics, and error
messages. Adds an exhaustive small-tree sweep over every (start, num)
combination and a fail-closed test for appending to a completely full
tree.
spalladino and others added 24 commits July 28, 2026 09:24
…ator message fetch (A-1388)

Drops the no-op first-in-checkpoint padding alias and its stale docs in world-state,
and removes the obsolete non-first-block-empty-bundle transitional test. The node
public-calls simulator no longer fetches next-checkpoint messages via the removed
per-checkpoint API and drops its now-unused l1ToL2MessageSource dependency, simulating
against the fork's current tree (streaming Inbox consumes per block). Rewrites the
THREAT_MODEL inHash/consume passages to the consensus rolling-hash / bucket model.
…ts (A-1388)

The optional bucket-reference tail keeps proposals that omit it round-tripping
cleanly; it does not make the wire byte-identical to the pre-inHash-removal format.
Reword the toBuffer/fromBuffer comments to describe only the tail's unset case.
… tests (A-1388)

The env sweep removed AZTEC_INBOX_LAG from scripts/network-defaults.json but
both deploy-script test setUps still readUint the key, which reverts
(vm.parseJsonUint on a missing path), failing the suites before any test
runs. The deploy configuration no longer consumes the env var.
…proposal formats (A-1388)

The attestation store persists raw BlockProposal and CheckpointAttestation
buffers. Both changed shape in this stack (the checkpoint header lost inHash
and the block-proposal wire format dropped its zeroed inHash), and stored
checkpoint attestations are decoded without a tolerant fallback, so a store
written by a pre-Fast-Inbox node would throw on read. Bump 2 -> 3 to wipe
stale pools, matching the archiver's no-migration bump.
…388)

Removes the dead AZTEC_INBOX_LAG entries from the spartan environment
profiles and the governance-upgrade tutorial, rewrites the validator and
sequencer README sections that still described the legacy inHash flow, and
drops e2e comments that still explained fixture settings in terms of the
deleted inboxLag / L1ToL2MessagesNotReadyError behavior. Versioned docs
snapshots are left untouched.
…(A-1388)

Finding the cut point for an L1-block rollback walked the messages backwards from
the tip, deserializing every message being removed just to learn where the removal
starts. A bucket lives entirely within one L1 block and its snapshot now records
that block plus the bucket's first message index, so scan the bucket snapshots
instead — a few records per L1 block rather than one per message.
…sage leaf index (A-1388)

Blocks consume Inbox messages in order into consecutive L1-to-L2 tree leaves, so a message is
available at a tip exactly when that tip's leaf count has grown past the message's index — one
comparison against data every block header already carries. `isL1ToL2MessageReady` now does that
via `getL1ToL2MessageIndex`, which also drops its stale claim that messages land in the first block
of a checkpoint. The `getL1ToL2MessageCheckpoint` node RPC method existed only to answer this
question and needed a binary search over block records to do it, so delete it: node service,
world-state queries, interface, schema, and its callers. `getL1ToL2MessageIndex` takes its place in
the operator API reference.
…ing public calls (A-1388)

A transaction that consumes an L1-to-L2 message sent moments ago failed public simulation until a
block actually consumed the message, because the simulation fork stops at the tip's message tree.
Predict the bundle the next block would take — the sequencer's own bucket selection, so lag
eligibility and the per-block/per-checkpoint caps match what will be built — and append it to the
fork. The prediction treats the next block as non-final (the censorship cutoff only widens
consumption on a checkpoint's last block, which the node cannot know) and derives its cursor from
the fork's own leaf count, so it can never re-insert messages the fork already holds. Best-effort:
unsynced buckets or a missing header leave the simulation on the bare tip, as before.
…ts generator (A-1434)

Add INBOX_LAG_SECONDS and MAX_L1_TO_L2_MSGS_PER_CHECKPOINT to the Solidity
constants allowlist so the generator emits them into ConstantsGen.sol, matching
the values already present in constants.gen.ts. ProposeLib and its consumption
test now read the generated Constants library instead of the hand-declared
file-scope copies, giving L1 and TS a single source of truth.
…neration (A-1435)

Make the prover-client regenerate_rollup_sample_inputs suite the sole owner of
every block-root, block-merge, checkpoint, tx-merge, and root rollup Prover.toml,
and stop the e2e full.test dump from writing the block-root and checkpoint samples
it overlapped on. The 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.

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. A dedicated three-tx scenario restores rollup-tx-merge
coverage (dropped when orchestrator_single_checkpoint.test.ts was replaced), so
the suite covers every rollup circuit at or above the transaction merge.

Update the regen docs (barretenberg cpp CLAUDE.md, update-prover-toml and
gate-counts skills) to the two-command split and drop the dead
orchestrator_single_checkpoint references.
…en and fix stale regen command patterns (A-1435)

Post-flip a zero-tx non-first block carrying a message bundle is a live block
shape routed through the msgs-only block root, so the regen suite gains a
scenario with a per-block message distribution that dumps
rollup-block-root-msgs-only, and the circuit joins the CI nargo-execute list.
The first regen run creates crates/rollup-block-root-msgs-only/Prover.toml,
which must be committed alongside the other regenerated tomls.

Also fixes the documented e2e regen command: the prover full test lives at
single-node/prover/server/full.test.ts, so the old e2e_prover/full.test jest
pattern matches nothing.
…-1435)

makeCheckpoint concentrates every scenario message into the first block, whose bundle pads to
MAX_L1_TO_L2_MSGS_PER_BLOCK (256), so seeding scenarios with the per-checkpoint cap (1024) threw
on regeneration. Also distribute messages across both blocks of the rollup-block-root scenario so
the committed non-first block-root sample carries a non-empty bundle.
…ader dedupe (A-1373)

Regenerates gas_benchmark.md, gas_benchmark_results.json and gas_report.json at the top of
the stack so the committed numbers reflect the deduped checkpoint-header verification on
submitEpochRootProof, the explicit end inbox rolling-hash check, and the packing of the inbox
consumption counts into the checkpoint log's slot-number slot.

Benchmark averages (no L1-to-L2 messages consumed in this scenario):
- No validators: propose 203,758 -> 199,557; submitEpochRootProof 1,137,731 -> 994,378.
- Validators:    propose 332,170 -> 327,969; submitEpochRootProof 1,719,902 -> 1,575,427.

The gas report, whose Rollup suite does consume messages, moves further on propose:
289,076 -> 266,918 average.
…bridge tutorials (A-1435)

The executable snippets already match the streaming Inbox, but the surrounding prose
still described the checkpoint-era behaviour: a removed MessageSent field list, and a
two-block lag before an L1-to-L2 message could be claimed. A message is now claimable
as soon as an L2 block includes it, which the network allows once the message is at
least 12 seconds old. The block-forcing helpers stay, since a local network only
produces blocks when transactions are submitted.
@spalladino
spalladino force-pushed the spl/a-1435-regen-dedup branch from c78ad70 to 7a0adec Compare July 28, 2026 12:38
@spalladino

Copy link
Copy Markdown
Contributor Author

Superseded by #25039.

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 #25039, 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
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