Skip to content

chore(fast-inbox): umbrella stack for circuits and L1#24774

Open
spalladino wants to merge 36 commits into
merge-train/spartanfrom
spl/a-1378-propose-validation
Open

chore(fast-inbox): umbrella stack for circuits and L1#24774
spalladino wants to merge 36 commits into
merge-train/spartanfrom
spl/a-1378-propose-validation

Conversation

@spalladino

@spalladino spalladino commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Umbrella / consolidated PR for the Fast Inbox (AZIP-22) work.

This PR spans the entire Fast Inbox stack — from the topmost branch down to merge-train/spartan — so CI runs once over the combined diff and reviewers get a single consolidated view of everything.

What it contains, at a high level

The stack lands the circuit and L1 (Solidity + archiver/TS) changes for Fast Inbox:

  • Circuits: message-bundle components in rollup-lib; the sha256 inboxRollingHash chain threaded end-to-end (dual with the legacy inHash); per-block L1-to-L2 message bundles with parity moved to the checkpoint root; a block_root_msgs_only_rollup circuit for no-tx blocks that still carry messages; a single variable-size InboxParity<S> proof per checkpoint (replacing the parity base ×4 + parity root fan-in); and same-block L1-to-L2 message consumption for non-first blocks (every block's BlockConstantData.l1_to_l2_tree_snapshot is its own post-bundle root).
  • L1: rolling-hash buckets maintained in the Inbox alongside the existing frontier trees, and the (still-unwired) streaming propose-time consumption validation (validateInboxConsumption).

Contained PRs (stack order, bottom → top)

  1. feat(fast-inbox): message-bundle components in rollup-lib (A-1372) #24587 — message-bundle components in rollup-lib (A-1372)
  2. feat(fast-inbox): introduce inboxRollingHash end to end, dual with inHash (A-1373) #24600 — introduce inboxRollingHash end to end, dual with inHash (A-1373)
  3. feat(fast-inbox): per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374) #24603 — per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374)
  4. feat(fast-inbox): add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375) #24612 — add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375)
  5. feat(fast-inbox): single variable-size InboxParity proof per checkpoint (A-1427) #24759 — single variable-size InboxParity proof per checkpoint (A-1427)
  6. feat(fast-inbox): same-block L1-to-L2 message consumption for non-first blocks (A-1432) #24781 — same-block L1-to-L2 message consumption for non-first blocks (A-1432)
  7. feat(fast-inbox): rolling-hash buckets in the Inbox alongside frontier trees (A-1377) #24771 — rolling-hash buckets in the Inbox alongside frontier trees (A-1377)
  8. feat(fast-inbox): streaming inbox propose validation, unwired (A-1378) #24773 — streaming inbox propose validation, unwired (A-1378)

Review and land the individual PRs above; this umbrella exists only to consolidate CI and provide a full-stack diff.

@spalladino spalladino changed the title chore(fast-inbox): umbrella stack for CI + consolidated review (circuits + L1, do not merge) chore(fast-inbox): umbrella stack for circuits and L1 Jul 17, 2026
Pure library components for the Fast Inbox (AZIP-22), consumed by nothing
yet: new MAX_L1_TO_L2_MSGS_PER_BLOCK / MAX_L1_TO_L2_MSGS_PER_CHECKPOINT
constants, a variable-length frontier-based append to an
AppendOnlyTreeSnapshot at arbitrary (non-aligned) indices, an absorb-only
poseidon message-bundle sponge (L1ToL2MessageSponge), and the rolling
sha256 chain helper (accumulate_inbox_rolling_hash) matching the L1
truncated-to-field policy. No circuit interface changes.
Replaces the per-leaf frontier walk (MaxLeaves x TreeHeight poseidon
hashes) with a level-by-level batched merge: the batch is prepended with
the pending left sibling when it starts as a right child, dangling odd
nodes become the new frontier entries, and remaining nodes are paired
into the next level with lane bounds halving per level. Total cost drops
to ~MaxLeaves + 3 x TreeHeight hashes (1,166 for 1024 leaves in the
height-36 tree vs ~36,900 before). Same signature, semantics, and error
messages. Adds an exhaustive small-tree sweep over every (start, num)
combination and a fail-closed test for appending to a completely full
tree.
Adds the Fast Inbox (AZIP-22) rolling-hash commitment as a dual of the
legacy inHash, flowing through parity circuits, block/checkpoint rollup
public inputs, the checkpoint header, L1, and the node/prover.

Circuits (noir):
- Parity base absorbs only real leaves into a truncated-to-field sha256
  chain (accumulate_inbox_rolling_hash, guarded by num_msgs +
  assert_trailing_zeros); merkle roots keep absorbing padding. Parity
  public inputs gain start_rolling_hash / end_rolling_hash / num_msgs;
  parity root threads the four base segments sequentially and sums counts.
- BlockRollupPublicInputs and CheckpointRollupPublicInputs carry a
  {start,end} pair propagated exactly like in_hash (first block root sets
  it, merges take left's, validate asserts right's is zero). Checkpoint
  merge asserts right.start == left.end (decision 11). CheckpointHeader
  gains inbox_rolling_hash (the end value) immediately after in_hash.
- Root rollup public inputs are unchanged: exposing the epoch pair is
  inseparable from the L1 epoch-proof anchoring that is out of scope here
  (FI-08/FI-14), so it is deferred with the rest of L1 anchoring.

TS: stdlib inbox_rolling_hash mirror (unit-tested against the FI-02
vectors), CheckpointHeader / parity / block / checkpoint PI classes,
noir-protocol-circuits-types conversions, orchestrator threading of
per-base start hashes + counts, and node header population at the
sequencer / prover / validator sites (sourced from the previous
checkpoint header via getPreviousCheckpointInboxRollingHash).

L1: ProposedHeader + ProposedHeaderLib.hash pack inboxRollingHash after
inHash (no new propose checks); test fixtures + headerHash regenerated.
Add the {previous, end} inbox rolling-hash pair to RootRollupPublicInputs so
the epoch's consumed chain segment is carried through to proof verification.
The root rollup sources the pair from the merged checkpoint public inputs;
continuity within the range is already enforced by the checkpoint merges.

On L1, PublicInputArgs gains previousInboxRollingHash/endInboxRollingHash and
EpochProofLib places them at public-input positions 3 and 4 (header hashes,
fees, constants and blob inputs shift by two). Both values are deliberately
unvalidated until the Fast Inbox flip, when they will be checked against the
per-checkpoint records written at propose; for now they are pass-through only.

Also fixes two ivc-integration benchmark generators whose hand-built parity
base inputs were missing start_rolling_hash/num_msgs.
…1373)

Exposing the inbox rolling hash on the parity-root and rollup public inputs
changed the ABI of the block-root-first variants, block-merge, checkpoint-merge
and root circuits, but their committed crates/rollup-*/Prover.toml sample inputs
were not refreshed. CI runs `nargo execute` against these fixtures, so all six
failed to deserialize (e.g. missing parity_root.public_inputs.start_rolling_hash
and previous_rollups[].public_inputs.start_inbox_rolling_hash).

Regenerate the six stale fixtures. The circuits push their serialized inputs via
pushTestData when run through the prover, so a new prover-client test drives
representative epochs through the simulated orchestrator and dumps the captured
inputs, replacing the dead orchestrator_single_checkpoint.test.ts regeneration
path. The suite is skipped unless AZTEC_GENERATE_TEST_DATA=1 is set.
A-1373 changes the rollup public-input ABIs, so the base branch's committed
pinned-build.tar.gz no longer matches the compiled circuits. Remove it rather than
carry a stale archive: bootstrap recompiles from source whenever the pin is absent,
and the pin is regenerated once the ABI settles.
getEpochProofPublicInputs sources the fee recipient/value from the supplied
checkpoint headers but never validated that they hash to the stored headers, so
a mismatch only reverted on the on-chain submit path instead of when an
off-chain caller assembles the public inputs. Call verifyHeaders in the getter
as well, and import ProposedHeaderLib in Rollup.t.sol so the test that exercises
this path compiles.
…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-1378-propose-validation branch from aa85a03 to 531e71a Compare July 17, 2026 19:38
…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.
Replace the parity base (x4) + parity root fan-in with one variable-size
InboxParity<S> proof per checkpoint, S in {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 one parity
verification, now accepting the 3-rung VK ladder. Net +1 VK.

Two transitional decisions (dev-only, closed by the future Fast Inbox flip):
- in_hash is kept in the parity public inputs as an unconstrained pass-through
  hint (the orchestrator supplies the true sha256 frontier root via
  computeInHashFromL1ToL2Messages), so L1's inHash == inbox.consume() check
  still passes with no L1 changes. The frontier tree is no longer built in
  circuit.
- The block-root message sponge absorbs at the real message count while the
  L1-to-L2 tree insert stays a padded fixed subtree, decoupled 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.

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.
…e parity method merge

Deleting getRootParityProof removed the last uses of NESTED_RECURSIVE_PROOF_LENGTH
in mock_prover.ts and broker_prover_facade.ts; @typescript-eslint/no-unused-vars
is error-level so yarn lint would fail. Also corrects two 'base parity' JSDoc
lines to 'inbox parity'.
The generated L1ToL2MessageBundle.messages type is a fixed-length (1024) array,
but the bundle's messages field is a plain Fr[], so mapFieldArrayToNoir inferred
length `number` and tsc rejected it (TS2322) at the six block-root mapper call
sites. Pass MAX_L1_TO_L2_MSGS_PER_BLOCK explicitly to produce the fixed length.
Removing the value constants (NUM_BASE_PARITY_PER_ROOT_PARITY, NUM_MSGS_PER_BASE_PARITY)
left an @aztec/constants import whose members are all inline-type, which
@typescript-eslint/no-import-type-side-effects rejects. Convert to import type.
Replacing the parity base/root artifacts with the single variable-size
InboxParity ladder (64/256/1024) grew the playground main entrypoint to
1936.74 KB on CI, just over the 1925 KB hard limit. Bump with headroom,
following the existing bump-log pattern in vite.config.ts.
ParityPublicInputs requires inHash to be a 31-byte (top-byte-zero) sha frontier
root; the InboxParity circuit passes in_hash through unconstrained, so a raw
Fr.random() input reappears in the output and fails the constructor check.
Compute inHash from the messages instead.
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch from 531e71a to b011406 Compare July 17, 2026 20:04
…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).
…1377)

Raise the constructor guard from `_bucketRingSize > 1` to a floor of 512.
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 re-consumed after
a prune are not overwritten first. 384 rounded up to the next power of two,
kept at or below the production ring of 1024.

Rework testRingWraparound to exercise a real wraparound against a 512-slot
ring and add a negative test asserting construction below the floor reverts.
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch from b853b87 to 6e642ec Compare July 18, 2026 05:23
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