feat(fast-inbox): resolve inbox endpoints by message total and preflight checkpoints against the effective parent - #25414
Draft
spalladino wants to merge 4 commits into
Conversation
…ght checkpoints against the effective parent Adds `IInbox.getBucketAtOrBeforeTotal(uint64)`: the newest live bucket whose cumulative total is at or below a bound, found by probing the four newest live ring entries and then binary-searching only the unscanned live interval. The live window is derived subtraction-first so the sequence maximum cannot overflow, no overwritten ring entry is dereferenced, and a bound below the oldest retained total reverts with `Inbox__NoBucketAtOrBeforeTotal`. Adds `IRollup.validateCheckpointHeaderAndInbox(CheckpointPreflightArgs)`, which derives the parent the way `propose` does (effective pending checkpoint at `block.timestamp`, i.e. after the automatic prune rule), checks the caller's parent claim against it, runs the shared header checks, resolves the consumed total to a live bucket and validates it with the same `validateInboxConsumption` predicate `propose` enforces, returning the bucket sequence to submit as `bucketHint`. The arguments are bundled in a calldata struct: the flat form hit "stack too deep" and the Rollup has little bytecode headroom left. `MAX_L1_TO_L2_MSGS_PER_BLOCK` is now exported to Solidity and `MAX_MSGS_PER_BUCKET` aliases it, with a Forge regression pinning the alias and the rollover behaviour to the generated value.
…heckpoint preflight
`InboxContract.getBucketAtOrBeforeTotal(upperBound)` returns the newest live
bucket boundary at or below a message total, or `undefined` when the ring has
evicted every candidate. `RollupContract.validateCheckpointHeaderAndInbox(
l1TxUtils, args, { time, stateOverrides, from })` simulates the parent-bound
preflight over eth_simulateV1 with the intended execution timestamp and state,
the transport the publisher's header preflight already uses, and returns the
`bucketHint` to submit. The publisher keeps calling the header-only entry point
for now.
… search's not-found path The preflight is exercised against an unpublished parent supplied through state overrides (tips, archive, slot, fee header and consumed total), and shown to reject the same child without those overrides and when the parent's recorded total is ahead of the child. The Inbox wrapper's `undefined` result is produced against a real wrapped ring by rewriting the Inbox's storage through anvil, and unrelated reverts and transport failures are shown to propagate.
spalladino
force-pushed
the
spl/fi2-l1-endpoint-resolver-preflight
branch
from
September 5, 2026 06:26
496cf9b to
942f6af
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
The bucketless Fast Inbox rebuild lets ordinary L2 blocks end at arbitrary message prefixes and resolves a live Inbox bucket only when a checkpoint is completed and published. Two L1 primitives are missing for that: a one-call way to find the newest live bucket boundary at or below a locally known message count, and a publication preflight that checks a checkpoint's header and its final Inbox consumption against the parent
proposewill actually use at execution time, rather than against a caller-supplied parent total.This PR adds both as additive contract APIs plus their TypeScript wrappers. Node behaviour is unchanged: the publisher still calls
validateHeaderWithAttestations, and the sequencer/validator selection logic is untouched. P3 switches the node over.Approach
IInbox.getBucketAtOrBeforeTotal(uint64 upperBound) → (uint64 seq, InboxBucket bucket). Cumulative totals strictly increase per bucket, so the answer is the newest live bucket whose total does not exceed the bound. The live interval is[oldest, current]witholdest = current - ringSize + 1once the ring wrapped (subtraction first, socurrent + 1cannot overflow at the sequence maximum). The search probes the newest four live entries one by one, checking foroldestbefore every step down, then binary-searches only the still unscanned interval[oldest, lastScanned - 1]. Overwritten ring entries are never dereferenced and genesis is a candidate only while sequence zero is live. Not found (the bound is below the oldest retained total) reverts withInbox__NoBucketAtOrBeforeTotal(upperBound, oldestLiveTotal).IRollup.validateCheckpointHeaderAndInbox(CheckpointPreflightArgs calldata) → uint64 bucketHint.CheckpointPreflightArgsbundles thevalidateHeaderWithAttestationsargument set withexpectedTotalandexpectedParentCheckpointNumber(the flat ten-argument form hit "stack too deep" and the Rollup has only 725 bytes of runtime headroom left; a single calldata struct keeps the forwarder small). InsideRollupOperationsExtLib: derive the effective parent withSTFLib.getEffectivePendingCheckpointNumber(block.timestamp), exactly asproposeprunes before validating; require it to equal the claimed parent (Rollup__UnexpectedParentCheckpoint(expected, actual)); run the shared header checks, which compare the header'slastArchiveRootwith that parent's archive; resolveexpectedTotalto a live bucket (Rollup__InboxTotalNotAtBucketBoundary(expected, actual)if it is interior) and runProposeLib.validateInboxConsumptionon it with the parent's stored total — the same settlement, monotonicity, cap and censorship predicateproposeenforces. The oldvalidateHeaderWithAttestationsentry point is untouched.MAX_L1_TO_L2_MSGS_PER_BLOCKis now exported to Solidity throughscripts/constants-codegen/solidity.json, andMAX_MSGS_PER_BUCKETaliases the generated constant so the node's completion guard and the Inbox rollover cannot drift apart. A Forge test pins both the alias and the rollover behaviour to the generated value.InboxContract.getBucketAtOrBeforeTotal(upperBound)returns{ seq, bucket } | undefined(undefined on the not-found revert);RollupContract.validateCheckpointHeaderAndInbox(l1TxUtils, args, { time, stateOverrides, from })runs the preflight overeth_simulateV1with a block-time override and the caller's state overrides — the transportSequencerPublisher.validateCheckpointHeaderalready uses — and returns thebucketHint.Rollup runtime bytecode is now 24,461 bytes (115 bytes under the limit; it was 725 under before). Gas report and benchmark regenerated.
Tests:
InboxBucketSearch.t.sol(genesis/overwritten genesis, exact/interior bounds, full and spilled buckets, a hit on each walkback probe with read-count bounds, fallback immediately beyond the walk excluding the scanned suffix, fewer than four live entries, ring wrap with oldest-entry hit/miss, maximum-sequence arithmetic via harness ring surgery, fuzz against a brute-force reference),CheckpointPreflight.t.sol(preflight vsproposeparity: fresh chain, consume nothing, interior total, wrong hash, wrong parent identity, equal/later execution timestamp settlement, censorship, cap escape, published parent, automatic prune, invalidated parent, shared header checks), resolve-by-total cases inProposeInboxConsumption.t.sol, and anvil-backed unit tests for both TS wrappers.Stacked on #25413.
Part of A-1928.