From fe938c7affd034907205f5d2aa01602466d6d654 Mon Sep 17 00:00:00 2001 From: Sebastian Stammler Date: Wed, 19 Nov 2025 22:41:35 +0100 Subject: [PATCH] fix(protocol/derive): fix singular batch extraction error handling --- .../derive/src/stages/batch/batch_stream.rs | 123 +++++++-- crates/protocol/protocol/src/batch/errors.rs | 3 + crates/protocol/protocol/src/batch/span.rs | 259 ++++++++++++------ 3 files changed, 287 insertions(+), 98 deletions(-) diff --git a/crates/protocol/derive/src/stages/batch/batch_stream.rs b/crates/protocol/derive/src/stages/batch/batch_stream.rs index 2fd6db0ac6..1039248d39 100644 --- a/crates/protocol/derive/src/stages/batch/batch_stream.rs +++ b/crates/protocol/derive/src/stages/batch/batch_stream.rs @@ -1,8 +1,8 @@ //! This module contains the `BatchStream` stage. use crate::{ - L2ChainProvider, NextBatchProvider, OriginAdvancer, OriginProvider, PipelineEncodingError, - PipelineError, PipelineResult, Signal, SignalReceiver, + L2ChainProvider, NextBatchProvider, OriginAdvancer, OriginProvider, PipelineError, + PipelineResult, Signal, SignalReceiver, }; use alloc::{boxed::Box, collections::VecDeque, sync::Arc}; use async_trait::async_trait; @@ -10,6 +10,7 @@ use core::fmt::Debug; use kona_genesis::RollupConfig; use kona_protocol::{ Batch, BatchValidity, BatchWithInclusionBlock, BlockInfo, L2BlockInfo, SingleBatch, SpanBatch, + SpanBatchError, }; /// Provides [`Batch`]es for the [`BatchStream`] stage. @@ -72,11 +73,11 @@ where &mut self, parent: L2BlockInfo, l1_origins: &[BlockInfo], - ) -> PipelineResult { + ) -> Result, SpanBatchError> { trace!(target: "batch_span", "Attempting to get a SingleBatch from buffer len: {}", self.buffer.len()); self.try_hydrate_buffer(parent, l1_origins)?; - self.buffer.pop_front().ok_or_else(|| PipelineError::NotEnoughData.temp()) + Ok(self.buffer.pop_front()) } /// Hydrates the buffer with single batches derived from the span batch, if there is one @@ -85,19 +86,17 @@ where &mut self, parent: L2BlockInfo, l1_origins: &[BlockInfo], - ) -> PipelineResult<()> { + ) -> Result<(), SpanBatchError> { if let Some(span) = self.span.take() { - self.buffer.extend( - span.get_singular_batches(l1_origins, parent).map_err(|e| { - PipelineError::BadEncoding(PipelineEncodingError::from(e)).crit() - })?, - ); + self.buffer.extend(span.get_singular_batches(l1_origins, parent)?); } - let batch_count = self.buffer.len() as f64; - kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_BUFFER, batch_count); #[cfg(feature = "metrics")] - let batch_size = std::mem::size_of_val(&self.buffer) as f64; - kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_MEM, batch_size); + { + let batch_count = self.buffer.len() as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_BUFFER, batch_count); + let batch_size = std::mem::size_of_val(&self.buffer) as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_MEM, batch_size); + } Ok(()) } } @@ -194,7 +193,17 @@ where } // Attempt to pull a SingleBatch out of the SpanBatch. - self.get_single_batch(parent, l1_origins).map(Batch::Single) + match self.get_single_batch(parent, l1_origins) { + Ok(Some(single_batch)) => Ok(Batch::Single(single_batch)), + Ok(None) => Err(PipelineError::NotEnoughData.temp()), + Err(e) => { + warn!(target: "batch_span", "Extracting singular batches from span batch failed: {}", e); + // If singular batch extraction fails, it should be handled the same as a + // dropped batch during span batch prefix checks. + self.flush(); + Err(PipelineError::NotEnoughData.temp()) + } + } } } @@ -241,9 +250,12 @@ mod test { types::ResetSignal, }; use alloc::vec; - use alloy_eips::NumHash; - use kona_genesis::HardForkConfig; + use alloy_consensus::{BlockBody, Header}; + use alloy_eips::{BlockNumHash, NumHash}; + use alloy_primitives::{FixedBytes, b256}; + use kona_genesis::{ChainGenesis, HardForkConfig}; use kona_protocol::{SingleBatch, SpanBatchElement}; + use op_alloy_consensus::OpBlock; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::test] @@ -399,6 +411,83 @@ mod test { assert!(stream.span.is_none()); } + #[tokio::test] + async fn test_span_batch_extraction_error_flushes_stage() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let config = Arc::new(RollupConfig { + seq_window_size: 100, + block_time: 10, + hardforks: HardForkConfig { + delta_time: Some(0), + holocene_time: Some(0), + ..Default::default() + }, + genesis: ChainGenesis { + l2: BlockNumHash { number: 40, hash: parent_hash }, + ..Default::default() + }, + ..Default::default() + }); + + let l1_block = + BlockInfo { number: 10, timestamp: 5, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: l1_block.id(), + ..Default::default() + }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 0, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let op_block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: BlockBody { transactions: vec![], ommers: vec![], withdrawals: None }, + }; + + let span_batch = SpanBatch { + batches: vec![ + SpanBatchElement { epoch_num: 9, timestamp: 10, ..Default::default() }, + SpanBatchElement { epoch_num: 9, timestamp: 20, ..Default::default() }, + SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }, + ], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + + let mut prev = TestBatchStreamProvider::new(vec![Ok(Batch::Span(span_batch))]); + prev.origin = Some(l1_block); + + let mut provider = TestL2ChainProvider::default(); + provider.blocks.push(l2_parent); + provider.op_blocks.push(op_block); + + let mut stream = BatchStream::new(prev, config, provider); + let err = stream.next_batch(l2_safe_head, &l1_blocks).await.unwrap_err(); + + assert_eq!(err, PipelineError::NotEnoughData.temp()); + assert!(stream.span.is_none()); + assert_eq!(stream.span_buffer_size(), 0); + + let logs = trace_store.get_by_level(tracing::Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("Extracting singular batches from span batch failed: Future batch L1 origin before safe head")); + } + #[tokio::test] async fn test_single_batch_pass_through() { let data = vec![Ok(Batch::Single(SingleBatch::default()))]; diff --git a/crates/protocol/protocol/src/batch/errors.rs b/crates/protocol/protocol/src/batch/errors.rs index 7a1c0e2c33..33f621af5d 100644 --- a/crates/protocol/protocol/src/batch/errors.rs +++ b/crates/protocol/protocol/src/batch/errors.rs @@ -12,6 +12,9 @@ pub enum SpanBatchError { /// Empty Span Batch #[error("Empty span batch")] EmptySpanBatch, + /// Future batch L1 origin before safe head + #[error("Future batch L1 origin before safe head")] + L1OriginBeforeSafeHead, /// Missing L1 origin #[error("Missing L1 origin")] MissingL1Origin, diff --git a/crates/protocol/protocol/src/batch/span.rs b/crates/protocol/protocol/src/batch/span.rs index 08aaf4f93c..903edba9b1 100644 --- a/crates/protocol/protocol/src/batch/span.rs +++ b/crates/protocol/protocol/src/batch/span.rs @@ -302,12 +302,17 @@ impl SpanBatch { l1_origins: &[BlockInfo], l2_safe_head: L2BlockInfo, ) -> Result, SpanBatchError> { - let mut single_batches = Vec::new(); + let mut single_batches = Vec::with_capacity(self.batches.len()); let mut origin_index = 0; for batch in &self.batches { if batch.timestamp <= l2_safe_head.block_info.timestamp { continue; } + // Overlapping span batches can pass the prefix checks but then the + // first batch after the safe head has an outdated L1 origin. + if batch.epoch_num < l2_safe_head.l1_origin.number { + return Err(SpanBatchError::L1OriginBeforeSafeHead); + } let origin_epoch_hash = l1_origins[origin_index..l1_origins.len()] .iter() .enumerate() @@ -390,29 +395,47 @@ impl SpanBatch { 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 { + let batch_timestamp = batch.timestamp; + let batch_epoch = batch.epoch_num; + + 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; - } + if batch_epoch < l2_safe_head.l1_origin.number { + warn!( + target: "batch_span", + "batch L1 origin is before safe head L1 origin, batch_epoch: {}, safe_head_epoch: {:?}", + batch_epoch, + l2_safe_head.l1_origin + ); + return BatchValidity::Drop; } - let l1_origin = l1_blocks[origin_index]; + + // Find the L1 origin for the batch. + let Some((offset, l1_origin)) = + l1_blocks[origin_index..].iter().enumerate().find(|(_, b)| batch_epoch == b.number) + else { + warn!( + target: "batch_span", + "unable to find L1 origin for batch, batch_epoch: {}, batch_timestamp: {}", + batch_epoch, + batch_timestamp + ); + return BatchValidity::Drop; + }; + origin_index += offset; + if i > 0 { origin_advanced = false; - if batch.epoch_num > self.batches[i - 1].epoch_num { + if batch_epoch > self.batches[i - 1].epoch_num { origin_advanced = true; } } - let block_timestamp = batch.timestamp; - if block_timestamp < l1_origin.timestamp { + if batch_timestamp < l1_origin.timestamp { warn!( target: "batch_span", - "block timestamp is less than L1 origin timestamp, l2_timestamp: {}, l1_timestamp: {}, origin: {:?}", - block_timestamp, + "batch timestamp is less than L1 origin timestamp, l2_timestamp: {}, l1_timestamp: {}, origin: {:?}", + batch_timestamp, l1_origin.timestamp, l1_origin.id() ); @@ -421,7 +444,7 @@ impl SpanBatch { // 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_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 @@ -436,9 +459,9 @@ impl SpanBatch { ); return BatchValidity::Undecided; } - if block_timestamp >= l1_blocks[origin_index + 1].timestamp { + if batch_timestamp >= l1_blocks[origin_index + 1].timestamp { // check if the next L1 origin could have been adopted - info!( + warn!( target: "batch_span", "batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid" ); @@ -729,12 +752,28 @@ mod tests { use alloc::vec; use alloy_consensus::{Header, constants::EIP1559_TX_TYPE_ID}; use alloy_eips::BlockNumHash; - use alloy_primitives::{Bytes, b256}; + use alloy_primitives::{B256, Bytes, b256}; use kona_genesis::{ChainGenesis, HardForkConfig}; use op_alloy_consensus::OpBlock; use tracing::Level; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + fn gen_l1_blocks( + start_num: u64, + count: u64, + start_timestamp: u64, + interval: u64, + ) -> Vec { + (0..count) + .map(|i| BlockInfo { + number: start_num + i, + timestamp: start_timestamp + i * interval, + hash: B256::left_padding_from(&i.to_be_bytes()), + ..Default::default() + }) + .collect() + } + #[test] fn test_timestamp() { let timestamp = 10; @@ -873,7 +912,7 @@ mod tests { } #[tokio::test] - async fn test_singular_batches_missing_l1_origin() { + async fn test_singular_batches_outdated_l1_origin() { let l1_block = BlockInfo { number: 10, timestamp: 20, ..Default::default() }; let l1_blocks = vec![l1_block]; let l2_safe_head = L2BlockInfo { @@ -882,6 +921,24 @@ mod tests { ..Default::default() }; let first = SpanBatchElement { epoch_num: 9, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.get_singular_batches(&l1_blocks, l2_safe_head), + Err(SpanBatchError::L1OriginBeforeSafeHead), + ); + } + + #[tokio::test] + async fn test_singular_batches_missing_l1_origin() { + let l1_block = BlockInfo { number: 10, timestamp: 20, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; let second = SpanBatchElement { epoch_num: 11, timestamp: 30, ..Default::default() }; let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; assert_eq!( @@ -1022,11 +1079,7 @@ mod tests { max_sequencer_drift: 1000, ..Default::default() }; - let l1_blocks = vec![ - BlockInfo { number: 9, timestamp: 0, ..Default::default() }, - BlockInfo { number: 10, timestamp: 10, ..Default::default() }, - BlockInfo { number: 11, timestamp: 20, ..Default::default() }, - ]; + let l1_blocks = gen_l1_blocks(9, 3, 0, 10); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { number: 10, timestamp: 20, ..Default::default() }, l1_origin: BlockNumHash { number: 11, ..Default::default() }, @@ -1066,7 +1119,7 @@ mod tests { timestamp: 10, transactions: vec![Bytes(vec![EIP1559_TX_TYPE_ID].into())], }; - let second = SpanBatchElement { epoch_num: 10, timestamp: 60, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 60, ..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, @@ -1091,11 +1144,7 @@ mod tests { max_sequencer_drift: 1000, ..Default::default() }; - let l1_blocks = vec![ - BlockInfo { number: 9, timestamp: 0, ..Default::default() }, - BlockInfo { number: 10, timestamp: 10, ..Default::default() }, - BlockInfo { number: 11, timestamp: 20, ..Default::default() }, - ]; + let l1_blocks = gen_l1_blocks(9, 3, 0, 10); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { number: 10, timestamp: 20, ..Default::default() }, l1_origin: BlockNumHash { number: 11, ..Default::default() }, @@ -1150,7 +1199,7 @@ mod tests { timestamp: 10, transactions: vec![Bytes(vec![EIP1559_TX_TYPE_ID].into())], }; - let second = SpanBatchElement { epoch_num: 10, timestamp: 60, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 60, ..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, @@ -1192,7 +1241,7 @@ mod tests { let logs = trace_store.get_by_level(Level::WARN); assert_eq!(logs.len(), 1); let str = alloc::format!( - "block timestamp is less than L1 origin timestamp, l2_timestamp: 19, l1_timestamp: 20, origin: {:?}", + "batch timestamp is less than L1 origin timestamp, l2_timestamp: 19, l1_timestamp: 20, origin: {:?}", l1_block.id(), ); assert!(logs[0].contains(&str)); @@ -1616,13 +1665,7 @@ mod tests { 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 l1_blocks = gen_l1_blocks(9, 3, 10, 0); let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { @@ -1642,19 +1685,19 @@ mod tests { let mut fetcher: TestBatchValidator = 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 second = SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 40, ..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]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[1].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); + let logs = trace_store.get_by_level(Level::WARN); 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")); } @@ -1663,7 +1706,10 @@ mod tests { 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(); + tracing_subscriber::Registry::default() + .with(layer) + .with(tracing_subscriber::fmt::layer()) + .init(); let cfg = RollupConfig { seq_window_size: 100, @@ -1672,13 +1718,11 @@ mod tests { 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]; + // Create two L1 blocks with number,timestamp: (10,10) and (11,40) so that the second batch + // in the span batch is valid even though it doesn't advance the origin, because its + // timestamp is 30 < 40. Then the third batch advances the origin to L1 block 11 + // with timestamp 40, which is also the third batch's timestamp. + let l1_blocks = gen_l1_blocks(10, 2, 10, 30); let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { @@ -1698,12 +1742,12 @@ mod tests { let mut fetcher: TestBatchValidator = 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 second = SpanBatchElement { epoch_num: 10, timestamp: 30, transactions: vec![] }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 40, 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]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[1].hash[..20]), txs: SpanBatchTransactions::default(), ..Default::default() }; @@ -1731,13 +1775,7 @@ mod tests { 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 l1_blocks = gen_l1_blocks(9, 3, 10, 0); let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { @@ -1774,7 +1812,7 @@ mod tests { 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]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[0].hash[..20]), txs: SpanBatchTransactions::default(), ..Default::default() }; @@ -1802,11 +1840,13 @@ mod tests { }; let l1_block_hash = b256!("3333333333333333333333333333333333333333000000000000000000000000"); - let block = + let l1_a = + BlockInfo { number: 10, timestamp: 5, hash: l1_block_hash, ..Default::default() }; + let l1_b = BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; - let second_block = + let l1_c = BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; - let l1_blocks = vec![block, second_block]; + let l1_blocks = vec![l1_a, l1_b, l1_c]; let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { @@ -1865,13 +1905,7 @@ mod tests { 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 l1_blocks = gen_l1_blocks(9, 3, 0, 10); let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { @@ -1906,7 +1940,7 @@ mod tests { 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]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[0].hash[..20]), txs: SpanBatchTransactions::default(), ..Default::default() }; @@ -1932,13 +1966,7 @@ mod tests { 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 l1_blocks = gen_l1_blocks(9, 3, 0, 10); let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); let l2_safe_head = L2BlockInfo { block_info: BlockInfo { @@ -1973,7 +2001,7 @@ mod tests { 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]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[0].hash[..20]), txs: SpanBatchTransactions::default(), ..Default::default() }; @@ -2179,6 +2207,75 @@ mod tests { assert!(logs[0].contains("overlapped block's L1 origin number does not match")); } + #[tokio::test] + async fn test_overlapped_blocks_origin_outdated() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 40, hash: parent_hash }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let l1_block = + BlockInfo { number: 10, timestamp: 5, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: l1_block.id(), + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 0, + ..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 = TestBatchValidator { + blocks: vec![l2_parent], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 9, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 9, timestamp: 20, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 10, timestamp: 30, ..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::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch L1 origin is before safe head L1 origin")); + } + #[tokio::test] async fn test_check_batch_valid_with_genesis_epoch() { let trace_store: TraceStorage = Default::default();