1- //! Stepping mode E2E test: verifies batch submission after extended L1 gap .
1+ //! Extended-gap batch submission E2E test .
22//!
3- //! When a zone node goes down for longer than the EIP-2935 history window (8192 blocks),
4- //! the batch submitter must split the batch into multiple direct-mode submissions.
5- //! This test validates that the stepping logic correctly handles this scenario.
3+ //! When a zone node goes down for long enough that even the first stepping
4+ //! boundary is outside the EIP-2935 history window, the sequencer must still
5+ //! submit batches successfully once it comes back. This exercises the
6+ //! long-downtime ancestry path instead of the simpler direct-mode case.
67
7- use crate :: utils:: { L1TestNode , ZoneAccount , ZoneTestNode , spawn_sequencer} ;
8+ use crate :: utils:: { L1TestNode , ZoneTestNode , poll_until , spawn_sequencer} ;
89use alloy:: providers:: Provider ;
910use std:: time:: Duration ;
10- use zone:: abi:: { ZONE_TOKEN_ADDRESS , ZonePortal } ;
11+ use zone:: abi:: ZonePortal ;
1112
12- /// Extended timeout for stepping tests — the L1 needs to mine >8200 blocks
13- /// and the zone must process them all.
14- const STEPPING_TIMEOUT : Duration = Duration :: from_secs ( 180 ) ;
13+ const EIP2935_HISTORY_WINDOW : u64 = 8192 ;
14+ const EIP2935_SAFETY_MARGIN : u64 = 360 ;
15+ const EIP2935_EFFECTIVE_WINDOW : u64 = EIP2935_HISTORY_WINDOW - EIP2935_SAFETY_MARGIN ;
16+ const EXTENDED_GAP_BLOCKS : u64 = EIP2935_HISTORY_WINDOW + EIP2935_EFFECTIVE_WINDOW + 64 ;
1517
16- /// Timeout for L1 operations.
17- const L1_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
18+ /// Extended timeout for stepping tests — the L1 needs to mine >16k blocks and
19+ /// the zone must replay enough history to cross the first stepping boundary.
20+ const STEPPING_TIMEOUT : Duration = Duration :: from_secs ( 300 ) ;
21+ const BATCH_TIMEOUT : Duration = Duration :: from_secs ( 90 ) ;
1822
1923/// Test that batch submission works after the zone's `tempoBlockNumber` has
20- /// fallen outside the EIP-2935 history window (gap > 8192 blocks).
21- ///
22- /// The stepping logic must split the batch into multiple direct-mode
23- /// submissions, each within the EIP-2935 effective window.
24+ /// fallen far enough behind L1 that the first stepped sub-batch still lands
25+ /// outside the EIP-2935 history window.
2426///
2527/// 1. Start L1 with 10ms block time to mine blocks quickly.
2628/// 2. Deploy zone portal on L1.
27- /// 3. Wait for L1 to advance >8200 blocks past genesis.
28- /// 4. Start zone node connected to L1.
29- /// 5. Wait for zone to process all L1 blocks.
30- /// 6. Fund user and deposit to create non-trivial state.
31- /// 7. Spawn sequencer (monitor + withdrawal processor).
32- /// 8. Assert multiple BatchSubmitted events (stepping produces >=2 submissions).
33- /// 9. Verify withdrawal works through stepped batches.
29+ /// 3. Wait for L1 to advance past `history + effective window`, so the first
30+ /// stepping boundary is still outside the history window.
31+ /// 4. Start zone node connected to L1, anchored at the portal genesis.
32+ /// 5. Wait for the zone to replay up to the first stepping boundary.
33+ /// 6. Spawn sequencer while the zone is still far behind L1.
34+ /// 7. Assert a `BatchSubmitted` event appears.
3435#[ tokio:: test( flavor = "multi_thread" ) ]
35- #[ ignore = "slow: mines >8200 L1 blocks (~82s) , run with --ignored or in nightly CI" ]
36+ #[ ignore = "slow: mines >16k L1 blocks and replays zone history , run with --ignored or in nightly CI" ]
3637async fn test_batch_submission_after_extended_l1_gap ( ) -> eyre:: Result < ( ) > {
3738 reth_tracing:: init_test_tracing ( ) ;
3839
@@ -45,19 +46,17 @@ async fn test_batch_submission_after_extended_l1_gap() -> eyre::Result<()> {
4546 // --- Step 2: Deploy zone portal ---
4647 let portal_address = l1. deploy_zone ( ) . await ?;
4748
48- // --- Step 3: Wait for L1 to advance past the EIP-2935 window ---
49- // The portal's genesisTempoBlockNumber is set to the current L1 block at
50- // deployment time. We need the L1 to advance >8200 blocks beyond that.
51- let genesis_block = l1. provider ( ) . get_block_number ( ) . await ?;
52- let target_block = genesis_block + 8200 ;
49+ let portal = ZonePortal :: new ( portal_address, l1. provider ( ) ) ;
50+ let genesis_block = portal. genesisTempoBlockNumber ( ) . call ( ) . await ?;
51+ let target_block = genesis_block + EXTENDED_GAP_BLOCKS ;
5352
5453 tracing:: info!(
5554 genesis_block,
5655 target_block,
57- "Waiting for L1 to advance past EIP-2935 window "
56+ "Waiting for L1 to advance past the extended ancestry threshold "
5857 ) ;
5958
60- // Poll until L1 reaches the target — at 10ms/block this takes ~82 seconds.
59+ // Poll until L1 reaches the target — at 10ms/block this takes ~160 seconds.
6160 let poll_start = std:: time:: Instant :: now ( ) ;
6261 loop {
6362 let current = l1. provider ( ) . get_block_number ( ) . await ?;
@@ -70,75 +69,69 @@ async fn test_batch_submission_after_extended_l1_gap() -> eyre::Result<()> {
7069 ) ;
7170 break ;
7271 }
73- if poll_start. elapsed ( ) > Duration :: from_secs ( 120 ) {
72+ if poll_start. elapsed ( ) > STEPPING_TIMEOUT {
7473 return Err ( eyre:: eyre!(
7574 "Timed out waiting for L1 to reach block {target_block} (current: {current})"
7675 ) ) ;
7776 }
7877 tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
7978 }
8079
81- // --- Step 4: Start zone node connected to L1 ---
82- let zone = ZoneTestNode :: start_from_l1 ( l1. http_url ( ) , l1. ws_url ( ) , portal_address) . await ?;
80+ // --- Step 4: Start zone node connected to L1, anchored at the portal genesis ---
81+ let zone =
82+ ZoneTestNode :: start_from_l1_portal_genesis ( l1. http_url ( ) , l1. ws_url ( ) , portal_address)
83+ . await ?;
8384
84- // --- Step 5: Wait for zone to catch up ---
85- zone. wait_for_l2_tempo_finalized ( 0 , STEPPING_TIMEOUT )
85+ // --- Step 5: Wait for the zone to replay to the first stepping boundary ---
86+ let first_step_tempo = genesis_block + EIP2935_EFFECTIVE_WINDOW ;
87+ zone. wait_for_tempo_block_number ( first_step_tempo, STEPPING_TIMEOUT )
8688 . await ?;
8789
88- // --- Step 6: Fund user and deposit ---
89- let mut account = ZoneAccount :: from_l1_and_zone ( & l1, & zone, portal_address) ;
90- l1. fund_user ( account. address ( ) , 2_000_000 ) . await ?;
91- account. deposit ( 1_000_000 , L1_TIMEOUT , & zone) . await ?;
92-
93- // --- Step 7: Spawn sequencer ---
94- let _seq = spawn_sequencer ( & l1, & zone, portal_address, l1. dev_signer ( ) ) . await ;
95-
96- // --- Step 8: Assert multiple BatchSubmitted events ---
97- // The gap exceeds the EIP-2935 window, so stepping must produce >=2 submitBatch calls.
98- let batch_timeout = Duration :: from_secs ( 60 ) ;
99- let batch_start = std:: time:: Instant :: now ( ) ;
100- let portal = ZonePortal :: new ( portal_address, l1. provider ( ) ) ;
101-
102- loop {
103- let events = portal. BatchSubmitted_filter ( ) . from_block ( 0 ) . query ( ) . await ?;
104-
105- let batch_count = events. len ( ) ;
106- if batch_count >= 2 {
107- tracing:: info!(
108- batch_count,
109- elapsed_secs = batch_start. elapsed( ) . as_secs( ) ,
110- "Stepping produced multiple batch submissions"
111- ) ;
112- break ;
113- }
114-
115- if batch_start. elapsed ( ) > batch_timeout {
116- return Err ( eyre:: eyre!(
117- "Expected >= 2 BatchSubmitted events from stepping, got {batch_count}"
118- ) ) ;
119- }
120- tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
121- }
122-
123- // --- Step 9: Verify withdrawal works through stepped batches ---
124- let withdrawal_amount: u128 = 500_000 ;
125- account. withdraw ( withdrawal_amount) . await ?;
90+ let l1_tip = l1. provider ( ) . get_block_number ( ) . await ?;
91+ eyre:: ensure!(
92+ l1_tip. saturating_sub( first_step_tempo) > EIP2935_HISTORY_WINDOW ,
93+ "test precondition not met: first step tempo {first_step_tempo} is only {} blocks behind L1 tip {l1_tip}" ,
94+ l1_tip. saturating_sub( first_step_tempo) ,
95+ ) ;
12696
127- l1. wait_for_withdrawal_on_l1 (
128- portal_address,
129- account. address ( ) ,
130- withdrawal_amount,
131- STEPPING_TIMEOUT ,
97+ // --- Step 6: Spawn sequencer while the zone still has a large backlog ---
98+ let seq = spawn_sequencer ( & l1, & zone, portal_address, l1. dev_signer ( ) ) . await ;
99+
100+ // --- Step 7: Assert batch submission succeeds after the long gap ---
101+ let batch_count = poll_until (
102+ BATCH_TIMEOUT ,
103+ Duration :: from_millis ( 500 ) ,
104+ "BatchSubmitted event after extended L1 gap" ,
105+ || {
106+ let portal = & portal;
107+ let seq = & seq;
108+ async move {
109+ if seq. monitor_handle . is_finished ( ) {
110+ eyre:: bail!( "monitor task exited before submitting a batch" ) ;
111+ }
112+
113+ if seq. withdrawal_handle . is_finished ( ) {
114+ eyre:: bail!( "withdrawal processor exited before batch submission completed" ) ;
115+ }
116+
117+ let events = portal. BatchSubmitted_filter ( ) . from_block ( 0 ) . query ( ) . await ?;
118+ let batch_count = events. len ( ) ;
119+ if batch_count >= 1 {
120+ Ok ( Some ( batch_count) )
121+ } else {
122+ Ok ( None )
123+ }
124+ }
125+ } ,
132126 )
133127 . await ?;
134128
135- // Verify L2 balance decreased
136- let l2_balance = zone
137- . balance_of ( ZONE_TOKEN_ADDRESS , account. address ( ) )
138- . await ?;
139- assert ! (
140- l2_balance <= alloy:: primitives:: U256 :: from( 1_000_000u128 - withdrawal_amount) ,
141- "L2 balance should decrease by at least the withdrawal amount (got {l2_balance})"
129+ tracing:: info!(
130+ batch_count,
131+ l1_tip,
132+ first_step_tempo,
133+ first_step_gap = l1_tip. saturating_sub( first_step_tempo) ,
134+ "Batch submission succeeded after extended L1 gap"
142135 ) ;
143136
144137 Ok ( ( ) )
0 commit comments