diff --git a/Cargo.toml b/Cargo.toml index 3b9692393..4ed6ff3ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,11 +85,16 @@ jsonrpsee-core = "0.24" jsonrpsee-types = "0.24" # misc -async-trait = "0.1.83" cfg-if = "1" +async-trait = "0.1.83" +unsigned-varint = "0.8.0" spin = { version = "0.9.8", features = ["mutex"] } derive_more = { version = "1.0", default-features = false } +# tracing +tracing-subscriber = "0.3.18" +tracing = { version = "0.1.40", default-features = false } + ## misc-testing arbitrary = { version = "1.3", features = ["derive"] } arbtest = "0.3" @@ -98,7 +103,6 @@ thiserror = "1.0" proptest = "1.5" proptest-derive = "0.5" tokio = "1" -unsigned-varint = "0.8.0" ## crypto c-kzg = { version = "1.0", default-features = false } diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml index 7321e32a8..bc35b2511 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/protocol/Cargo.toml @@ -26,6 +26,7 @@ alloy-eips.workspace = true alloy-consensus.workspace = true # Misc +tracing.workspace = true derive_more.workspace = true async-trait.workspace = true unsigned-varint.workspace = true @@ -37,17 +38,28 @@ arbitrary = { workspace = true, features = ["derive"], optional = true } serde = { workspace = true, optional = true } alloy-serde = { workspace = true, optional = true } +# `test-utils` feature +spin = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["fmt"], optional = true } + [dev-dependencies] +spin.workspace = true proptest.workspace = true +tokio = { workspace = true, features = ["full"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } arbitrary = { workspace = true, features = ["derive"] } rand.workspace = true serde_json.workspace = true -rstest = "0.22.0" +rstest = "0.23.0" revm = "14.0.2" alloy-sol-types.workspace = true [features] default = ["serde", "std"] +test-utils = [ + "dep:spin", + "dep:tracing-subscriber", +] arbitrary = ["std", "dep:arbitrary", "alloy-consensus/arbitrary", "alloy-eips/arbitrary", "alloy-primitives/rand"] std = ["op-alloy-consensus/std", "op-alloy-genesis/std"] serde = ["dep:serde", "dep:alloy-serde", "op-alloy-consensus/serde", "op-alloy-genesis/serde"] diff --git a/crates/protocol/src/batch/span.rs b/crates/protocol/src/batch/span.rs index 769068d6d..a4ab51e29 100644 --- a/crates/protocol/src/batch/span.rs +++ b/crates/protocol/src/batch/span.rs @@ -1,11 +1,16 @@ //! The Span Batch Type -use crate::{ - BlockInfo, L2BlockInfo, SingleBatch, SpanBatchBits, SpanBatchElement, SpanBatchError, - SpanBatchTransactions, -}; use alloc::vec::Vec; +use alloy_eips::eip2718::Encodable2718; use alloy_primitives::FixedBytes; +use op_alloy_consensus::OpTxType; +use op_alloy_genesis::RollupConfig; +use tracing::{info, warn}; + +use crate::{ + BatchValidationProvider, BatchValidity, BlockInfo, L2BlockInfo, SingleBatch, SpanBatchBits, + SpanBatchElement, SpanBatchError, SpanBatchTransactions, +}; /// The span batch contains the input to build a span of L2 blocks in derived form. #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -142,13 +147,332 @@ impl SpanBatch { // Add the new transactions to the transaction cache. self.txs.add_txs(new_txs, self.chain_id) } + + /// Checks if the span batch is valid. + pub async fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + fetcher: &mut BV, + ) -> BatchValidity { + let (prefix_validity, parent_block) = + self.check_batch_prefix(cfg, l1_blocks, l2_safe_head, inclusion_block, fetcher).await; + if !matches!(prefix_validity, BatchValidity::Accept) { + return prefix_validity; + } + + let starting_epoch_num = self.starting_epoch_num(); + let parent_block = parent_block.expect("parent_block must be Some"); + + let mut origin_index = 0; + let mut origin_advanced = starting_epoch_num == parent_block.l1_origin.number + 1; + for (i, batch) in self.batches.iter().enumerate() { + if batch.timestamp <= l2_safe_head.block_info.timestamp { + continue; + } + // Find the L1 origin for the batch. + for (j, j_block) in l1_blocks.iter().enumerate().skip(origin_index) { + if batch.epoch_num == j_block.number { + origin_index = j; + break; + } + } + let l1_origin = l1_blocks[origin_index]; + if i > 0 { + origin_advanced = false; + if batch.epoch_num > self.batches[i - 1].epoch_num { + origin_advanced = true; + } + } + let block_timestamp = batch.timestamp; + if block_timestamp < l1_origin.timestamp { + warn!( + "block timestamp is less than L1 origin timestamp, l2_timestamp: {}, l1_timestamp: {}, origin: {:?}", + block_timestamp, + l1_origin.timestamp, + l1_origin.id() + ); + return BatchValidity::Drop; + } + + // Check if we ran out of sequencer time drift + let max_drift = cfg.max_sequencer_drift(l1_origin.timestamp); + if block_timestamp > l1_origin.timestamp + max_drift { + if batch.transactions.is_empty() { + // If the sequencer is co-operating by producing an empty batch, + // then allow the batch if it was the right thing to do to maintain the L2 time + // >= L1 time invariant. We only check batches that do not + // advance the epoch, to ensure epoch advancement regardless of time drift is + // allowed. + if !origin_advanced { + if origin_index + 1 >= l1_blocks.len() { + info!("without the next L1 origin we cannot determine yet if this empty batch that exceeds the time drift is still valid"); + return BatchValidity::Undecided; + } + if block_timestamp >= l1_blocks[origin_index + 1].timestamp { + // check if the next L1 origin could have been adopted + info!("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid"); + return BatchValidity::Drop; + } else { + info!("continuing with empty batch before late L1 block to preserve L2 time invariant"); + } + } + } else { + // If the sequencer is ignoring the time drift rule, then drop the batch and + // force an empty batch instead, as the sequencer is not + // allowed to include anything past this point without moving to the next epoch. + warn!( + "batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again, max_time: {}", + l1_origin.timestamp + max_drift + ); + return BatchValidity::Drop; + } + } + + // Check that the transactions are not empty and do not contain any deposits. + for (tx_index, tx_bytes) in batch.transactions.iter().enumerate() { + if tx_bytes.is_empty() { + warn!( + "transaction data must not be empty, but found empty tx, tx_index: {}", + tx_index + ); + return BatchValidity::Drop; + } + if tx_bytes.0[0] == OpTxType::Deposit as u8 { + warn!("sequencers may not embed any deposits into batch data, but found tx that has one, tx_index: {}", tx_index); + return BatchValidity::Drop; + } + } + } + + // Check overlapped blocks + let parent_num = parent_block.block_info.number; + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + if self.starting_timestamp() < next_timestamp { + for i in 0..(l2_safe_head.block_info.number - parent_num) { + let safe_block_num = parent_num + i + 1; + let safe_block_payload = match fetcher.block_by_number(safe_block_num).await { + Ok(p) => p, + Err(e) => { + warn!("failed to fetch block number {safe_block_num}: {e}"); + return BatchValidity::Undecided; + } + }; + let safe_block = &safe_block_payload.body; + let batch_txs = &self.batches[i as usize].transactions; + // Execution payload has deposit txs but batch does not. + let deposit_count: usize = safe_block + .transactions + .iter() + .map(|tx| if tx.is_deposit() { 1 } else { 0 }) + .sum(); + if safe_block.transactions.len() - deposit_count != batch_txs.len() { + warn!( + "overlapped block's tx count does not match, safe_block_txs: {}, batch_txs: {}", + safe_block.transactions.len(), + batch_txs.len() + ); + return BatchValidity::Drop; + } + let batch_txs_len = batch_txs.len(); + #[allow(clippy::needless_range_loop)] + for j in 0..batch_txs_len { + let mut buf = Vec::new(); + safe_block.transactions[j + deposit_count].encode_2718(&mut buf); + if buf != batch_txs[j].0 { + warn!("overlapped block's transaction does not match"); + return BatchValidity::Drop; + } + } + let safe_block_ref = match L2BlockInfo::from_block_and_genesis( + &safe_block_payload, + &cfg.genesis, + ) { + Ok(r) => r, + Err(e) => { + warn!("failed to extract L2BlockInfo from execution payload, hash: {}, err: {e}", safe_block_payload.header.hash_slow()); + return BatchValidity::Drop; + } + }; + if safe_block_ref.l1_origin.number != self.batches[i as usize].epoch_num { + warn!( + "overlapped block's L1 origin number does not match {}, {}", + safe_block_ref.l1_origin.number, self.batches[i as usize].epoch_num + ); + return BatchValidity::Drop; + } + } + } + + BatchValidity::Accept + } + + /// Checks the validity of the batch's prefix. + /// + /// This function is used for post-Holocene hardfork to perform batch validation + /// as each batch is being loaded in. + pub async fn check_batch_prefix( + &self, + cfg: &RollupConfig, + l1_origins: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + fetcher: &mut BF, + ) -> (BatchValidity, Option) { + if l1_origins.is_empty() { + warn!("missing L1 block input, cannot proceed with batch checking"); + return (BatchValidity::Undecided, None); + } + if self.batches.is_empty() { + warn!("empty span batch, cannot proceed with batch checking"); + return (BatchValidity::Undecided, None); + } + + let epoch = l1_origins[0]; + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + + let starting_epoch_num = self.starting_epoch_num(); + let mut batch_origin = epoch; + if starting_epoch_num == batch_origin.number + 1 { + if l1_origins.len() < 2 { + info!("eager batch wants to advance current epoch {:?}, but could not without more L1 blocks", epoch.id()); + return (BatchValidity::Undecided, None); + } + batch_origin = l1_origins[1]; + } + if !cfg.is_delta_active(batch_origin.timestamp) { + warn!( + "received SpanBatch (id {:?}) with L1 origin (timestamp {}) before Delta hard fork", + batch_origin.id(), + batch_origin.timestamp + ); + return (BatchValidity::Drop, None); + } + + if self.starting_timestamp() > next_timestamp { + warn!( + "received out-of-order batch for future processing after next batch ({} > {})", + self.starting_timestamp(), + next_timestamp + ); + + // After holocene is activated, gaps are disallowed. + if cfg.is_holocene_active(inclusion_block.timestamp) { + return (BatchValidity::Drop, None); + } + return (BatchValidity::Future, None); + } + + // Drop the batch if it has no new blocks after the safe head. + if self.final_timestamp() < next_timestamp { + warn!("span batch has no new blocks after safe head"); + return if cfg.is_holocene_active(inclusion_block.timestamp) { + (BatchValidity::Past, None) + } else { + (BatchValidity::Drop, None) + }; + } + + // Find the parent block of the span batch. + // If the span batch does not overlap the current safe chain, parent block should be the L2 + // safe head. + let mut parent_num = l2_safe_head.block_info.number; + let mut parent_block = l2_safe_head; + if self.starting_timestamp() < next_timestamp { + if self.starting_timestamp() > l2_safe_head.block_info.timestamp { + // Batch timestamp cannot be between safe head and next timestamp. + warn!("batch has misaligned timestamp, block time is too short"); + return (BatchValidity::Drop, None); + } + if (l2_safe_head.block_info.timestamp - self.starting_timestamp()) % cfg.block_time != 0 + { + warn!("batch has misaligned timestamp, not overlapped exactly"); + return (BatchValidity::Drop, None); + } + parent_num = l2_safe_head.block_info.number + - (l2_safe_head.block_info.timestamp - self.starting_timestamp()) / cfg.block_time + - 1; + parent_block = match fetcher.l2_block_info_by_number(parent_num).await { + Ok(block) => block, + Err(e) => { + warn!("failed to fetch L2 block number {parent_num}: {e}"); + // Unable to validate the batch for now. Retry later. + return (BatchValidity::Undecided, None); + } + }; + } + if !self.check_parent_hash(parent_block.block_info.hash) { + warn!( + "parent block mismatch, expected: {parent_num}, received: {}. parent hash: {}, parent hash check: {}", + parent_block.block_info.number, + parent_block.block_info.hash, + self.parent_check, + ); + return (BatchValidity::Drop, None); + } + + // Filter out batches that were included too late. + if starting_epoch_num + cfg.seq_window_size < inclusion_block.number { + warn!("batch was included too late, sequence window expired"); + return (BatchValidity::Drop, None); + } + + // Check the L1 origin of the batch + if starting_epoch_num > parent_block.l1_origin.number + 1 { + warn!( + "batch is for future epoch too far ahead, while it has the next timestamp, so it must be invalid. starting epoch: {} | next epoch: {}", + starting_epoch_num, + parent_block.l1_origin.number + 1 + ); + return (BatchValidity::Drop, None); + } + + // Verify the l1 origin hash for each l1 block. + // SAFETY: `Self::batches` is not empty, so the last element is guaranteed to exist. + let end_epoch_num = self.batches.last().unwrap().epoch_num; + let mut origin_checked = false; + // l1Blocks is supplied from batch queue and its length is limited to SequencerWindowSize. + for l1_block in l1_origins { + if l1_block.number == end_epoch_num { + if !self.check_origin_hash(l1_block.hash) { + warn!( + "batch is for different L1 chain, epoch hash does not match, expected: {}", + l1_block.hash + ); + return (BatchValidity::Drop, None); + } + origin_checked = true; + break; + } + } + if !origin_checked { + info!("need more l1 blocks to check entire origins of span batch"); + return (BatchValidity::Undecided, None); + } + + if starting_epoch_num < parent_block.l1_origin.number { + warn!("dropped batch, epoch is too old, minimum: {:?}", parent_block.block_info.id()); + return (BatchValidity::Drop, None); + } + + (BatchValidity::Accept, Some(parent_block)) + } } #[cfg(test)] mod tests { use super::*; + use crate::test_utils::{CollectingLayer, TestBatchValidator, TraceStorage}; use alloc::vec; - use alloy_primitives::b256; + use alloy_consensus::Header; + use alloy_eips::BlockNumHash; + use alloy_primitives::{b256, Bytes}; + use op_alloy_consensus::{OpBlock, OpTxType}; + use op_alloy_genesis::ChainGenesis; + use tracing::Level; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[test] fn test_timestamp() { @@ -207,4 +531,1112 @@ mod tests { let invalid = b256!("1111111111111111111111111111111111111100000000000000000000000000"); assert!(!batch.check_parent_hash(invalid)); } + + #[tokio::test] + async fn test_check_batch_missing_l1_block_input() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let l1_blocks = vec![]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let batch = SpanBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("missing L1 block input, cannot proceed with batch checking")); + } + + #[tokio::test] + async fn test_check_batches_is_empty() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let l1_blocks = vec![BlockInfo::default()]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let batch = SpanBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("empty span batch, cannot proceed with batch checking")); + } + + #[tokio::test] + async fn test_eager_block_missing_origins() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let block = BlockInfo { number: 9, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "eager batch wants to advance current epoch {:?}, but could not without more L1 blocks", + block.id() + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_delta_inactive() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(10), ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 9, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "received SpanBatch (id {:?}) with L1 origin (timestamp {}) before Delta hard fork", + block.id(), + block.timestamp + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_out_of_order() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 21, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Future + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains( + "received out-of-order batch for future processing after next batch (21 > 20)" + )); + } + + #[tokio::test] + async fn test_check_batch_no_new_blocks() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("span batch has no new blocks after safe head")); + } + + #[tokio::test] + async fn test_check_batch_misaligned_timestamp() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 11, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 21, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch has misaligned timestamp, block time is too short")); + } + + #[tokio::test] + async fn test_check_batch_misaligned_without_overlap() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 8, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch has misaligned timestamp, not overlapped exactly")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_fetch_l2_block() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("failed to fetch L2 block number 40: Block not found")); + } + + #[tokio::test] + async fn test_check_batch_parent_hash_fail() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + fetcher.short_circuit = true; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice( + &b256!("1111111111111111111111111111111111111111000000000000000000000000")[..20], + ), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("parent block mismatch, expected: 40, received: 41")); + } + + #[tokio::test] + async fn test_check_sequence_window_expired() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch was included too late, sequence window expired")); + } + + #[tokio::test] + async fn test_starting_epoch_too_far_ahead() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 8, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 8, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = "batch is for future epoch too far ahead, while it has the next timestamp, so it must be invalid. starting epoch: 10 | next epoch: 9"; + assert!(logs[0].contains(str)); + } + + #[tokio::test] + async fn test_check_batch_epoch_hash_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "batch is for different L1 chain, epoch hash does not match, expected: {}", + l1_block_hash, + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_need_more_l1_blocks() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 10, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("need more l1 blocks to check entire origins of span batch")); + } + + #[tokio::test] + async fn test_drop_batch_epoch_too_old() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 13, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 14, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "dropped batch, epoch is too old, minimum: {:?}", + l2_block.block_info.id(), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_exceeds_max_seq_drif() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid")); + } + + #[tokio::test] + async fn test_continuing_with_empty_batch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, transactions: vec![] }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 20, transactions: vec![] }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Accept + ); + let infos = trace_store.get_by_level(Level::INFO); + assert_eq!(infos.len(), 1); + assert!(infos[0].contains( + "continuing with empty batch before late L1 block to preserve L2 time invariant" + )); + } + + #[tokio::test] + async fn test_check_batch_exceeds_sequencer_time_drift() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let third = SpanBatchElement { + epoch_num: 11, + timestamp: 20, + transactions: vec![Default::default()], + }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again, max_time: 10")); + } + + #[tokio::test] + async fn test_check_batch_empty_txs() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("transaction data must not be empty, but found empty tx")); + } + + #[tokio::test] + async fn test_check_batch_with_deposit_tx() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let filler_bytes = Bytes::copy_from_slice(&[OpTxType::Eip1559 as u8]); + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![filler_bytes.clone()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Bytes::copy_from_slice(&[OpTxType::Deposit as u8])], + }; + let third = + SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![filler_bytes] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("sequencers may not embed any deposits into batch data, but found tx that has one, tx_index: 0")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_fetch_payload() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("failed to fetch block number 41: L2 Block not found")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_extract_l2_block_info() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "failed to extract L2BlockInfo from execution payload, hash: {:?}", + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_overlapped_blocks_origin_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let payload_block_hash = + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"); + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 41, hash: payload_block_hash }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("overlapped block's L1 origin number does not match")); + } + + #[tokio::test] + async fn test_check_batch_valid_with_genesis_epoch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let payload_block_hash = + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"); + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 41, hash: payload_block_hash }, + l1: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Accept + ); + assert!(trace_store.is_empty()); + } } diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index b14eecffb..b6bc3a8e4 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -58,3 +58,6 @@ pub use fee::{ calculate_tx_l1_cost_bedrock, calculate_tx_l1_cost_ecotone, calculate_tx_l1_cost_fjord, calculate_tx_l1_cost_regolith, data_gas_bedrock, data_gas_fjord, data_gas_regolith, }; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/crates/protocol/src/test_utils.rs b/crates/protocol/src/test_utils.rs new file mode 100644 index 000000000..2cea48ac4 --- /dev/null +++ b/crates/protocol/src/test_utils.rs @@ -0,0 +1,119 @@ +//! Test utilities for the protocol crate. + +use alloc::{boxed::Box, format, string::String, sync::Arc, vec::Vec}; +use async_trait::async_trait; +use op_alloy_consensus::OpBlock; +use spin::Mutex; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::{layer::Context, Layer}; + +use crate::{BatchValidationProvider, L2BlockInfo}; + +/// An error for implementations of the [BatchValidationProvider] trait. +#[derive(Debug, derive_more::Display)] +pub enum TestBatchValidatorError { + /// The block was not found. + #[display("Block not found")] + BlockNotFound, + /// The L2 block was not found. + #[display("L2 Block not found")] + L2BlockNotFound, +} + +impl core::error::Error for TestBatchValidatorError {} + +/// An [TestBatchValidator] implementation for testing. +#[derive(Debug, Default, Clone)] +pub struct TestBatchValidator { + /// Blocks + pub blocks: Vec, + /// Short circuit the block return to be the first block. + pub short_circuit: bool, + /// Blocks + pub op_blocks: Vec, +} + +impl TestBatchValidator { + /// Creates a new []TestBatchValidator with the given origin and batches. + pub const fn new(blocks: Vec, op_blocks: Vec) -> Self { + Self { blocks, short_circuit: false, op_blocks } + } +} + +#[async_trait] +impl BatchValidationProvider for TestBatchValidator { + type Error = TestBatchValidatorError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + if self.short_circuit { + return self + .blocks + .first() + .copied() + .ok_or_else(|| TestBatchValidatorError::BlockNotFound); + } + self.blocks + .iter() + .find(|b| b.block_info.number == number) + .cloned() + .ok_or_else(|| TestBatchValidatorError::BlockNotFound) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + self.op_blocks + .iter() + .find(|p| p.header.number == number) + .cloned() + .ok_or_else(|| TestBatchValidatorError::L2BlockNotFound) + } +} + +/// The storage for the collected traces. +#[derive(Debug, Default, Clone)] +pub struct TraceStorage(pub Arc>>); + +impl TraceStorage { + /// Returns the items in the storage that match the specified level. + pub fn get_by_level(&self, level: Level) -> Vec { + self.0 + .lock() + .iter() + .filter_map(|(l, message)| if *l == level { Some(message.clone()) } else { None }) + .collect() + } + + /// Locks the storage and returns the items. + pub fn lock(&self) -> spin::MutexGuard<'_, Vec<(Level, String)>> { + self.0.lock() + } + + /// Returns if the storage is empty. + pub fn is_empty(&self) -> bool { + self.0.lock().is_empty() + } +} + +/// A subscriber layer that collects traces and their log levels. +#[derive(Debug, Default)] +pub struct CollectingLayer { + /// The storage for the collected traces. + pub storage: TraceStorage, +} + +impl CollectingLayer { + /// Creates a new collecting layer with the specified storage. + pub const fn new(storage: TraceStorage) -> Self { + Self { storage } + } +} + +impl Layer for CollectingLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = *metadata.level(); + let message = format!("{:?}", event); + + let mut storage = self.storage.0.lock(); + storage.push((level, message)); + } +}