diff --git a/crates/node/tests/e2e.rs b/crates/node/tests/e2e.rs index e202318f..7ffa3e1d 100644 --- a/crates/node/tests/e2e.rs +++ b/crates/node/tests/e2e.rs @@ -36,6 +36,7 @@ use rollup_node_sequencer::L1MessageInclusionMode; use rollup_node_watcher::L1Notification; use scroll_alloy_consensus::TxL1Message; use scroll_alloy_rpc_types::Transaction as ScrollAlloyTransaction; +use scroll_db::L1MessageStart; use scroll_network::{NewBlockWithPeer, SCROLL_MAINNET}; use scroll_wire::{ScrollWireConfig, ScrollWireProtocolHandler}; use std::{path::PathBuf, sync::Arc, time::Duration}; @@ -1123,6 +1124,147 @@ async fn can_handle_l1_message_reorg() -> eyre::Result<()> { Ok(()) } +/// Tests that a follower node correctly rejects L2 blocks containing L1 messages it hasn't received +/// yet. +/// +/// This test verifies the security mechanism that prevents nodes from processing blocks with +/// unknown L1 messages, ensuring L2 chain consistency. +/// +/// # Test scenario +/// 1. Sets up two nodes: a sequencer and a follower +/// 2. The sequencer builds 10 initial blocks that are successfully imported by the follower +/// 3. An L1 message is sent only to the sequencer (not to the follower) +/// 4. The sequencer includes this L1 message in block 11 and continues building blocks up to block +/// 15 +/// 5. The follower detects the unknown L1 message and stops processing at block 10 +/// 6. Once the L1 message is finally sent to the follower, it can process the previously rejected +/// blocks +/// 7. The test confirms both nodes are synchronized at block 16 after the follower catches up +/// +/// # Key verification points +/// - The follower correctly identifies missing L1 messages with a `L1MessageMissingInDatabase` +/// event +/// - Block processing halts at the last valid block when an unknown L1 message is encountered +/// - The follower can resume processing and catch up once it receives the missing L1 message +/// - This prevents nodes from accepting blocks with L1 messages they cannot validate +#[tokio::test] +async fn can_reject_l2_block_with_unknown_l1_message() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + color_eyre::install()?; + let chain_spec = (*SCROLL_DEV).clone(); + + // Launch 2 nodes: node0=sequencer and node1=follower. + let config = default_sequencer_test_scroll_rollup_node_config(); + let (mut nodes, _tasks, _) = setup_engine(config, 2, chain_spec.clone(), false, false).await?; + let node0 = nodes.remove(0); + let node1 = nodes.remove(0); + + // Get handles + let node0_rnm_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone(); + let mut node0_rnm_events = node0_rnm_handle.get_event_listener().await?; + let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap(); + + let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); + let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?; + let node1_l1_watcher_tx = node1.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap(); + + // Let the sequencer build 10 blocks before performing the reorg process. + for i in 1..=10 { + node0_rnm_handle.build_block().await; + let b = wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + println!("Sequenced block {} {:?}", b.header.number, b.header.hash_slow()); + } + + // Assert that the follower node has received all 10 blocks from the sequencer node. + wait_for_block_imported_5s(&mut node1_rnm_events, 10).await?; + + // Send a L1 message and wait for it to be indexed. + let l1_message_notification = L1Notification::L1Message { + message: TxL1Message { + queue_index: 0, + gas_limit: 21000, + to: Default::default(), + value: Default::default(), + sender: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + input: Default::default(), + }, + block_number: 10, + block_timestamp: 0, + }; + + // Send the L1 message to the sequencer node but not to follower node. + node0_l1_watcher_tx.send(Arc::new(l1_message_notification.clone())).await?; + node0_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(10))).await?; + wait_for_event_5s( + &mut node0_rnm_events, + RollupManagerEvent::ChainOrchestratorEvent(ChainOrchestratorEvent::L1MessageCommitted(0)), + ) + .await?; + + // Build block that contains the L1 message. + node0_rnm_handle.build_block().await; + wait_for_event_predicate_5s(&mut node0_rnm_events, |e| { + if let RollupManagerEvent::BlockSequenced(block) = e { + if block.header.number == 11 && + block.body.transactions.len() == 1 && + block.body.transactions.iter().any(|tx| tx.is_l1_message()) + { + return true; + } + } + + false + }) + .await?; + + for i in 12..=15 { + node0_rnm_handle.build_block().await; + wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + } + + wait_for_event_5s( + &mut node1_rnm_events, + RollupManagerEvent::L1MessageMissingInDatabase { + start: L1MessageStart::Hash(b256!( + "0x0a2f8e75392ab51a26a2af835042c614eb141cd934fe1bdd4934c10f2fe17e98" + )), + }, + ) + .await?; + + // follower node should not import block 15 + // follower node doesn't know about the L1 message so stops processing the chain at block 10 + assert_eq!(latest_block(&node1).await?.header.number, 10); + + // Finally send L1 the L1 message to follower node. + node1_l1_watcher_tx.send(Arc::new(l1_message_notification)).await?; + node1_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(10))).await?; + wait_for_event_5s( + &mut node1_rnm_events, + RollupManagerEvent::ChainOrchestratorEvent(ChainOrchestratorEvent::L1MessageCommitted(0)), + ) + .await?; + + // Produce another block and send to follower node. + node0_rnm_handle.build_block().await; + wait_for_block_sequenced_5s(&mut node0_rnm_events, 16).await?; + + // Assert that the follower node has received the latest block from the sequencer node and + // processed the missing chain before. + // This is possible now because it has received the L1 message. + wait_for_block_imported_5s(&mut node1_rnm_events, 16).await?; + + // Assert both nodes are at block 16. + let node0_latest_block = latest_block(&node0).await?; + assert_eq!(node0_latest_block.header.number, 16); + assert_eq!( + node0_latest_block.header.hash_slow(), + latest_block(&node1).await?.header.hash_slow() + ); + + Ok(()) +} + #[tokio::test] async fn can_gossip_over_eth_wire() -> eyre::Result<()> { reth_tracing::init_test_tracing(); @@ -1416,6 +1558,7 @@ async fn wait_for_event_predicate( } Some(e) => { tracing::debug!(target: "TODO:nodeX", "ignoring event {:?}", e); + // println!("++++ ignoring event {:?}", e); }, // Ignore other events None => return Err(eyre::eyre!("Event stream ended unexpectedly")), }