diff --git a/crates/derive/src/stages/attributes_queue.rs b/crates/derive/src/stages/attributes_queue.rs index 98383de8..fe549fae 100644 --- a/crates/derive/src/stages/attributes_queue.rs +++ b/crates/derive/src/stages/attributes_queue.rs @@ -16,7 +16,7 @@ mod deposits; pub(crate) use deposits::derive_deposits; mod builder; -pub use builder::{AttributesBuilder, StatefulAttributesBuilder}; +pub use builder::{AttributesBuilder, StatefulAttributesBuilder, SystemConfigL2Fetcher}; /// [AttributesProvider] is a trait abstraction that generalizes the [BatchQueue] stage. /// diff --git a/crates/derive/src/stages/attributes_queue/builder.rs b/crates/derive/src/stages/attributes_queue/builder.rs index 65c26ca2..ee0d9ac6 100644 --- a/crates/derive/src/stages/attributes_queue/builder.rs +++ b/crates/derive/src/stages/attributes_queue/builder.rs @@ -172,3 +172,210 @@ where }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + stages::test_utils::MockSystemConfigL2Fetcher, traits::test_utils::TestChainProvider, + types::BlockInfo, + }; + use alloy_consensus::Header; + use alloy_primitives::b256; + + #[tokio::test] + async fn test_prepare_payload_block_mismatch_epoch_reset() { + let cfg = Arc::new(RollupConfig::default()); + let l2_hash = b256!("0000000000000000000000000000000000000000000000000000000000000002"); + let mut fetcher = MockSystemConfigL2Fetcher::default(); + fetcher.insert(l2_hash, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header::default(); + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider); + let epoch = BlockID { hash, number: 1 }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: l2_hash, number: 1, ..Default::default() }, + l1_origin: BlockID { hash: l2_hash, number: 2 }, + seq_num: 0, + }; + // This should error because the l2 parent's l1_origin.hash should equal the epoch header + // hash. Here we use the default header whose hash will not equal the custom `l2_hash`. + let expected = + BuilderError::BlockMismatchEpochReset(epoch, l2_parent.l1_origin, B256::default()); + let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err(); + assert_eq!(err, expected); + } + + #[tokio::test] + async fn test_prepare_payload_block_mismatch() { + let cfg = Arc::new(RollupConfig::default()); + let l2_hash = b256!("0000000000000000000000000000000000000000000000000000000000000002"); + let mut fetcher = MockSystemConfigL2Fetcher::default(); + fetcher.insert(l2_hash, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header::default(); + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider); + let epoch = BlockID { hash, number: 1 }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: l2_hash, number: 1, ..Default::default() }, + l1_origin: BlockID { hash: l2_hash, number: 1 }, + seq_num: 0, + }; + // This should error because the l2 parent's l1_origin.hash should equal the epoch hash + // Here the default header is used whose hash will not equal the custom `l2_hash` above. + let expected = BuilderError::BlockMismatch(epoch, l2_parent.l1_origin); + let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err(); + assert_eq!(err, expected); + } + + #[tokio::test] + async fn test_prepare_payload_broken_time_invariant() { + let block_time = 10; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { block_time, ..Default::default() }); + let l2_hash = b256!("0000000000000000000000000000000000000000000000000000000000000002"); + let mut fetcher = MockSystemConfigL2Fetcher::default(); + fetcher.insert(l2_hash, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider); + let epoch = BlockID { hash, number: 1 }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: l2_hash, number: 1, ..Default::default() }, + l1_origin: BlockID { hash, number: 1 }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let block_id = BlockID { hash, number: 0 }; + let expected = BuilderError::BrokenTimeInvariant( + l2_parent.l1_origin, + next_l2_time, + block_id, + timestamp, + ); + let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err(); + assert_eq!(err, expected); + } + + #[tokio::test] + async fn test_prepare_payload_without_forks() { + let block_time = 10; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { block_time, ..Default::default() }); + let l2_hash = b256!("0000000000000000000000000000000000000000000000000000000000000002"); + let mut fetcher = MockSystemConfigL2Fetcher::default(); + fetcher.insert(l2_hash, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider); + let epoch = BlockID { hash, number: 1 }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: l2_hash, number: 1, timestamp, parent_hash: hash }, + l1_origin: BlockID { hash, number: 1 }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = L2PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS, + transactions: payload.transactions.clone(), + no_tx_pool: true, + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + withdrawals: None, + parent_beacon_block_root: None, + }; + assert_eq!(payload, expected); + assert_eq!(payload.transactions.len(), 1); + } + + #[tokio::test] + async fn test_prepare_payload_with_canyon() { + let block_time = 10; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { block_time, canyon_time: Some(0), ..Default::default() }); + let l2_hash = b256!("0000000000000000000000000000000000000000000000000000000000000002"); + let mut fetcher = MockSystemConfigL2Fetcher::default(); + fetcher.insert(l2_hash, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider); + let epoch = BlockID { hash, number: 1 }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: l2_hash, number: 1, timestamp, parent_hash: hash }, + l1_origin: BlockID { hash, number: 1 }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = L2PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS, + transactions: payload.transactions.clone(), + no_tx_pool: true, + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + withdrawals: Some(Vec::default()), + parent_beacon_block_root: None, + }; + assert_eq!(payload, expected); + assert_eq!(payload.transactions.len(), 1); + } + + #[tokio::test] + async fn test_prepare_payload_with_ecotone() { + let block_time = 10; + let timestamp = 100; + let cfg = + Arc::new(RollupConfig { block_time, ecotone_time: Some(0), ..Default::default() }); + let l2_hash = b256!("0000000000000000000000000000000000000000000000000000000000000002"); + let mut fetcher = MockSystemConfigL2Fetcher::default(); + fetcher.insert(l2_hash, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let parent_beacon_block_root = Some(header.parent_beacon_block_root.unwrap_or_default()); + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider); + let epoch = BlockID { hash, number: 1 }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: l2_hash, number: 1, timestamp, parent_hash: hash }, + l1_origin: BlockID { hash, number: 1 }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = L2PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS, + transactions: payload.transactions.clone(), + no_tx_pool: true, + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + withdrawals: None, + parent_beacon_block_root, + }; + assert_eq!(payload, expected); + assert_eq!(payload.transactions.len(), 7); + } +} diff --git a/crates/derive/src/stages/mod.rs b/crates/derive/src/stages/mod.rs index a428572e..9517d343 100644 --- a/crates/derive/src/stages/mod.rs +++ b/crates/derive/src/stages/mod.rs @@ -34,6 +34,7 @@ pub use batch_queue::{BatchQueue, BatchQueueProvider}; mod attributes_queue; pub use attributes_queue::{ AttributesBuilder, AttributesProvider, AttributesQueue, StatefulAttributesBuilder, + SystemConfigL2Fetcher, }; #[cfg(test)] diff --git a/crates/derive/src/stages/test_utils/mod.rs b/crates/derive/src/stages/test_utils/mod.rs index 8dbb0681..ddc4b963 100644 --- a/crates/derive/src/stages/test_utils/mod.rs +++ b/crates/derive/src/stages/test_utils/mod.rs @@ -20,3 +20,6 @@ pub use channel_reader::MockChannelReaderProvider; mod tracing; pub use tracing::{CollectingLayer, TraceStorage}; + +mod sys_config_fetcher; +pub use sys_config_fetcher::MockSystemConfigL2Fetcher; diff --git a/crates/derive/src/stages/test_utils/sys_config_fetcher.rs b/crates/derive/src/stages/test_utils/sys_config_fetcher.rs new file mode 100644 index 00000000..2a34ff69 --- /dev/null +++ b/crates/derive/src/stages/test_utils/sys_config_fetcher.rs @@ -0,0 +1,33 @@ +//! Implements a mock [L2SystemConfigFetcher] for testing. + +use crate::{stages::attributes_queue::SystemConfigL2Fetcher, types::SystemConfig}; +use alloy_primitives::B256; +use hashbrown::HashMap; + +/// A mock implementation of the [`SystemConfigL2Fetcher`] for testing. +#[derive(Debug, Default)] +pub struct MockSystemConfigL2Fetcher { + /// A map from [B256] block hash to a [SystemConfig]. + pub system_configs: HashMap, +} + +impl MockSystemConfigL2Fetcher { + /// Inserts a new system config into the mock fetcher with the given hash. + pub fn insert(&mut self, hash: B256, config: SystemConfig) { + self.system_configs.insert(hash, config); + } + + /// Clears all system configs from the mock fetcher. + pub fn clear(&mut self) { + self.system_configs.clear(); + } +} + +impl SystemConfigL2Fetcher for MockSystemConfigL2Fetcher { + fn system_config_by_l2_hash(&self, hash: B256) -> anyhow::Result { + self.system_configs + .get(&hash) + .cloned() + .ok_or_else(|| anyhow::anyhow!("system config not found")) + } +} diff --git a/crates/derive/src/traits/test_utils/data_sources.rs b/crates/derive/src/traits/test_utils/data_sources.rs index 8f28f934..ae122c56 100644 --- a/crates/derive/src/traits/test_utils/data_sources.rs +++ b/crates/derive/src/traits/test_utils/data_sources.rs @@ -67,6 +67,16 @@ impl TestChainProvider { self.receipts.push((hash, receipts)); } + /// Insert a header into the mock chain provider. + pub fn insert_header(&mut self, hash: B256, header: Header) { + self.headers.push((hash, header)); + } + + /// Clears headers from the mock chain provider. + pub fn clear_headers(&mut self) { + self.headers.clear(); + } + /// Clears blocks from the mock chain provider. pub fn clear_blocks(&mut self) { self.blocks.clear(); @@ -81,6 +91,7 @@ impl TestChainProvider { pub fn clear(&mut self) { self.clear_blocks(); self.clear_receipts(); + self.clear_headers(); } } @@ -90,7 +101,7 @@ impl ChainProvider for TestChainProvider { if let Some((_, header)) = self.headers.iter().find(|(_, b)| b.hash_slow() == hash) { Ok(header.clone()) } else { - Err(anyhow::anyhow!("Block not found")) + Err(anyhow::anyhow!("Header not found")) } }