From 462e9df2ab0cdca4d96c22934c60966e37b5b272 Mon Sep 17 00:00:00 2001 From: refcell Date: Mon, 28 Oct 2024 15:32:46 -0400 Subject: [PATCH 1/3] feat(protocol): span batch validity checks --- Cargo.toml | 8 +- crates/protocol/Cargo.toml | 14 +- crates/protocol/src/batch/span.rs | 1442 ++++++++++++++++++++++++++++- crates/protocol/src/lib.rs | 3 + crates/protocol/src/test_utils.rs | 119 +++ 5 files changed, 1578 insertions(+), 8 deletions(-) create mode 100644 crates/protocol/src/test_utils.rs 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)); + } +} From a6d23b713d0a548f36b3c62958399fb29360c4fd Mon Sep 17 00:00:00 2001 From: refcell Date: Mon, 28 Oct 2024 16:36:42 -0400 Subject: [PATCH 2/3] feat(protocol): adds the remaining batch types --- crates/protocol/src/batch/bits.rs | 5 + crates/protocol/src/batch/core.rs | 51 +++++ crates/protocol/src/batch/errors.rs | 36 ++++ crates/protocol/src/batch/inclusion.rs | 44 ++++ crates/protocol/src/batch/mod.rs | 17 +- crates/protocol/src/batch/payload.rs | 196 ++++++++++++++++++ crates/protocol/src/batch/prefix.rs | 97 +++++++++ crates/protocol/src/batch/raw.rs | 175 ++++++++++++++++ .../protocol/src/batch/testdata/raw_batch.hex | Bin 0 -> 23886 bytes crates/protocol/src/lib.rs | 9 +- 10 files changed, 625 insertions(+), 5 deletions(-) create mode 100644 crates/protocol/src/batch/core.rs create mode 100644 crates/protocol/src/batch/inclusion.rs create mode 100644 crates/protocol/src/batch/payload.rs create mode 100644 crates/protocol/src/batch/prefix.rs create mode 100644 crates/protocol/src/batch/raw.rs create mode 100644 crates/protocol/src/batch/testdata/raw_batch.hex diff --git a/crates/protocol/src/batch/bits.rs b/crates/protocol/src/batch/bits.rs index a82b937f0..9fa938c36 100644 --- a/crates/protocol/src/batch/bits.rs +++ b/crates/protocol/src/batch/bits.rs @@ -16,6 +16,11 @@ impl AsRef<[u8]> for SpanBatchBits { } impl SpanBatchBits { + /// Creates a new span batch bits. + pub const fn new(inner: Vec) -> Self { + Self(inner) + } + /// Decodes a standard span-batch bitlist from a reader. /// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 /// bits. The encoded bitlist cannot be longer than `bit_length`. diff --git a/crates/protocol/src/batch/core.rs b/crates/protocol/src/batch/core.rs new file mode 100644 index 000000000..8637abece --- /dev/null +++ b/crates/protocol/src/batch/core.rs @@ -0,0 +1,51 @@ +//! Module containing the core [Batch] enum. + +use crate::{BatchDecodingError, BatchType, RawSpanBatch, SingleBatch, SpanBatch}; +use alloy_rlp::{Buf, Decodable}; +use op_alloy_genesis::RollupConfig; + +/// A Batch. +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Batch { + /// A single batch + Single(SingleBatch), + /// Span Batches + Span(SpanBatch), +} + +impl Batch { + /// Returns the timestamp for the batch. + pub fn timestamp(&self) -> u64 { + match self { + Self::Single(sb) => sb.timestamp, + Self::Span(sb) => sb.starting_timestamp(), + } + } + + /// Attempts to decode a batch from a reader. + pub fn decode(r: &mut &[u8], cfg: &RollupConfig) -> Result { + if r.is_empty() { + return Err(BatchDecodingError::EmptyBuffer); + } + + // Read the batch type + let batch_type = BatchType::from(r[0]); + r.advance(1); + + match batch_type { + BatchType::Single => { + let single_batch = + SingleBatch::decode(r).map_err(BatchDecodingError::AlloyRlpError)?; + Ok(Self::Single(single_batch)) + } + BatchType::Span => { + let mut raw_span_batch = RawSpanBatch::decode(r)?; + let span_batch = raw_span_batch + .derive(cfg.block_time, cfg.genesis.l2_time, cfg.l2_chain_id) + .map_err(BatchDecodingError::SpanBatchError)?; + Ok(Self::Span(span_batch)) + } + } + } +} diff --git a/crates/protocol/src/batch/errors.rs b/crates/protocol/src/batch/errors.rs index 8422f4aba..73993a3c4 100644 --- a/crates/protocol/src/batch/errors.rs +++ b/crates/protocol/src/batch/errors.rs @@ -35,6 +35,42 @@ impl core::error::Error for SpanBatchError { } } +/// An error decoding a batch. +#[derive(Debug, derive_more::Display, Clone, PartialEq, Eq)] +pub enum BatchDecodingError { + /// Empty buffer + #[display("Empty buffer")] + EmptyBuffer, + /// Error decoding an Alloy RLP + #[display("Error decoding an Alloy RLP: {_0}")] + AlloyRlpError(alloy_rlp::Error), + /// Error decoding a span batch + #[display("Error decoding a span batch: {_0}")] + SpanBatchError(SpanBatchError), +} + +impl From for BatchDecodingError { + fn from(err: alloy_rlp::Error) -> Self { + Self::AlloyRlpError(err) + } +} + +impl From for BatchDecodingError { + fn from(err: SpanBatchError) -> Self { + Self::SpanBatchError(err) + } +} + +impl core::error::Error for BatchDecodingError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::AlloyRlpError(err) => Some(err), + Self::SpanBatchError(err) => Some(err), + _ => None, + } + } +} + /// Decoding Error #[derive(Debug, derive_more::Display, Clone, PartialEq, Eq)] pub enum SpanDecodingError { diff --git a/crates/protocol/src/batch/inclusion.rs b/crates/protocol/src/batch/inclusion.rs new file mode 100644 index 000000000..1a630f91b --- /dev/null +++ b/crates/protocol/src/batch/inclusion.rs @@ -0,0 +1,44 @@ +//! Module containing the [BatchWithInclusionBlock] struct. + +use crate::{Batch, BatchValidationProvider, BatchValidity, BlockInfo, L2BlockInfo}; +use op_alloy_genesis::RollupConfig; + +/// A batch with its inclusion block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BatchWithInclusionBlock { + /// The inclusion block + pub inclusion_block: BlockInfo, + /// The batch + pub batch: Batch, +} + +impl BatchWithInclusionBlock { + /// Creates a new batch with inclusion block. + pub const fn new(inclusion_block: BlockInfo, batch: Batch) -> Self { + Self { inclusion_block, batch } + } + + /// Validates the batch can be applied on top of the specified L2 safe head. + /// The first entry of the l1_blocks should match the origin of the l2_safe_head. + /// One or more consecutive l1_blocks should be provided. + /// In case of only a single L1 block, the decision whether a batch is valid may have to stay + /// undecided. + pub async fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + fetcher: &mut BF, + ) -> BatchValidity { + match &self.batch { + Batch::Single(single_batch) => { + single_batch.check_batch(cfg, l1_blocks, l2_safe_head, &self.inclusion_block) + } + Batch::Span(span_batch) => { + span_batch + .check_batch(cfg, l1_blocks, l2_safe_head, &self.inclusion_block, fetcher) + .await + } + } + } +} diff --git a/crates/protocol/src/batch/mod.rs b/crates/protocol/src/batch/mod.rs index 9fecc001b..a350f2e0c 100644 --- a/crates/protocol/src/batch/mod.rs +++ b/crates/protocol/src/batch/mod.rs @@ -3,8 +3,23 @@ mod r#type; pub use r#type::*; +mod core; +pub use core::Batch; + +mod raw; +pub use raw::RawSpanBatch; + +mod payload; +pub use payload::SpanBatchPayload; + +mod prefix; +pub use prefix::SpanBatchPrefix; + +mod inclusion; +pub use inclusion::BatchWithInclusionBlock; + mod errors; -pub use errors::{SpanBatchError, SpanDecodingError}; +pub use errors::{BatchDecodingError, SpanBatchError, SpanDecodingError}; mod bits; pub use bits::SpanBatchBits; diff --git a/crates/protocol/src/batch/payload.rs b/crates/protocol/src/batch/payload.rs new file mode 100644 index 000000000..6b713a54e --- /dev/null +++ b/crates/protocol/src/batch/payload.rs @@ -0,0 +1,196 @@ +//! Raw Span Batch Payload + +use super::MAX_SPAN_BATCH_ELEMENTS; +use crate::{SpanBatchBits, SpanBatchError, SpanBatchTransactions, SpanDecodingError}; +use alloc::vec::Vec; + +/// Span Batch Payload +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SpanBatchPayload { + /// Number of L2 block in the span + pub block_count: u64, + /// Standard span-batch bitlist of blockCount bits. Each bit indicates if the L1 origin is + /// changed at the L2 block. + pub origin_bits: SpanBatchBits, + /// List of transaction counts for each L2 block + pub block_tx_counts: Vec, + /// Transactions encoded in SpanBatch specs + pub txs: SpanBatchTransactions, +} + +impl SpanBatchPayload { + /// Decodes a [SpanBatchPayload] from a reader. + pub fn decode_payload(r: &mut &[u8]) -> Result { + let mut payload = Self::default(); + payload.decode_block_count(r)?; + payload.decode_origin_bits(r)?; + payload.decode_block_tx_counts(r)?; + payload.decode_txs(r)?; + Ok(payload) + } + + /// Encodes a [SpanBatchPayload] into a writer. + pub fn encode_payload(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.encode_block_count(w); + self.encode_origin_bits(w)?; + self.encode_block_tx_counts(w); + self.encode_txs(w) + } + + /// Decodes the origin bits from a reader. + pub fn decode_origin_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.block_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + self.origin_bits = SpanBatchBits::decode(r, self.block_count as usize)?; + Ok(()) + } + + /// Decode a block count from a reader. + pub fn decode_block_count(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (block_count, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::BlockCount))?; + // The number of transactions in a single L2 block cannot be greater than + // [MAX_SPAN_BATCH_ELEMENTS]. + if block_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + if block_count == 0 { + return Err(SpanBatchError::EmptySpanBatch); + } + self.block_count = block_count; + *r = remaining; + Ok(()) + } + + /// Decode block transaction counts from a reader. + pub fn decode_block_tx_counts(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + // Initially allocate the vec with the block count, to reduce re-allocations in the first + // few blocks. + let mut block_tx_counts = Vec::with_capacity(self.block_count as usize); + + for _ in 0..self.block_count { + let (block_tx_count, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::BlockTxCounts))?; + + // The number of transactions in a single L2 block cannot be greater than + // [MAX_SPAN_BATCH_ELEMENTS]. + if block_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + block_tx_counts.push(block_tx_count); + *r = remaining; + } + self.block_tx_counts = block_tx_counts; + Ok(()) + } + + /// Decode transactions from a reader. + pub fn decode_txs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.block_tx_counts.is_empty() { + return Err(SpanBatchError::EmptySpanBatch); + } + + let total_block_tx_count = + self.block_tx_counts.iter().try_fold(0u64, |acc, block_tx_count| { + acc.checked_add(*block_tx_count).ok_or(SpanBatchError::TooBigSpanBatchSize) + })?; + + // The total number of transactions in a span batch cannot be greater than + // [MAX_SPAN_BATCH_ELEMENTS]. + if total_block_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + self.txs.total_block_tx_count = total_block_tx_count; + self.txs.decode(r)?; + Ok(()) + } + + /// Encode the origin bits into a writer. + pub fn encode_origin_bits(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.block_count as usize, &self.origin_bits) + } + + /// Encode the block count into a writer. + pub fn encode_block_count(&self, w: &mut Vec) { + let mut u64_varint_buf = [0u8; 10]; + w.extend_from_slice(unsigned_varint::encode::u64(self.block_count, &mut u64_varint_buf)); + } + + /// Encode the block transaction counts into a writer. + pub fn encode_block_tx_counts(&self, w: &mut Vec) { + let mut u64_varint_buf = [0u8; 10]; + for block_tx_count in &self.block_tx_counts { + u64_varint_buf.fill(0); + w.extend_from_slice(unsigned_varint::encode::u64(*block_tx_count, &mut u64_varint_buf)); + } + } + + /// Encode the transactions into a writer. + pub fn encode_txs(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.txs.encode(w) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_decode_origin_bits() { + let block_count = 10; + let encoded = vec![2; block_count / 8 + 1]; + let mut payload = + SpanBatchPayload { block_count: block_count as u64, ..Default::default() }; + payload.decode_origin_bits(&mut encoded.as_slice()).unwrap(); + assert_eq!(payload.origin_bits, SpanBatchBits::new(vec![2; block_count / 8 + 1])); + } + + #[test] + fn test_zero_block_count() { + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(0, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + let err = payload.decode_block_count(&mut encoded).unwrap_err(); + assert_eq!(err, SpanBatchError::EmptySpanBatch); + } + + #[test] + fn test_decode_block_count() { + let block_count = MAX_SPAN_BATCH_ELEMENTS; + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(block_count, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + payload.decode_block_count(&mut encoded).unwrap(); + assert_eq!(payload.block_count, block_count); + } + + #[test] + fn test_decode_block_count_errors() { + let block_count = MAX_SPAN_BATCH_ELEMENTS + 1; + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(block_count, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + let err = payload.decode_block_count(&mut encoded).unwrap_err(); + assert_eq!(err, SpanBatchError::TooBigSpanBatchSize); + } + + #[test] + fn test_decode_block_tx_counts() { + let block_count = 2; + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(block_count, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + payload.decode_block_count(&mut encoded).unwrap(); + let mut r: Vec = Vec::new(); + for _ in 0..2 { + let mut buf = [0u8; 10]; + let encoded = unsigned_varint::encode::u64(2, &mut buf); + r.append(&mut encoded.to_vec()); + } + payload.decode_block_tx_counts(&mut r.as_slice()).unwrap(); + assert_eq!(payload.block_tx_counts, vec![2, 2]); + } +} diff --git a/crates/protocol/src/batch/prefix.rs b/crates/protocol/src/batch/prefix.rs new file mode 100644 index 000000000..e3e5fc32b --- /dev/null +++ b/crates/protocol/src/batch/prefix.rs @@ -0,0 +1,97 @@ +//! Raw Span Batch Prefix + +use crate::{SpanBatchError, SpanDecodingError}; +use alloc::vec::Vec; +use alloy_primitives::FixedBytes; + +/// Span Batch Prefix +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SpanBatchPrefix { + /// Relative timestamp of the first block + pub rel_timestamp: u64, + /// L1 origin number + pub l1_origin_num: u64, + /// First 20 bytes of the first block's parent hash + pub parent_check: FixedBytes<20>, + /// First 20 bytes of the last block's L1 origin hash + pub l1_origin_check: FixedBytes<20>, +} + +impl SpanBatchPrefix { + /// Decodes a [SpanBatchPrefix] from a reader. + pub fn decode_prefix(r: &mut &[u8]) -> Result { + let mut prefix = Self::default(); + prefix.decode_rel_timestamp(r)?; + prefix.decode_l1_origin_num(r)?; + prefix.decode_parent_check(r)?; + prefix.decode_l1_origin_check(r)?; + Ok(prefix) + } + + /// Decodes the relative timestamp from a reader. + pub fn decode_rel_timestamp(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (rel_timestamp, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::RelativeTimestamp))?; + *r = remaining; + self.rel_timestamp = rel_timestamp; + Ok(()) + } + + /// Decodes the L1 origin number from a reader. + pub fn decode_l1_origin_num(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (l1_origin_num, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::L1OriginNumber))?; + *r = remaining; + self.l1_origin_num = l1_origin_num; + Ok(()) + } + + /// Decodes the parent check from a reader. + pub fn decode_parent_check(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (parent_check, remaining) = r.split_at(20); + let parent_check = FixedBytes::<20>::from_slice(parent_check); + *r = remaining; + self.parent_check = parent_check; + Ok(()) + } + + /// Decodes the L1 origin check from a reader. + pub fn decode_l1_origin_check(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (l1_origin_check, remaining) = r.split_at(20); + let l1_origin_check = FixedBytes::<20>::from_slice(l1_origin_check); + *r = remaining; + self.l1_origin_check = l1_origin_check; + Ok(()) + } + + /// Encodes the [SpanBatchPrefix] into a writer. + pub fn encode_prefix(&self, w: &mut Vec) { + let mut u64_buf = [0u8; 10]; + w.extend_from_slice(unsigned_varint::encode::u64(self.rel_timestamp, &mut u64_buf)); + w.extend_from_slice(unsigned_varint::encode::u64(self.l1_origin_num, &mut u64_buf)); + w.extend_from_slice(self.parent_check.as_slice()); + w.extend_from_slice(self.l1_origin_check.as_slice()); + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec::Vec; + use alloy_primitives::address; + + #[test] + fn test_span_batch_prefix_encoding_roundtrip() { + let expected = SpanBatchPrefix { + rel_timestamp: 0xFF, + l1_origin_num: 0xEE, + parent_check: address!("beef00000000000000000000000000000000beef").into(), + l1_origin_check: address!("babe00000000000000000000000000000000babe").into(), + }; + + let mut buf = Vec::new(); + expected.encode_prefix(&mut buf); + + assert_eq!(SpanBatchPrefix::decode_prefix(&mut buf.as_slice()).unwrap(), expected); + } +} diff --git a/crates/protocol/src/batch/raw.rs b/crates/protocol/src/batch/raw.rs new file mode 100644 index 000000000..59d990f3c --- /dev/null +++ b/crates/protocol/src/batch/raw.rs @@ -0,0 +1,175 @@ +//! Module containing the [RawSpanBatch] struct. + +use alloc::{vec, vec::Vec}; + +use crate::{ + BatchType, SpanBatch, SpanBatchElement, SpanBatchError, SpanBatchPayload, SpanBatchPrefix, + SpanDecodingError, +}; + +/// Raw Span Batch +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawSpanBatch { + /// The span batch prefix + pub prefix: SpanBatchPrefix, + /// The span batch payload + pub payload: SpanBatchPayload, +} + +impl TryFrom for RawSpanBatch { + type Error = SpanBatchError; + + fn try_from(value: SpanBatch) -> Result { + if value.batches.is_empty() { + return Err(SpanBatchError::EmptySpanBatch); + } + + // These should never error since we check for an empty batch above. + let span_start = value.batches.first().ok_or(SpanBatchError::EmptySpanBatch)?; + let span_end = value.batches.last().ok_or(SpanBatchError::EmptySpanBatch)?; + + Ok(Self { + prefix: SpanBatchPrefix { + rel_timestamp: span_start.timestamp - value.genesis_timestamp, + l1_origin_num: span_end.epoch_num, + parent_check: value.parent_check, + l1_origin_check: value.l1_origin_check, + }, + payload: SpanBatchPayload { + block_count: value.batches.len() as u64, + origin_bits: value.origin_bits.clone(), + block_tx_counts: value.block_tx_counts.clone(), + txs: value.txs.clone(), + }, + }) + } +} + +impl RawSpanBatch { + /// Returns the batch type + pub const fn get_batch_type(&self) -> BatchType { + BatchType::Span + } + + /// Encodes the [RawSpanBatch] into a writer. + pub fn encode(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.prefix.encode_prefix(w); + self.payload.encode_payload(w) + } + + /// Decodes the [RawSpanBatch] from a reader.] + pub fn decode(r: &mut &[u8]) -> Result { + let prefix = SpanBatchPrefix::decode_prefix(r)?; + let payload = SpanBatchPayload::decode_payload(r)?; + Ok(Self { prefix, payload }) + } + + /// Converts a [RawSpanBatch] into a [SpanBatch], which has a list of [SpanBatchElement]s. Thos + /// function does not populate the [SpanBatch] with chain configuration data, which is + /// required for making payload attributes. + pub fn derive( + &mut self, + block_time: u64, + genesis_time: u64, + chain_id: u64, + ) -> Result { + if self.payload.block_count == 0 { + return Err(SpanBatchError::EmptySpanBatch); + } + + let mut block_origin_nums = vec![0u64; self.payload.block_count as usize]; + let mut l1_origin_number = self.prefix.l1_origin_num; + for i in (0..self.payload.block_count).rev() { + block_origin_nums[i as usize] = l1_origin_number; + if self + .payload + .origin_bits + .get_bit(i as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::L1OriginCheck))? + == 1 + && i > 0 + { + l1_origin_number -= 1; + } + } + + // Recover `v` values in transaction signatures within the batch. + self.payload.txs.recover_v(chain_id)?; + + // Get all transactions in the batch. + let enveloped_txs = self.payload.txs.full_txs(chain_id)?; + + let mut tx_idx = 0; + let batches = (0..self.payload.block_count).fold(Vec::new(), |mut acc, i| { + let transactions = + (0..self.payload.block_tx_counts[i as usize]).fold(Vec::new(), |mut acc, _| { + acc.push(enveloped_txs[tx_idx].clone()); + tx_idx += 1; + acc + }); + acc.push(SpanBatchElement { + epoch_num: block_origin_nums[i as usize], + timestamp: genesis_time + self.prefix.rel_timestamp + block_time * i, + transactions: transactions.into_iter().map(|v| v.into()).collect(), + }); + acc + }); + + Ok(SpanBatch { + parent_check: self.prefix.parent_check, + l1_origin_check: self.prefix.l1_origin_check, + batches, + ..Default::default() + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloy_primitives::FixedBytes; + + #[test] + fn test_try_from_span_batch_empty_batches_errors() { + let span_batch = SpanBatch::default(); + let raw_span_batch = RawSpanBatch::try_from(span_batch).unwrap_err(); + assert_eq!(raw_span_batch, SpanBatchError::EmptySpanBatch); + } + + #[test] + fn test_try_from_span_batch_succeeds() { + let parent_check = FixedBytes::from([2u8; 20]); + let l1_origin_check = FixedBytes::from([3u8; 20]); + let first = SpanBatchElement { epoch_num: 100, timestamp: 400, transactions: Vec::new() }; + let last = SpanBatchElement { epoch_num: 200, timestamp: 500, transactions: Vec::new() }; + let span_batch = SpanBatch { + batches: vec![first, last], + genesis_timestamp: 300, + parent_check, + l1_origin_check, + ..Default::default() + }; + let expected_prefix = SpanBatchPrefix { + rel_timestamp: 100, + l1_origin_num: 200, + parent_check, + l1_origin_check, + }; + let expected_payload = SpanBatchPayload { block_count: 2, ..Default::default() }; + let raw_span_batch = RawSpanBatch::try_from(span_batch).unwrap(); + assert_eq!(raw_span_batch.prefix, expected_prefix); + assert_eq!(raw_span_batch.payload, expected_payload); + } + + #[test] + fn test_decode_encode_raw_span_batch() { + // Load in the raw span batch from the `op-node` derivation pipeline implementation. + let raw_span_batch_hex = include_bytes!("./testdata/raw_batch.hex"); + let mut raw_span_batch = RawSpanBatch::decode(&mut raw_span_batch_hex.as_slice()).unwrap(); + raw_span_batch.payload.txs.recover_v(981).unwrap(); + + let mut encoding_buf = Vec::new(); + raw_span_batch.encode(&mut encoding_buf).unwrap(); + assert_eq!(encoding_buf, raw_span_batch_hex); + } +} diff --git a/crates/protocol/src/batch/testdata/raw_batch.hex b/crates/protocol/src/batch/testdata/raw_batch.hex new file mode 100644 index 0000000000000000000000000000000000000000..311a7b99981d2e3edfca3ee9e98693e25810f904 GIT binary patch literal 23886 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 L`qSZsTvhddvFU9} literal 0 HcmV?d00001 diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index b6bc3a8e4..f9f46a226 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -11,11 +11,12 @@ extern crate alloc; mod batch; pub use batch::{ - BatchType, BatchValidationProvider, BatchValidity, SingleBatch, SpanBatch, SpanBatchBits, + Batch, BatchDecodingError, BatchType, BatchValidationProvider, BatchValidity, + BatchWithInclusionBlock, RawSpanBatch, SingleBatch, SpanBatch, SpanBatchBits, SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, SpanBatchElement, - SpanBatchError, SpanBatchLegacyTransactionData, SpanBatchTransactionData, - SpanBatchTransactions, SpanDecodingError, MAX_SPAN_BATCH_ELEMENTS, SINGLE_BATCH_TYPE, - SPAN_BATCH_TYPE, + SpanBatchError, SpanBatchLegacyTransactionData, SpanBatchPayload, SpanBatchPrefix, + SpanBatchTransactionData, SpanBatchTransactions, SpanDecodingError, MAX_SPAN_BATCH_ELEMENTS, + SINGLE_BATCH_TYPE, SPAN_BATCH_TYPE, }; mod block; From 37ce4ef2bf2798aa93480761d357e4dff111d00d Mon Sep 17 00:00:00 2001 From: refcell Date: Mon, 28 Oct 2024 18:55:34 -0400 Subject: [PATCH 3/3] fix: feature powerset --- crates/protocol/src/batch/errors.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/protocol/src/batch/errors.rs b/crates/protocol/src/batch/errors.rs index 73993a3c4..8ab4b5eec 100644 --- a/crates/protocol/src/batch/errors.rs +++ b/crates/protocol/src/batch/errors.rs @@ -64,7 +64,6 @@ impl From for BatchDecodingError { impl core::error::Error for BatchDecodingError { fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { match self { - Self::AlloyRlpError(err) => Some(err), Self::SpanBatchError(err) => Some(err), _ => None, }