diff --git a/Cargo.toml b/Cargo.toml index 6f5a3443..8730dbfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.16.0-rc.13" +version = "0.16.0-rc.14" edition = "2021" rust-version = "1.87" authors = ["init4"] @@ -34,19 +34,19 @@ debug = false incremental = false [workspace.dependencies] -signet-bundle = { version = "0.16.0-rc.13", path = "crates/bundle" } -signet-constants = { version = "0.16.0-rc.13", path = "crates/constants" } -signet-evm = { version = "0.16.0-rc.13", path = "crates/evm" } -signet-extract = { version = "0.16.0-rc.13", path = "crates/extract" } -signet-journal = { version = "0.16.0-rc.13", path = "crates/journal" } -signet-node = { version = "0.16.0-rc.13", path = "crates/node" } -signet-orders = { version = "0.16.0-rc.13", path = "crates/orders" } -signet-sim = { version = "0.16.0-rc.13", path = "crates/sim" } -signet-types = { version = "0.16.0-rc.13", path = "crates/types" } -signet-tx-cache = { version = "0.16.0-rc.13", path = "crates/tx-cache" } -signet-zenith = { version = "0.16.0-rc.13", path = "crates/zenith" } +signet-bundle = { version = "0.16.0-rc.14", path = "crates/bundle" } +signet-constants = { version = "0.16.0-rc.14", path = "crates/constants" } +signet-evm = { version = "0.16.0-rc.14", path = "crates/evm" } +signet-extract = { version = "0.16.0-rc.14", path = "crates/extract" } +signet-journal = { version = "0.16.0-rc.14", path = "crates/journal" } +signet-node = { version = "0.16.0-rc.14", path = "crates/node" } +signet-orders = { version = "0.16.0-rc.14", path = "crates/orders" } +signet-sim = { version = "0.16.0-rc.14", path = "crates/sim" } +signet-types = { version = "0.16.0-rc.14", path = "crates/types" } +signet-tx-cache = { version = "0.16.0-rc.14", path = "crates/tx-cache" } +signet-zenith = { version = "0.16.0-rc.14", path = "crates/zenith" } -signet-test-utils = { version = "0.16.0-rc.13", path = "crates/test-utils" } +signet-test-utils = { version = "0.16.0-rc.14", path = "crates/test-utils" } # trevm trevm = { version = "0.34.1", features = ["full_env_cfg", "asyncdb"] } diff --git a/crates/extract/src/lib.rs b/crates/extract/src/lib.rs index 753a69ae..a681eccb 100644 --- a/crates/extract/src/lib.rs +++ b/crates/extract/src/lib.rs @@ -38,7 +38,7 @@ mod extractor; pub use extractor::Extractor; mod r#trait; -pub use r#trait::{Extractable, HasTxns}; +pub use r#trait::{BlockAndReceipts, Extractable, HasTxns}; mod step; pub use step::ExtractStep; diff --git a/crates/extract/src/step.rs b/crates/extract/src/step.rs index 7c20673e..9fb56cd0 100644 --- a/crates/extract/src/step.rs +++ b/crates/extract/src/step.rs @@ -83,7 +83,7 @@ pub trait ExtractStep { { extractable .blocks_and_receipts() - .map(|(block, receipts)| (block, self.extract_block(block, receipts))) + .map(|bar| (bar.block, self.extract_block(bar.block, bar.receipts))) } } @@ -142,9 +142,7 @@ where { extractable .blocks_and_receipts() - .filter(|(host_block, _)| host_block.number() > self.host_deploy_height()) - .map(|(host_block, receipts)| { - (host_block, ExtractStep::::extract_block(self, host_block, receipts)) - }) + .filter(|bar| bar.block.number() > self.host_deploy_height()) + .map(|bar| (bar.block, ExtractStep::::extract_block(self, bar.block, bar.receipts))) } } diff --git a/crates/extract/src/trait.rs b/crates/extract/src/trait.rs index 2dfbe635..df91bbb6 100644 --- a/crates/extract/src/trait.rs +++ b/crates/extract/src/trait.rs @@ -1,9 +1,38 @@ -use alloy::{consensus::TxReceipt, primitives::Log}; +use alloy::{ + consensus::{BlockHeader, TxReceipt}, + primitives::Log, +}; use signet_types::primitives::TransactionSigned; +/// A block with its associated receipts, yielded by +/// [`Extractable::blocks_and_receipts`]. +/// +/// ``` +/// # use alloy::consensus::BlockHeader; +/// # use signet_extract::BlockAndReceipts; +/// # fn example(bar: BlockAndReceipts<'_, alloy::consensus::Header, alloy::consensus::ReceiptEnvelope>) { +/// let _number = bar.block.number(); +/// let _count = bar.receipts.len(); +/// # } +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct BlockAndReceipts<'a, B, R> { + /// The block. + pub block: &'a B, + /// The receipts for this block's transactions. + pub receipts: &'a [R], +} + /// A trait for types from which data can be extracted. This currently exists /// to provide a common interface for extracting data from host chain blocks /// and receipts which may be in alloy or reth types. +/// +/// Implementors must guarantee that the segment is non-empty — i.e., +/// [`blocks_and_receipts`] always yields at least one item. An empty +/// extractable segment is not meaningful. +/// +/// [`blocks_and_receipts`]: Extractable::blocks_and_receipts +#[allow(clippy::len_without_is_empty)] pub trait Extractable: core::fmt::Debug + Sync { /// The block type that this extractor works with. type Block: alloy::consensus::BlockHeader + HasTxns + core::fmt::Debug + Sync; @@ -11,7 +40,33 @@ pub trait Extractable: core::fmt::Debug + Sync { type Receipt: TxReceipt + core::fmt::Debug + Sync; /// An iterator over the blocks and their receipts. - fn blocks_and_receipts(&self) -> impl Iterator)>; + /// + /// Blocks must be yielded in ascending order by block number. The + /// iterator must yield at least one item. + fn blocks_and_receipts( + &self, + ) -> impl Iterator>; + + /// Block number of the first block in the segment. + fn first_number(&self) -> u64 { + self.blocks_and_receipts().next().expect("Extractable must be non-empty").block.number() + } + + /// Block number of the tip (last block) in the segment. + /// + /// The default implementation consumes the entire iterator. Implementors + /// with indexed access should override this. + fn tip_number(&self) -> u64 { + self.blocks_and_receipts().last().expect("Extractable must be non-empty").block.number() + } + + /// Number of blocks in the segment. Always `>= 1`. + /// + /// The default implementation consumes the entire iterator. Implementors + /// that know their length should override this. + fn len(&self) -> usize { + self.blocks_and_receipts().count() + } } /// A trait for types that contain transactions. This currently exists to diff --git a/crates/test-utils/src/chain.rs b/crates/test-utils/src/chain.rs index 1a1d1cd8..a30b98cd 100644 --- a/crates/test-utils/src/chain.rs +++ b/crates/test-utils/src/chain.rs @@ -1,19 +1,18 @@ use alloy::{ - consensus::{Header, ReceiptEnvelope}, + consensus::{BlockHeader, Header, ReceiptEnvelope}, primitives::{B256, B64, U256}, }; pub use signet_constants::test_utils::*; use signet_evm::ExecutionOutcome; -use signet_extract::Extractable; +use signet_extract::{BlockAndReceipts, Extractable}; use signet_types::primitives::{RecoveredBlock, SealedBlock, SealedHeader}; -/// A simple chain of blocks with receipts. -#[derive(Clone, Default, PartialEq, Eq)] +/// A simple, non-empty chain of blocks with receipts. +#[derive(Clone, PartialEq, Eq)] pub struct Chain { - /// The blocks - pub blocks: Vec, - - pub execution_outcome: ExecutionOutcome, + /// The blocks. Invariant: always non-empty. + blocks: Vec, + execution_outcome: ExecutionOutcome, } impl core::fmt::Debug for Chain { @@ -23,26 +22,67 @@ impl core::fmt::Debug for Chain { } impl Chain { - /// Create a new chain from a block + /// Create a new chain from a single block. pub fn from_block(block: RecoveredBlock, execution_outcome: ExecutionOutcome) -> Self { Self { blocks: vec![block], execution_outcome } } + /// Append a block to the chain. pub fn append_block(&mut self, block: RecoveredBlock, outcome: ExecutionOutcome) { self.blocks.push(block); self.execution_outcome.append(outcome); } + + /// Get the blocks in the chain. + pub fn blocks(&self) -> &[RecoveredBlock] { + &self.blocks + } + + /// Get the execution outcome. + pub fn execution_outcome(&self) -> &ExecutionOutcome { + &self.execution_outcome + } } impl Extractable for Chain { type Block = RecoveredBlock; type Receipt = ReceiptEnvelope; - fn blocks_and_receipts(&self) -> impl Iterator)> { - self.blocks.iter().zip(self.execution_outcome.receipts().iter()) + fn blocks_and_receipts( + &self, + ) -> impl Iterator> { + self.blocks + .iter() + .zip(self.execution_outcome.receipts().iter()) + .map(|(block, receipts)| BlockAndReceipts { block, receipts }) + } + + fn first_number(&self) -> u64 { + self.blocks.first().expect("Chain must be non-empty").number() + } + + fn tip_number(&self) -> u64 { + self.blocks.last().expect("Chain must be non-empty").number() + } + + fn len(&self) -> usize { + self.blocks.len() } } +/// Make a chain with `count` fake blocks numbered `0..count`. +/// +/// # Panics +/// +/// Panics if `count` is 0, as an empty chain is not valid. +pub fn fake_chain(count: u64) -> Chain { + assert!(count > 0, "fake_chain requires at least one block"); + let blocks: Vec<_> = (0..count).map(fake_block).collect(); + let receipts = vec![vec![]; count as usize]; + let execution_outcome = ExecutionOutcome::new(Default::default(), receipts, 0); + Chain { blocks, execution_outcome } +} + /// Make a fake block with a specific number. pub fn fake_block(number: u64) -> RecoveredBlock { let header = Header { @@ -57,3 +97,30 @@ pub fn fake_block(number: u64) -> RecoveredBlock { let sealed = SealedHeader::new(header); SealedBlock::new(sealed, vec![]).recover_unchecked(vec![]) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "fake_chain requires at least one block")] + fn fake_chain_rejects_zero() { + fake_chain(0); + } + + #[test] + fn single_block_metadata() { + let chain = fake_chain(1); + assert_eq!(chain.len(), 1); + assert_eq!(chain.first_number(), 0); + assert_eq!(chain.tip_number(), 0); + } + + #[test] + fn multi_block_metadata() { + let chain = fake_chain(5); + assert_eq!(chain.len(), 5); + assert_eq!(chain.first_number(), 0); + assert_eq!(chain.tip_number(), 4); + } +}