Repository navigation
feat(fast-inbox): same-block L1-to-L2 message consumption for non-first blocks (A-1432) - #24781
Closed
spalladino wants to merge 5 commits into
Closed
spalladino wants to merge 5 commits into
spalladino wants to merge 5 commits into
Conversation
This was referenced Jul 17, 2026
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
from
July 19, 2026 14:05
42da857 to
f525ade
Compare
spalladino
force-pushed
the
spl/a-1427-inbox-parity
branch
from
July 19, 2026 17:57
00c602f to
f6f7fa3
Compare
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
from
July 19, 2026 17:57
f525ade to
317c21d
Compare
spalladino
force-pushed
the
spl/a-1427-inbox-parity
branch
from
July 19, 2026 20:48
f6f7fa3 to
c211ed4
Compare
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
3 times, most recently
from
July 20, 2026 17:28
de68191 to
f3c230d
Compare
spalladino
force-pushed
the
spl/a-1427-inbox-parity
branch
from
July 20, 2026 17:28
1a60442 to
6ad8216
Compare
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
from
July 20, 2026 21:21
f3c230d to
f93449b
Compare
spalladino
force-pushed
the
spl/a-1427-inbox-parity
branch
from
July 20, 2026 21:21
6ad8216 to
20b46ed
Compare
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
from
July 21, 2026 02:58
f93449b to
ce32e51
Compare
spalladino
force-pushed
the
spl/a-1427-inbox-parity
branch
2 times, most recently
from
July 21, 2026 03:39
c88bdaa to
64aef28
Compare
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
2 times, most recently
from
July 27, 2026 20:53
54342eb to
f3e5328
Compare
…st blocks (A-1432) The non-first tx-carrying block-root variants (block_root, block_root_single_tx) now take their start L1-to-L2 tree snapshot as a witness input, pinned by block-merge continuity (right.start_state == left.end_state) to the previous block's end state, and assert the tx constants carry their own post-bundle root via validate_l1_to_l2_tree_snapshot_in_constants, exactly like the first-block variants. This lets a public/AVM tx in block N read the messages block N inserts (same-block consumption) instead of only from block N+1. Previously the non-first variants read their start snapshot from constants.l1_to_l2_tree_snapshot and never asserted the post-bundle root, so mid-checkpoint insertions were next-block-visible. Bit-identical pre-flip: non-first bundles are empty, so post-bundle == start == today's constants value, and the new assert passes with unchanged values. The tx-less variants (empty-first, msgs-only) carry no tx constants and already witness their start snapshot, so they are unchanged. Adds negative nargo tests (a non-first block whose constants snapshot differs from its post-bundle root must fail) and mirrors the new witness field through stdlib serialization, the Noir ABI conversion, and the prover-client block-root input builders. VK/artifact regen, checkpoint-root/block-root Prover.toml sample-input regen, and the generated noir-protocol-circuits-types (index.ts) regen ride the mainframe/CI flow; they are not regenerated locally (bb write_vk OOMs on large circuits on dev boxes).
…in the TS BlockConstantData (A-1432)
…minant (A-1432) The same-block input reshape made the private-inputs classes structurally assignable, so the orchestrator's instanceof chain collapsed the union to never (TS2358). Type the block-root selection as a discriminated union and switch on rollupType instead.
… consumption (A-1432) The same-block reshape adds previous_l1_to_l2 to the non-first block-root inputs; regenerate the two affected crates' sample inputs at this state.
…wn terms (A-1432)
spalladino
force-pushed
the
spl/a-1427-inbox-parity
branch
from
July 28, 2026 12:37
1aa7d2f to
e5efd1d
Compare
spalladino
force-pushed
the
spl/a-1432-same-block-msgs
branch
from
July 28, 2026 12:38
f3e5328 to
37ee1a7
Compare
Contributor
Author
|
Superseded by #25036. The Fast Inbox stack has been regrouped from 20 per-issue PRs (plus 2 umbrellas) down to 3 area PRs plus an umbrella, and rebased onto New structure: #25036 (circuits + L1) → #25037 (node + flip) → #25038 (cleanup), with #25039 as the full-stack umbrella. The original branch for this PR is left on the remote as a recovery point. Closing here to cut the rebase and review overhead of maintaining 22 PRs; the work is not abandoned. |
spalladino
added a commit
that referenced
this pull request
Aug 18, 2026
…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.
spalladino
added a commit
that referenced
this pull request
Aug 18, 2026
…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.
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.
What
Makes every block's
BlockConstantData.l1_to_l2_tree_snapshotthe 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 fromconstants.l1_to_l2_tree_snapshotand never asserted the post-bundle root, giving next-block visibility for mid-checkpoint insertions. Now they: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;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_l2is already exercised byblock_merge::tests::consecutive_block_rollups_tests::non_consecutive_l1_to_l2_message_tree_snapshots(a right block whosestart_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_snapshotdiffers 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_libblock_root suite green (63 tests), block_merge (45), checkpoint_root structure tests (17).TS wiring
Mirrors the new
previous_l1_to_l2field throughstdlib(BlockRootRollupPrivateInputs/BlockRootSingleTxRollupPrivateInputsserialization + factory), the Noir ABI conversion (server.ts), and the prover-client block-root input builders (block-proving-state.ts), which pass the block'slastL1ToL2MessageTreeSnapshot(for non-first blocks, the checkpoint's post-first-block snapshot).stdlibbuilds and its serialization test passes.Artifact regen (rides CI / mainframe)
This changes the
block_rootandblock_root_single_txcircuit ABIs, so it invalidates their VKs, the checkpoint-root/block-rootProver.tomlsample inputs, and the generatednoir-protocol-circuits-types/src/types/index.ts. These are not regenerated locally (bb write_vkOOMs on large circuits on dev boxes; the generatedindex.tsalso 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