test(e2e): fix race in 'proposer invalidates multiple checkpoints'#23259
Merged
Conversation
Wait for an L2 slot boundary before computing bad slots, and add an extra slot of margin, so the malicious config is applied to badSlot1's proposer well before it constructs its CheckpointProposalJob (which snapshots this.config). Previously, monitor.run() could return a slot value near its end, leaving only milliseconds before the next pipelined work() loop captured the old config.
PhilWindle
approved these changes
May 14, 2026
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.
Addresses a config-timing race in
epochs_invalidate_block.parallel.test.ts > "proposer invalidates multiple checkpoints"that caused intermittent CI failures withexpect(validCount).toBeLessThan(quorum)(e.g. 5/6 attestations when quorum=5).The race
The test reads
currentSlotviamonitor.run()right after waiting for the first checkpoint to land — that read can land anywhere within the current L2 slot, including near its end. It then computesbadSlot1 = currentSlot + 2and races to push malicious config (skipCollectingAttestations: true, …) to that slot's proposer viaawait node.setConfig({...}).CheckpointProposalJobis constructed withthis.configpassed by reference (sequencer-client/src/sequencer/sequencer.ts:559), andSequencer.updateConfigreassignsthis.config = merge(...)rather than mutating, so a job built beforesetConfiglands keeps the old config object. Under proposer pipelining (PROPOSER_PIPELINING_SLOT_OFFSET = 1,epoch-cache/src/epoch_cache.ts:26), the job forbadSlot1is built during the last L1 slot of L2 slotbadSlot1 - 1. With 32s L2 slots and 8s L1 slots, that's ~24s into the previous L2 slot — so ifcurrentSlotwas read late, badSlot1's proposer can snapshot the old config before oursetConfiground-trip completes.Fix
monitor.waitUntilNextL2Slot()) before readingcurrentSlot, so we start from the beginning of a slot rather than wherever we happened to land.+2/+3to+3/+4for a second slot of margin.Cost is up to one additional L2 slot of test runtime in the worst case; the existing 8-slot wait window for both checkpoints still fits.