Skip to content

Commit ce1eeb3

Browse files
authored
fix: allow ancestry past the EIP-2935 history window (#414)
1 parent a1620eb commit ce1eeb3

3 files changed

Lines changed: 136 additions & 95 deletions

File tree

crates/tempo-zone/src/batch.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -315,14 +315,6 @@ impl BatchSubmitter {
315315
return Ok(AnchorMode::Direct);
316316
}
317317

318-
if gap > EIP2935_HISTORY_WINDOW {
319-
return Err(eyre::eyre!(
320-
"tempo_block_number ({tempo_block_number}) is outside the EIP-2935 history \
321-
window (gap={gap}, max={EIP2935_HISTORY_WINDOW}) — must split via stepping"
322-
));
323-
}
324-
325-
// Within ancestry range — collect L1 headers as proof chain.
326318
let anchor_block = current_l1_block.saturating_sub(EIP2935_SAFETY_MARGIN);
327319
let ancestry_headers = self
328320
.fetch_ancestry_headers(tempo_block_number, anchor_block)

crates/tempo-zone/tests/it/stepping_e2e.rs

Lines changed: 78 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
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};
89
use alloy::providers::Provider;
910
use 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"]
3637
async 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(())

crates/tempo-zone/tests/it/utils.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,38 @@ impl ZoneTestNode {
403403
.await
404404
}
405405

406+
/// Start a zone node connected to a real L1, anchoring genesis to the
407+
/// portal's on-chain `genesisTempoBlockNumber`.
408+
///
409+
/// Unlike [`start_from_l1`], this preserves the full replay gap between the
410+
/// portal genesis and the current L1 tip, which is useful for long-downtime
411+
/// catch-up tests.
412+
pub(crate) async fn start_from_l1_portal_genesis(
413+
l1_http_url: &url::Url,
414+
l1_ws_url: &url::Url,
415+
portal_address: Address,
416+
) -> eyre::Result<Self> {
417+
let l1_provider =
418+
ProviderBuilder::new_with_network::<TempoNetwork>().connect_http(l1_http_url.clone());
419+
let portal = zone::abi::ZonePortal::new(portal_address, &l1_provider);
420+
let genesis_block_number = portal.genesisTempoBlockNumber().call().await?;
421+
let (genesis, genesis_block_number) =
422+
build_l1_anchored_genesis_at_block(l1_http_url, portal_address, genesis_block_number)
423+
.await?;
424+
425+
let throwaway_key = k256::SecretKey::from_slice(&[0x01; 32]).expect("valid throwaway key");
426+
Self::launch_with_genesis(
427+
l1_ws_url.to_string(),
428+
portal_address,
429+
Some(genesis_block_number),
430+
next_unique_chain_id(),
431+
Some(genesis),
432+
Address::ZERO,
433+
throwaway_key,
434+
)
435+
.await
436+
}
437+
406438
/// Start a zone node connected to a real L1, with a sequencer key for ECIES decryption.
407439
///
408440
/// Same as [`start_from_l1`] but passes the sequencer key through to `ZoneNode::new`
@@ -1542,8 +1574,6 @@ async fn build_l1_anchored_genesis(
15421574
l1_http_url: &url::Url,
15431575
portal_address: Address,
15441576
) -> eyre::Result<(Genesis, u64)> {
1545-
use alloy_primitives::address;
1546-
15471577
let l1_provider =
15481578
ProviderBuilder::new_with_network::<TempoNetwork>().connect_http(l1_http_url.clone());
15491579

@@ -1552,6 +1582,32 @@ async fn build_l1_anchored_genesis(
15521582
.await?
15531583
.ok_or_else(|| eyre::eyre!("L1 latest block not found"))?;
15541584
let l1_header: &TempoHeader = block.header.as_ref();
1585+
build_l1_anchored_genesis_from_header(l1_header, portal_address)
1586+
}
1587+
1588+
/// Build a zone test genesis anchored to a specific L1 block number.
1589+
async fn build_l1_anchored_genesis_at_block(
1590+
l1_http_url: &url::Url,
1591+
portal_address: Address,
1592+
block_number: u64,
1593+
) -> eyre::Result<(Genesis, u64)> {
1594+
let l1_provider =
1595+
ProviderBuilder::new_with_network::<TempoNetwork>().connect_http(l1_http_url.clone());
1596+
1597+
let block = l1_provider
1598+
.get_block_by_number(block_number.into())
1599+
.await?
1600+
.ok_or_else(|| eyre::eyre!("L1 block {block_number} not found"))?;
1601+
let l1_header: &TempoHeader = block.header.as_ref();
1602+
build_l1_anchored_genesis_from_header(l1_header, portal_address)
1603+
}
1604+
1605+
fn build_l1_anchored_genesis_from_header(
1606+
l1_header: &TempoHeader,
1607+
portal_address: Address,
1608+
) -> eyre::Result<(Genesis, u64)> {
1609+
use alloy_primitives::address;
1610+
15551611
let genesis_block_number = l1_header.inner.number;
15561612

15571613
let mut rlp_buf = Vec::new();

0 commit comments

Comments
 (0)