@@ -495,41 +495,44 @@ describe('e2e_epochs/epochs_invalidate_block', () => {
495495 minTxsPerBlock : 0 ,
496496 } ) ,
497497 ) ;
498- await Promise . all ( sequencers . map ( s => s . start ( ) ) ) ;
499- logger . warn ( `Started all sequencers, waiting for first checkpoint before applying malicious config` ) ;
500-
501- // Wait for at least one good checkpoint to be mined so any in-progress slot has completed.
502- const initialCheckpointNumber = ( await nodes [ 0 ] . getChainTips ( ) ) . checkpointed . checkpoint . number ;
503- await test . waitUntilCheckpointNumber ( CheckpointNumber ( initialCheckpointNumber + 1 ) , test . L2_SLOT_DURATION_IN_S * 4 ) ;
504-
505- // Align to the start of an L2 slot, then pick two slots with a 3-slot gap so the malicious
506- // config has time to land on each proposer's job snapshot under pipelining, and P1's proposal
507- // has time to propagate to P2 before P2 starts pipelined building.
508- await test . monitor . waitUntilNextL2Slot ( ) ;
509- const { l2SlotNumber : currentSlot } = await test . monitor . run ( ) ;
510- logger . warn ( `First checkpoint mined, current slot is ${ currentSlot } ` ) ;
511-
512- let badSlot1 = SlotNumber . add ( currentSlot , 3 ) ;
513- let badSlot2 = SlotNumber . add ( currentSlot , 4 ) ;
514- let p1Proposer = await test . epochCache . getProposerAttesterAddressInSlot ( badSlot1 ) ;
515- let p2Proposer = await test . epochCache . getProposerAttesterAddressInSlot ( badSlot2 ) ;
516-
517- // Ensure the two slots belong to different proposers; retry by walking forward one slot at
518- // a time. With committee size 6 and random shuffling this should usually succeed first try.
519- let attempts = 0 ;
520- while ( p1Proposer && p2Proposer && p1Proposer . equals ( p2Proposer ) ) {
521- attempts += 1 ;
522- if ( attempts > 6 ) {
523- throw new Error ( `Could not find two consecutive slots with different proposers` ) ;
498+ let badSlot1 : SlotNumber | undefined ;
499+ let p1Proposer : EthAddress | undefined ;
500+ let p2Proposer : EthAddress | undefined ;
501+ let candidate = Number ( test . epochCache . getEpochAndSlotNow ( ) . slot ) + 4 ;
502+ const maxAttempts = 200 ;
503+ for ( let attempt = 0 ; attempt < maxAttempts && badSlot1 === undefined ; attempt ++ ) {
504+ try {
505+ const [ p1 , p2 ] = await Promise . all ( [
506+ test . epochCache . getProposerAttesterAddressInSlot ( SlotNumber ( candidate ) ) ,
507+ test . epochCache . getProposerAttesterAddressInSlot ( SlotNumber ( candidate + 1 ) ) ,
508+ ] ) ;
509+ if ( p1 && p2 && ! p1 . equals ( p2 ) ) {
510+ badSlot1 = SlotNumber ( candidate ) ;
511+ p1Proposer = p1 ;
512+ p2Proposer = p2 ;
513+ break ;
514+ }
515+ candidate ++ ;
516+ } catch ( err ) {
517+ const msg = err instanceof Error ? err . message : String ( err ) ;
518+ if ( ! msg . includes ( 'EpochNotStable' ) ) {
519+ throw err ;
520+ }
521+ const block = await test . l1Client . getBlock ( { includeTransactions : false } ) ;
522+ const warpBy = test . epochDuration * test . L2_SLOT_DURATION_IN_S ;
523+ const newTs = Number ( block . timestamp ) + warpBy ;
524+ logger . warn ( `Hit EpochNotStable at candidate ${ candidate } , warping L1 forward by ${ warpBy } s to ${ newTs } ` ) ;
525+ await test . context . cheatCodes . eth . warp ( newTs , { resetBlockInterval : true } ) ;
526+ const newCurrentSlot = Number ( test . epochCache . getEpochAndSlotNow ( ) . slot ) ;
527+ if ( candidate < newCurrentSlot + 4 ) {
528+ candidate = newCurrentSlot + 4 ;
529+ }
524530 }
525- badSlot1 = SlotNumber . add ( badSlot1 , 1 ) ;
526- badSlot2 = SlotNumber . add ( badSlot2 , 1 ) ;
527- p1Proposer = await test . epochCache . getProposerAttesterAddressInSlot ( badSlot1 ) ;
528- p2Proposer = await test . epochCache . getProposerAttesterAddressInSlot ( badSlot2 ) ;
529531 }
530- if ( ! p1Proposer || ! p2Proposer ) {
531- throw new Error ( `Could not resolve proposers for slots ${ badSlot1 } and ${ badSlot2 } ` ) ;
532+ if ( badSlot1 === undefined || ! p1Proposer || ! p2Proposer ) {
533+ throw new Error ( `Could not find two consecutive slots with different proposers after ${ maxAttempts } attempts ` ) ;
532534 }
535+ const badSlot2 = SlotNumber . add ( badSlot1 , 1 ) ;
533536
534537 const p1NodeIndex = nodes . findIndex ( n => n . getSequencer ( ) ! . validatorAddresses ! . some ( a => a . equals ( p1Proposer ! ) ) ) ;
535538 const p2NodeIndex = nodes . findIndex ( n => n . getSequencer ( ) ! . validatorAddresses ! . some ( a => a . equals ( p2Proposer ! ) ) ) ;
@@ -580,10 +583,6 @@ describe('e2e_epochs/epochs_invalidate_block', () => {
580583
581584 observerArchiver . events . on ( L2BlockSourceEvents . DescendentOfInvalidAttestationsCheckpointDetected , onDescendant ) ;
582585
583- // Send a couple of txs so there's content for both checkpoints.
584- logger . warn ( 'Sending transactions to fill the bad checkpoints' ) ;
585- await Promise . all ( times ( 4 , i => testContract . methods . emit_nullifier ( BigInt ( i + 1 ) ) . send ( { from, wait : NO_WAIT } ) ) ) ;
586-
587586 // Watch for both CheckpointProposed events at the targeted slots.
588587 const p1CheckpointPromise = promiseWithResolvers < CheckpointNumber > ( ) ;
589588 const p2CheckpointPromise = promiseWithResolvers < CheckpointNumber > ( ) ;
@@ -596,6 +595,22 @@ describe('e2e_epochs/epochs_invalidate_block', () => {
596595 }
597596 } ) ;
598597
598+ // Send a couple of txs so there's content for both checkpoints.
599+ logger . warn ( 'Sending transactions to fill the bad checkpoints' ) ;
600+ await Promise . all ( times ( 4 , i => testContract . methods . emit_nullifier ( BigInt ( i + 1 ) ) . send ( { from, wait : NO_WAIT } ) ) ) ;
601+
602+ // Sequencers are still stopped. Warp to the L1 block immediately before the pipelined build
603+ // window for P1, so the first proposer job that can observe the malicious config is the
604+ // intended checkpoint, not an earlier slot owned by the same validator.
605+ const buildSlot = SlotNumber . add ( badSlot1 , - 1 ) ;
606+ const buildSlotStart = getTimestampForSlot ( buildSlot , test . constants ) ;
607+ const warpTo = buildSlotStart - BigInt ( test . L1_BLOCK_TIME_IN_S ) ;
608+ logger . warn ( `Warping L1 to timestamp ${ warpTo } (one L1 block before build slot ${ buildSlot } )` ) ;
609+ await test . context . cheatCodes . eth . warp ( Number ( warpTo ) , { resetBlockInterval : true } ) ;
610+
611+ await Promise . all ( sequencers . map ( s => s . start ( ) ) ) ;
612+ logger . warn ( `Started all sequencers after warping to the target build window` ) ;
613+
599614 logger . warn ( `Waiting for two checkpoints to be mined on slots ${ badSlot1 } and ${ badSlot2 } ` ) ;
600615 const [ p1Checkpoint , p2Checkpoint ] = await executeTimeout (
601616 ( ) => Promise . all ( [ p1CheckpointPromise . promise , p2CheckpointPromise . promise ] ) ,
0 commit comments