-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add chain segment metadata methods to Extractable and enforce non-empty invariant #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
080686a
50da666
4b3ec79
507a6f2
9d0dbb9
1f7fbca
5441290
565660c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,72 @@ | ||
| 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; | ||
| /// The receipt type that this extractor works with. | ||
| type Receipt: TxReceipt<Log = Log> + core::fmt::Debug + Sync; | ||
|
|
||
| /// An iterator over the blocks and their receipts. | ||
| fn blocks_and_receipts(&self) -> impl Iterator<Item = (&Self::Block, &Vec<Self::Receipt>)>; | ||
| /// | ||
| /// 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<Item = BlockAndReceipts<'_, Self::Block, Self::Receipt>>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could maybe provide something like pub struct NonEmptyIterator<T, I> {
first: T
remainder: I
}where this guarantees the iterator has at least one item. We'd probably still have an Maybe overkill though - I'm not strongly in favour of this idea :) |
||
|
|
||
| /// 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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be helpful to use different strings for the two |
||
| } | ||
|
|
||
| /// 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<RecoveredBlock>, | ||
|
|
||
| pub execution_outcome: ExecutionOutcome, | ||
| /// The blocks. Invariant: always non-empty. | ||
| blocks: Vec<RecoveredBlock>, | ||
| 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<Item = (&Self::Block, &Vec<Self::Receipt>)> { | ||
| self.blocks.iter().zip(self.execution_outcome.receipts().iter()) | ||
| fn blocks_and_receipts( | ||
| &self, | ||
| ) -> impl Iterator<Item = BlockAndReceipts<'_, Self::Block, Self::Receipt>> { | ||
| 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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing here re disambiguating the |
||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.