feat(l1): gate inbox bucket eviction on proven consumption - #25457
Open
spalladino wants to merge 1 commit into
Open
feat(l1): gate inbox bucket eviction on proven consumption#25457spalladino wants to merge 1 commit into
spalladino wants to merge 1 commit into
Conversation
Opening an Inbox bucket reuses the ring slot of the bucket `INBOX_BUCKET_RING_SIZE` positions back. Until now that overwrite was unconditional, so a burst of traffic could destroy messages that were still in flight, and worse, could slide the retained window off the only buckets a chain whose consumption had stalled could still propose against -- a gap that never closes, because every bucket left in the window is too far ahead of the stalled parent total to be consumed within one checkpoint's cap. Eviction is now gated on the proven chain having consumed the evicted entry. The Rollup pushes `markProvenConsumed` on every proven-tip advance that moved the rolling hash, reading the bucket sequence the checkpoint recorded at propose time; the Inbox refuses to open a bucket whose ring slot is not covered by that record, so exhausting the ring halts sends rather than overwriting. Anchoring on proven consumption is prune-immune -- the proven tip never rewinds -- and fail-closed, since the record can only lag the truth. `getRingHeadroom()` exposes how many bucket openings are left, and the ring grows from 1024 to 4096 so it covers the worst-case proving lag with enough margin that adversarial bucket creation cannot cheaply exhaust it. Carrying the consumed bucket needs one more `uint64` on the checkpoint temp log, which shares the slot-number storage word with the message total, so recording it costs no extra slot. Propose grows about 310 gas and an epoch proof about 40.
spalladino
requested review from
iAmMichaelConnor,
just-mitch and
koenmtb1
as code owners
September 10, 2026 15:25
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.
Opening an Inbox bucket reuses the ring slot of the bucket
INBOX_BUCKET_RING_SIZEpositions back. Until now that overwrite was unconditional, which is wrong in two ways. Directly, a burst of traffic destroys messages that are still in flight. Less obviously, it can deadlock consumption: a proposal may only reference a retained bucket, and may only consumeMAX_L1_TO_L2_MSGS_PER_CHECKPOINTmessages beyond its parent's cumulative total. Heavy traffic pushes the buckets whose delta from a stalled parent total still fits that cap out of the retained window, while every bucket left in the window is already too far ahead of that parent to be consumed in one checkpoint. A chain whose consumption stalled then has no proposable cursor at all, and waiting does not produce one.This PR gates eviction on the proven chain having consumed the entry being evicted.
Inbox.markProvenConsumed(uint64)is rollup-only and monotonic, and reverts if asked to record a bucket ahead of the current one. The Rollup calls it fromsubmitEpochRootProofon every proven-tip advance that moved the rolling hash, passing the bucket sequence the newly proven checkpoint recorded at propose time. The bucket-open path then refuses to evict a slot the record does not cover, reverting withInbox__WouldOverwriteUnconsumedBucket.Anchoring on proven consumption rather than pending consumption is deliberate. The proven tip never rewinds, so the record is prune-immune: a pruned chain's consumption never unlocks eviction, and re-proposing a checkpoint number overwrites its temp log wholesale, so the replacement's consumption is what eventually gets recorded. The record can only lag the truth, never lead it, so the failure mode is refusing a legal eviction rather than allowing an illegal one.
Exhausting the ring now halts sends instead of overwriting.
getRingHeadroom()exposes how many bucket openings are left, so a caller can see the wall coming. Note it counts openings, not messages: at zero headroom a bucket still open in the current L1 block and below the per-bucket cap keeps absorbing.The ring grows from 1024 to 4096 entries. It has to cover the worst-case proving lag with enough margin that adversarial bucket creation cannot cheaply exhaust it — a forced rollover costs roughly 2.2M gas, so burning 4096 buckets of headroom takes about 250 continuously-owned L1 blocks. Ring size bounds retention only: two storage slots per live bucket, no per-send gas.
Carrying the consumed bucket needs one more
uint64on the checkpoint temp log. It goes next to the slot number and the cumulative message total, which already share one storage word, so recording it costs no extra slot.STFLib.getInboxConsumedBucketreads it back on the proven-tip advance.New tests cover the Inbox side (
InboxOverwriteProtection.t.sol: the exact boundary, resumability, monotonicity, authorization, headroom semantics, batch atomicity, and a fuzz run against a model of the ring) and the deadlock argument from the consumption side (InboxRingDeadlock.t.sol, which reproduces the gap against a contract whose record is pushed ahead of real consumption and shows it cannot open with protection in place).Rollup.t.solcovers the proven-tip advance, the empty-epoch skip, and both prune cases.Propose grows about 310 gas and an epoch proof about 40.
The TypeScript mirror rides the foundation patch queue as
labs-patches/0015-*.patch: the checkpoint-log override builders have to write the new field, since supplying either of the other two fields in that word would otherwise zero it.Stacked on #25456.