Skip to content

Commit 892a314

Browse files
jonastheisgreged93
andauthored
feat(e2e test): check that L2 block with unknown L1 message is rejected by follower node (#255)
* Refactor `can_handle_reorgs_while_sequencing` test to also include a follower node and add a bunch of convenience methods * feat: attach L1 block number to ScrollPayloadAttributes * feat: correctly handle L1 reorgs in driver * adjust can_handle_l1_message_reorg test to assert correct reorg conditions * fix linter errors * make sequencer not issue blocks by default in tests * improve test * add test can_reject_l2_block_with_unknown_l1_message * finish test * replace println with logging --------- Co-authored-by: Gregory Edison <[email protected]>
1 parent 00a8898 commit 892a314

File tree

1 file changed

+145
-2
lines changed

1 file changed

+145
-2
lines changed

crates/node/tests/e2e.rs

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use rollup_node_sequencer::L1MessageInclusionMode;
3636
use rollup_node_watcher::L1Notification;
3737
use scroll_alloy_consensus::TxL1Message;
3838
use scroll_alloy_rpc_types::Transaction as ScrollAlloyTransaction;
39+
use scroll_db::L1MessageStart;
3940
use scroll_network::{NewBlockWithPeer, SCROLL_MAINNET};
4041
use scroll_wire::{ScrollWireConfig, ScrollWireProtocolHandler};
4142
use std::{path::PathBuf, sync::Arc, time::Duration};
@@ -1180,7 +1181,7 @@ async fn can_handle_l1_message_reorg() -> eyre::Result<()> {
11801181
for i in 1..=10 {
11811182
node0_rnm_handle.build_block().await;
11821183
let b = wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?;
1183-
println!("Sequenced block {} {:?}", b.header.number, b.header.hash_slow());
1184+
tracing::info!(target: "scroll::test", block_number = ?b.header.number, block_hash = ?b.header.hash_slow(), "Sequenced block");
11841185
}
11851186

11861187
// Assert that the follower node has received all 10 blocks from the sequencer node.
@@ -1281,6 +1282,147 @@ async fn can_handle_l1_message_reorg() -> eyre::Result<()> {
12811282
Ok(())
12821283
}
12831284

1285+
/// Tests that a follower node correctly rejects L2 blocks containing L1 messages it hasn't received
1286+
/// yet.
1287+
///
1288+
/// This test verifies the security mechanism that prevents nodes from processing blocks with
1289+
/// unknown L1 messages, ensuring L2 chain consistency.
1290+
///
1291+
/// # Test scenario
1292+
/// 1. Sets up two nodes: a sequencer and a follower
1293+
/// 2. The sequencer builds 10 initial blocks that are successfully imported by the follower
1294+
/// 3. An L1 message is sent only to the sequencer (not to the follower)
1295+
/// 4. The sequencer includes this L1 message in block 11 and continues building blocks up to block
1296+
/// 15
1297+
/// 5. The follower detects the unknown L1 message and stops processing at block 10
1298+
/// 6. Once the L1 message is finally sent to the follower, it can process the previously rejected
1299+
/// blocks
1300+
/// 7. The test confirms both nodes are synchronized at block 16 after the follower catches up
1301+
///
1302+
/// # Key verification points
1303+
/// - The follower correctly identifies missing L1 messages with a `L1MessageMissingInDatabase`
1304+
/// event
1305+
/// - Block processing halts at the last valid block when an unknown L1 message is encountered
1306+
/// - The follower can resume processing and catch up once it receives the missing L1 message
1307+
/// - This prevents nodes from accepting blocks with L1 messages they cannot validate
1308+
#[tokio::test]
1309+
async fn can_reject_l2_block_with_unknown_l1_message() -> eyre::Result<()> {
1310+
reth_tracing::init_test_tracing();
1311+
color_eyre::install()?;
1312+
let chain_spec = (*SCROLL_DEV).clone();
1313+
1314+
// Launch 2 nodes: node0=sequencer and node1=follower.
1315+
let config = default_sequencer_test_scroll_rollup_node_config();
1316+
let (mut nodes, _tasks, _) = setup_engine(config, 2, chain_spec.clone(), false, false).await?;
1317+
let node0 = nodes.remove(0);
1318+
let node1 = nodes.remove(0);
1319+
1320+
// Get handles
1321+
let node0_rnm_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone();
1322+
let mut node0_rnm_events = node0_rnm_handle.get_event_listener().await?;
1323+
let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap();
1324+
1325+
let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone();
1326+
let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?;
1327+
let node1_l1_watcher_tx = node1.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap();
1328+
1329+
// Let the sequencer build 10 blocks before performing the reorg process.
1330+
for i in 1..=10 {
1331+
node0_rnm_handle.build_block().await;
1332+
let b = wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?;
1333+
tracing::info!(target: "scroll::test", block_number = ?b.header.number, block_hash = ?b.header.hash_slow(), "Sequenced block")
1334+
}
1335+
1336+
// Assert that the follower node has received all 10 blocks from the sequencer node.
1337+
wait_for_block_imported_5s(&mut node1_rnm_events, 10).await?;
1338+
1339+
// Send a L1 message and wait for it to be indexed.
1340+
let l1_message_notification = L1Notification::L1Message {
1341+
message: TxL1Message {
1342+
queue_index: 0,
1343+
gas_limit: 21000,
1344+
to: Default::default(),
1345+
value: Default::default(),
1346+
sender: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
1347+
input: Default::default(),
1348+
},
1349+
block_number: 10,
1350+
block_timestamp: 0,
1351+
};
1352+
1353+
// Send the L1 message to the sequencer node but not to follower node.
1354+
node0_l1_watcher_tx.send(Arc::new(l1_message_notification.clone())).await?;
1355+
node0_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(10))).await?;
1356+
wait_for_event_5s(
1357+
&mut node0_rnm_events,
1358+
RollupManagerEvent::ChainOrchestratorEvent(ChainOrchestratorEvent::L1MessageCommitted(0)),
1359+
)
1360+
.await?;
1361+
1362+
// Build block that contains the L1 message.
1363+
node0_rnm_handle.build_block().await;
1364+
wait_for_event_predicate_5s(&mut node0_rnm_events, |e| {
1365+
if let RollupManagerEvent::BlockSequenced(block) = e {
1366+
if block.header.number == 11 &&
1367+
block.body.transactions.len() == 1 &&
1368+
block.body.transactions.iter().any(|tx| tx.is_l1_message())
1369+
{
1370+
return true;
1371+
}
1372+
}
1373+
1374+
false
1375+
})
1376+
.await?;
1377+
1378+
for i in 12..=15 {
1379+
node0_rnm_handle.build_block().await;
1380+
wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?;
1381+
}
1382+
1383+
wait_for_event_5s(
1384+
&mut node1_rnm_events,
1385+
RollupManagerEvent::L1MessageMissingInDatabase {
1386+
start: L1MessageStart::Hash(b256!(
1387+
"0x0a2f8e75392ab51a26a2af835042c614eb141cd934fe1bdd4934c10f2fe17e98"
1388+
)),
1389+
},
1390+
)
1391+
.await?;
1392+
1393+
// follower node should not import block 15
1394+
// follower node doesn't know about the L1 message so stops processing the chain at block 10
1395+
assert_eq!(latest_block(&node1).await?.header.number, 10);
1396+
1397+
// Finally send L1 the L1 message to follower node.
1398+
node1_l1_watcher_tx.send(Arc::new(l1_message_notification)).await?;
1399+
node1_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(10))).await?;
1400+
wait_for_event_5s(
1401+
&mut node1_rnm_events,
1402+
RollupManagerEvent::ChainOrchestratorEvent(ChainOrchestratorEvent::L1MessageCommitted(0)),
1403+
)
1404+
.await?;
1405+
1406+
// Produce another block and send to follower node.
1407+
node0_rnm_handle.build_block().await;
1408+
wait_for_block_sequenced_5s(&mut node0_rnm_events, 16).await?;
1409+
1410+
// Assert that the follower node has received the latest block from the sequencer node and
1411+
// processed the missing chain before.
1412+
// This is possible now because it has received the L1 message.
1413+
wait_for_block_imported_5s(&mut node1_rnm_events, 16).await?;
1414+
1415+
// Assert both nodes are at block 16.
1416+
let node0_latest_block = latest_block(&node0).await?;
1417+
assert_eq!(node0_latest_block.header.number, 16);
1418+
assert_eq!(
1419+
node0_latest_block.header.hash_slow(),
1420+
latest_block(&node1).await?.header.hash_slow()
1421+
);
1422+
1423+
Ok(())
1424+
}
1425+
12841426
#[tokio::test]
12851427
async fn can_gossip_over_eth_wire() -> eyre::Result<()> {
12861428
reth_tracing::init_test_tracing();
@@ -1570,10 +1712,11 @@ async fn wait_for_event_predicate(
15701712
maybe_event = event_stream.next() => {
15711713
match maybe_event {
15721714
Some(e) if predicate(e.clone()) => {
1715+
tracing::debug!(target: "scroll::test", event = ?e, "Received event");
15731716
return Ok(());
15741717
}
15751718
Some(e) => {
1576-
tracing::debug!(target: "TODO:nodeX", "ignoring event {:?}", e);
1719+
tracing::debug!(target: "scroll::test", event = ?e, "Ignoring event");
15771720
}, // Ignore other events
15781721
None => return Err(eyre::eyre!("Event stream ended unexpectedly")),
15791722
}

0 commit comments

Comments
 (0)