Skip to content

feat: introduce inboxRollingHash end to end, dual with inHash (A-1373)#24600

Open
spalladino wants to merge 5 commits into
spl/a-1372-msg-bundle-componentsfrom
spl/a-1373-inbox-rolling-hash
Open

feat: introduce inboxRollingHash end to end, dual with inHash (A-1373)#24600
spalladino wants to merge 5 commits into
spl/a-1372-msg-bundle-componentsfrom
spl/a-1373-inbox-rolling-hash

Conversation

@spalladino

@spalladino spalladino commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #24587 (A-1372). Part of the AZIP-22 Fast Inbox work.

What

Introduces the inboxRollingHash — a truncated-to-field sha256 rolling chain over the L1→L2 message leaves — end to end, carried as a dual of the legacy inHash. The legacy inHash remains authoritative; the rolling hash is computed and threaded everywhere but not yet enforced on L1, so this change is behavior-preserving pre-flip.

Each link is h' = sha256ToField(h_be32 || leaf_be32) with the top byte of the digest zeroed, genesis value zero.

Changes

Circuits (noir)

  • Parity base computes the rolling chain over its real message leaves (start_rolling_hashend_rolling_hash, num_msgs), asserting trailing padding lanes are zero. Parity root asserts chunk continuity (children[i].start == children[i-1].end) and sums the counts.
  • Block and checkpoint rollup public inputs carry a {start, end} rolling-hash pair, propagated exactly like in_hash. Checkpoint merges assert right.start == left.end (decision 11 anchoring).
  • The checkpoint header gains inbox_rolling_hash immediately after in_hash.
  • The root rollup public inputs expose the {previous, end} pair sourced from the merged checkpoint public inputs, so the epoch's consumed chain segment is passed through to proof verification.

L1 (Solidity)

  • ProposedHeaderLib serializes the new header field (header 348 → 380 bytes).
  • PublicInputArgs gains previousInboxRollingHash / endInboxRollingHash; 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 — for now they are pass-through only.

TypeScript

  • updateInboxRollingHash / accumulateInboxRollingHash mirror the circuit chain; getPreviousCheckpointInboxRollingHash sources the previous checkpoint's end value (returns zero for checkpoint ≤ 1). The sequencer, validator, and prover populate the header field; the orchestrator threads per-base start hashes; the prover-node publisher fills the two new PublicInputArgs.
  • RootRollupPublicInputs gains the pair with matching serialization, conversion, factories and viem types.

Constants

  • CHECKPOINT_HEADER_LENGTH 13 → 14, BLOCK_ROLLUP_PUBLIC_INPUTS_LENGTH 56 → 58, CHECKPOINT_ROLLUP_PUBLIC_INPUTS_LENGTH 149 → 151, ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH 111 → 113.
  • Circuit ABIs changed, so the base branch's committed pinned-build.tar.gz no longer matches the compiled circuits. It is dropped here rather than carried stale — bootstrap.sh recompiles from source whenever the pin is absent — and regenerated once the ABI settles. (feat: message-bundle components in rollup-lib (A-1372) #24587 keeps the base pin untouched, so the artifacts stay pinned on the train until this PR.)

Testing

  • yarn build green; forge test green (870 passed); noir rollup-lib root suites green (176 passed).
  • stdlib serde, prover-node publisher, and the checkpoint-sub-tree / top-tree orchestrator suites green.
  • Cross-chain l1_to_l2 e2e confirms the header field round-trips through L1 and the archiver (decoded inboxRollingHash = 0 for the genesis checkpoint, as expected). This suite is registered flaky (.test_patterns.yml); the repeated-consumption cases exhibit a pre-existing nullifier-sync timing race unrelated to this change.

Comment on lines +146 to +155
const start = baseParityIndex * NUM_MSGS_PER_BASE_PARITY;
const realMessages = this.l1ToL2Messages.slice(start, start + NUM_MSGS_PER_BASE_PARITY);
const messages = padArrayEnd(realMessages, Fr.ZERO, NUM_MSGS_PER_BASE_PARITY);
// Thread the rolling hash: this base's start is the chain value after all real messages in earlier bases, so the
// four bases chain sequentially and the parity root ends at the checkpoint's rolling hash. Only real (non-padding)
// messages are absorbed, matching the proposer's `accumulateInboxRollingHash`.
const startRollingHash = accumulateInboxRollingHash(
this.startInboxRollingHash,
this.l1ToL2Messages.slice(0, start),
);

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.

I don't think this is correct. Aren't the this.l1ToL2Messages already padded? And shouldn't realMessages drop the pads? Or is that left for a future PR?

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.

No — this.l1ToL2Messages is unpadded here. It's the array handed to CheckpointSubTreeOrchestrator.start, which comes from l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber) — the archiver's message_store.getL1ToL2Messages, which returns only the real messages (e.g. archiver-sync.test.ts expects length 0 for an empty checkpoint). Padding happens downstream in two places: per-base in this very method (padArrayEnd to NUM_MSGS_PER_BASE_PARITY, unchanged from before this PR) and inside appendL1ToL2MessagesToTree for the world-state insert. So realMessages is exactly the real slice, realMessages.length is the true per-base num_msgs, and the rolling hash accumulates over real messages only — matching the proposer's accumulateInboxRollingHash over the same raw archiver list. (In #24603 this class additionally gains an explicit paddedL1ToL2Messages for the sponge/first-block bundle, padded once in the constructor, which keeps the two representations clearly separated.)

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.

Why is this needed? How were those inputs generated before for the tests? Or are these new tests?

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.

The committed crates/rollup-*/Prover.toml sample inputs (which CI nargo executes) had to be regenerated because this PR changes the rollup public-input ABIs. Previously they were regenerated by running the heavyweight single-node prover e2e (end-to-end/src/single-node/prover/server/full.test.ts) with AZTEC_GENERATE_TEST_DATA=1 — and that test's dump list has several of the affected circuits commented out (rollup-block-merge, rollup-block-root-first-empty-tx, …), so it couldn't refresh everything this PR touches. The new file is a lightweight prover-client-level harness: it drives the simulated orchestrator through a handful of scenarios chosen to produce every changed circuit's inputs and dumps them via the same updateProtocolCircuitSampleInputs mechanism. It's describe.skip unless AZTEC_GENERATE_TEST_DATA=1, so it's a no-op in normal CI. Follow-up worth considering: dedup with (or replace) the e2e dump list so there's one canonical regeneration path.

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.
@spalladino spalladino force-pushed the spl/a-1373-inbox-rolling-hash branch from fe246df to b76ab03 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