feat(fast-inbox): streaming L1-to-L2 messages, AZIP-22 (umbrella) - #25041
Open
spalladino wants to merge 29 commits into
Open
feat(fast-inbox): streaming L1-to-L2 messages, AZIP-22 (umbrella)#25041spalladino wants to merge 29 commits into
spalladino wants to merge 29 commits into
Conversation
spalladino
requested review from
a team,
LeilaWang,
Thunkar and
just-mitch
as code owners
July 28, 2026 23:02
spalladino
force-pushed
the
spl/fast-inbox-umbrella
branch
4 times, most recently
from
July 29, 2026 00:43
0393b00 to
2f1ec21
Compare
First PR of the Fast Inbox circuits series (A-1372). Pure library components in `rollup-lib`/`types` plus new constants — **no circuit interface changes**, nothing consumes these yet. They exist so the follow-up PRs (`inboxRollingHash` end to end, per-block bundles) are reviewable as wiring rather than wiring + primitives. ### Components - **`append_leaves_to_snapshot`** (`types/src/merkle_tree/append_only_tree.nr`): variable-length append of up to `MaxLeaves` leaves into a poseidon `AppendOnlyTreeSnapshot` at an arbitrary (non-aligned) index, replacing the alignment assumption of the fixed height-10 subtree insert. Takes a frontier hint validated against the snapshot root (same trust model as sibling-path hints; unpinned hint lanes are provably never read). Implemented as a level-by-level batched merge costing ~`MaxLeaves + 3·TreeHeight` poseidon hashes — 1,166 for a 1024-leaf bundle in the height-36 tree, 398 for 256 — matching the cost model from the design analysis. An exhaustive small-tree sweep tests every `(start, num)` combination against a reference tree, and an equivalence test proves a 1024-leaf append at an aligned index reproduces `insert_subtree_root_to_snapshot` bit for bit (root `0x27b87d4d3b78d1fc49a6cb4d83e0cc81de7f5972cda7fe29a6a82aa2c255cb3d`) — this is what keeps the transitional wiring identical to production today. - **`L1ToL2MessageSponge`** (`rollup-lib/src/abis/l1_to_l2_message_sponge.nr`): absorb-only poseidon2 sponge over message leaves (the `inHashSponge` from AZIP-22 Option 3), modeled on `SpongeBlob`. Compared by state equality, never squeezed on the block path; iv = 0 (unlike `SpongeBlob`, whose iv encodes expected length because its squeeze feeds the blob challenge — this sponge's TS mirror must use iv 0). Length bound is enforced by the underlying `poseidon2_absorb_in_chunks_existing_sponge`. - **`accumulate_inbox_rolling_hash`** (`rollup-lib/src/inbox_rolling_hash.nr`): rolling truncated sha256 chain, each link `h' = sha256ToField(h_32be || leaf_32be)` via the existing `accumulate_sha256` — byte-identical to Solidity `Hash.sol::sha256ToField` and TS `truncateAndPad`. Takes a start, returns an end, no chunk-position assumptions, so it threads sequentially across chunked circuits. - **Constants**: `MAX_L1_TO_L2_MSGS_PER_BLOCK = 1024` (transitional; drops to 256 at the flip) and `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT = 1024` (semantically today's `NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP`, which stays untouched until cleanup). TS constants regenerated; `ConstantsGen.sol` is intentionally unchanged (the Solidity emitter allow-list doesn't include them — L1 constants land with the Inbox L1 work). ### Shared test vectors Generated from an independent sha256 implementation and embedded in the noir tests; the TS mirror and the L1 Foundry tests must pin the same values. `chain(start, leaves)` folds `h' = sha256ToField(h || leaf)`: | Case | Value | | --- | --- | | `chain(0, [11])` | `0x00815fb1e9d2076ae5761439b6144ad11da69eb6c41ab2aca39e770407ad8d12` | | `chain(0, [11, 22, 33])` | `0x0014cae968461979aab6d33266a2310ed234d3f6cf4472737c57551db07bd0da` | | `chain(0, [1..=256])` | `0x00ea95b96f17b75be03525b35a2a1918b42f03ad8c00a437cf641751825f3992` | | `chain(0x2a, [7, 8])` | `0x0054d96b8a074a5030a5838972d0a3c04ba47cf5956348c853e02e9566233f65` | | `chain(0x2a, [7])` (intermediate link) | `0x0032a934005556d1b9d22708666ee8b05f91fafad624dd64a6ea878e048e5438` | ### Testing - 25 new noir tests across both crates (append: empty/single/non-aligned/max/partial/equivalence/exhaustive sweep/failure cases; sponge: split-boundary threading, padding exclusion; rolling hash: reference vectors, segment continuity, padding exclusion). Full suites green: types 373, rollup-lib 381. - `yarn build` green. No compiled circuit artifacts change (library-only code, unused constants). Replaces #24587.
…Hash (A-1373) Stacked on #24587 (A-1372). Part of the AZIP-22 Fast Inbox work. ## What Introduces the `inboxRollingHash` — a truncated-to-field sha256 rolling chain over the L1→L2 message leaves — end to end, carried as a **dual** of the legacy `inHash`. The legacy `inHash` remains authoritative; the rolling hash is computed and threaded everywhere but not yet enforced on L1, so this change is behavior-preserving pre-flip. Each link is `h' = sha256ToField(h_be32 || leaf_be32)` with the top byte of the digest zeroed, genesis value zero. ## Changes **Circuits (noir)** - Parity base computes the rolling chain over its real message leaves (`start_rolling_hash` → `end_rolling_hash`, `num_msgs`), asserting trailing padding lanes are zero. Parity root asserts chunk continuity (`children[i].start == children[i-1].end`) and sums the counts. - Block and checkpoint rollup public inputs carry a `{start, end}` rolling-hash pair, propagated exactly like `in_hash`. Checkpoint merges assert `right.start == left.end` (decision 11 anchoring). - The checkpoint header gains `inbox_rolling_hash` immediately after `in_hash`. - The root rollup public inputs expose the `{previous, end}` pair sourced from the merged checkpoint public inputs, so the epoch's consumed chain segment is passed through to proof verification. **L1 (Solidity)** - `ProposedHeaderLib` serializes the new header field (header 348 → 380 bytes). - `PublicInputArgs` gains `previousInboxRollingHash` / `endInboxRollingHash`; `EpochProofLib` places them at public-input positions 3 and 4 (header hashes, fees, constants and blob inputs shift by two). Both values are deliberately **unvalidated** until the Fast Inbox flip — for now they are pass-through only. **TypeScript** - `updateInboxRollingHash` / `accumulateInboxRollingHash` mirror the circuit chain; `getPreviousCheckpointInboxRollingHash` sources the previous checkpoint's end value (returns zero for checkpoint ≤ 1). The sequencer, validator, and prover populate the header field; the orchestrator threads per-base start hashes; the prover-node publisher fills the two new `PublicInputArgs`. - `RootRollupPublicInputs` gains the pair with matching serialization, conversion, factories and viem types. ## Constants - `CHECKPOINT_HEADER_LENGTH` 13 → 14, `BLOCK_ROLLUP_PUBLIC_INPUTS_LENGTH` 56 → 58, `CHECKPOINT_ROLLUP_PUBLIC_INPUTS_LENGTH` 149 → 151, `ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH` 111 → 113. - Circuit ABIs changed, so the base branch's committed `pinned-build.tar.gz` no longer matches the compiled circuits. It is dropped here rather than carried stale — `bootstrap.sh` recompiles from source whenever the pin is absent — and regenerated once the ABI settles. (#24587 keeps the base pin untouched, so the artifacts stay pinned on the train until this PR.) ## Testing - `yarn build` green; `forge test` green (870 passed); noir rollup-lib root suites green (176 passed). - stdlib serde, prover-node publisher, and the checkpoint-sub-tree / top-tree orchestrator suites green. - Cross-chain `l1_to_l2` e2e confirms the header field round-trips through L1 and the archiver (decoded `inboxRollingHash` = 0 for the genesis checkpoint, as expected). This suite is registered flaky (`.test_patterns.yml`); the repeated-consumption cases exhibit a pre-existing nullifier-sync timing race unrelated to this change. Replaces #24600.
…checkpoint root (A-1374) Stacked on #24600 (A-1373). AZIP-22 Fast Inbox, FI-04. Moves parity verification from the block-root circuits to the **checkpoint root**, and gives every block-root variant a per-block L1→L2 message bundle. This gets the circuit structure right for per-block messages while keeping behavior bit-identical to today — the flip to compact per-block distribution (FI-14) becomes a constant change plus padding removal, not a circuit restructure. - **Block-root variants** take a message bundle (`l1_to_l2_messages`, `num_msgs`, frontier hint) and append it to the L1→L2 tree via `append_leaves_to_snapshot` (the A-1372 gadget), absorbing the same leaves into an `L1ToL2MessageSponge` (Poseidon2) threaded across the checkpoint's blocks. First-block variants lose the parity-root proof. - **`BlockRollupPublicInputs`** drops `in_hash` and the FI-03 `inbox_rolling_hash` pair; gains `is_first_block: bool` and `start_msg_sponge`/`end_msg_sponge`. `is_first_block` takes over `in_hash`'s former structural role (leftmost asserted true at the checkpoint root, every right rollup asserted false at merge) and drives the block-end blob-absorb flag. - **Block merge** propagates `is_first_block` and `start_msg_sponge` from the left, `end_msg_sponge` from the right; `validate_consecutive_block_rollups` asserts `!right.is_first_block` and `right.start_msg_sponge == left.end_msg_sponge`. - **Checkpoint root** verifies the moved parity-root proof, asserts `parity.start_sponge == empty` and `merged.end_msg_sponge == parity.end_sponge` (the "blocks inserted exactly the parity-committed list, in order" check — no leaf arrays cross a circuit boundary), and sources the header's `in_hash = parity.sha_root` and `inbox_rolling_hash = parity.end_rolling_hash`. - **Parity** base absorbs the padded batch into a message sponge; parity root threads it across the four bases alongside the rolling hash. - New `stdlib/src/messaging/l1_to_l2_message_sponge.ts` mirrors the noir sponge (iv-0, absorb-only). Parity/block/checkpoint-root inputs and `conversion/server.ts` mirror the field changes. - Orchestrator DAG: parity proofs move from `block-proving-state.ts` to `checkpoint-proving-state.ts`; the sub-tree enqueues parity once per checkpoint and surfaces it in `SubTreeResult`; block roots no longer gate on parity. Transitional wiring: first block carries the checkpoint's messages padded to 1024 (`num_msgs=1024`), non-first blocks carry empty bundles inheriting the checkpoint sponge. `L1_TO_L2_MESSAGE_SPONGE_LENGTH = 10`; `BLOCK_ROLLUP_PUBLIC_INPUTS_LENGTH` 58 → 76. Circuit ABIs changed, so `pinned-build.tar.gz` is regenerated and committed. - noir `rollup_lib` 386 passed (adds sponge-threading, `is_first_block` structural, and checkpoint-root parity/sponge-mismatch failure cases); transitional equivalence held (`append_matches_subtree_insert_1024`, unchanged checkpoint-header fixtures). - `yarn build`, `yarn lint`/`format`, forge, orchestrator suites (15), and touched stdlib suites (22) all green. - Cross-chain `l1_to_l2` e2e: the real send+consume path (the new per-block bundle circuit path) works; the suite's only two cases are the duplicate re-consumption ones, which hit a pre-existing PXE nullifier-sync issue registered flaky in `.test_patterns.yml`, unrelated to this change. - A codex cross-layer review found no correctness bugs; it confirmed sponge chunking associativity, tree/list pinning, `is_first_block` soundness, header sourcing/continuity, the blob-flag derivation, and TS↔noir serde order. This branch now carries the oldest-epoch-first proving-broker scheduling fix (originally the head commit of #24759 / A-1427), moved down to the base of this segment. On the intermediate stack the single prover agent otherwise starves `PARITY_BASE` behind later-epoch `BLOCK_ROOT` jobs, so `long_proving_time` deterministically times out here; with the commit present it passes (`generates proof over multiple epochs`, verified on a bb-capable box). The move is content-neutral at the stack top — only the commit's position changed. The relocated commit's proving-broker test asserts an older epoch beats a higher-priority proof type from a younger epoch. It needs a proof type that is (a) lower-priority than `PRIVATE_TX_BASE_ROLLUP` and (b) present under the same name across the whole stack, since this commit sits at the base and its content flows up unchanged. No parity name qualifies: it originally used `INBOX_PARITY` (only exists at A-1427 and above), and `PARITY_BASE` only exists at A-1374/A-1375 because A-1427 replaces both `PARITY_BASE`/`PARITY_ROOT` with a single `INBOX_PARITY`. The test now uses `PUBLIC_VM`, which is stable across the entire stack and sits just below `PRIVATE_TX_BASE_ROLLUP` in `PROOF_TYPES_IN_PRIORITY_ORDER` at every branch. Verified with a real `tsc` (not jest) at the owning branch (below the flip), at A-1384 (above the flip), and at the stack top. Replaces #24603.
…locks with messages (A-1375) Stacked on #24603 (A-1374). AZIP-22 Fast Inbox, FI-05. ## What Adds `block_root_msgs_only_rollup` (crate `rollup-block-root-msgs-only`, VK index `BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX`) — a **non-first, transaction-less** block that still inserts a non-empty L1→L2 message bundle. This lets a proposer keep draining the Inbox into a checkpoint when the tx pool is dry. It is completely inert pre-flip: nothing in the node produces this shape until FI-12. ## Circuit Modeled on the first-empty variant but for a mid-checkpoint position (`block_root_msgs_only_rollup.nr`): - asserts `num_msgs > 0` (a message-only block with no messages has no reason to exist); - inherits `start_sponge_blob` and `start_msg_sponge` from the previous block (non-empty), sets `is_first_block = false`, appends the bundle to the L1→L2 tree and absorbs the message sponge, threads the start sponge blob through unchanged (no tx effects); - deliberately cannot be leftmost — the checkpoint root requires the leftmost rollup's `is_first_block` to be true. Composes via a new `new_from_no_rollups_with_start_sponge_blob` (a `new_from_no_rollups` that takes an inherited sponge blob instead of an empty one; the first-empty variant now delegates to it, preserving its empty-sponge start). ## Wiring - `BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX` added to the vk tree and the `ALLOWED_PREVIOUS_VK_INDICES` of the block-merge and both checkpoint-root variants. - New crate + Nargo registration; `pinned-build.tar.gz` regenerated. - TS: proving request type + schema + `ProvingJobResultsMap` entry, the `ServerCircuitProver` interface method and all implementers (bb-prover, `TestCircuitProver`, `MockProver`, `BrokerCircuitProverFacade`), the proving-broker queue map + priority list + job-controller dispatch, and npct artifact/type generation. ## Tests Noir (`rollup_lib`, 386 passed) covers the roadmap done-when — a checkpoint `[first block, msgs-only block, normal block]` — plus the failure cases: `num_msgs == 0` rejected, a msgs-only block cannot be leftmost, and merge continuity catches a bad hinted `start_msg_sponge`. `yarn build`, lint, and the orchestrator + proving-broker suites (123) are green. Cross-chain `l1_to_l2` e2e: only the two pre-registered-flaky duplicate-consume cases fail (a pre-existing PXE nullifier-sync issue, unrelated). A codex review found no soundness or wiring bugs. ## Deferred to FI-12/FI-15 (orchestrator selection + integration test) `block-proving-state.ts` selecting this variant, and an orchestrator integration test driving a mid-checkpoint msgs-only block, are intentionally **not** in this PR. Driving one requires reworking the transitional per-block message *distribution*: today the first block carries the whole padded checkpoint bundle and non-first blocks inherit the full checkpoint sponge and absorb nothing, so a msgs-only block absorbing real messages mid-chain would break both the block-merge `right.start_msg_sponge == left.end_msg_sponge` continuity and the checkpoint-root `merged.end_msg_sponge == parity.end_sponge` check. That distribution rework belongs with the sequencer that actually produces these blocks (FI-12) and its live-path e2e (FI-15). The circuit is fully proven by the noir tests, and the existing `BlockProvingState` guard still rejects non-first zero-tx blocks, so nothing can construct the shape until that work lands. ## Proving-result schema (CI fix) The `ProvingJobResult` Zod discriminated union was missing a member for the new `BLOCK_ROOT_MSGS_ONLY_ROLLUP` request type added here, so once the flip (A-1384) lets the node actually produce a message-only block, decoding its proving result threw `Invalid discriminator value` in `jsonParseWithSchema` and stalled the epoch (surfaced as a cross-chain e2e timeout up-stack). The union member is added alongside the request type in this PR — its natural home, since this is where the type is introduced. A stdlib round-trip regression test (`proving-job-source.test.ts`) serializes a message-only block-root result and asserts it decodes back with `type === BLOCK_ROOT_MSGS_ONLY_ROLLUP`. Replaces #24612.
…nt (A-1427)
Replaces the parity base (×4) + parity root fan-in with **one variable-size `InboxParity<S>` proof per checkpoint**, S ∈ {64, 256, 1024} (one VK per size; the prover proves the smallest rung ≥ the checkpoint's message count). The parity-root circuit is deleted; the checkpoint root keeps a single parity verification, now accepting the 3-rung VK ladder. Net **+1 VK**.
Stacked on #24612 (`spl/a-1375-msgs-only-block`).
- **`in_hash` unconstrained pass-through.** The circuit no longer builds the sha256 frontier tree. `in_hash` stays in the parity public inputs as an unconstrained hint — the orchestrator supplies the true frontier root via `computeInHashFromL1ToL2Messages`, the circuit echoes it out, and the checkpoint root copies it into the header. L1's `require(header.inHash == inbox.consume())` still passes, so **no L1 changes** (`ConstantsGen.sol` regenerates byte-identical to the base).
- **Sponge real-count decoupling.** Block roots absorb the message sponge at the real count while the L1-to-L2 tree insert stays a padded fixed subtree, via a new `L1ToL2MessageBundle { messages, num_msgs, num_real_msgs }` struct so the real count can be dropped later with minimal change. `num_msgs` is not otherwise pinned in-circuit — documented as a dev-mode gap backstopped by the L1 pending-chain header hash, parallel to `in_hash`.
Threads the single sized proof through the orchestrator, bb-prover, proving broker, and TS bindings — collapsing `getBaseParityProof`/`getRootParityProof` into `getInboxParityProof` and `PARITY_BASE`/`PARITY_ROOT` into one `INBOX_PARITY` request type. The UltraHonk parity benchmark and bb.js debug test move to `InboxParity256`.
- `rollup_lib` nargo suite: 387/387 passing.
- Reviewed by a second model (Fable) and codex sol; their build-break and consistency findings are folded in.
- Full `yarn build`, VK/artifact regen, real proofs, and e2e run in CI (blocked locally by a `bb write_vk` memory-arena OOM on the unchanged `rollup_root` circuit).
`inbox_rolling_hash.nr`'s doc comment claims the sha256 chain "matches the L1 Inbox," but `Inbox.sol` accumulates `bytes16(keccak256(...))`. Harmless today (the rolling hash isn't validated on L1), but the comment should be corrected on the base branch.
The oldest-epoch-first proving-broker scheduling fix that previously headed this branch has been moved down to #24603 (A-1374), where it is required to keep `long_proving_time` green on the intermediate stack. The stack top is unchanged — only the commit's position changed.
Replaces #24759.
…st blocks (A-1432) ## What Makes every block's `BlockConstantData.l1_to_l2_tree_snapshot` the **post-bundle** snapshot for that block — first block or not — so a public/AVM tx in block N can read the L1-to-L2 messages block N inserts (same-block consumption), instead of only from block N+1. Previously the non-first tx-carrying variants (`block_root`, `block_root_single_tx`) read their start snapshot from `constants.l1_to_l2_tree_snapshot` and never asserted the post-bundle root, giving next-block visibility for mid-checkpoint insertions. Now they: - take the start L1-to-L2 snapshot as a **witness input** (`previous_l1_to_l2`), pinned by block-merge continuity (`right.start_state == left.end_state`) to the previous block's end state — the checkpoint root forces the leftmost block to be a first-block variant, so every non-first block has a left neighbour pinning its start; - append their bundle and assert `validate_l1_to_l2_tree_snapshot_in_constants(constants, new_l1_to_l2)`, exactly like the first-block variants. The tx-less variants (`block_root_empty_tx_first`, `block_root_msgs_only`) carry no tx constants to read against and already witness their start snapshot, so they are unchanged. ## Soundness The witnessed start snapshot cannot be forged: block-merge / checkpoint-root continuity asserts `right.start_state == left.end_state`, and the checkpoint root asserts the leftmost block is a first-block variant (whose start is pinned to the previous checkpoint). The start is therefore anchored, and the new assert pins the tx-constants snapshot to the computed post-bundle root. The start is **not** derived from "constants minus the bundle" (not expressible). ## Bit-identical pre-flip (public inputs) Non-first bundles are empty today, so `new_l1_to_l2 == start == today's constants value`; the new assert passes with unchanged values and the block-root **public inputs / state are unchanged**. The circuit constraints, private-input ABI, proof, and VK do change (hence the regen below). This is a restructure-now / flip-minimal change. ## Flip follow-on (not in this PR) The recursive pinning of the witnessed `previous_l1_to_l2` is already exercised by `block_merge::tests::consecutive_block_rollups_tests::non_consecutive_l1_to_l2_message_tree_snapshots` (a right block whose `start_state.l1_to_l2_message_tree` — i.e. this witness — doesn't match the left block's end is rejected on continuity). One flip requirement this change surfaces: today the prover gives every non-first block the checkpoint's post-first-block snapshot as its `previousL1ToL2` (`block-proving-state.ts`), which is correct only while non-first bundles are empty. Once non-first blocks insert their own bundles (A-1384), the prover must instead feed block N+1 the end snapshot of block N plus the matching frontier hint. Flagged for the flip plan (A-1384). ## Tests Adds negative nargo tests: a non-first block (two-tx and single-tx) whose `constants.l1_to_l2_tree_snapshot` differs from its post-bundle root must fail. Red/green verified locally — with the new asserts removed the four negative tests report "Test passed when it should have failed"; restored, they pass. `rollup_lib` block_root suite green (63 tests), block_merge (45), checkpoint_root structure tests (17). ## TS wiring Mirrors the new `previous_l1_to_l2` field through `stdlib` (`BlockRootRollupPrivateInputs` / `BlockRootSingleTxRollupPrivateInputs` serialization + factory), the Noir ABI conversion (`server.ts`), and the prover-client block-root input builders (`block-proving-state.ts`), which pass the block's `lastL1ToL2MessageTreeSnapshot` (for non-first blocks, the checkpoint's post-first-block snapshot). `stdlib` builds and its serialization test passes. ## Artifact regen (rides CI / mainframe) This changes the `block_root` and `block_root_single_tx` circuit ABIs, so it invalidates their VKs, the checkpoint-root/block-root `Prover.toml` sample inputs, and the generated `noir-protocol-circuits-types/src/types/index.ts`. These are **not** regenerated locally (`bb write_vk` OOMs on large circuits on dev boxes; the generated `index.ts` also depends on recompiled circuit artifacts). They ride the seeded-S3-cache mainframe/CI flow, consistent with the rest of the Fast Inbox stack. ## Stack Stacked on `spl/a-1427-inbox-parity`; the L1 PRs (#24771, #24773) are rebased on top of this. Part of the Fast Inbox stack (umbrella #24774). Fixes A-1432 Replaces #24781.
…r trees (A-1377) AZIP-22 Fast Inbox, FI-07. First L1 PR of the series; a follow-up stacks the `propose`-side streaming validation on top of this branch. Stacked on #24781 (`spl/a-1432-same-block-msgs`), the tip of the Fast Inbox circuits stack (#24587 → #24600 → #24603 → #24612 → #24759 → #24781). That stack already carries the small L1 changes this feature builds on — the header's `inboxRollingHash` field, the two epoch-proof public inputs, and the regenerated checkpoint fixtures — so this PR adds only the bucket machinery on top. ## What `sendL2Message` additionally maintains the **consensus rolling hash** — the truncated-per-link sha256 chain the circuits recompute (`h' = sha256ToField(h || leaf)`, genesis zero; new `Hash.accumulateInboxRollingHash`) — and snapshots it into a fixed-size ring of **buckets**. Frontier trees, `LAG`, the legacy `consume()` flow, and the legacy `bytes16` keccak rolling hash are untouched (the legacy hash now carries a TODO to remove it at cleanup); buckets are completely inert for the legacy flow. Per the pinned design decisions: - **Bucket struct** `{rollingHash | totalMsgCount: uint64, timestamp: uint64, msgCount: uint32}` packs into two slots; `totalMsgCount` is the Inbox-wide cumulative count (what the censorship cap-escape check reads), `timestamp` is the bucket's opening L1 block timestamp (recency checks are done in seconds), `msgCount` sits in slot 2's spare bits so the per-bucket cap costs no extra storage access. - **Ring** indexed by dense bucket sequence number (`seq % BUCKET_RING_SIZE`), 1024 entries in production, sized as an immutable constructor parameter. `getBucket(seq)` reverts with `Inbox__BucketOutOfWindow` outside the live window (`seq <= current < seq + ringSize`, same idiom as the Rollup's roundabout). Overwrite protection for unconsumed buckets is deliberately **not** enforced yet (happy path assumes the ring never wraps into live data). - **Bucket boundaries**: a bucket only holds messages from a single L1 block; the first message of a new block opens the next bucket, and the 257th message within one block (`MAX_MSGS_PER_BUCKET = 256`, the post-flip per-L2-block insertion cap) **rolls over** into the next bucket with the same timestamp. New buckets inherit the rolling hash and cumulative count, so the chain is continuous across buckets. - **Genesis base case**: bucket 0 is `{0, 0, deployTime}` and never absorbs (even for a message sent in the deployment block), so a checkpoint consuming nothing against an empty Inbox can always reference a bucket matching its parent's chain position — no special case at `propose`. - **Event**: `MessageSent` gains `bytes32 inboxRollingHash` and `uint256 bucketSeq` after the legacy args. ## Archiver / TS The generated `InboxAbi` picks up the new event signature at build time; `MessageSentArgs` and the decode in `ethereum/src/contracts/inbox.ts` carry the two new fields, and the archiver keeps relying only on the legacy args for now (`InboxMessage` unchanged). The archiver test fake fills the new fields with placeholders. Nothing on this branch consumes the new values yet — the node-side cross-check of its TS rolling hash against L1-emitted values comes with the archiver bucket-sync work. ## Testing - New `InboxBuckets.t.sol` covers the roadmap done-when: accumulation within a block, snapshot freezing at block boundaries with chain continuity into the next bucket, 256-cap rollover within one block, ring wraparound + out-of-window reverts on a small ring, the genesis bucket (including a first message in the deploy block), and event contents. - **Shared rolling-hash test vectors** pinned in #24587's noir tests (`chain(0,[11])`, `chain(0,[11,22,33])`, `chain(0,[1..=256])`, `chain(0x2a,[7])`, `chain(0x2a,[7,8])`) are asserted against the L1 implementation, so L1, TS, and the circuits provably compute the same chain. - `forge test` at the current stack top: 890 passed, 0 failed, 3 skipped (the `testGetEpochProofPublicInputsVerifiesHeaders` failure previously inherited from the base has been fixed by the rebased base branches). The 13 `InboxBuckets.t.sol` tests all pass (9 behavioral + 4 gas measurements). Existing `MessageSent` expectations in Inbox/TokenPortal/FeeJuicePortal tests updated for the extended event. - TS: the regenerated `@aztec/l1-artifacts` `InboxAbi` picks up the two new event fields, and `@aztec/ethereum` typechecks clean against it. `@aztec/archiver`'s full typecheck is blocked only by stale Noir-generated artifacts in a transitive dependency (base-branch circuit changes that require the Noir toolchain, not run in this workspace), so it runs in CI; the archiver change is decode-only and mechanical. ## Gas (`sendL2Message`) Four Forge gas measurements in `InboxBuckets.t.sol` cover the bucket write paths (inline `gasleft()` deltas, matching the `RollupGetters.t.sol` convention). These feed the capacity analysis (max messages per L1 block from gas): | Scenario | Gas | |---|---| | Absorb into an already-open bucket (common per-message case) | 34,468 | | First message of a new L1 block (opens a bucket via timestamp) | 78,958 | | Rollover opening mid-block (256-cap reached, same timestamp) | 55,593 | | First-ever message (cold state struct + bucket 1) | 128,118 | Caveat for downstream use: these are **warm execution gas including the CALL overhead** from the test harness. They exclude the 21k intrinsic tx cost, calldata gas, and the cold-access surcharge a standalone EOA transaction pays on its first touch of each slot — the capacity analysis must add those separately. The first-ever figure is the cold-storage case for the bucket/state slots, **not** the global worst-case insert: later frontier-tree leaf indices with more levels to hash can cost more. Also documents the timestamp-key assumption in `_absorbIntoBucket` (post-merge Ethereum increases `block.timestamp` strictly per block; anvil manual-mining can collapse two blocks into one bucket, which is harmless because the consumption cutoff is timestamp-based). ## Ring-size floor Raises the constructor guard from `_bucketRingSize > 1` to a floor of **512** (`MIN_BUCKET_RING_SIZE`); production stays at 1024. Rationale: the ring must cover the longest stall the chain recovers from on its own — the prune-and-repropose window of 64 checkpoints (2 epochs = 384 L1 blocks) at the natural one-bucket-per-L1-block cadence, so buckets it re-consumes after a prune are not overwritten first — 384 rounded up to the next power of two, kept at or below the production ring. `testRingWraparound` now exercises a real wraparound against a 512-slot ring, and a new `testConstructorRevertsBelowRingFloor` asserts that constructing below the floor reverts. The full capacity/liveness analysis behind this number feeds the AZIP-22 review. Replaces #24771.
Stacked on #24771 (A-1377). AZIP-22 Fast Inbox, FI-08. ## What Implements the AZIP's censorship assert as `ProposeLib.validateInboxConsumption` — a new library function that is **not yet called**: today's `inbox.consume()` / `inHash` check in `propose` is untouched, so this is dead code until the flip wires it in. Given the checkpoint header's `inboxRollingHash`, an unsigned calldata bucket hint, the proposed slot, and the parent checkpoint's cumulative consumed total (which the flip will source from the temp checkpoint log), the function enforces: 1. **End anchoring**: the header's rolling hash must equal the snapshot in `inbox.getBucket(hint)` (`Rollup__InvalidInboxRollingHash`). The hint is a lookup aid only — a wrong hint reverts, it cannot change what gets accepted — and `getBucket` itself rejects hints beyond the current bucket or already overwritten in the ring. A checkpoint consuming nothing references the same bucket as its parent (the genesis bucket for the first checkpoint), so there is no base case. 2. **Mandatory consumption**: the first unconsumed bucket (`hint + 1`) must be absent, **past the cutoff**, or **cap-escaped** (`Rollup__UnconsumedInboxMessages`). The cutoff is the build-frame start minus `INBOX_LAG_SECONDS`: a checkpoint proposed in slot S is built during slot S-1 (proposer pipelining), and validators are not required to act on buckets younger than one L1 slot at build start, so `cutoff = toTimestamp(S-1) - 12`. Cap escape allows stopping when consuming through the next bucket would exceed `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT` (1024) messages since the parent's total. Both comparisons are exact-boundary tested. The function performs **no Inbox write**. It is a pure `view` that **returns** the consumed cumulative total (`bucket.totalMsgCount`). The flip (FI-14) stores that consumed position as part of the per-checkpoint temp-log record, extended to `{inboxRollingHash, inboxMsgTotal, inboxConsumedBucket}` and written by `propose`; that record is the authoritative consumed position and is prune-consistent, since temp logs rewind with the pending chain. Two new checks guard the returned value: consumption must **move forward** (`Rollup__InboxConsumptionBehindParent`, equal allowed — a proposal cannot consume behind its parent, and the check precedes the delta subtractions so a backwards proposal reverts descriptively rather than underflowing), and the consumed delta cannot exceed `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT` in one checkpoint (`Rollup__TooManyInboxMessagesConsumed`). The Inbox-side proven-consumed cache that ring overwrite protection needs moved to **FI-20**, anchored to the proven tip rather than the pending chain (decided 2026-07-17): a pointer advanced with the pending chain is not prune-safe, since after a prune it would sit ahead of what the replacement chain consumed. `INBOX_LAG_SECONDS = 12` and `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT = 1024` are defined at file level in `ProposeLib` for now; they mirror the protocol constants and should move into the generated `Constants` library once the Solidity emitter allow-list includes them. ## Testing New `ProposeInboxConsumption.t.sol` (15 tests) drives the library function through a harness that owns the Inbox and TimeLib storage, covering the roadmap done-when plus boundaries: - **empty Inbox** (hint 0 / hash 0 passes and returns 0; non-zero hash rejected), - **exact-cutoff bucket** (timestamp == cutoff must be consumed; cutoff + 1 need not), - **cap escape** (1025 messages spill into buckets of 256+1: stopping at bucket 4 = exactly 1024 escapes and returns the full cap; stopping at bucket 3 = 1024-through-next does not; parent total shifts the arithmetic and kills the escape), - **cap upper bound** (consuming more than 1024 in one checkpoint from a fresh parent reverts `Rollup__TooManyInboxMessagesConsumed`), - **moves forward** (a proposal whose referenced bucket total sits behind the parent reverts `Rollup__InboxConsumptionBehindParent`; an equal reference consumes nothing and returns the unchanged total), - **stale hash** (previous bucket's hash against the current bucket) and **unknown hash** rejection, plus out-of-window hints (future bucket, ring-overwritten bucket), - **return value**: the successful paths assert the consumed cumulative total the flip will record (0, 3, cap). The three former consumed-pointer tests are removed along with the pointer. Full `forge test` at the current rebased tip: 890 passed, 0 failed, 3 skipped (the previously noted `testGetEpochProofPublicInputsVerifiesHeaders` failure inherited from the base has since been fixed). Replaces #24773.
spalladino
force-pushed
the
spl/fast-inbox-umbrella
branch
2 times, most recently
from
July 29, 2026 23:17
f26c162 to
81b45b3
Compare
…nputs (A-1538) The `InboxParity` circuit took a `start_sponge` seed and echoed it back in its public inputs, but the seed was structurally always the empty sponge: the L1-to-L2 message sponge resets per checkpoint and there is exactly one `InboxParity` proof per checkpoint, so no caller could ever pass anything else. The single production construction (`CheckpointProvingState.getInboxParityInputs`) passed `L1ToL2MessageSponge.empty()` unconditionally. - `InboxParity` now seeds its message sponge from the empty sponge in-circuit instead of witnessing it, and `ParityPublicInputs` no longer carries `start_sponge`. - The checkpoint root's "inbox parity message sponge must start empty" assert goes away with the field. The invariant it protected is now structural: the verified `InboxParity` proof can only have started from the empty sponge. The equality between the parity end sponge and the blocks' accumulated sponge is unchanged. - Drops the field from the stdlib types, the noir conversion mappers, the test factories, and the parity test fixtures. The parity unit test that seeded a non-empty sponge to prove pass-through loses its subject and is deleted; the rolling-hash threading it also covered is already covered by `accumulate_inbox_rolling_hash`'s own tests (`non_zero_start_matches_reference`, `padding_lanes_are_not_absorbed`). This changes a public-input layout, so the VK tree root moves; the rollup sample inputs are regenerated in a follow-up commit batched with the block-root variant merge.
…ere, merge block-root variants (A-1539) Replaces the "a fully-empty block may only be the first block of a checkpoint" rule with an explicit cap on how many blocks a checkpoint may contain, asserted in the checkpoint root. Empty (0-tx, 0-msg) blocks become legal at any position, the six block-root variants collapse to three, and the `is_first_block` public input goes away. Why: there are no per-block rewards to abuse — L1 pays per proven checkpoint and per mana, never per block — so the emptiness rule was only weak anti-griefing. Without an explicit cap a proposer could already stuff a checkpoint with well over a thousand cheap blocks, and the cap bounds the epoch prover's worst case identically for empty and full blocks. Once it exists the emptiness rule buys nothing, and empty blocks let the sequencer keep a regular block cadence with no "nothing to include" special case. Circuits: - `MAX_BLOCKS_PER_CHECKPOINT` (72, one block per second over the production slot) is added to `constants.nr` and emitted to `constants.gen.ts` by the generator, so the node and the circuits share one value. It is not consumed by Solidity, so `ConstantsGen.sol` is unchanged: `ProposedHeader` carries no block count, so L1 cannot check the cap at propose time and validators must reject over-cap proposals before attesting. - `CheckpointRootInputsValidator` sums `num_blocks()` over its child rollups and asserts the total is within the cap. Both checkpoint root variants share the validator, so both get it. - The six block roots merge to three, keeping the transaction-count axis (0 / 1 / 2 children) because that is what drives proving time: `block_root`, `block_root_single_tx` and `block_root_no_txs` (the former msgs-only variant, renamed and now also usable as the first block of a checkpoint). The first/non-first axis disappears: every variant takes the start message sponge — and, for the tx-less variant, the start sponge blob — as inputs, which the checkpoint root pins to their initial values for the leftmost block and the block merge pins to the previous block's end values otherwise. The msgs-only variant's `num_msgs != 0` assert is dropped. - `is_first_block` is removed from `BlockRollupPublicInputs`, its propagation in `merge_block_rollups`, the right-child assert in `validate_consecutive_block_rollups` and the leftmost assert in the checkpoint root's inputs validator. The block-end blob absorb still needs to know whether a block is the checkpoint's first (the l1-to-l2 tree root is absorbed once per checkpoint), and the composer now derives that from `start_sponge_blob.num_absorbed_fields == 0` rather than from a witnessed flag. Every block absorbs its block-end fields, so only the leftmost block can start from an uninitialized sponge blob, and `start_sponge_blob` is itself pinned by the checkpoint root and the block merge — so the derivation cannot be steered by the prover, while a witnessed flag could have been. - The checkpoint root's four start-value asserts are now the sole anchoring of a checkpoint to its start; a comment on them says so, so a later refactor does not drop one assuming a flag still marks the first block. - Six block-root VK indices become three; the allowed-index lists in the block merge and both checkpoint root variants shrink accordingly. The block merge no longer needs distinct left/right allowed sets, and the single-block checkpoint root's footnote about the msgs-only entry being unreachable goes away with the entry itself. - Deletes the `rollup-block-root-first`, `-first-single-tx`, `-first-empty-tx` crates and the two `-first*-simulated` crates; renames `rollup-block-root-msgs-only` to `rollup-block-root-no-txs`. TypeScript: the same three-variant shape in `stdlib` (proving request types, private-input classes, prover interface), the circuit bindings and artifact lists, and the orchestrator, which now selects the block root by transaction count alone and passes the start sponges the position implies. Tests: checkpoint-root tests for exactly-at-cap and cap+1; a fully empty non-first block and a tx-less leftmost block in the block-root tests, plus a check that only the first block absorbs the extra blob field; a block-merge test that a mid-checkpoint block claiming an initialized start sponge blob fails continuity.
Both preceding commits change public-input layouts (`ParityPublicInputs` loses `start_sponge`, `BlockRollupPublicInputs` loses `is_first_block`), and the block-root VK indices move, so the committed rollup sample inputs carry a stale ABI and a stale `vk_tree_root`. 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. Adds the `rollup-block-root-no-txs` sample, which the merged tx-less block root needs now that it is in the `nargo execute` list, and drops nothing else: the samples for the deleted first-block variants went with their crates. Verified with `nargo execute` on all eight regenerated crates (block root, block root single tx, block root no txs, block merge, both checkpoint roots, checkpoint merge, root): every one solves its witness. VKs, the VK tree, the generated Noir ABI types and the Solidity verifier are generated-and-gitignored, so they are not committed here; CI rebuilds them.
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
force-pushed
the
spl/fast-inbox-umbrella
branch
from
July 30, 2026 05:06
81b45b3 to
e5e1691
Compare
…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.
spalladino
force-pushed
the
spl/fast-inbox-umbrella
branch
from
July 30, 2026 06:26
e5e1691 to
259c70f
Compare
…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.
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.
…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. - 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). - 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. - 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. 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. 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.
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`. - **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. - **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. 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. 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. 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. - 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.
…ts generator (A-1434) Moves `INBOX_LAG_SECONDS` (12) and `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT` (1024) out of the hand-declared file-scope constants in `ProposeLib.sol` and into the generated `Constants` library, giving L1 Solidity and the TS node a single source of truth (A-1434, Fast Inbox cleanup). Both values already lived in `constants.nr` (the Noir source of truth) and were already emitted to `constants.gen.ts`; the only gap was the Solidity side, which gates emission behind an allowlist. - Add both names to the `SOLIDITY_CONSTANTS` allowlist in `constants/src/scripts/constants.in.ts`, so `yarn remake-constants` emits them into `ConstantsGen.sol`. `constants.gen.ts` is unchanged (already present). - Regenerate `ConstantsGen.sol` (generator output; not hand-edited). - `ProposeLib.sol` imports `Constants` and references the generated values; the hand-declared file-scope copies are deleted. - Update `ProposeInboxConsumption.t.sol` to import the constants from `ConstantsGen.sol` instead of `ProposeLib.sol`. TS node code that validates inbox consumption (`stdlib` cutoff predicate, `validator-client` streaming checks, `sequencer-client` bucket selector) already reads these from `@aztec/constants`, so no TS source change is needed. Testing: - `forge build` and `forge test test/rollup/ProposeInboxConsumption.t.sol` (15 tests) pass. - The three TS regression suites pass: `stdlib` inbox_consumption (10), `validator-client` streaming_inbox_checks (14), `sequencer-client` inbox_bucket_selector (9). - `@aztec/constants`, `@aztec/l1-artifacts`, and `@aztec/ethereum` build. Part of the Fast Inbox stack, on top of #24784..#24793. Replaces #24794.
…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.
…sites (A-1387) The circuits cleanup dropped an argument from `fromMessages`, leaving three call sites hand-wrapped at the old width. All three now fit on one line at the repo's 120-column limit, so `yarn format --check` fails on them in CI. Prettier output only, no behaviour change.
spalladino
force-pushed
the
spl/fast-inbox-umbrella
branch
from
July 30, 2026 07:22
259c70f to
7c2c127
Compare
Every Inbox message leaf advanced the consensus rolling hash as `h' = sha256ToField(h || leaf)` over the two 32-byte
big-endian values. That 64-byte preimage is byte-identical to the one the L2-to-L1 `out_hash` merkle node hash absorbs,
so nothing distinguished a chain link from a merkle node. Each link now absorbs a domain separator first:
h' = sha256ToField(DOM_SEP__INBOX_ROLLING_HASH || h || leaf)
The tag is a 4-byte big-endian u32 written as a prefix, not a padded field element, so the preimage grows from 64 to 68
bytes and stays inside two sha256 compression blocks in-circuit. On L1 the precompile moves up one word tier
(60 + 12*ceil(len/32), 84 -> 96 gas per link); the measured end-to-end `sendL2Message` delta is +36-38 gas including
the extra `abi.encodePacked` memory word (absorb into an open bucket: 8369 -> 8405). The genesis rolling hash is
unchanged at zero, so `Inbox.sol` needs no migration. `out_hash` is deliberately left untagged and is not touched here.
- `DOM_SEP__INBOX_ROLLING_HASH` joins the domain-separator registry in `constants.nr`, derived and collision-checked
like its siblings in `constants_tests.nr` (`hash_to_u32("az_dom_sep", "inbox_rolling_hash")` = 3737216265).
- `generateSolidityConstants` now emits domain separators alongside plain constants, mirroring the C++, PIL and Rust
generators, so the tag can reach `ConstantsGen.sol` through the `solidity.json` allowlist rather than by hand.
- `Hash.accumulateInboxRollingHash` prepends `uint32(Constants.DOM_SEP__INBOX_ROLLING_HASH)`.
- `types::hash` gains `accumulate_sha256_with_separator`, the tagged sibling of `accumulate_sha256` (this is the
variant the `accumulate_sha256` TODO asked for); `accumulate_inbox_rolling_hash` uses it.
- The TS mirror in `stdlib` prepends the same 4-byte tag from the generated `DomainSeparator` enum.
The pinned cross-language test vectors in the noir helper, the `stdlib` suite and `InboxBuckets.t.sol` were recomputed
from an independent node `crypto.createHash` reference over the exact 68-byte preimages, and the nine rollup
`Prover.toml` sample inputs that carry rolling-hash values were regenerated with
`AZTEC_GENERATE_TEST_DATA=1 yarn workspace @aztec/prover-client test regenerate_rollup_sample_inputs`.
Testing:
- `nargo test --package rollup_lib`: 385/385 pass; `--package types`: 413/413 pass.
- `nargo execute` passes for all 16 crates with committed sample inputs.
- `forge test` (full l1-contracts suite): 889 passed, 0 failed, 3 skipped.
- `@aztec/stdlib` inbox_rolling_hash (6) and `@aztec/archiver` message_store (38) pass; constants-codegen node tests
(20) pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Umbrella / consolidated PR for the whole Fast Inbox (AZIP-22) stack.
This PR spans the entire 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. The four part PRs below are the review surface; this one exists to consolidate CI and provide the full-stack diff.Structure
The stack was previously 20 per-issue PRs plus 2 umbrellas. It is now 4 area PRs plus this umbrella, each area PR holding one squashed commit per issue with the original PR description preserved in the commit body, plus follow-up commits at each area's end.
What the stack does
Fast Inbox replaces the per-checkpoint frontier-tree L1-to-L2 message path with a streaming one:
Inbox.sendL2Messagefolds each message into a rolling sha256 hash and snapshots{rollingHash, cumulativeTotal, key}into a per-L1-block bucket in a ring. A proposer names a bucket;proposevalidates the consumption against the previous checkpoint's record; the circuits absorb each block's own message bundle and prove one variable-sizeInboxParity<S>per checkpoint. The result is message latency measured in a block or two rather than a full checkpoint.rollup-lib;inboxRollingHashthreaded end-to-end; per-block bundles with parity at the checkpoint root; a msgs-only block-root circuit for no-tx blocks carrying messages; one variable-sizeInboxParity<S>per checkpoint replacing the parity base ×4 + root fan-in; same-block consumption for non-first blocks.Inboxwith rollover-on-overflow and a 512 ring floor; streaming propose-time consumption validation; the censorship cap-escape branch.Rollup.proposecuts over to streaming validation, the per-block cap drops 1024 → 256, every block carries its own L1-to-L2 root in the blob, and thestreamingInboxflag is removed.consume(),LAG,AZTEC_INBOX_LAG, the checkpoint header'sinHash, the 128-bit keccak rolling hash, and the node's legacy per-checkpoint paths are all deleted.sha256ToField(DOM_SEP__INBOX_ROLLING_HASH || h || leaf)), so a chain link can no longer be reinterpreted as an untagged two-field sha256 hash such as anout_hashmerkle node.Rebase notes
Rebased onto
merge-train/spartanat63aa610685, which includes #25007 (split noir-projects intofnd/andlabs/) — every circuit path moved tonoir-projects/fnd/noir-protocol-circuits/. Conflicts of substance, resolved against the new base:ProvingBroker.#getProvingJob— this stack's oldest-epoch-first selection across queues (soInboxParityis not starved by sustained block production) now runs over the base's claim-then-read-inputs-from-database protocol (feat(prover-client): stop duplicating broker job inputs/results in memory (A-1215) #24990, A-1215). The selection re-runs if the claimed winner has no inputs; losers return to their queues while the claimed job stays popped, so it cannot be re-selected.CheckpointProver— kept the base's removal of the in-memorytxscache (feat(prover-node): stop caching checkpoint txs; re-fetch from the pool for failure upload (A-1216) #24983, A-1216) alongside this stack'sCheckpointSubTreeProofsresult type.This stack deletes
pinned-build.tar.gz, so it builds circuits from source and runs gates the pinned path skips (nargo fmt --check, reset-cost staleness, full VK generation).Merge notes
inHash; the block-proposal p2p format drops its zeroedinHash; the per-block blob encoding always carries the L1-to-L2 root; the per-block message cap is 256; the rolling-hash link is domain-tagged.ARCHIVER_DB_VERSIONand the p2p attestation store are bumped.streaming_inbox.test.tstests 1–2 (mid-checkpoint inclusion, latency bound) flake early, add tighterror_regexentries to.test_patterns.ymlfor the specific assertion rather than broad skips. None are pre-added.sendL2Messagemean 53,020 → 44,796 (−15.5%),Inboxbytecode −19%;propose+~4% frombucketHintcalldata and the consumption validation. The domain tag adds +36–38 gas persendL2Messageon top (one extra sha256 precompile word plus encoding). Known quirk for future regens: threeRollup.t.soltests fail only underFORGE_GAS_REPORT=true, a Foundryvm.expectRevertcall-depth interaction with theIInbox.getBucket()staticcall invalidateInboxConsumption; invisible to normal CI.Branch note
This umbrella lives on its own branch (
spl/fast-inbox-umbrella), kept at the same commit as#25067's
spl/fast-inbox-4-domain-sep. They are deliberately not the same branch: CI3 derives itsspot-fleet name from the branch (
aztec-packages_<branch>_amd64_x-fast), so two PRs sharing a headbranch make each run terminate the other's build instance, and both fail with
SSM ... statusDetails=Undeliverable. Keep the two branches in sync when force-pushing.