feat: per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374)#24603
feat: per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374)#24603spalladino wants to merge 7 commits into
Conversation
| // Whether this block range starts at the first block of its checkpoint. Only the first block root sets this true; | ||
| // merges propagate it from the left rollup and `validate_consecutive_block_rollups` asserts the right rollup's is | ||
| // false, so it can only reach the checkpoint root through the leftmost leaf. It replaces `in_hash`'s former | ||
| // structural role: the checkpoint root asserts the merged value is true, and it drives the block-end blob-absorb | ||
| // flag (the l1-to-l2 tree root is absorbed only for the first block). | ||
| pub is_first_block: bool, |
There was a problem hiding this comment.
Is this flag temporary? Can we remove it once we allow any block to add messages to the tree, and just check that the start sponge of the leftmost block is empty, as we do with the blob sponge?
There was a problem hiding this comment.
Mostly yes — after the flip, and only after the blob-format question is settled. The flag does two jobs today:
- Structural leftmost pinning (the old
in_hash != 0role). This one the sponge can take over exactly likeSpongeBlob: leftmoststart_msg_sponge == empty+ merge continuity. One wrinkle: pre-flip, non-first blocks inherit the checkpoint sponge which is never empty (the padded 1024-zero bundle is absorbed even for an empty checkpoint), so the check is airtight; post-flip, an empty checkpoint gives every block an empty sponge, so a non-first variant could sit leftmost. What stops that from mattering is (2). - The blob-absorb gate:
absorb_block_end_dataemits the l1-to-l2 root only whenis_first_block, premised on the root being checkpoint-constant — which stops being true post-flip anyway. Once the flip decides the blob format (absorb the root on every block, or derive it from the replayed message stream), this gate — and with it the flag — can go.
So: keep it through the transition, revisit together with the blob decision. Two things to do in the same change: (a) remove BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX from the single-block checkpoint root's allowlist first — today is_first_block is the only thing making a message-only sole block unprovable, so dropping the flag without that tweak silently legalizes a new checkpoint shape; (b) decide whether a "messages-only checkpoint" should in fact be provable post-flip (it's arguably desirable for Inbox draining) — if yes, that's a deliberate follow-up, not a side effect.
I've added a comment on that allowlist entry in #24612 flagging the coupling, so the two changes stay tied together and the entry isn't dropped independently.
c84a6cc to
c818a3d
Compare
cce1329 to
d6ea15d
Compare
fe246df to
b76ab03
Compare
…oot (A-1374) Implements AZIP-22 Fast Inbox FI-04. Parity verification moves from the block-root variants to the checkpoint root. Every block-root variant now takes a per-block message bundle (l1_to_l2_messages, num_msgs, frontier hint) that appends to the L1-to-L2 tree via append_leaves_to_snapshot and absorbs the same leaves into an L1ToL2MessageSponge threaded across the checkpoint's blocks. BlockRollupPublicInputs replaces in_hash and the FI-03 inbox_rolling_hash pair with an explicit is_first_block flag and start/end message sponges. The checkpoint root gains the parity-root proof, asserts sponge continuity and that the merged block sponge equals the parity sponge, and sources the header's legacy in_hash and inbox_rolling_hash from parity. Transitionally the first block carries the whole checkpoint's messages padded to 1024 with num_msgs=1024; non-first blocks carry empty bundles. Behavior stays bit-identical to the current state (same tree roots, same header bytes).
…undle change (A-1374)
…checkpoint (A-1374) A no-tx block has no base or merge proof whose completion enqueues its block root, and A-1374 moved the parity proof (which previously fired it) from the first block root to the checkpoint root. Nothing then enqueued the empty-tx first block root, so its sub-tree never resolved and proving deadlocked. Enqueue it directly once the empty block's end state is set.
…BI (A-1374) Regenerates the stale rollup Prover.toml fixtures for A-1374's ABI (per-block message bundles, parity moved to checkpoint root). Extends the regeneration harness to cover the non-first block-root and block-root-single-tx variants and fixes its blockProofs mapping for the checkpoint sub-tree's parity root proof.
d6ea15d to
84ffffa
Compare
Stacked on #24600 (A-1373). AZIP-22 Fast Inbox, FI-04.
What
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.
Circuits (noir)
l1_to_l2_messages,num_msgs, frontier hint) and append it to the L1→L2 tree viaappend_leaves_to_snapshot(the A-1372 gadget), absorbing the same leaves into anL1ToL2MessageSponge(Poseidon2) threaded across the checkpoint's blocks. First-block variants lose the parity-root proof.BlockRollupPublicInputsdropsin_hashand the FI-03inbox_rolling_hashpair; gainsis_first_block: boolandstart_msg_sponge/end_msg_sponge.is_first_blocktakes overin_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.is_first_blockandstart_msg_spongefrom the left,end_msg_spongefrom the right;validate_consecutive_block_rollupsasserts!right.is_first_blockandright.start_msg_sponge == left.end_msg_sponge.parity.start_sponge == emptyandmerged.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'sin_hash = parity.sha_rootandinbox_rolling_hash = parity.end_rolling_hash.TypeScript
stdlib/src/messaging/l1_to_l2_message_sponge.tsmirrors the noir sponge (iv-0, absorb-only). Parity/block/checkpoint-root inputs andconversion/server.tsmirror the field changes.block-proving-state.tstocheckpoint-proving-state.ts; the sub-tree enqueues parity once per checkpoint and surfaces it inSubTreeResult; 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.Constants
L1_TO_L2_MESSAGE_SPONGE_LENGTH = 10;BLOCK_ROLLUP_PUBLIC_INPUTS_LENGTH58 → 76. Circuit ABIs changed, sopinned-build.tar.gzis regenerated and committed.Testing
rollup_lib386 passed (adds sponge-threading,is_first_blockstructural, 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.l1_to_l2e2e: 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.is_first_blocksoundness, header sourcing/continuity, the blob-flag derivation, and TS↔noir serde order.