Repository navigation
feat(fast-inbox): add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375) - #24612
Closed
spalladino wants to merge 5 commits into
Closed
feat(fast-inbox): add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375)#24612spalladino wants to merge 5 commits into
spalladino wants to merge 5 commits into
Conversation
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
from
July 9, 2026 00:40
ef2b683 to
cce1329
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 9, 2026 00:40
425f45b to
a5eca42
Compare
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
from
July 13, 2026 19:37
cce1329 to
d6ea15d
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 13, 2026 19:52
a5eca42 to
1dd74af
Compare
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
from
July 13, 2026 21:52
d6ea15d to
84ffffa
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 13, 2026 21:53
1dd74af to
bae1a1f
Compare
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
from
July 14, 2026 20:02
84ffffa to
4f4bbc5
Compare
spalladino
requested review from
a team,
IlyasRidhuan,
MirandaWood,
Thunkar,
charlielye,
nventuro and
sirasistant
as code owners
July 14, 2026 20:02
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 14, 2026 20:06
bae1a1f to
f7cca39
Compare
This was referenced Jul 17, 2026
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
from
July 17, 2026 19:38
4f4bbc5 to
6a5308f
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
3 times, most recently
from
July 19, 2026 14:05
5f7c796 to
225af28
Compare
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
2 times, most recently
from
July 19, 2026 17:57
2182e4a to
d833027
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
2 times, most recently
from
July 19, 2026 20:48
71e0c1b to
f0ea9e4
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
3 times, most recently
from
July 20, 2026 21:21
415a05a to
aacbdb1
Compare
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
2 times, most recently
from
July 21, 2026 02:58
2051695 to
444c427
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 21, 2026 02:59
aacbdb1 to
00a27bf
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 21, 2026 03:39
00a27bf to
8f692f1
Compare
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
2 times, most recently
from
July 27, 2026 20:53
6a7ffee to
85bfeba
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 27, 2026 20:53
8f692f1 to
7d1183e
Compare
…essages (A-1375) Adds the block_root_msgs_only_rollup variant (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-to-L2 message bundle, so a proposer can keep draining the Inbox when the tx pool is dry (AZIP-22 Fast Inbox, FI-05). The circuit asserts num_msgs > 0, inherits the sponge blob and message sponge from the previous block, sets is_first_block = false, appends the bundle and absorbs the message sponge, and threads the start sponge blob through unchanged (no tx effects). It is deliberately unable to be leftmost: the checkpoint root requires the leftmost rollup's is_first_block to be true. Wires the new VK into the allowed-VK lists of the block merge and both checkpoint-root variants, the vk tree, and the proving request / artifact / bb-prover registrations. Noir tests prove a checkpoint [first block, msgs-only block, normal block] and cover the num_msgs == 0, non-leftmost, and sponge-continuity failure cases. The orchestrator selection (block-proving-state picking this variant) and its integration test are deferred to FI-12/FI-15: driving a mid-checkpoint msgs-only block requires reworking the transitional per-block message distribution (today the first block carries the whole padded checkpoint bundle and non-first blocks are empty), which belongs with the sequencer that actually produces these blocks. Nothing produces the shape pre-flip.
…s_first_block (A-1375) The message-only block root is listed in the single-block checkpoint-root allowlist only for symmetry; today the inputs validator's is_first_block assertion is the sole reason it cannot stand as a checkpoint's only block. Note that if that assertion is ever relaxed, this entry must be dropped in the same change, so the two decisions are not made independently.
…vingJobResult schema (A-1375) BLOCK_ROOT_MSGS_ONLY_ROLLUP was added to the enum, the ProvingJobInputs union, and the result type map, but omitted from the ProvingJobResult discriminated union. A checkpoint that builds a message-only block produces this proof output, which the proof store then fails to decode with an invalid-discriminator error, stalling proving. Add the missing union member, mirroring the other block-root results. Adds a schema round-trip regression test.
…375) Persisted broker jobs are keyed by the numeric ProvingRequestType, and this branch inserts BLOCK_ROOT_MSGS_ONLY_ROLLUP into the middle of the enum, shifting every later value. A prover restarted in place over an existing data directory would otherwise dequeue a stale job and hand it to the wrong prover method; the version check already discards a database whose schema version does not match.
spalladino
force-pushed
the
spl/a-1374-per-block-bundles
branch
from
July 28, 2026 12:37
85bfeba to
f515cb2
Compare
spalladino
force-pushed
the
spl/a-1375-msgs-only-block
branch
from
July 28, 2026 12:38
7d1183e to
b3f77b1
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
…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.
spalladino
added a commit
that referenced
this pull request
Aug 18, 2026
…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.
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.
Stacked on #24603 (A-1374). AZIP-22 Fast Inbox, FI-05.
What
Adds
block_root_msgs_only_rollup(craterollup-block-root-msgs-only, VK indexBLOCK_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):num_msgs > 0(a message-only block with no messages has no reason to exist);start_sponge_blobandstart_msg_spongefrom the previous block (non-empty), setsis_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);is_first_blockto be true.Composes via a new
new_from_no_rollups_with_start_sponge_blob(anew_from_no_rollupsthat 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_INDEXadded to the vk tree and theALLOWED_PREVIOUS_VK_INDICESof the block-merge and both checkpoint-root variants.pinned-build.tar.gzregenerated.ProvingJobResultsMapentry, theServerCircuitProverinterface 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 == 0rejected, a msgs-only block cannot be leftmost, and merge continuity catches a bad hintedstart_msg_sponge.yarn build, lint, and the orchestrator + proving-broker suites (123) are green. Cross-chainl1_to_l2e2e: 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.tsselecting 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-mergeright.start_msg_sponge == left.end_msg_spongecontinuity and the checkpoint-rootmerged.end_msg_sponge == parity.end_spongecheck. 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 existingBlockProvingStateguard still rejects non-first zero-tx blocks, so nothing can construct the shape until that work lands.Proving-result schema (CI fix)
The
ProvingJobResultZod discriminated union was missing a member for the newBLOCK_ROOT_MSGS_ONLY_ROLLUPrequest type added here, so once the flip (A-1384) lets the node actually produce a message-only block, decoding its proving result threwInvalid discriminator valueinjsonParseWithSchemaand 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 withtype === BLOCK_ROOT_MSGS_ONLY_ROLLUP.