fix(l1): freeze the escape hatch candidate set before its seed is knowable - #25510
Open
spalladino wants to merge 1 commit into
Open
spalladino wants to merge 1 commit into
spalladino wants to merge 1 commit into
Conversation
…wable EscapeHatch snapshots the candidate set an epoch before the entropy that draws its designated proposer, so the set is meant to be closed by the time anyone can know who wins. It was not. selectCandidates asked for the seed with ROLLUP.getSampleSeedAt(seedTs), which is not a raw randao getter: Rollup converts the timestamp back to an epoch and ValidatorSelectionLib.getSampleSeed subtracts the rollup's own lagInEpochsForRandao again. The entropy actually came from start(F - 1 - lagInEpochsForRandao), at or before the freeze at start(F - 2) for every lag we ship. With the shipped lag of 2 that leaves a full epoch in which an attacker can read the seed, compute the fewest addresses that move the draw into their own range, and join that many times -- buying the sole permitted proposer slot for a hatch window rather than winning it. Add ValidatorSelectionLib.getCheckpointedRandaoAt, which reads the randao checkpointed at a timestamp with no lag of its own and returns the key it found alongside the value, and draw against that instead. EscapeHatch now refuses to designate a proposer unless the checkpoint sits exactly on the seed epoch, so entropy predating the freeze can never select. Its timing no longer depends on the rollup's validator-selection lag at all. Two things fall out of the same boundary. Snapshot reads move to one second before the freeze, because Checkpoints.upperLookup is inclusive of its key and was admitting joins made in the freeze block into the frozen set; that also makes initiateExit's strict comparison against the next freeze exactly right rather than one second conservative. And the constructor check that claimed to enforce all this compared two compile-time constants, so it could never fail; the invariant is now covered by tests that vary the rollup's lag. A hatch whose seed epoch passed with no randao checkpointed stays closed for that cycle, which is the same outcome as a hatch with no candidates. Drawing against the older checkpoint upperLookup would otherwise fall back to means drawing against entropy that predates the freeze, which is the bug itself. Fixes A-1663.
spalladino
requested review from
iAmMichaelConnor,
just-mitch and
koenmtb1
as code owners
September 17, 2026 23:19
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.
EscapeHatchsnapshots the candidate set an epoch before the entropy that draws its designated proposer, so the set is meant to be closed by the time anyone can know who wins. It was not.Context
selectCandidatesasked for the seed withROLLUP.getSampleSeedAt(seedTs), which is not a raw randao getter:Rollupconverts the timestamp back to an epoch andValidatorSelectionLib.getSampleSeedsubtracts the rollup's ownlagInEpochsForRandaoagain. WithF = firstEpoch(H - LAG_IN_HATCHES)the entropy actually came fromstart(F - 1 - lagInEpochsForRandao), at or before the freeze atstart(F - 2)for every lag we ship.scripts/network-defaults.jsoncarries a lag of 2, which leaves a full epoch in which an attacker can read the seed, compute the fewest addresses that move the draw into their own range, and join that many times — buying the sole permitted proposer slot for a hatch window rather than winning it. The constructor check that claimed to prevent this compared two compile-time constants, so it could never fail.Approach
ValidatorSelectionLib.getCheckpointedRandaoAtreads the randao checkpointed at a timestamp with no lag of its own, and returns the key it found alongside the value so a caller that depends on when the entropy was revealed can tell an exact hit from anupperLookupfall-back. Plumbed throughValidatorOperationsExtLib,RollupandIValidatorSelection.EscapeHatchdraws against that and refuses to designate a proposer unless the checkpoint sits exactly on the seed epoch, so entropy predating the freeze can never select. Its timing no longer depends on the rollup's validator-selection lag at all.Checkpoints.upperLookupis inclusive of its key, so joins made in the freeze block were landing in the frozen set even thoughselectCandidatesuses a strict comparison. This also makesinitiateExit's existing comparison against the next freeze exactly right rather than one second conservative.requireis gone; the invariant it claimed is now covered by a test that varies the rollup'slagInEpochsForRandaoand asserts the entropy stays unreadable until after the freeze.A hatch whose seed epoch passed with no randao checkpointed stays closed for that cycle — the same outcome as a hatch with no candidates.
checkpointRandaois permissionless, so any party can record it, but it cannot be created retroactively. Drawing against the older checkpointupperLookupwould otherwise return means drawing against entropy that predates the freeze, which is the bug itself.The escape-hatch suite is at the same 8 pre-existing
Rollup__InvalidArchivefixture failures asnext, with 6 new tests covering the seed timing and 4 covering the new getter againstupperLookupsemantics.API changes
IValidatorSelectiongainsgetCheckpointedRandaoAt(Timestamp) returns (bool exists, uint32 keyTs, uint224 randao).IEscapeHatchgainsgetSeed(Hatch), which reverts withEscapeHatch__EntropyNotReadyuntil the seed epoch's randao is checkpointed, and aHatchPreparationSkippedevent for the case where it never was.Fixes A-1663