Skip to content

feat(fast-inbox): rolling-hash buckets in the Inbox alongside frontier trees (A-1377) - #24771

Closed
spalladino wants to merge 7 commits into
spl/a-1432-same-block-msgsfrom
spl/a-1377-inbox-buckets
Closed

spalladino wants to merge 7 commits into
spl/a-1432-same-block-msgsfrom
spl/a-1377-inbox-buckets

Conversation

@spalladino

@spalladino spalladino commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

AZIP-22 Fast Inbox, FI-07. First L1 PR of the series; a follow-up stacks the propose-side streaming validation on top of this branch.

Stacked on #24781 (spl/a-1432-same-block-msgs), the tip of the Fast Inbox circuits stack (#24587 → #24600 → #24603 → #24612 → #24759 → #24781). That stack already carries the small L1 changes this feature builds on — the header's inboxRollingHash field, the two epoch-proof public inputs, and the regenerated checkpoint fixtures — so this PR adds only the bucket machinery on top.

What

sendL2Message additionally maintains the consensus rolling hash — the truncated-per-link sha256 chain the circuits recompute (h' = sha256ToField(h || leaf), genesis zero; new Hash.accumulateInboxRollingHash) — and snapshots it into a fixed-size ring of buckets. Frontier trees, LAG, the legacy consume() flow, and the legacy bytes16 keccak rolling hash are untouched (the legacy hash now carries a TODO to remove it at cleanup); buckets are completely inert for the legacy flow.

Per the pinned design decisions:

  • Bucket struct {rollingHash | totalMsgCount: uint64, timestamp: uint64, msgCount: uint32} packs into two slots; totalMsgCount is the Inbox-wide cumulative count (what the censorship cap-escape check reads), timestamp is the bucket's opening L1 block timestamp (recency checks are done in seconds), msgCount sits in slot 2's spare bits so the per-bucket cap costs no extra storage access.
  • Ring indexed by dense bucket sequence number (seq % BUCKET_RING_SIZE), 1024 entries in production, sized as an immutable constructor parameter. getBucket(seq) reverts with Inbox__BucketOutOfWindow outside the live window (seq <= current < seq + ringSize, same idiom as the Rollup's roundabout). Overwrite protection for unconsumed buckets is deliberately not enforced yet (happy path assumes the ring never wraps into live data).
  • Bucket boundaries: a bucket only holds messages from a single L1 block; the first message of a new block opens the next bucket, and the 257th message within one block (MAX_MSGS_PER_BUCKET = 256, the post-flip per-L2-block insertion cap) rolls over into the next bucket with the same timestamp. New buckets inherit the rolling hash and cumulative count, so the chain is continuous across buckets.
  • Genesis base case: bucket 0 is {0, 0, deployTime} and never absorbs (even for a message sent in the deployment block), so a checkpoint consuming nothing against an empty Inbox can always reference a bucket matching its parent's chain position — no special case at propose.
  • Event: MessageSent gains bytes32 inboxRollingHash and uint256 bucketSeq after the legacy args.

Archiver / TS

The generated InboxAbi picks up the new event signature at build time; MessageSentArgs and the decode in ethereum/src/contracts/inbox.ts carry the two new fields, and the archiver keeps relying only on the legacy args for now (InboxMessage unchanged). The archiver test fake fills the new fields with placeholders. Nothing on this branch consumes the new values yet — the node-side cross-check of its TS rolling hash against L1-emitted values comes with the archiver bucket-sync work.

Testing

  • New InboxBuckets.t.sol covers the roadmap done-when: accumulation within a block, snapshot freezing at block boundaries with chain continuity into the next bucket, 256-cap rollover within one block, ring wraparound + out-of-window reverts on a small ring, the genesis bucket (including a first message in the deploy block), and event contents.
  • Shared rolling-hash test vectors pinned in feat(fast-inbox): message-bundle components in rollup-lib (A-1372) #24587's noir tests (chain(0,[11]), chain(0,[11,22,33]), chain(0,[1..=256]), chain(0x2a,[7]), chain(0x2a,[7,8])) are asserted against the L1 implementation, so L1, TS, and the circuits provably compute the same chain.
  • forge test at the current stack top: 890 passed, 0 failed, 3 skipped (the testGetEpochProofPublicInputsVerifiesHeaders failure previously inherited from the base has been fixed by the rebased base branches). The 13 InboxBuckets.t.sol tests all pass (9 behavioral + 4 gas measurements). Existing MessageSent expectations in Inbox/TokenPortal/FeeJuicePortal tests updated for the extended event.
  • TS: the regenerated @aztec/l1-artifacts InboxAbi picks up the two new event fields, and @aztec/ethereum typechecks clean against it. @aztec/archiver's full typecheck is blocked only by stale Noir-generated artifacts in a transitive dependency (base-branch circuit changes that require the Noir toolchain, not run in this workspace), so it runs in CI; the archiver change is decode-only and mechanical.

Gas (sendL2Message)

Four Forge gas measurements in InboxBuckets.t.sol cover the bucket write paths (inline gasleft() deltas, matching the RollupGetters.t.sol convention). These feed the capacity analysis (max messages per L1 block from gas):

Scenario Gas
Absorb into an already-open bucket (common per-message case) 34,468
First message of a new L1 block (opens a bucket via timestamp) 78,958
Rollover opening mid-block (256-cap reached, same timestamp) 55,593
First-ever message (cold state struct + bucket 1) 128,118

Caveat for downstream use: these are warm execution gas including the CALL overhead from the test harness. They exclude the 21k intrinsic tx cost, calldata gas, and the cold-access surcharge a standalone EOA transaction pays on its first touch of each slot — the capacity analysis must add those separately. The first-ever figure is the cold-storage case for the bucket/state slots, not the global worst-case insert: later frontier-tree leaf indices with more levels to hash can cost more.

Also documents the timestamp-key assumption in _absorbIntoBucket (post-merge Ethereum increases block.timestamp strictly per block; anvil manual-mining can collapse two blocks into one bucket, which is harmless because the consumption cutoff is timestamp-based).

Ring-size floor

Raises the constructor guard from _bucketRingSize > 1 to a floor of 512 (MIN_BUCKET_RING_SIZE); production stays at 1024. Rationale: 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 it re-consumes after a prune are not overwritten first — 384 rounded up to the next power of two, kept at or below the production ring. testRingWraparound now exercises a real wraparound against a 512-slot ring, and a new testConstructorRevertsBelowRingFloor asserts that constructing below the floor reverts. The full capacity/liveness analysis behind this number feeds the AZIP-22 review.

@spalladino
spalladino requested a review from just-mitch as a code owner July 17, 2026 18:07
@spalladino
spalladino force-pushed the spl/a-1427-inbox-parity branch from c18b69c to 1872323 Compare July 17, 2026 19:38
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from 630c509 to 20656f8 Compare July 17, 2026 19:38
@spalladino
spalladino force-pushed the spl/a-1427-inbox-parity branch from 1872323 to 347a6e1 Compare July 17, 2026 20:04
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from 20656f8 to d12875f Compare July 17, 2026 20:04
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from d12875f to 68714ab Compare July 17, 2026 23:29
@spalladino
spalladino changed the base branch from spl/a-1427-inbox-parity to spl/a-1432-same-block-msgs July 17, 2026 23:29
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from ec1326a to db14374 Compare July 18, 2026 05:22
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from 42da857 to f525ade Compare July 19, 2026 14:05
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch 2 times, most recently from a93a3eb to 9be7a9f Compare July 19, 2026 17:57
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from 317c21d to 8794d58 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-1377-inbox-buckets branch from 9be7a9f to 695a55a Compare July 19, 2026 20:48
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from 8794d58 to de68191 Compare July 20, 2026 15:30
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from 695a55a to 13d85e2 Compare July 20, 2026 15:30
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from de68191 to f3c230d Compare July 20, 2026 17:28
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from 13d85e2 to d747a5c Compare July 20, 2026 17:28
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from f3c230d to f93449b Compare July 20, 2026 21:21
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from d747a5c to 27e2670 Compare July 20, 2026 21:21
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from f93449b to ce32e51 Compare July 21, 2026 02:58
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch 2 times, most recently from 258788d to e7fa949 Compare July 21, 2026 03:39
@spalladino
spalladino force-pushed the spl/a-1432-same-block-msgs branch from 54342eb to f3e5328 Compare July 27, 2026 20:53
@spalladino
spalladino force-pushed the spl/a-1377-inbox-buckets branch from e7fa949 to 17af4b0 Compare July 27, 2026 20:53
…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.
@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 Aug 18, 2026
…st blocks (A-1432)

## What

Makes every block's `BlockConstantData.l1_to_l2_tree_snapshot` the **post-bundle** snapshot for that block — first block or not — so a public/AVM tx in block N can read the L1-to-L2 messages block N inserts (same-block consumption), instead of only from block N+1.

Previously the non-first tx-carrying variants (`block_root`, `block_root_single_tx`) read their start snapshot from `constants.l1_to_l2_tree_snapshot` and never asserted the post-bundle root, giving next-block visibility for mid-checkpoint insertions. Now they:

- take the start L1-to-L2 snapshot as a **witness input** (`previous_l1_to_l2`), pinned by block-merge continuity (`right.start_state == left.end_state`) to the previous block's end state — the checkpoint root forces the leftmost block to be a first-block variant, so every non-first block has a left neighbour pinning its start;
- append their bundle and assert `validate_l1_to_l2_tree_snapshot_in_constants(constants, new_l1_to_l2)`, exactly like the first-block variants.

The tx-less variants (`block_root_empty_tx_first`, `block_root_msgs_only`) carry no tx constants to read against and already witness their start snapshot, so they are unchanged.

## Soundness

The witnessed start snapshot cannot be forged: block-merge / checkpoint-root continuity asserts `right.start_state == left.end_state`, and the checkpoint root asserts the leftmost block is a first-block variant (whose start is pinned to the previous checkpoint). The start is therefore anchored, and the new assert pins the tx-constants snapshot to the computed post-bundle root. The start is **not** derived from "constants minus the bundle" (not expressible).

## Bit-identical pre-flip (public inputs)

Non-first bundles are empty today, so `new_l1_to_l2 == start == today's constants value`; the new assert passes with unchanged values and the block-root **public inputs / state are unchanged**. The circuit constraints, private-input ABI, proof, and VK do change (hence the regen below). This is a restructure-now / flip-minimal change.

## Flip follow-on (not in this PR)

The recursive pinning of the witnessed `previous_l1_to_l2` is already exercised by `block_merge::tests::consecutive_block_rollups_tests::non_consecutive_l1_to_l2_message_tree_snapshots` (a right block whose `start_state.l1_to_l2_message_tree` — i.e. this witness — doesn't match the left block's end is rejected on continuity).

One flip requirement this change surfaces: today the prover gives every non-first block the checkpoint's post-first-block snapshot as its `previousL1ToL2` (`block-proving-state.ts`), which is correct only while non-first bundles are empty. Once non-first blocks insert their own bundles (A-1384), the prover must instead feed block N+1 the end snapshot of block N plus the matching frontier hint. Flagged for the flip plan (A-1384).

## Tests

Adds negative nargo tests: a non-first block (two-tx and single-tx) whose `constants.l1_to_l2_tree_snapshot` differs from its post-bundle root must fail. Red/green verified locally — with the new asserts removed the four negative tests report "Test passed when it should have failed"; restored, they pass. `rollup_lib` block_root suite green (63 tests), block_merge (45), checkpoint_root structure tests (17).

## TS wiring

Mirrors the new `previous_l1_to_l2` field through `stdlib` (`BlockRootRollupPrivateInputs` / `BlockRootSingleTxRollupPrivateInputs` serialization + factory), the Noir ABI conversion (`server.ts`), and the prover-client block-root input builders (`block-proving-state.ts`), which pass the block's `lastL1ToL2MessageTreeSnapshot` (for non-first blocks, the checkpoint's post-first-block snapshot). `stdlib` builds and its serialization test passes.

## Artifact regen (rides CI / mainframe)

This changes the `block_root` and `block_root_single_tx` circuit ABIs, so it invalidates their VKs, the checkpoint-root/block-root `Prover.toml` sample inputs, and the generated `noir-protocol-circuits-types/src/types/index.ts`. These are **not** regenerated locally (`bb write_vk` OOMs on large circuits on dev boxes; the generated `index.ts` also depends on recompiled circuit artifacts). They ride the seeded-S3-cache mainframe/CI flow, consistent with the rest of the Fast Inbox stack.

## Stack

Stacked on `spl/a-1427-inbox-parity`; the L1 PRs (#24771, #24773) are rebased on top of this. Part of the Fast Inbox stack (umbrella #24774).

Fixes A-1432


Replaces #24781.
spalladino added a commit that referenced this pull request Aug 18, 2026
…r trees (A-1377)

AZIP-22 Fast Inbox, FI-07. First L1 PR of the series; a follow-up stacks the `propose`-side streaming validation on top of this branch.

Stacked on #24781 (`spl/a-1432-same-block-msgs`), the tip of the Fast Inbox circuits stack (#24587 → #24600 → #24603 → #24612 → #24759 → #24781). That stack already carries the small L1 changes this feature builds on — the header's `inboxRollingHash` field, the two epoch-proof public inputs, and the regenerated checkpoint fixtures — so this PR adds only the bucket machinery on top.

## What

`sendL2Message` additionally maintains the **consensus rolling hash** — the truncated-per-link sha256 chain the circuits recompute (`h' = sha256ToField(h || leaf)`, genesis zero; new `Hash.accumulateInboxRollingHash`) — and snapshots it into a fixed-size ring of **buckets**. Frontier trees, `LAG`, the legacy `consume()` flow, and the legacy `bytes16` keccak rolling hash are untouched (the legacy hash now carries a TODO to remove it at cleanup); buckets are completely inert for the legacy flow.

Per the pinned design decisions:

- **Bucket struct** `{rollingHash | totalMsgCount: uint64, timestamp: uint64, msgCount: uint32}` packs into two slots; `totalMsgCount` is the Inbox-wide cumulative count (what the censorship cap-escape check reads), `timestamp` is the bucket's opening L1 block timestamp (recency checks are done in seconds), `msgCount` sits in slot 2's spare bits so the per-bucket cap costs no extra storage access.
- **Ring** indexed by dense bucket sequence number (`seq % BUCKET_RING_SIZE`), 1024 entries in production, sized as an immutable constructor parameter. `getBucket(seq)` reverts with `Inbox__BucketOutOfWindow` outside the live window (`seq <= current < seq + ringSize`, same idiom as the Rollup's roundabout). Overwrite protection for unconsumed buckets is deliberately **not** enforced yet (happy path assumes the ring never wraps into live data).
- **Bucket boundaries**: a bucket only holds messages from a single L1 block; the first message of a new block opens the next bucket, and the 257th message within one block (`MAX_MSGS_PER_BUCKET = 256`, the post-flip per-L2-block insertion cap) **rolls over** into the next bucket with the same timestamp. New buckets inherit the rolling hash and cumulative count, so the chain is continuous across buckets.
- **Genesis base case**: bucket 0 is `{0, 0, deployTime}` and never absorbs (even for a message sent in the deployment block), so a checkpoint consuming nothing against an empty Inbox can always reference a bucket matching its parent's chain position — no special case at `propose`.
- **Event**: `MessageSent` gains `bytes32 inboxRollingHash` and `uint256 bucketSeq` after the legacy args.

## Archiver / TS

The generated `InboxAbi` picks up the new event signature at build time; `MessageSentArgs` and the decode in `ethereum/src/contracts/inbox.ts` carry the two new fields, and the archiver keeps relying only on the legacy args for now (`InboxMessage` unchanged). The archiver test fake fills the new fields with placeholders. Nothing on this branch consumes the new values yet — the node-side cross-check of its TS rolling hash against L1-emitted values comes with the archiver bucket-sync work.

## Testing

- New `InboxBuckets.t.sol` covers the roadmap done-when: accumulation within a block, snapshot freezing at block boundaries with chain continuity into the next bucket, 256-cap rollover within one block, ring wraparound + out-of-window reverts on a small ring, the genesis bucket (including a first message in the deploy block), and event contents.
- **Shared rolling-hash test vectors** pinned in #24587's noir tests (`chain(0,[11])`, `chain(0,[11,22,33])`, `chain(0,[1..=256])`, `chain(0x2a,[7])`, `chain(0x2a,[7,8])`) are asserted against the L1 implementation, so L1, TS, and the circuits provably compute the same chain.
- `forge test` at the current stack top: 890 passed, 0 failed, 3 skipped (the `testGetEpochProofPublicInputsVerifiesHeaders` failure previously inherited from the base has been fixed by the rebased base branches). The 13 `InboxBuckets.t.sol` tests all pass (9 behavioral + 4 gas measurements). Existing `MessageSent` expectations in Inbox/TokenPortal/FeeJuicePortal tests updated for the extended event.
- TS: the regenerated `@aztec/l1-artifacts` `InboxAbi` picks up the two new event fields, and `@aztec/ethereum` typechecks clean against it. `@aztec/archiver`'s full typecheck is blocked only by stale Noir-generated artifacts in a transitive dependency (base-branch circuit changes that require the Noir toolchain, not run in this workspace), so it runs in CI; the archiver change is decode-only and mechanical.

## Gas (`sendL2Message`)

Four Forge gas measurements in `InboxBuckets.t.sol` cover the bucket write paths (inline `gasleft()` deltas, matching the `RollupGetters.t.sol` convention). These feed the capacity analysis (max messages per L1 block from gas):

| Scenario | Gas |
|---|---|
| Absorb into an already-open bucket (common per-message case) | 34,468 |
| First message of a new L1 block (opens a bucket via timestamp) | 78,958 |
| Rollover opening mid-block (256-cap reached, same timestamp) | 55,593 |
| First-ever message (cold state struct + bucket 1) | 128,118 |

Caveat for downstream use: these are **warm execution gas including the CALL overhead** from the test harness. They exclude the 21k intrinsic tx cost, calldata gas, and the cold-access surcharge a standalone EOA transaction pays on its first touch of each slot — the capacity analysis must add those separately. The first-ever figure is the cold-storage case for the bucket/state slots, **not** the global worst-case insert: later frontier-tree leaf indices with more levels to hash can cost more.

Also documents the timestamp-key assumption in `_absorbIntoBucket` (post-merge Ethereum increases `block.timestamp` strictly per block; anvil manual-mining can collapse two blocks into one bucket, which is harmless because the consumption cutoff is timestamp-based).


## Ring-size floor

Raises the constructor guard from `_bucketRingSize > 1` to a floor of **512** (`MIN_BUCKET_RING_SIZE`); production stays at 1024. Rationale: 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 it re-consumes after a prune are not overwritten first — 384 rounded up to the next power of two, kept at or below the production ring. `testRingWraparound` now exercises a real wraparound against a 512-slot ring, and a new `testConstructorRevertsBelowRingFloor` asserts that constructing below the floor reverts. The full capacity/liveness analysis behind this number feeds the AZIP-22 review.



Replaces #24771.
spalladino added a commit that referenced this pull request Aug 18, 2026
Stacked on #24771 (A-1377). AZIP-22 Fast Inbox, FI-08.

## What

Implements the AZIP's censorship assert as `ProposeLib.validateInboxConsumption` — a new library function that is **not yet called**: today's `inbox.consume()` / `inHash` check in `propose` is untouched, so this is dead code until the flip wires it in.

Given the checkpoint header's `inboxRollingHash`, an unsigned calldata bucket hint, the proposed slot, and the parent checkpoint's cumulative consumed total (which the flip will source from the temp checkpoint log), the function enforces:

1. **End anchoring**: the header's rolling hash must equal the snapshot in `inbox.getBucket(hint)` (`Rollup__InvalidInboxRollingHash`). The hint is a lookup aid only — a wrong hint reverts, it cannot change what gets accepted — and `getBucket` itself rejects hints beyond the current bucket or already overwritten in the ring. A checkpoint consuming nothing references the same bucket as its parent (the genesis bucket for the first checkpoint), so there is no base case.
2. **Mandatory consumption**: the first unconsumed bucket (`hint + 1`) must be absent, **past the cutoff**, or **cap-escaped** (`Rollup__UnconsumedInboxMessages`). The cutoff is the build-frame start minus `INBOX_LAG_SECONDS`: a checkpoint proposed in slot S is built during slot S-1 (proposer pipelining), and validators are not required to act on buckets younger than one L1 slot at build start, so `cutoff = toTimestamp(S-1) - 12`. Cap escape allows stopping when consuming through the next bucket would exceed `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT` (1024) messages since the parent's total. Both comparisons are exact-boundary tested.

The function performs **no Inbox write**. It is a pure `view` that **returns** the consumed cumulative total (`bucket.totalMsgCount`). The flip (FI-14) stores that consumed position as part of the per-checkpoint temp-log record, extended to `{inboxRollingHash, inboxMsgTotal, inboxConsumedBucket}` and written by `propose`; that record is the authoritative consumed position and is prune-consistent, since temp logs rewind with the pending chain. Two new checks guard the returned value: consumption must **move forward** (`Rollup__InboxConsumptionBehindParent`, equal allowed — a proposal cannot consume behind its parent, and the check precedes the delta subtractions so a backwards proposal reverts descriptively rather than underflowing), and the consumed delta cannot exceed `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT` in one checkpoint (`Rollup__TooManyInboxMessagesConsumed`). The Inbox-side proven-consumed cache that ring overwrite protection needs moved to **FI-20**, anchored to the proven tip rather than the pending chain (decided 2026-07-17): a pointer advanced with the pending chain is not prune-safe, since after a prune it would sit ahead of what the replacement chain consumed.

`INBOX_LAG_SECONDS = 12` and `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT = 1024` are defined at file level in `ProposeLib` for now; they mirror the protocol constants and should move into the generated `Constants` library once the Solidity emitter allow-list includes them.

## Testing

New `ProposeInboxConsumption.t.sol` (15 tests) drives the library function through a harness that owns the Inbox and TimeLib storage, covering the roadmap done-when plus boundaries:

- **empty Inbox** (hint 0 / hash 0 passes and returns 0; non-zero hash rejected),
- **exact-cutoff bucket** (timestamp == cutoff must be consumed; cutoff + 1 need not),
- **cap escape** (1025 messages spill into buckets of 256+1: stopping at bucket 4 = exactly 1024 escapes and returns the full cap; stopping at bucket 3 = 1024-through-next does not; parent total shifts the arithmetic and kills the escape),
- **cap upper bound** (consuming more than 1024 in one checkpoint from a fresh parent reverts `Rollup__TooManyInboxMessagesConsumed`),
- **moves forward** (a proposal whose referenced bucket total sits behind the parent reverts `Rollup__InboxConsumptionBehindParent`; an equal reference consumes nothing and returns the unchanged total),
- **stale hash** (previous bucket's hash against the current bucket) and **unknown hash** rejection, plus out-of-window hints (future bucket, ring-overwritten bucket),
- **return value**: the successful paths assert the consumed cumulative total the flip will record (0, 3, cap).

The three former consumed-pointer tests are removed along with the pointer. Full `forge test` at the current rebased tip: 890 passed, 0 failed, 3 skipped (the previously noted `testGetEpochProofPublicInputsVerifiesHeaders` failure inherited from the base has since been fixed).



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