Skip to content

feat(fast-inbox): require every block to start consuming at an inbox bucket boundary - #25322

Open
spalladino wants to merge 2 commits into
spl/a-1748-inbox-bucket-boundary-separatorfrom
spl/inbox-block-bucket-boundary
Open

feat(fast-inbox): require every block to start consuming at an inbox bucket boundary#25322
spalladino wants to merge 2 commits into
spl/a-1748-inbox-bucket-boundary-separatorfrom
spl/inbox-block-bucket-boundary

Conversation

@spalladino

@spalladino spalladino commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Stacked on #25319.

Why

L1 forces a checkpoint to consume whole Inbox buckets (the header rolling hash must match a bucket snapshot), but nothing in the circuits stopped a proposer from splitting one bucket across two blocks of the same checkpoint. Block roots absorbed their bundle into the threaded L1-to-L2 message sponge with no notion of buckets; per-block alignment was only rejected by the validator's TS checks.

What

  • L1ToL2MessageBundle (Noir + TS) gains per-lane bucket_starts. Each block root asserts its first consumed message opens a bucket (and no flag sits in padding) via a shared assert_valid_bucket_starts, also used by InboxParity.
  • L1ToL2MessageSponge absorbs each leaf packed with its flag: leaf + flag * 2^248, one field per message (stream length unchanged). Leaves are truncated sha256 digests, so they always fit in 248 bits; the sponge range-checks them so the packing is injective (otherwise a block root could absorb leaf + 2^248 with the flag cleared and still match the parity's sponge).
  • Since InboxParity and the block roots absorb the same packed stream, the checkpoint-root sponge equality now forces the blocks' flags to equal the checkpoint's L1-anchored flags. A block consuming several whole buckets is still allowed.
  • TS: the prover threads the bucket-grouped InboxMessageBundle down to BlockProvingState; sliceBundle cuts a checkpoint bundle into per-block bundles and refuses to cut through a bucket; the sponge mirror packs identically and rejects oversize leaves. Tests that fed Fr.random() as message leaves now use randomL1ToL2MessageLeaf().

Public input shapes are unchanged. VKs of the three inbox parity circuits and all block-root variants change.

Cost (bb gates --scheme ultra_honk)

circuit before after Δ
inbox_parity_64 579,384 580,362 +0.17%
inbox_parity_256 2,307,528 2,310,906 +0.15%
inbox_parity_1024 9,220,104 9,233,082 +0.14%
rollup_block_root 1,552,519 1,556,236 +0.24%
rollup_block_root_single_tx 771,821 775,538 +0.48%
rollup_block_root_no_txs 66,927 70,819 +3,892 (+5.8% of a small circuit)

The alternative of absorbing the flag as a separate field was measured at roughly 3x this (+0.44% parity, +0.69% / +1.38% / +15.9% on the block roots) and rejected.

The rollup sample inputs (Prover.toml) are regenerated for the new VK tree root.

…bucket boundary

L1 already forces a checkpoint to consume whole Inbox buckets, but nothing in the circuits stopped a
proposer from splitting one bucket across two blocks of the same checkpoint: block roots absorbed their
message bundle into the threaded L1-to-L2 message sponge with no notion of buckets, and the per-block
alignment was only rejected by the validator's TS checks.

The block bundle now carries per-lane bucket-start flags, and the message sponge absorbs each leaf packed
with its flag as `leaf + flag * 2^248` (leaves are truncated sha256 digests, so they always fit in 248
bits, and the sponge range-checks them so the packing is injective). InboxParity and every block root
absorb the same packed stream, so the checkpoint-root sponge equality now also forces the concatenation
of the blocks' flags to equal the checkpoint's L1-anchored flags; each block root then asserts its first
consumed message opens a bucket. A block consuming several whole buckets is still allowed.

Packing the flag rather than absorbing it as a separate field keeps the cost to a range check per lane:
+0.14-0.17% on the inbox parity circuits, +0.24% / +0.48% on the block root / single-tx block root, and
+3.9k gates on the 67k-gate no-txs block root. Public input shapes are unchanged.

On the TS side the prover threads the bucket-grouped bundle down to the block proving state,
`sliceBundle` cuts a checkpoint's bundle into per-block bundles and refuses to cut through a bucket,
and the message sponge mirror packs identically.
…-boundary block bundle

The block bundle gains bucket_starts and the inbox-parity and block-root circuits
change bytecode, moving the VK tree root to
0x0113e960c28312482428135b1343817f6d1da382f8324fc6a399eb85e367fb05. Regenerated
with AZTEC_GENERATE_TEST_DATA=1; all nine crates execute against the new tomls.
@spalladino
spalladino force-pushed the spl/inbox-block-bucket-boundary branch from c04980f to 0fb5708 Compare August 29, 2026 11:10
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