Skip to content

feat: per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374)#24603

Open
spalladino wants to merge 7 commits into
spl/a-1373-inbox-rolling-hashfrom
spl/a-1374-per-block-bundles
Open

feat: per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374)#24603
spalladino wants to merge 7 commits into
spl/a-1373-inbox-rolling-hashfrom
spl/a-1374-per-block-bundles

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

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)

  • 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.

TypeScript

  • 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.

Constants

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.

Testing

  • 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.

Comment on lines +43 to +48
// 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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mostly yes — after the flip, and only after the blob-format question is settled. The flag does two jobs today:

  1. Structural leftmost pinning (the old in_hash != 0 role). This one the sponge can take over exactly like SpongeBlob: leftmost start_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).
  2. The blob-absorb gate: absorb_block_end_data emits the l1-to-l2 root only when is_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.

@spalladino spalladino force-pushed the spl/a-1373-inbox-rolling-hash branch from c84a6cc to c818a3d Compare July 9, 2026 00:40
@spalladino spalladino force-pushed the spl/a-1374-per-block-bundles branch 2 times, most recently from cce1329 to d6ea15d Compare July 13, 2026 19:37
@spalladino spalladino force-pushed the spl/a-1373-inbox-rolling-hash branch from fe246df to b76ab03 Compare July 13, 2026 21:52
@spalladino spalladino requested a review from just-mitch as a code owner July 13, 2026 21:52
…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).
…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.
@spalladino spalladino force-pushed the spl/a-1374-per-block-bundles branch from d6ea15d to 84ffffa Compare July 13, 2026 21:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant