Skip to content

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

Closed
spalladino wants to merge 63 commits into
merge-train/spartanfrom
spl/a-1378-propose-validation
Closed

chore(fast-inbox): umbrella stack for circuits and L1#24774
spalladino wants to merge 63 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
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch 2 times, most recently from 531e71a to b011406 Compare July 17, 2026 20:04
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch 3 times, most recently from b853b87 to 6e642ec Compare July 18, 2026 05:23
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch 3 times, most recently from e1600a5 to 5088258 Compare July 19, 2026 20:48
@spalladino
spalladino requested a review from a team as a code owner July 19, 2026 20:48
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch 5 times, most recently from 5a9c4e0 to 7f41bee Compare July 21, 2026 03:39
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch from 7f41bee to df088ff Compare July 27, 2026 20:53
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.
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.
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.
…e_bundle input shape (A-1427)

The inbox-parity branch reshapes the block-root private inputs around
message_bundle; carry the sample inputs generated at this state so nargo
execute passes for the five block-root crates.
…hat it holds (A-1427)

The top tree's per-checkpoint `blockProofs` promise stopped holding just block proofs when
parity moved to the checkpoint root: it now carries the block-rollup proof outputs plus the
checkpoint's InboxParity proof. Rename it to `subTreeProofs`, typed by a single exported
`CheckpointSubTreeProofs` in prover-client (dropping the duplicate alias in prover-node), and
rename the block-proof-array parameters in the orchestrator so `blockProofOutputs` means the
same thing everywhere. `CheckpointProver.whenBlockProofsReady` follows suit.
…ker and restart tests (A-1427)

The base parity and root parity request types collapsed into a single INBOX_PARITY,
so the broker abort/revive tests and the prover-restart parity predicate reference it.
Destructuring the sub-tree proof bundle left the helper's inferred return type
unnameable outside the package, so declare it explicitly.
…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).
…minant (A-1432)

The same-block input reshape made the private-inputs classes structurally
assignable, so the orchestrator's instanceof chain collapsed the union to
never (TS2358). Type the block-root selection as a discriminated union and
switch on rollupType instead.
… consumption (A-1432)

The same-block reshape adds previous_l1_to_l2 to the non-first block-root
inputs; regenerate the two affected crates' sample inputs at this state.
…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.
Move the internal _absorbIntoBucket helper below the external functions so
solhint's ordering rule (external before internal) passes. Pure move, no
behavior change.
…box ABIs (A-1377)

The Inbox MessageSent event gains inboxRollingHash and bucketSeq fields, changing its topic. The
examples decode deposit receipts against a hardcoded Inbox ABI, so the stale signature made the
event filter come up empty.
Place the IInbox import after IRollup so solhint's imports-order rule passes.
… (A-1378)

Remove the transitional skip added at the per-block-bundle branch. multi_proof was
observed passing in CI at this content (2-CPU, 65s on 2026-07-19) and passes on
next/merge-train, so the constrained-CPU slowdown introduced earlier in the stack is
resolved by here. The skip stays protective over the intervening branches where we
have no passing observation; from this branch upward the test runs normally.
…(A-1378)

A bucket keeps absorbing messages for the whole L1 block that opened it, so a
reference to the current bucket is not a reference to any lasting snapshot. A lone
designated proposer could bundle send(A) -> propose(hash-after-A) -> send(B) into one
L1 transaction: L1 accepts the checkpoint, then the trailing send overwrites the
bucket in place, leaving the checkpoint committed to a rolling hash that exists
neither on L1 nor in any node, since nodes only ever observe a bucket's end-of-block
state. No honest node can resolve the consumed position, so the pending chain stalls
until the checkpoint is pruned.

validateInboxConsumption now requires the referenced bucket to be settled, which is
the negation of the Inbox's own rollover condition: the genesis bucket (which never
absorbs), a bucket whose L1 block has passed, or a full bucket whose successor takes
the next message. The per-bucket cap moves to IInbox.sol so the Inbox and the propose
path share one definition without the rollup libraries depending on the Inbox
implementation. Honest proposers never reference a bucket younger than
INBOX_LAG_SECONDS, so nothing legitimate is excluded.
@spalladino
spalladino force-pushed the spl/a-1378-propose-validation branch from df088ff to e5284d7 Compare July 28, 2026 12:37
@spalladino

Copy link
Copy Markdown
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 merge-train/spartan including #25007 (the noir-projects fnd//labs/ split). This PR's diff is preserved verbatim as a single squashed commit in #25036, and this description is preserved in that commit's message.

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 spalladino closed this Jul 28, 2026
spalladino added a commit that referenced this pull request Jul 28, 2026
…387)

FI-17 of the Fast Inbox (AZIP-22) stack. Stacked on #24791 (A-1386); it removes the legacy `inHash` from the checkpoint header now that the consensus flip (#24789, A-1384) has switched validation to the streaming rolling hash. After this PR, `inboxRollingHash` is the checkpoint's only inbox commitment.

## Consensus-format change

- The checkpoint header loses its `inHash` field across all three preimage layers in lockstep: noir `CheckpointHeader` (`checkpoint_header.nr`), TS `CheckpointHeader` (`stdlib/src/rollup/checkpoint_header.ts`), and Solidity `ProposedHeaderLib` (struct + hash packing). The field ordering is otherwise unchanged; the header shrinks by one field (32 bytes: `CHECKPOINT_HEADER_SIZE_IN_BYTES` 380 to 348, `CHECKPOINT_HEADER_LENGTH` 14 to 13).
- The header-hash fixtures were regenerated from `checkpoint_header.test.ts` (the established workflow) and pasted into the noir tests; the p2p golden-byte fixture (`wire_compat_fixtures.ts`) was refreshed for the checkpoint-proposal serialization, which shrinks by exactly the one removed field. The block-proposal wire fixture is unchanged (block-level `inHash` is out of scope, see A-1388).

## Circuits and constants

- Strips the dead unconstrained `in_hash` pass-through from the sized `InboxParity<S>` circuit and `ParityPublicInputs` (noir + TS + the hand-written `noir-protocol-circuits-types` conversion wrappers). The circuit is kept (sized-parity, per FI-06's topology decision), so no VK-tree indices move.
- Retires `NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP` (replaced everywhere by `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`, same value 1024) and the now-dead `NUM_MSGS_PER_BASE_PARITY`, `NUM_BASE_PARITY_PER_ROOT_PARITY`, and `L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH`. Generated constants (`constants.gen.ts`, `ConstantsGen.sol`) were regenerated via `remake-constants`.
- Deletes `FrontierLib.sol` and its tests (`Parity.t.sol`, the `Frontier` harness and merkle test) now that their last users are gone.

## Verification

- Noir: `types::checkpoint_header` (5/5), `rollup_lib::parity` and `checkpoint_root::parity_tests` (15/15), and the checkpoint-root header-assembly path pass locally. The full `checkpoint_root` suite (68 tests) is too slow to run in full locally and rides CI.
- Solidity: `forge test` green across `Rollup`, `RollupFieldRange`, `Inbox`, `InboxBuckets`, `RollupGetters` (76/76), covering header hashing and the regenerated checkpoint fixtures.
- Jest: `checkpoint_header`, the p2p wire-compat suites, and the parity/messaging suites pass (43 tests), confirming the fixture regeneration is byte-consistent.
- VK/artifact/Prover.toml regeneration and the generated Noir ABI types ride CI (local `yarn build` in `noir-protocol-circuits-types` / `simulator` / `prover-client` is expected to fail on stale circuit artifacts until then); e2e and the broken-zone typecheck are the CI-of-record for those.

## Stack

Part of the Fast Inbox stack #24784 through #24791 (umbrella #24774): A-1379 (#24784), A-1380 (#24785), A-1381 (#24786), A-1382 (#24787), A-1383 (#24788), A-1384 flip (#24789), A-1385 (#24790), A-1386 (#24791, base of this PR).

Resolves A-1387.


## l1 publisher inbox consumption via the streaming selector (CI fix, pass-8)

The A-1387 inHash-removal rewrite left `l1_publisher.integration.test.ts` hardcoding `previousInboxRollingHash = Fr.ZERO` and `bucketHint = 0n` while consuming real L1→L2 messages, so message-consuming checkpoints reverted on L1 with `Rollup__InvalidInboxRollingHash` / `UnconsumedInboxMessages` (masked every prior CI round behind earlier failures). `INBOX_LAG_SECONDS` is an L1-time censorship cutoff, not whole checkpoints, so no checkpoint-depth shift register reproduces the aged bucket set. The test now mirrors the real Inbox buckets into its `MockL1ToL2MessageSource` and reuses the production `selectInboxBucketForBlock` (which mirrors `ProposeLib.validateInboxConsumption`) to pick exactly the buckets each checkpoint must consume, deriving the consumed bundle, the propose bucket hint, and the header rolling hash from that one selection. Box: 13/13, verified at a-1387 and a-1388.


Replaces #24792.
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