From 31f949e7c276804fc155937e1ebfd852bac106b6 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 13:20:15 -0500 Subject: [PATCH 1/9] feature: add signet-cold-mdbx crate with MDBX table definitions Introduces a new crate for cold storage MDBX tables with 8 tables: - Primary data: ColdHeaders, ColdTransactions, ColdReceipts, ColdSignetEvents, ColdZenithHeaders - Index: ColdBlockHashIndex, ColdTxHashIndex - Metadata: ColdMetadata with MetadataKey enum Also adds: - TxLocation type in signet-storage-types for transaction location indexing - KeySer/ValSer implementations for cold storage types in signet-hot Co-Authored-By: Claude Opus 4.5 --- Cargo.toml | 2 + crates/cold-mdbx/Cargo.toml | 26 ++ crates/cold-mdbx/src/error.rs | 11 + crates/cold-mdbx/src/lib.rs | 42 +++ crates/cold-mdbx/src/tables.rs | 322 ++++++++++++++++ crates/hot/Cargo.toml | 1 + crates/hot/src/ser/cold_impls.rs | 606 +++++++++++++++++++++++++++++++ crates/hot/src/ser/mod.rs | 1 + crates/types/src/lib.rs | 3 + crates/types/src/tx_location.rs | 55 +++ 10 files changed, 1069 insertions(+) create mode 100644 crates/cold-mdbx/Cargo.toml create mode 100644 crates/cold-mdbx/src/error.rs create mode 100644 crates/cold-mdbx/src/lib.rs create mode 100644 crates/cold-mdbx/src/tables.rs create mode 100644 crates/hot/src/ser/cold_impls.rs create mode 100644 crates/types/src/tx_location.rs diff --git a/Cargo.toml b/Cargo.toml index 5de845a..27700f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,10 @@ incremental = false [workspace.dependencies] # internal signet-hot = { version = "0.0.1", path = "./crates/hot" } +signet-hot-mdbx = { version = "0.0.1", path = "./crates/hot-mdbx" } signet-mdbx-hot = { version = "0.0.1", path = "./crates/mdbx-hot" } signet-cold = { version = "0.0.1", path = "./crates/cold" } +signet-cold-mdbx = { version = "0.0.1", path = "./crates/cold-mdbx" } signet-storage = { version = "0.0.1", path = "./crates/storage" } signet-storage-types = { version = "0.0.1", path = "./crates/types" } diff --git a/crates/cold-mdbx/Cargo.toml b/crates/cold-mdbx/Cargo.toml new file mode 100644 index 0000000..2ea6387 --- /dev/null +++ b/crates/cold-mdbx/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "signet-cold-mdbx" +description = "MDBX table definitions for signet-cold storage" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +keywords = ["mdbx", "storage", "cold-storage", "blockchain"] +categories = ["database-implementations"] + +[dependencies] +alloy.workspace = true +signet-hot.workspace = true +signet-storage-types.workspace = true +thiserror.workspace = true + +[dev-dependencies] +signet-hot-mdbx = { workspace = true, features = ["test-utils"] } +signet-libmdbx.workspace = true +tempfile.workspace = true + +[features] +default = [] diff --git a/crates/cold-mdbx/src/error.rs b/crates/cold-mdbx/src/error.rs new file mode 100644 index 0000000..1aa5fe6 --- /dev/null +++ b/crates/cold-mdbx/src/error.rs @@ -0,0 +1,11 @@ +//! Error types for cold MDBX storage. + +use signet_hot::ser::DeserError; + +/// Errors that can occur in cold MDBX storage operations. +#[derive(Debug, thiserror::Error)] +pub enum MdbxColdError { + /// A serialization/deserialization error occurred. + #[error("serialization error: {0}")] + Ser(#[from] DeserError), +} diff --git a/crates/cold-mdbx/src/lib.rs b/crates/cold-mdbx/src/lib.rs new file mode 100644 index 0000000..b34887f --- /dev/null +++ b/crates/cold-mdbx/src/lib.rs @@ -0,0 +1,42 @@ +//! MDBX table definitions for cold storage. +//! +//! This crate provides table definitions for storing historical blockchain data +//! in MDBX. It defines 8 tables: +//! +//! ## Primary Data Tables +//! +//! - [`ColdHeaders`]: Block headers indexed by block number. +//! - [`ColdTransactions`]: Transactions indexed by (block number, tx index). +//! - [`ColdReceipts`]: Receipts indexed by (block number, tx index). +//! - [`ColdSignetEvents`]: Signet events indexed by (block number, event index). +//! - [`ColdZenithHeaders`]: Zenith headers indexed by block number. +//! +//! ## Index Tables +//! +//! - [`ColdBlockHashIndex`]: Maps block hash to block number. +//! - [`ColdTxHashIndex`]: Maps transaction hash to (block number, tx index). +//! +//! ## Metadata Tables +//! +//! - [`ColdMetadata`]: Storage metadata (latest block, finalized, safe, earliest). + +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod error; +pub use error::MdbxColdError; + +mod tables; +pub use tables::{ + ColdBlockHashIndex, ColdHeaders, ColdMetadata, ColdReceipts, ColdSignetEvents, + ColdTransactions, ColdTxHashIndex, ColdZenithHeaders, MetadataKey, +}; diff --git a/crates/cold-mdbx/src/tables.rs b/crates/cold-mdbx/src/tables.rs new file mode 100644 index 0000000..34bb6dd --- /dev/null +++ b/crates/cold-mdbx/src/tables.rs @@ -0,0 +1,322 @@ +//! MDBX table definitions for cold storage. +//! +//! This module defines all tables used by cold storage by manually implementing +//! the [`Table`], [`SingleKey`], and [`DualKey`] traits. + +use alloy::{consensus::Header, primitives::B256, primitives::BlockNumber}; +use signet_hot::ser::{DeserError, KeySer, MAX_KEY_SIZE}; +use signet_hot::tables::{DualKey, SingleKey, Table}; +use signet_storage_types::{DbSignetEvent, DbZenithHeader, Receipt, TransactionSigned, TxLocation}; + +// ============================================================================ +// Metadata Key Enum +// ============================================================================ + +/// Keys for the cold storage metadata table. +/// +/// These are used to store semantic block references like "latest" or +/// "finalized" that need to be resolved to actual block numbers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum MetadataKey { + /// The latest (most recent) block number stored. + LatestBlock = 0, + /// The finalized block number. + FinalizedBlock = 1, + /// The safe block number. + SafeBlock = 2, + /// The earliest block number stored. + EarliestBlock = 3, +} + +impl TryFrom for MetadataKey { + type Error = DeserError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::LatestBlock), + 1 => Ok(Self::FinalizedBlock), + 2 => Ok(Self::SafeBlock), + 3 => Ok(Self::EarliestBlock), + _ => Err(DeserError::String(format!("Invalid MetadataKey value: {}", value))), + } + } +} + +impl KeySer for MetadataKey { + const SIZE: usize = 1; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + buf[0] = *self as u8; + &buf[..1] + } + + fn decode_key(data: &[u8]) -> Result { + if data.is_empty() { + return Err(DeserError::InsufficientData { needed: 1, available: 0 }); + } + MetadataKey::try_from(data[0]) + } +} + +// ============================================================================ +// Primary Data Tables +// ============================================================================ + +/// Headers indexed by block number. +/// +/// Supports: `HeaderSpecifier::Number` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdHeaders; + +impl Table for ColdHeaders { + const NAME: &'static str = "ColdHeaders"; + const INT_KEY: bool = true; + type Key = BlockNumber; + type Value = Header; +} + +impl SingleKey for ColdHeaders {} + +/// Transactions indexed by (block number, tx index). +/// +/// Uses DUPSORT to allow efficient per-block queries. +/// +/// Supports: +/// - `TransactionSpecifier::BlockAndIndex` +/// - `GetTransactionsInBlock` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdTransactions; + +impl Table for ColdTransactions { + const NAME: &'static str = "ColdTransactions"; + const INT_KEY: bool = true; + const DUAL_KEY_SIZE: Option = Some(::SIZE); + type Key = BlockNumber; + type Value = TransactionSigned; +} + +impl DualKey for ColdTransactions { + type Key2 = u64; +} + +/// Receipts indexed by (block number, tx index). +/// +/// Uses DUPSORT to allow efficient per-block queries. +/// +/// Supports: +/// - `ReceiptSpecifier::BlockAndIndex` +/// - `GetReceiptsInBlock` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdReceipts; + +impl Table for ColdReceipts { + const NAME: &'static str = "ColdReceipts"; + const INT_KEY: bool = true; + const DUAL_KEY_SIZE: Option = Some(::SIZE); + type Key = BlockNumber; + type Value = Receipt; +} + +impl DualKey for ColdReceipts { + type Key2 = u64; +} + +/// Signet events indexed by (block number, event index). +/// +/// Uses DUPSORT to allow efficient per-block and range queries. +/// +/// Supports: +/// - `SignetEventsSpecifier::Block` +/// - `SignetEventsSpecifier::BlockRange` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdSignetEvents; + +impl Table for ColdSignetEvents { + const NAME: &'static str = "ColdSignetEvents"; + const INT_KEY: bool = true; + const DUAL_KEY_SIZE: Option = Some(::SIZE); + type Key = BlockNumber; + type Value = DbSignetEvent; +} + +impl DualKey for ColdSignetEvents { + type Key2 = u64; +} + +/// Zenith headers indexed by block number. +/// +/// Supports: +/// - `ZenithHeaderSpecifier::Number` +/// - `ZenithHeaderSpecifier::Range` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdZenithHeaders; + +impl Table for ColdZenithHeaders { + const NAME: &'static str = "ColdZenithHeaders"; + const INT_KEY: bool = true; + type Key = BlockNumber; + type Value = DbZenithHeader; +} + +impl SingleKey for ColdZenithHeaders {} + +// ============================================================================ +// Index Tables +// ============================================================================ + +/// Block hash to block number index. +/// +/// Supports: +/// - `HeaderSpecifier::Hash` +/// - `TransactionSpecifier::BlockHashAndIndex` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdBlockHashIndex; + +impl Table for ColdBlockHashIndex { + const NAME: &'static str = "ColdBlockHashIndex"; + type Key = B256; + type Value = BlockNumber; +} + +impl SingleKey for ColdBlockHashIndex {} + +/// Transaction hash to (block number, tx index) index. +/// +/// Supports: +/// - `TransactionSpecifier::Hash` +/// - `ReceiptSpecifier::TxHash` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdTxHashIndex; + +impl Table for ColdTxHashIndex { + const NAME: &'static str = "ColdTxHashIndex"; + const FIXED_VAL_SIZE: Option = Some(16); + type Key = B256; + type Value = TxLocation; +} + +impl SingleKey for ColdTxHashIndex {} + +// ============================================================================ +// Metadata Table +// ============================================================================ + +/// Cold storage metadata with semantic block references. +/// +/// Keys: `LatestBlock(0)`, `FinalizedBlock(1)`, `SafeBlock(2)`, `EarliestBlock(3)` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColdMetadata; + +impl Table for ColdMetadata { + const NAME: &'static str = "ColdMetadata"; + type Key = MetadataKey; + type Value = BlockNumber; +} + +impl SingleKey for ColdMetadata {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_table_names() { + assert_eq!(ColdHeaders::NAME, "ColdHeaders"); + assert_eq!(ColdTransactions::NAME, "ColdTransactions"); + assert_eq!(ColdReceipts::NAME, "ColdReceipts"); + assert_eq!(ColdSignetEvents::NAME, "ColdSignetEvents"); + assert_eq!(ColdZenithHeaders::NAME, "ColdZenithHeaders"); + assert_eq!(ColdBlockHashIndex::NAME, "ColdBlockHashIndex"); + assert_eq!(ColdTxHashIndex::NAME, "ColdTxHashIndex"); + assert_eq!(ColdMetadata::NAME, "ColdMetadata"); + } + + #[test] + fn test_single_key_tables() { + // Single key tables should implement SingleKey + fn assert_single_key() {} + + assert_single_key::(); + assert_single_key::(); + assert_single_key::(); + assert_single_key::(); + assert_single_key::(); + } + + #[test] + fn test_dual_key_tables() { + // Dual key tables should implement DualKey + fn assert_dual_key() {} + + assert_dual_key::(); + assert_dual_key::(); + assert_dual_key::(); + } + + #[test] + fn test_int_key_tables() { + // Tables with int_key should have INT_KEY = true + assert!(ColdHeaders::INT_KEY); + assert!(ColdTransactions::INT_KEY); + assert!(ColdReceipts::INT_KEY); + assert!(ColdSignetEvents::INT_KEY); + assert!(ColdZenithHeaders::INT_KEY); + + // Non-int_key tables should have INT_KEY = false + assert!(!ColdBlockHashIndex::INT_KEY); + assert!(!ColdTxHashIndex::INT_KEY); + assert!(!ColdMetadata::INT_KEY); + } + + #[test] + fn test_fixed_val_size() { + // ColdTxHashIndex should have fixed value size of 16 + assert_eq!(ColdTxHashIndex::FIXED_VAL_SIZE, Some(16)); + + // ColdBlockHashIndex has BlockNumber (u64) = 8 bytes + assert_eq!(ColdBlockHashIndex::FIXED_VAL_SIZE, Some(8)); + + // ColdMetadata also has BlockNumber (u64) = 8 bytes + assert_eq!(ColdMetadata::FIXED_VAL_SIZE, Some(8)); + + // Variable-size value tables should not have fixed value size + assert_eq!(ColdHeaders::FIXED_VAL_SIZE, None); + assert_eq!(ColdZenithHeaders::FIXED_VAL_SIZE, None); + } + + #[test] + fn test_metadata_key_ordering() { + // MetadataKey should have proper ordering for use as a key + assert!(MetadataKey::LatestBlock < MetadataKey::FinalizedBlock); + assert!(MetadataKey::FinalizedBlock < MetadataKey::SafeBlock); + assert!(MetadataKey::SafeBlock < MetadataKey::EarliestBlock); + } + + #[test] + fn test_metadata_key_try_from() { + assert_eq!(MetadataKey::try_from(0).unwrap(), MetadataKey::LatestBlock); + assert_eq!(MetadataKey::try_from(1).unwrap(), MetadataKey::FinalizedBlock); + assert_eq!(MetadataKey::try_from(2).unwrap(), MetadataKey::SafeBlock); + assert_eq!(MetadataKey::try_from(3).unwrap(), MetadataKey::EarliestBlock); + assert!(MetadataKey::try_from(4).is_err()); + assert!(MetadataKey::try_from(255).is_err()); + } + + #[test] + fn test_metadata_key_ser_roundtrip() { + for key in [ + MetadataKey::LatestBlock, + MetadataKey::FinalizedBlock, + MetadataKey::SafeBlock, + MetadataKey::EarliestBlock, + ] { + let mut buf = [0u8; MAX_KEY_SIZE]; + let encoded = key.encode_key(&mut buf); + assert_eq!(encoded.len(), 1); + + let decoded = MetadataKey::decode_key(encoded).unwrap(); + assert_eq!(key, decoded); + } + } +} diff --git a/crates/hot/Cargo.toml b/crates/hot/Cargo.toml index 9098a2c..df5267b 100644 --- a/crates/hot/Cargo.toml +++ b/crates/hot/Cargo.toml @@ -13,6 +13,7 @@ categories = ["database-implementations"] [dependencies] signet-storage-types.workspace = true +signet-zenith.workspace = true trevm.workspace = true diff --git a/crates/hot/src/ser/cold_impls.rs b/crates/hot/src/ser/cold_impls.rs new file mode 100644 index 0000000..fc46cd0 --- /dev/null +++ b/crates/hot/src/ser/cold_impls.rs @@ -0,0 +1,606 @@ +//! Serialization implementations for cold storage types. +//! +//! This module provides [`KeySer`] and [`ValSer`] implementations for types +//! used in cold storage that are defined in [`signet_storage_types`]. + +use alloy::primitives::{Address, Bytes, FixedBytes, U256}; +use bytes::BufMut; +use signet_storage_types::{DbSignetEvent, DbZenithHeader, Receipt, TxLocation}; +use signet_zenith::{ + Passage::{Enter, EnterToken}, + Transactor::Transact, + Zenith, +}; + +use super::{DeserError, KeySer, MAX_KEY_SIZE, ValSer}; + +// ============================================================================ +// TxLocation - 16 bytes fixed size (block: u64 + index: u64) +// ============================================================================ + +impl KeySer for TxLocation { + const SIZE: usize = 16; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + buf[0..8].copy_from_slice(&self.block.to_be_bytes()); + buf[8..16].copy_from_slice(&self.index.to_be_bytes()); + &buf[..Self::SIZE] + } + + fn decode_key(data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(DeserError::InsufficientData { needed: Self::SIZE, available: data.len() }); + } + let block = u64::from_be_bytes(data[0..8].try_into().unwrap()); + let index = u64::from_be_bytes(data[8..16].try_into().unwrap()); + Ok(TxLocation::new(block, index)) + } +} + +impl ValSer for TxLocation { + const FIXED_SIZE: Option = Some(16); + + fn encoded_size(&self) -> usize { + 16 + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + buf.put_u64(self.block); + buf.put_u64(self.index); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < 16 { + return Err(DeserError::InsufficientData { needed: 16, available: data.len() }); + } + let block = u64::from_be_bytes(data[0..8].try_into().unwrap()); + let index = u64::from_be_bytes(data[8..16].try_into().unwrap()); + Ok(TxLocation::new(block, index)) + } +} + +// ============================================================================ +// DbSignetEvent - Variable size signet event +// ============================================================================ + +/// Event type discriminant for serialization. +const EVENT_TRANSACT: u8 = 0; +const EVENT_ENTER: u8 = 1; +const EVENT_ENTER_TOKEN: u8 = 2; + +impl ValSer for DbSignetEvent { + fn encoded_size(&self) -> usize { + // 1 byte discriminant + 8 bytes event index + event-specific data + 1 + 8 + + match self { + DbSignetEvent::Transact(_, t) => encoded_size_transact(t), + DbSignetEvent::Enter(_, e) => encoded_size_enter(e), + DbSignetEvent::EnterToken(_, e) => encoded_size_enter_token(e), + } + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + match self { + DbSignetEvent::Transact(idx, t) => { + buf.put_u8(EVENT_TRANSACT); + buf.put_u64(*idx); + encode_transact(t, buf); + } + DbSignetEvent::Enter(idx, e) => { + buf.put_u8(EVENT_ENTER); + buf.put_u64(*idx); + encode_enter(e, buf); + } + DbSignetEvent::EnterToken(idx, e) => { + buf.put_u8(EVENT_ENTER_TOKEN); + buf.put_u64(*idx); + encode_enter_token(e, buf); + } + } + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < 9 { + return Err(DeserError::InsufficientData { needed: 9, available: data.len() }); + } + + let discriminant = data[0]; + let idx = u64::from_be_bytes(data[1..9].try_into().unwrap()); + let rest = &data[9..]; + + match discriminant { + EVENT_TRANSACT => { + let t = decode_transact(rest)?; + Ok(DbSignetEvent::Transact(idx, t)) + } + EVENT_ENTER => { + let e = decode_enter(rest)?; + Ok(DbSignetEvent::Enter(idx, e)) + } + EVENT_ENTER_TOKEN => { + let e = decode_enter_token(rest)?; + Ok(DbSignetEvent::EnterToken(idx, e)) + } + _ => Err(DeserError::String(format!( + "Invalid DbSignetEvent discriminant: {}", + discriminant + ))), + } + } +} + +// ============================================================================ +// Transact event helpers +// Transact fields: rollupChainId (U256), sender (Address), to (Address), +// value (U256), gas (U256), maxFeePerGas (U256), data (Bytes) +// ============================================================================ + +fn encoded_size_transact(t: &Transact) -> usize { + // rollupChainId: 32 bytes + // sender: 20 bytes + // to: 20 bytes + // value: 32 bytes + // gas: 32 bytes (U256) + // maxFeePerGas: 32 bytes + // data: 4 bytes length prefix + variable data + 32 + 20 + 20 + 32 + 32 + 32 + 4 + t.data.len() +} + +fn encode_transact(t: &Transact, buf: &mut B) { + buf.put_slice(&t.rollupChainId.to_be_bytes::<32>()); + buf.put_slice(t.sender.as_slice()); + buf.put_slice(t.to.as_slice()); + buf.put_slice(&t.value.to_be_bytes::<32>()); + buf.put_slice(&t.gas.to_be_bytes::<32>()); + buf.put_slice(&t.maxFeePerGas.to_be_bytes::<32>()); + buf.put_u32(t.data.len() as u32); + buf.put_slice(&t.data); +} + +fn decode_transact(data: &[u8]) -> Result { + // rollupChainId: 32, sender: 20, to: 20, value: 32, gas: 32, maxFeePerGas: 32, data_len: 4 + const MIN_SIZE: usize = 32 + 20 + 20 + 32 + 32 + 32 + 4; + if data.len() < MIN_SIZE { + return Err(DeserError::InsufficientData { needed: MIN_SIZE, available: data.len() }); + } + + let rollup_chain_id = U256::from_be_slice(&data[0..32]); + let sender = Address::from_slice(&data[32..52]); + let to = Address::from_slice(&data[52..72]); + let value = U256::from_be_slice(&data[72..104]); + let gas = U256::from_be_slice(&data[104..136]); + let max_fee_per_gas = U256::from_be_slice(&data[136..168]); + let data_len = u32::from_be_bytes(data[168..172].try_into().unwrap()) as usize; + + if data.len() < MIN_SIZE + data_len { + return Err(DeserError::InsufficientData { + needed: MIN_SIZE + data_len, + available: data.len(), + }); + } + + let tx_data = Bytes::copy_from_slice(&data[172..172 + data_len]); + + Ok(Transact { + rollupChainId: rollup_chain_id, + sender, + to, + value, + gas, + maxFeePerGas: max_fee_per_gas, + data: tx_data, + }) +} + +// ============================================================================ +// Enter event helpers +// Enter fields: rollupChainId (U256), rollupRecipient (Address), amount (U256) +// Note: Enter does NOT have a token field +// ============================================================================ + +fn encoded_size_enter(_e: &Enter) -> usize { + // rollupChainId: 32 bytes + // rollupRecipient: 20 bytes + // amount: 32 bytes + 32 + 20 + 32 +} + +fn encode_enter(e: &Enter, buf: &mut B) { + buf.put_slice(&e.rollupChainId.to_be_bytes::<32>()); + buf.put_slice(e.rollupRecipient.as_slice()); + buf.put_slice(&e.amount.to_be_bytes::<32>()); +} + +fn decode_enter(data: &[u8]) -> Result { + const SIZE: usize = 32 + 20 + 32; + if data.len() < SIZE { + return Err(DeserError::InsufficientData { needed: SIZE, available: data.len() }); + } + + let rollup_chain_id = U256::from_be_slice(&data[0..32]); + let rollup_recipient = Address::from_slice(&data[32..52]); + let amount = U256::from_be_slice(&data[52..84]); + + Ok(Enter { rollupChainId: rollup_chain_id, rollupRecipient: rollup_recipient, amount }) +} + +// ============================================================================ +// EnterToken event helpers +// EnterToken fields: rollupChainId (U256), token (Address), +// rollupRecipient (Address), amount (U256) +// ============================================================================ + +fn encoded_size_enter_token(_e: &EnterToken) -> usize { + // rollupChainId: 32 bytes + // token: 20 bytes + // rollupRecipient: 20 bytes + // amount: 32 bytes + 32 + 20 + 20 + 32 +} + +fn encode_enter_token(e: &EnterToken, buf: &mut B) { + buf.put_slice(&e.rollupChainId.to_be_bytes::<32>()); + buf.put_slice(e.token.as_slice()); + buf.put_slice(e.rollupRecipient.as_slice()); + buf.put_slice(&e.amount.to_be_bytes::<32>()); +} + +fn decode_enter_token(data: &[u8]) -> Result { + const SIZE: usize = 32 + 20 + 20 + 32; + if data.len() < SIZE { + return Err(DeserError::InsufficientData { needed: SIZE, available: data.len() }); + } + + let rollup_chain_id = U256::from_be_slice(&data[0..32]); + let token = Address::from_slice(&data[32..52]); + let rollup_recipient = Address::from_slice(&data[52..72]); + let amount = U256::from_be_slice(&data[72..104]); + + Ok(EnterToken { + rollupChainId: rollup_chain_id, + token, + rollupRecipient: rollup_recipient, + amount, + }) +} + +// ============================================================================ +// DbZenithHeader - Zenith block header +// BlockHeader fields: hostBlockNumber (U256), rollupChainId (U256), +// gasLimit (U256), rewardAddress (Address), +// blockDataHash (FixedBytes<32>) +// ============================================================================ + +impl ValSer for DbZenithHeader { + fn encoded_size(&self) -> usize { + // hostBlockNumber: 32 bytes (U256) + // rollupChainId: 32 bytes (U256) + // gasLimit: 32 bytes (U256) + // rewardAddress: 20 bytes + // blockDataHash: 32 bytes (FixedBytes<32>) + 32 + 32 + 32 + 20 + 32 + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + let h = &self.0; + buf.put_slice(&h.hostBlockNumber.to_be_bytes::<32>()); + buf.put_slice(&h.rollupChainId.to_be_bytes::<32>()); + buf.put_slice(&h.gasLimit.to_be_bytes::<32>()); + buf.put_slice(h.rewardAddress.as_slice()); + buf.put_slice(h.blockDataHash.as_slice()); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + const SIZE: usize = 32 + 32 + 32 + 20 + 32; + if data.len() < SIZE { + return Err(DeserError::InsufficientData { needed: SIZE, available: data.len() }); + } + + let host_block_number = U256::from_be_slice(&data[0..32]); + let rollup_chain_id = U256::from_be_slice(&data[32..64]); + let gas_limit = U256::from_be_slice(&data[64..96]); + let reward_address = Address::from_slice(&data[96..116]); + let block_data_hash = FixedBytes::<32>::from_slice(&data[116..148]); + + Ok(DbZenithHeader(Zenith::BlockHeader { + hostBlockNumber: host_block_number, + rollupChainId: rollup_chain_id, + gasLimit: gas_limit, + rewardAddress: reward_address, + blockDataHash: block_data_hash, + })) + } +} + +// ============================================================================ +// Receipt - Transaction receipt +// ============================================================================ + +use alloy::consensus::{Receipt as AlloyReceipt, TxType}; +use alloy::primitives::{Log, LogData}; + +impl ValSer for Receipt { + fn encoded_size(&self) -> usize { + // tx_type: 1 byte + // status: 1 byte (bool) + // cumulative_gas_used: 8 bytes + // logs: 2 bytes length + variable + 1 + 1 + 8 + 2 + self.inner.logs.iter().map(encoded_size_log).sum::() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + buf.put_u8(self.tx_type as u8); + buf.put_u8(self.inner.status.coerce_status() as u8); + buf.put_u64(self.inner.cumulative_gas_used); + buf.put_u16(self.inner.logs.len() as u16); + for log in &self.inner.logs { + encode_log(log, buf); + } + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < 12 { + return Err(DeserError::InsufficientData { needed: 12, available: data.len() }); + } + + let tx_type = TxType::try_from(data[0]) + .map_err(|_| DeserError::String(format!("Invalid TxType: {}", data[0])))?; + let status = data[1] != 0; + let cumulative_gas_used = u64::from_be_bytes(data[2..10].try_into().unwrap()); + let logs_len = u16::from_be_bytes(data[10..12].try_into().unwrap()) as usize; + + let mut offset = 12; + let mut logs = Vec::with_capacity(logs_len); + for _ in 0..logs_len { + let (log, consumed) = decode_log(&data[offset..])?; + logs.push(log); + offset += consumed; + } + + Ok(Receipt { + tx_type, + inner: AlloyReceipt { status: status.into(), cumulative_gas_used, logs }, + }) + } +} + +fn encoded_size_log(log: &Log) -> usize { + // address: 20 bytes + // topics count: 1 byte + // topics: 32 bytes each + // data length: 4 bytes + // data: variable + 20 + 1 + log.topics().len() * 32 + 4 + log.data.data.len() +} + +fn encode_log(log: &Log, buf: &mut B) { + buf.put_slice(log.address.as_slice()); + buf.put_u8(log.topics().len() as u8); + for topic in log.topics() { + buf.put_slice(topic.as_slice()); + } + buf.put_u32(log.data.data.len() as u32); + buf.put_slice(&log.data.data); +} + +fn decode_log(data: &[u8]) -> Result<(Log, usize), DeserError> { + if data.len() < 21 { + return Err(DeserError::InsufficientData { needed: 21, available: data.len() }); + } + + let address = Address::from_slice(&data[0..20]); + let topics_len = data[20] as usize; + + let topics_start = 21; + let topics_end = topics_start + topics_len * 32; + if data.len() < topics_end + 4 { + return Err(DeserError::InsufficientData { + needed: topics_end + 4, + available: data.len(), + }); + } + + let mut topics = Vec::with_capacity(topics_len); + for i in 0..topics_len { + let start = topics_start + i * 32; + let topic = alloy::primitives::B256::from_slice(&data[start..start + 32]); + topics.push(topic); + } + + let data_len = + u32::from_be_bytes(data[topics_end..topics_end + 4].try_into().unwrap()) as usize; + let data_start = topics_end + 4; + let data_end = data_start + data_len; + + if data.len() < data_end { + return Err(DeserError::InsufficientData { needed: data_end, available: data.len() }); + } + + let log_data = Bytes::copy_from_slice(&data[data_start..data_end]); + + let log = Log { address, data: LogData::new_unchecked(topics, log_data) }; + + Ok((log, data_end)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tx_location_key_roundtrip() { + let loc = TxLocation::new(12345, 42); + let mut buf = [0u8; MAX_KEY_SIZE]; + let encoded = loc.encode_key(&mut buf); + assert_eq!(encoded.len(), TxLocation::SIZE); + + let decoded = TxLocation::decode_key(encoded).unwrap(); + assert_eq!(loc, decoded); + } + + #[test] + fn test_tx_location_val_roundtrip() { + let loc = TxLocation::new(12345, 42); + let mut buf = bytes::BytesMut::new(); + loc.encode_value_to(&mut buf); + assert_eq!(buf.len(), 16); + + let decoded = TxLocation::decode_value(&buf).unwrap(); + assert_eq!(loc, decoded); + } + + #[test] + fn test_db_signet_event_transact_roundtrip() { + let event = DbSignetEvent::Transact( + 5, + Transact { + rollupChainId: U256::from(42u64), + sender: Address::repeat_byte(0x11), + to: Address::repeat_byte(0x22), + value: U256::from(1000u64), + gas: U256::from(21000u64), + maxFeePerGas: U256::from(20_000_000_000u64), + data: Bytes::from_static(b"hello"), + }, + ); + + let mut buf = bytes::BytesMut::new(); + event.encode_value_to(&mut buf); + assert_eq!(buf.len(), event.encoded_size()); + + let decoded = DbSignetEvent::decode_value(&buf).unwrap(); + assert_eq!(event, decoded); + } + + #[test] + fn test_db_signet_event_enter_roundtrip() { + let event = DbSignetEvent::Enter( + 3, + Enter { + rollupChainId: U256::from(42u64), + rollupRecipient: Address::repeat_byte(0x44), + amount: U256::from(500u64), + }, + ); + + let mut buf = bytes::BytesMut::new(); + event.encode_value_to(&mut buf); + assert_eq!(buf.len(), event.encoded_size()); + + let decoded = DbSignetEvent::decode_value(&buf).unwrap(); + assert_eq!(event, decoded); + } + + #[test] + fn test_db_signet_event_enter_token_roundtrip() { + let event = DbSignetEvent::EnterToken( + 7, + EnterToken { + rollupChainId: U256::from(100u64), + token: Address::repeat_byte(0x55), + rollupRecipient: Address::repeat_byte(0x66), + amount: U256::from(1000000u64), + }, + ); + + let mut buf = bytes::BytesMut::new(); + event.encode_value_to(&mut buf); + assert_eq!(buf.len(), event.encoded_size()); + + let decoded = DbSignetEvent::decode_value(&buf).unwrap(); + assert_eq!(event, decoded); + } + + #[test] + fn test_db_zenith_header_roundtrip() { + let header = DbZenithHeader(Zenith::BlockHeader { + hostBlockNumber: U256::from(12345u64), + rollupChainId: U256::from(42u64), + gasLimit: U256::from(30_000_000u64), + rewardAddress: Address::repeat_byte(0x77), + blockDataHash: FixedBytes::repeat_byte(0x88), + }); + + let mut buf = bytes::BytesMut::new(); + header.encode_value_to(&mut buf); + assert_eq!(buf.len(), header.encoded_size()); + + let decoded = DbZenithHeader::decode_value(&buf).unwrap(); + assert_eq!(header.0.hostBlockNumber, decoded.0.hostBlockNumber); + assert_eq!(header.0.rollupChainId, decoded.0.rollupChainId); + assert_eq!(header.0.gasLimit, decoded.0.gasLimit); + assert_eq!(header.0.rewardAddress, decoded.0.rewardAddress); + assert_eq!(header.0.blockDataHash, decoded.0.blockDataHash); + } + + #[test] + fn test_receipt_roundtrip() { + let receipt = Receipt { + tx_type: TxType::Eip1559, + inner: AlloyReceipt { + status: true.into(), + cumulative_gas_used: 100000, + logs: vec![Log { + address: Address::repeat_byte(0x88), + data: LogData::new_unchecked( + vec![ + alloy::primitives::B256::repeat_byte(0x11), + alloy::primitives::B256::repeat_byte(0x22), + ], + Bytes::from_static(b"log data"), + ), + }], + }, + }; + + let mut buf = bytes::BytesMut::new(); + receipt.encode_value_to(&mut buf); + assert_eq!(buf.len(), receipt.encoded_size()); + + let decoded = Receipt::decode_value(&buf).unwrap(); + assert_eq!(receipt.tx_type, decoded.tx_type); + assert_eq!(receipt.inner.status, decoded.inner.status); + assert_eq!(receipt.inner.cumulative_gas_used, decoded.inner.cumulative_gas_used); + assert_eq!(receipt.inner.logs.len(), decoded.inner.logs.len()); + } + + #[test] + fn test_receipt_empty_logs_roundtrip() { + let receipt = Receipt { + tx_type: TxType::Legacy, + inner: AlloyReceipt { status: false.into(), cumulative_gas_used: 21000, logs: vec![] }, + }; + + let mut buf = bytes::BytesMut::new(); + receipt.encode_value_to(&mut buf); + assert_eq!(buf.len(), receipt.encoded_size()); + + let decoded = Receipt::decode_value(&buf).unwrap(); + assert_eq!(receipt.tx_type, decoded.tx_type); + assert_eq!(receipt.inner.logs.len(), 0); + } +} diff --git a/crates/hot/src/ser/mod.rs b/crates/hot/src/ser/mod.rs index 0fa2803..3315231 100644 --- a/crates/hot/src/ser/mod.rs +++ b/crates/hot/src/ser/mod.rs @@ -21,6 +21,7 @@ mod error; pub use error::DeserError; +mod cold_impls; mod impls; mod reth_impls; mod traits; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index b7508c5..bd3a51f 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -30,6 +30,9 @@ pub use int_list::{BlockNumberList, IntegerList, IntegerListError}; mod sharded; pub use sharded::ShardedKey; +mod tx_location; +pub use tx_location::TxLocation; + mod util; pub use util::{EthereumHardfork, genesis_header}; diff --git a/crates/types/src/tx_location.rs b/crates/types/src/tx_location.rs new file mode 100644 index 0000000..af56c54 --- /dev/null +++ b/crates/types/src/tx_location.rs @@ -0,0 +1,55 @@ +//! Transaction location within a block. + +use alloy::primitives::BlockNumber; + +/// Location of a transaction within a block. +/// +/// This is a 16-byte fixed-size type that stores the block number and +/// transaction index. It is used as the value type in the `ColdTxHashIndex` +/// table to map transaction hashes to their location. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TxLocation { + /// The block number containing the transaction. + pub block: BlockNumber, + /// The index of the transaction within the block. + pub index: u64, +} + +impl TxLocation { + /// Create a new transaction location. + pub const fn new(block: BlockNumber, index: u64) -> Self { + Self { block, index } + } + + /// Returns the block number. + pub const fn block(&self) -> BlockNumber { + self.block + } + + /// Returns the transaction index within the block. + pub const fn index(&self) -> u64 { + self.index + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tx_location_new() { + let loc = TxLocation::new(100, 5); + assert_eq!(loc.block(), 100); + assert_eq!(loc.index(), 5); + } + + #[test] + fn test_tx_location_equality() { + let loc1 = TxLocation::new(100, 5); + let loc2 = TxLocation::new(100, 5); + let loc3 = TxLocation::new(100, 6); + + assert_eq!(loc1, loc2); + assert_ne!(loc1, loc3); + } +} From 776a268d8157bfbc17ef73127aa8c52a754c0e06 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 13:32:43 -0500 Subject: [PATCH 2/9] lint: fix clippy warnings on constant assertions Co-Authored-By: Claude Opus 4.5 --- crates/cold-mdbx/src/tables.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/cold-mdbx/src/tables.rs b/crates/cold-mdbx/src/tables.rs index 34bb6dd..9fa4951 100644 --- a/crates/cold-mdbx/src/tables.rs +++ b/crates/cold-mdbx/src/tables.rs @@ -257,16 +257,16 @@ mod tests { #[test] fn test_int_key_tables() { // Tables with int_key should have INT_KEY = true - assert!(ColdHeaders::INT_KEY); - assert!(ColdTransactions::INT_KEY); - assert!(ColdReceipts::INT_KEY); - assert!(ColdSignetEvents::INT_KEY); - assert!(ColdZenithHeaders::INT_KEY); + const { assert!(ColdHeaders::INT_KEY) }; + const { assert!(ColdTransactions::INT_KEY) }; + const { assert!(ColdReceipts::INT_KEY) }; + const { assert!(ColdSignetEvents::INT_KEY) }; + const { assert!(ColdZenithHeaders::INT_KEY) }; // Non-int_key tables should have INT_KEY = false - assert!(!ColdBlockHashIndex::INT_KEY); - assert!(!ColdTxHashIndex::INT_KEY); - assert!(!ColdMetadata::INT_KEY); + const { assert!(!ColdBlockHashIndex::INT_KEY) }; + const { assert!(!ColdTxHashIndex::INT_KEY) }; + const { assert!(!ColdMetadata::INT_KEY) }; } #[test] From 7c10cf7c00076e9c2e9c7815e5468474cba454dc Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 13:32:58 -0500 Subject: [PATCH 3/9] fmt: apply nightly rustfmt Co-Authored-By: Claude Opus 4.5 --- crates/hot/src/ser/cold_impls.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/hot/src/ser/cold_impls.rs b/crates/hot/src/ser/cold_impls.rs index fc46cd0..9088526 100644 --- a/crates/hot/src/ser/cold_impls.rs +++ b/crates/hot/src/ser/cold_impls.rs @@ -418,10 +418,7 @@ fn decode_log(data: &[u8]) -> Result<(Log, usize), DeserError> { let topics_start = 21; let topics_end = topics_start + topics_len * 32; if data.len() < topics_end + 4 { - return Err(DeserError::InsufficientData { - needed: topics_end + 4, - available: data.len(), - }); + return Err(DeserError::InsufficientData { needed: topics_end + 4, available: data.len() }); } let mut topics = Vec::with_capacity(topics_len); From 1f3d8588c0be9e29a5142161826c7b4387f6200e Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 14:09:35 -0500 Subject: [PATCH 4/9] lint: add const to encoded_size_enter functions Co-Authored-By: Claude Opus 4.5 --- crates/hot/src/ser/cold_impls.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/hot/src/ser/cold_impls.rs b/crates/hot/src/ser/cold_impls.rs index 9088526..1e45607 100644 --- a/crates/hot/src/ser/cold_impls.rs +++ b/crates/hot/src/ser/cold_impls.rs @@ -210,7 +210,7 @@ fn decode_transact(data: &[u8]) -> Result { // Note: Enter does NOT have a token field // ============================================================================ -fn encoded_size_enter(_e: &Enter) -> usize { +const fn encoded_size_enter(_e: &Enter) -> usize { // rollupChainId: 32 bytes // rollupRecipient: 20 bytes // amount: 32 bytes @@ -242,7 +242,7 @@ fn decode_enter(data: &[u8]) -> Result { // rollupRecipient (Address), amount (U256) // ============================================================================ -fn encoded_size_enter_token(_e: &EnterToken) -> usize { +const fn encoded_size_enter_token(_e: &EnterToken) -> usize { // rollupChainId: 32 bytes // token: 20 bytes // rollupRecipient: 20 bytes From 65253e84e60989098946eb522955f8af257c7441 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 13:31:17 -0500 Subject: [PATCH 5/9] feature: add MdbxColdBackend implementation for cold storage Implements the ColdStorage trait using MDBX as the backend. Includes proper feature flagging with `backend` and `test-utils` features. Co-Authored-By: Claude Opus 4.5 --- crates/cold-mdbx/Cargo.toml | 6 + crates/cold-mdbx/src/backend.rs | 574 ++++++++++++++++++++++++++++++++ crates/cold-mdbx/src/error.rs | 9 + crates/cold-mdbx/src/lib.rs | 16 +- 4 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 crates/cold-mdbx/src/backend.rs diff --git a/crates/cold-mdbx/Cargo.toml b/crates/cold-mdbx/Cargo.toml index 2ea6387..43e8489 100644 --- a/crates/cold-mdbx/Cargo.toml +++ b/crates/cold-mdbx/Cargo.toml @@ -13,14 +13,20 @@ categories = ["database-implementations"] [dependencies] alloy.workspace = true +signet-cold = { version = "0.0.1", path = "../cold", optional = true } signet-hot.workspace = true +signet-hot-mdbx = { workspace = true, optional = true } signet-storage-types.workspace = true thiserror.workspace = true [dev-dependencies] +signet-cold = { version = "0.0.1", path = "../cold", features = ["test-utils"] } signet-hot-mdbx = { workspace = true, features = ["test-utils"] } signet-libmdbx.workspace = true tempfile.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [features] default = [] +backend = ["dep:signet-cold", "dep:signet-hot-mdbx"] +test-utils = ["backend", "signet-cold/test-utils"] diff --git a/crates/cold-mdbx/src/backend.rs b/crates/cold-mdbx/src/backend.rs new file mode 100644 index 0000000..151d1b0 --- /dev/null +++ b/crates/cold-mdbx/src/backend.rs @@ -0,0 +1,574 @@ +//! MDBX backend implementation for [`ColdStorage`]. +//! +//! This module provides an MDBX-based implementation of the cold storage +//! backend. It uses the table definitions from this crate and the MDBX +//! database environment from `signet-hot-mdbx`. + +use crate::{ + ColdBlockHashIndex, ColdHeaders, ColdMetadata, ColdReceipts, ColdSignetEvents, + ColdTransactions, ColdTxHashIndex, ColdZenithHeaders, MdbxColdError, MetadataKey, +}; +use alloy::{consensus::Header, primitives::BlockNumber}; +use signet_cold::{ + BlockData, BlockTag, ColdResult, ColdStorage, ColdStorageError, HeaderSpecifier, + ReceiptSpecifier, SignetEventsSpecifier, TransactionSpecifier, ZenithHeaderSpecifier, +}; +use signet_hot::{ + KeySer, MAX_KEY_SIZE, ValSer, + model::{DualTableTraverse, HotKvWrite, KvTraverse, TableTraverse}, + tables::Table, +}; +use signet_hot_mdbx::{DatabaseArguments, DatabaseEnv, DatabaseEnvKind}; +use signet_storage_types::{DbSignetEvent, DbZenithHeader, Receipt, TransactionSigned, TxLocation}; +use std::path::Path; + +/// MDBX-based cold storage backend. +/// +/// This backend stores historical blockchain data in an MDBX database. +/// It implements the [`ColdStorage`] trait for use with the cold storage +/// task runner. +#[derive(Debug)] +pub struct MdbxColdBackend { + /// The MDBX environment. + env: DatabaseEnv, +} + +impl MdbxColdBackend { + /// Open an existing MDBX cold storage database in read-only mode. + pub fn open_ro(path: &Path) -> Result { + let env = DatabaseArguments::new().open_ro(path)?; + Ok(Self { env }) + } + + /// Open or create an MDBX cold storage database in read-write mode. + pub fn open_rw(path: &Path) -> Result { + let env = DatabaseArguments::new().open_rw(path)?; + let backend = Self { env }; + backend.create_tables()?; + Ok(backend) + } + + /// Open an MDBX cold storage database with custom arguments. + pub fn open( + path: &Path, + kind: DatabaseEnvKind, + args: DatabaseArguments, + ) -> Result { + let env = DatabaseEnv::open(path, kind, args)?; + let backend = Self { env }; + if kind.is_rw() { + backend.create_tables()?; + } + Ok(backend) + } + + /// Create all required tables if they don't exist. + fn create_tables(&self) -> Result<(), MdbxColdError> { + let tx = self.env.tx_rw()?; + + // Create single-key tables + tx.queue_raw_create( + ColdHeaders::NAME, + ColdHeaders::DUAL_KEY_SIZE, + ColdHeaders::FIXED_VAL_SIZE, + ColdHeaders::INT_KEY, + )?; + tx.queue_raw_create( + ColdZenithHeaders::NAME, + ColdZenithHeaders::DUAL_KEY_SIZE, + ColdZenithHeaders::FIXED_VAL_SIZE, + ColdZenithHeaders::INT_KEY, + )?; + tx.queue_raw_create( + ColdBlockHashIndex::NAME, + ColdBlockHashIndex::DUAL_KEY_SIZE, + ColdBlockHashIndex::FIXED_VAL_SIZE, + ColdBlockHashIndex::INT_KEY, + )?; + tx.queue_raw_create( + ColdTxHashIndex::NAME, + ColdTxHashIndex::DUAL_KEY_SIZE, + ColdTxHashIndex::FIXED_VAL_SIZE, + ColdTxHashIndex::INT_KEY, + )?; + tx.queue_raw_create( + ColdMetadata::NAME, + ColdMetadata::DUAL_KEY_SIZE, + ColdMetadata::FIXED_VAL_SIZE, + ColdMetadata::INT_KEY, + )?; + + // Create dual-key (DUPSORT) tables + tx.queue_raw_create( + ColdTransactions::NAME, + ColdTransactions::DUAL_KEY_SIZE, + ColdTransactions::FIXED_VAL_SIZE, + ColdTransactions::INT_KEY, + )?; + tx.queue_raw_create( + ColdReceipts::NAME, + ColdReceipts::DUAL_KEY_SIZE, + ColdReceipts::FIXED_VAL_SIZE, + ColdReceipts::INT_KEY, + )?; + tx.queue_raw_create( + ColdSignetEvents::NAME, + ColdSignetEvents::DUAL_KEY_SIZE, + ColdSignetEvents::FIXED_VAL_SIZE, + ColdSignetEvents::INT_KEY, + )?; + + tx.raw_commit()?; + Ok(()) + } + + /// Resolve a block tag to a block number. + fn resolve_tag(&self, tag: BlockTag) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + let metadata_key = match tag { + BlockTag::Latest => MetadataKey::LatestBlock, + BlockTag::Finalized => MetadataKey::FinalizedBlock, + BlockTag::Safe => MetadataKey::SafeBlock, + BlockTag::Earliest => MetadataKey::EarliestBlock, + }; + TableTraverse::::exact( + &mut tx.new_cursor::()?, + &metadata_key, + ) + .map_err(MdbxColdError::from) + } + + /// Get the block number for a block hash. + fn get_block_by_hash( + &self, + hash: alloy::primitives::B256, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + TableTraverse::::exact( + &mut tx.new_cursor::()?, + &hash, + ) + .map_err(MdbxColdError::from) + } + + /// Get transaction location by hash. + fn get_tx_location( + &self, + hash: alloy::primitives::B256, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + TableTraverse::::exact(&mut tx.new_cursor::()?, &hash) + .map_err(MdbxColdError::from) + } +} + +fn to_cold_result(result: Result) -> ColdResult { + result.map_err(ColdStorageError::backend) +} + +impl ColdStorage for MdbxColdBackend { + async fn get_header(&self, spec: HeaderSpecifier) -> ColdResult> { + to_cold_result((|| { + let block_num = match spec { + HeaderSpecifier::Number(n) => Some(n), + HeaderSpecifier::Hash(h) => self.get_block_by_hash(h)?, + HeaderSpecifier::Tag(tag) => self.resolve_tag(tag)?, + }; + + let Some(block_num) = block_num else { + return Ok(None); + }; + + let tx = self.env.tx()?; + TableTraverse::::exact(&mut tx.new_cursor::()?, &block_num) + .map_err(MdbxColdError::from) + })()) + } + + async fn get_headers(&self, specs: Vec) -> ColdResult>> { + let mut results = Vec::with_capacity(specs.len()); + for spec in specs { + results.push(self.get_header(spec).await?); + } + Ok(results) + } + + async fn get_transaction( + &self, + spec: TransactionSpecifier, + ) -> ColdResult> { + to_cold_result((|| { + let (block, index) = match spec { + TransactionSpecifier::Hash(h) => { + let Some(loc) = self.get_tx_location(h)? else { + return Ok(None); + }; + (loc.block, loc.index) + } + TransactionSpecifier::BlockAndIndex { block, index } => (block, index), + TransactionSpecifier::BlockHashAndIndex { block_hash, index } => { + let Some(block) = self.get_block_by_hash(block_hash)? else { + return Ok(None); + }; + (block, index) + } + }; + + let tx = self.env.tx()?; + DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + ) + .map_err(MdbxColdError::from) + })()) + } + + async fn get_transactions_in_block( + &self, + block: BlockNumber, + ) -> ColdResult> { + to_cold_result((|| { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; + + let mut transactions = Vec::new(); + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + let (_, tx_signed) = item?; + transactions.push(tx_signed); + } + + Ok(transactions) + })()) + } + + async fn get_transaction_count(&self, block: BlockNumber) -> ColdResult { + to_cold_result((|| { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; + + let mut count = 0u64; + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + let _ = item?; + count += 1; + } + + Ok(count) + })()) + } + + async fn get_receipt(&self, spec: ReceiptSpecifier) -> ColdResult> { + to_cold_result((|| { + let (block, index) = match spec { + ReceiptSpecifier::TxHash(h) => { + let Some(loc) = self.get_tx_location(h)? else { + return Ok(None); + }; + (loc.block, loc.index) + } + ReceiptSpecifier::BlockAndIndex { block, index } => (block, index), + }; + + let tx = self.env.tx()?; + DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + ) + .map_err(MdbxColdError::from) + })()) + } + + async fn get_receipts_in_block(&self, block: BlockNumber) -> ColdResult> { + to_cold_result((|| { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; + + let mut receipts = Vec::new(); + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + let (_, receipt) = item?; + receipts.push(receipt); + } + + Ok(receipts) + })()) + } + + async fn get_signet_events( + &self, + spec: SignetEventsSpecifier, + ) -> ColdResult> { + to_cold_result((|| { + let tx = self.env.tx()?; + + match spec { + SignetEventsSpecifier::Block(block) => { + let mut cursor = tx.new_cursor::()?; + let mut events = Vec::new(); + for item in + DualTableTraverse::::iter_k2(&mut cursor, &block)? + { + let (_, event) = item?; + events.push(event); + } + Ok(events) + } + SignetEventsSpecifier::BlockRange { start, end } => { + let mut events = Vec::new(); + for block in start..=end { + let mut cursor = tx.new_cursor::()?; + for item in + DualTableTraverse::::iter_k2(&mut cursor, &block)? + { + let (_, event) = item?; + events.push(event); + } + } + Ok(events) + } + } + })()) + } + + async fn get_zenith_header( + &self, + spec: ZenithHeaderSpecifier, + ) -> ColdResult> { + to_cold_result((|| { + let block = match spec { + ZenithHeaderSpecifier::Number(n) => n, + ZenithHeaderSpecifier::Range { start, .. } => start, + }; + + let tx = self.env.tx()?; + TableTraverse::::exact( + &mut tx.new_cursor::()?, + &block, + ) + .map_err(MdbxColdError::from) + })()) + } + + async fn get_zenith_headers( + &self, + spec: ZenithHeaderSpecifier, + ) -> ColdResult> { + to_cold_result((|| { + let tx = self.env.tx()?; + + match spec { + ZenithHeaderSpecifier::Number(n) => { + let header: Option = + TableTraverse::::exact( + &mut tx.new_cursor::()?, + &n, + )?; + Ok(header.into_iter().collect()) + } + ZenithHeaderSpecifier::Range { start, end } => { + let mut cursor = tx.new_cursor::()?; + let mut headers = Vec::new(); + + // Position at start + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = start.encode_key(&mut key_buf); + + if let Some((key, value)) = + KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? + { + let block_num = BlockNumber::decode_key(&key)?; + if block_num <= end { + headers.push(DbZenithHeader::decode_value(&value)?); + } + + while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { + let block_num = BlockNumber::decode_key(&key)?; + if block_num > end { + break; + } + headers.push(DbZenithHeader::decode_value(&value)?); + } + } + + Ok(headers) + } + } + })()) + } + + async fn get_latest_block(&self) -> ColdResult> { + to_cold_result((|| { + let tx = self.env.tx()?; + TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::LatestBlock, + ) + .map_err(MdbxColdError::from) + })()) + } + + async fn append_block(&self, data: BlockData) -> ColdResult<()> { + to_cold_result((|| { + let tx = self.env.tx_rw()?; + let block = data.block_number(); + + // Store header + tx.queue_put::(&block, &data.header)?; + + // Store header hash index + let header_hash = data.header.hash_slow(); + tx.queue_put::(&header_hash, &block)?; + + // Store transactions and their hash index + for (idx, tx_signed) in data.transactions.iter().enumerate() { + let tx_idx = idx as u64; + tx.queue_put_dual::(&block, &tx_idx, tx_signed)?; + + // Store tx hash index + let tx_hash = *tx_signed.hash(); + let location = TxLocation::new(block, tx_idx); + tx.queue_put::(&tx_hash, &location)?; + } + + // Store receipts + for (idx, receipt) in data.receipts.iter().enumerate() { + let receipt_idx = idx as u64; + tx.queue_put_dual::(&block, &receipt_idx, receipt)?; + } + + // Store signet events + for (idx, event) in data.signet_events.iter().enumerate() { + let event_idx = idx as u64; + tx.queue_put_dual::(&block, &event_idx, event)?; + } + + // Store zenith header if present + if let Some(zh) = &data.zenith_header { + tx.queue_put::(&block, zh)?; + } + + // Update latest block if this is higher + let current_latest: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::LatestBlock, + )?; + let new_latest = current_latest.map_or(block, |prev: BlockNumber| prev.max(block)); + tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; + + // Update earliest block if not set or if this is lower + let current_earliest: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::EarliestBlock, + )?; + let new_earliest = current_earliest.map_or(block, |prev: BlockNumber| prev.min(block)); + tx.queue_put::(&MetadataKey::EarliestBlock, &new_earliest)?; + + tx.raw_commit()?; + Ok(()) + })()) + } + + async fn append_blocks(&self, data: Vec) -> ColdResult<()> { + for block_data in data { + self.append_block(block_data).await?; + } + Ok(()) + } + + async fn truncate_above(&self, block: BlockNumber) -> ColdResult<()> { + to_cold_result((|| { + let tx = self.env.tx_rw()?; + + // Collect headers to remove for hash index cleanup + let mut headers_to_remove: Vec<(BlockNumber, Header)> = Vec::new(); + { + let mut cursor = tx.new_cursor::()?; + + // Position after the block we want to keep + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let start_block = block + 1; + let key_bytes = start_block.encode_key(&mut key_buf); + + if let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? { + let block_num = BlockNumber::decode_key(&key)?; + let header = Header::decode_value(&value)?; + headers_to_remove.push((block_num, header)); + + while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { + let block_num = BlockNumber::decode_key(&key)?; + let header = Header::decode_value(&value)?; + headers_to_remove.push((block_num, header)); + } + } + } + + // Collect transactions to remove for hash index cleanup + let mut tx_hashes_to_remove: Vec = Vec::new(); + for (block_num, _) in &headers_to_remove { + let mut cursor = tx.new_cursor::()?; + for item in + DualTableTraverse::::iter_k2(&mut cursor, block_num)? + { + let (_, tx_signed): (u64, TransactionSigned) = item?; + tx_hashes_to_remove.push(*tx_signed.hash()); + } + } + + // Delete headers and their hash index entries + for (block_num, header) in &headers_to_remove { + tx.queue_delete::(block_num)?; + tx.queue_delete::(&header.hash_slow())?; + } + + // Delete transaction hash index entries + for tx_hash in &tx_hashes_to_remove { + tx.queue_delete::(tx_hash)?; + } + + // Delete transactions, receipts, and signet events for removed blocks + for (block_num, _) in &headers_to_remove { + // Delete all transactions for this block + tx.clear_k1_for::(block_num)?; + + // Delete all receipts for this block + tx.clear_k1_for::(block_num)?; + + // Delete all signet events for this block + tx.clear_k1_for::(block_num)?; + + // Delete zenith header for this block + tx.queue_delete::(block_num)?; + } + + // Update latest block metadata + if !headers_to_remove.is_empty() { + // Find the new latest block + let mut cursor = tx.new_cursor::()?; + if let Some((key, _)) = KvTraverse::<_>::last(&mut cursor)? { + let new_latest = BlockNumber::decode_key(&key)?; + tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; + } else { + // No blocks left, remove latest block metadata + tx.queue_delete::(&MetadataKey::LatestBlock)?; + } + } + + tx.raw_commit()?; + Ok(()) + })()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use signet_cold::conformance::conformance; + use tempfile::tempdir; + + #[tokio::test] + async fn mdbx_backend_conformance() { + let dir = tempdir().unwrap(); + let backend = MdbxColdBackend::open_rw(dir.path()).unwrap(); + conformance(&backend).await.unwrap(); + } +} diff --git a/crates/cold-mdbx/src/error.rs b/crates/cold-mdbx/src/error.rs index 1aa5fe6..d3dccb2 100644 --- a/crates/cold-mdbx/src/error.rs +++ b/crates/cold-mdbx/src/error.rs @@ -8,4 +8,13 @@ pub enum MdbxColdError { /// A serialization/deserialization error occurred. #[error("serialization error: {0}")] Ser(#[from] DeserError), + + /// An MDBX error occurred. + #[cfg(feature = "backend")] + #[error("mdbx error: {0}")] + Mdbx(#[from] signet_hot_mdbx::MdbxError), + + /// Database is read-only. + #[error("database is read-only")] + ReadOnly, } diff --git a/crates/cold-mdbx/src/lib.rs b/crates/cold-mdbx/src/lib.rs index b34887f..12fc799 100644 --- a/crates/cold-mdbx/src/lib.rs +++ b/crates/cold-mdbx/src/lib.rs @@ -1,4 +1,4 @@ -//! MDBX table definitions for cold storage. +//! MDBX table definitions and backend for cold storage. //! //! This crate provides table definitions for storing historical blockchain data //! in MDBX. It defines 8 tables: @@ -19,6 +19,12 @@ //! ## Metadata Tables //! //! - [`ColdMetadata`]: Storage metadata (latest block, finalized, safe, earliest). +//! +//! ## Backend Implementation +//! +//! When the `backend` feature is enabled, this crate also provides +//! [`MdbxColdBackend`], an MDBX-based implementation of the [`signet_cold::ColdStorage`] +//! trait. #![warn( missing_copy_implementations, @@ -40,3 +46,11 @@ pub use tables::{ ColdBlockHashIndex, ColdHeaders, ColdMetadata, ColdReceipts, ColdSignetEvents, ColdTransactions, ColdTxHashIndex, ColdZenithHeaders, MetadataKey, }; + +#[cfg(feature = "backend")] +mod backend; +#[cfg(feature = "backend")] +pub use backend::MdbxColdBackend; + +#[cfg(feature = "backend")] +pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnvKind}; From 0bca683741ff4f265cf308692859b1dfdb6be83a Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 14:30:03 -0500 Subject: [PATCH 6/9] refactor: simplify error conversion with From trait in cold-mdbx Add `impl From for ColdStorageError` to enable automatic error conversion via the `?` operator. Refactor backend methods to use internal helpers returning `MdbxColdError`, eliminating ~90 repetitive `.map_err(ColdStorageError::backend)` calls. Co-Authored-By: Claude Opus 4.5 --- crates/cold-mdbx/src/backend.rs | 678 +++++++++++++++++--------------- crates/cold-mdbx/src/error.rs | 7 + 2 files changed, 367 insertions(+), 318 deletions(-) diff --git a/crates/cold-mdbx/src/backend.rs b/crates/cold-mdbx/src/backend.rs index 151d1b0..5dc07ef 100644 --- a/crates/cold-mdbx/src/backend.rs +++ b/crates/cold-mdbx/src/backend.rs @@ -10,8 +10,8 @@ use crate::{ }; use alloy::{consensus::Header, primitives::BlockNumber}; use signet_cold::{ - BlockData, BlockTag, ColdResult, ColdStorage, ColdStorageError, HeaderSpecifier, - ReceiptSpecifier, SignetEventsSpecifier, TransactionSpecifier, ZenithHeaderSpecifier, + BlockData, BlockTag, ColdResult, ColdStorage, HeaderSpecifier, ReceiptSpecifier, + SignetEventsSpecifier, TransactionSpecifier, ZenithHeaderSpecifier, }; use signet_hot::{ KeySer, MAX_KEY_SIZE, ValSer, @@ -131,11 +131,10 @@ impl MdbxColdBackend { BlockTag::Safe => MetadataKey::SafeBlock, BlockTag::Earliest => MetadataKey::EarliestBlock, }; - TableTraverse::::exact( + Ok(TableTraverse::::exact( &mut tx.new_cursor::()?, &metadata_key, - ) - .map_err(MdbxColdError::from) + )?) } /// Get the block number for a block hash. @@ -144,11 +143,10 @@ impl MdbxColdBackend { hash: alloy::primitives::B256, ) -> Result, MdbxColdError> { let tx = self.env.tx()?; - TableTraverse::::exact( + Ok(TableTraverse::::exact( &mut tx.new_cursor::()?, &hash, - ) - .map_err(MdbxColdError::from) + )?) } /// Get transaction location by hash. @@ -157,405 +155,449 @@ impl MdbxColdBackend { hash: alloy::primitives::B256, ) -> Result, MdbxColdError> { let tx = self.env.tx()?; - TableTraverse::::exact(&mut tx.new_cursor::()?, &hash) - .map_err(MdbxColdError::from) + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &hash, + )?) } -} - -fn to_cold_result(result: Result) -> ColdResult { - result.map_err(ColdStorageError::backend) -} - -impl ColdStorage for MdbxColdBackend { - async fn get_header(&self, spec: HeaderSpecifier) -> ColdResult> { - to_cold_result((|| { - let block_num = match spec { - HeaderSpecifier::Number(n) => Some(n), - HeaderSpecifier::Hash(h) => self.get_block_by_hash(h)?, - HeaderSpecifier::Tag(tag) => self.resolve_tag(tag)?, - }; - let Some(block_num) = block_num else { - return Ok(None); - }; + /// Internal implementation of get_header that returns MdbxColdError. + fn get_header_inner(&self, spec: HeaderSpecifier) -> Result, MdbxColdError> { + let block_num = match spec { + HeaderSpecifier::Number(n) => Some(n), + HeaderSpecifier::Hash(h) => self.get_block_by_hash(h)?, + HeaderSpecifier::Tag(tag) => self.resolve_tag(tag)?, + }; - let tx = self.env.tx()?; - TableTraverse::::exact(&mut tx.new_cursor::()?, &block_num) - .map_err(MdbxColdError::from) - })()) - } + let Some(block_num) = block_num else { + return Ok(None); + }; - async fn get_headers(&self, specs: Vec) -> ColdResult>> { - let mut results = Vec::with_capacity(specs.len()); - for spec in specs { - results.push(self.get_header(spec).await?); - } - Ok(results) + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &block_num, + )?) } - async fn get_transaction( + /// Internal implementation of get_transaction that returns MdbxColdError. + fn get_transaction_inner( &self, spec: TransactionSpecifier, - ) -> ColdResult> { - to_cold_result((|| { - let (block, index) = match spec { - TransactionSpecifier::Hash(h) => { - let Some(loc) = self.get_tx_location(h)? else { - return Ok(None); - }; - (loc.block, loc.index) - } - TransactionSpecifier::BlockAndIndex { block, index } => (block, index), - TransactionSpecifier::BlockHashAndIndex { block_hash, index } => { - let Some(block) = self.get_block_by_hash(block_hash)? else { - return Ok(None); - }; - (block, index) - } - }; + ) -> Result, MdbxColdError> { + let (block, index) = match spec { + TransactionSpecifier::Hash(h) => { + let Some(loc) = self.get_tx_location(h)? else { + return Ok(None); + }; + (loc.block, loc.index) + } + TransactionSpecifier::BlockAndIndex { block, index } => (block, index), + TransactionSpecifier::BlockHashAndIndex { block_hash, index } => { + let Some(block) = self.get_block_by_hash(block_hash)? else { + return Ok(None); + }; + (block, index) + } + }; - let tx = self.env.tx()?; - DualTableTraverse::::exact_dual( - &mut tx.new_cursor::()?, - &block, - &index, - ) - .map_err(MdbxColdError::from) - })()) + let tx = self.env.tx()?; + Ok(DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + )?) } - async fn get_transactions_in_block( + /// Internal implementation of get_transactions_in_block. + fn get_transactions_in_block_inner( &self, block: BlockNumber, - ) -> ColdResult> { - to_cold_result((|| { - let tx = self.env.tx()?; - let mut cursor = tx.new_cursor::()?; + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; - let mut transactions = Vec::new(); - for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { - let (_, tx_signed) = item?; - transactions.push(tx_signed); - } + let mut transactions = Vec::new(); + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + let (_, tx_signed) = item?; + transactions.push(tx_signed); + } - Ok(transactions) - })()) + Ok(transactions) } - async fn get_transaction_count(&self, block: BlockNumber) -> ColdResult { - to_cold_result((|| { - let tx = self.env.tx()?; - let mut cursor = tx.new_cursor::()?; + /// Internal implementation of get_transaction_count. + fn get_transaction_count_inner(&self, block: BlockNumber) -> Result { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; - let mut count = 0u64; - for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { - let _ = item?; - count += 1; - } + let mut count = 0u64; + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + let _ = item?; + count += 1; + } - Ok(count) - })()) + Ok(count) } - async fn get_receipt(&self, spec: ReceiptSpecifier) -> ColdResult> { - to_cold_result((|| { - let (block, index) = match spec { - ReceiptSpecifier::TxHash(h) => { - let Some(loc) = self.get_tx_location(h)? else { - return Ok(None); - }; - (loc.block, loc.index) - } - ReceiptSpecifier::BlockAndIndex { block, index } => (block, index), - }; + /// Internal implementation of get_receipt. + fn get_receipt_inner(&self, spec: ReceiptSpecifier) -> Result, MdbxColdError> { + let (block, index) = match spec { + ReceiptSpecifier::TxHash(h) => { + let Some(loc) = self.get_tx_location(h)? else { + return Ok(None); + }; + (loc.block, loc.index) + } + ReceiptSpecifier::BlockAndIndex { block, index } => (block, index), + }; - let tx = self.env.tx()?; - DualTableTraverse::::exact_dual( - &mut tx.new_cursor::()?, - &block, - &index, - ) - .map_err(MdbxColdError::from) - })()) + let tx = self.env.tx()?; + Ok(DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + )?) } - async fn get_receipts_in_block(&self, block: BlockNumber) -> ColdResult> { - to_cold_result((|| { - let tx = self.env.tx()?; - let mut cursor = tx.new_cursor::()?; - - let mut receipts = Vec::new(); - for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { - let (_, receipt) = item?; - receipts.push(receipt); - } + /// Internal implementation of get_receipts_in_block. + fn get_receipts_in_block_inner( + &self, + block: BlockNumber, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; - Ok(receipts) - })()) + let mut receipts = Vec::new(); + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + let (_, receipt) = item?; + receipts.push(receipt); + } + + Ok(receipts) } - async fn get_signet_events( + /// Internal implementation of get_signet_events. + fn get_signet_events_inner( &self, spec: SignetEventsSpecifier, - ) -> ColdResult> { - to_cold_result((|| { - let tx = self.env.tx()?; + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; - match spec { - SignetEventsSpecifier::Block(block) => { + match spec { + SignetEventsSpecifier::Block(block) => { + let mut cursor = tx.new_cursor::()?; + let mut events = Vec::new(); + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? + { + let (_, event) = item?; + events.push(event); + } + Ok(events) + } + SignetEventsSpecifier::BlockRange { start, end } => { + let mut events = Vec::new(); + for block in start..=end { let mut cursor = tx.new_cursor::()?; - let mut events = Vec::new(); for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { let (_, event) = item?; events.push(event); } - Ok(events) - } - SignetEventsSpecifier::BlockRange { start, end } => { - let mut events = Vec::new(); - for block in start..=end { - let mut cursor = tx.new_cursor::()?; - for item in - DualTableTraverse::::iter_k2(&mut cursor, &block)? - { - let (_, event) = item?; - events.push(event); - } - } - Ok(events) } + Ok(events) } - })()) + } } - async fn get_zenith_header( + /// Internal implementation of get_zenith_header. + fn get_zenith_header_inner( &self, spec: ZenithHeaderSpecifier, - ) -> ColdResult> { - to_cold_result((|| { - let block = match spec { - ZenithHeaderSpecifier::Number(n) => n, - ZenithHeaderSpecifier::Range { start, .. } => start, - }; + ) -> Result, MdbxColdError> { + let block = match spec { + ZenithHeaderSpecifier::Number(n) => n, + ZenithHeaderSpecifier::Range { start, .. } => start, + }; - let tx = self.env.tx()?; - TableTraverse::::exact( - &mut tx.new_cursor::()?, - &block, - ) - .map_err(MdbxColdError::from) - })()) + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &block, + )?) } - async fn get_zenith_headers( + /// Internal implementation of get_zenith_headers. + fn get_zenith_headers_inner( &self, spec: ZenithHeaderSpecifier, - ) -> ColdResult> { - to_cold_result((|| { - let tx = self.env.tx()?; - - match spec { - ZenithHeaderSpecifier::Number(n) => { - let header: Option = - TableTraverse::::exact( - &mut tx.new_cursor::()?, - &n, - )?; - Ok(header.into_iter().collect()) - } - ZenithHeaderSpecifier::Range { start, end } => { - let mut cursor = tx.new_cursor::()?; - let mut headers = Vec::new(); + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; - // Position at start - let mut key_buf = [0u8; MAX_KEY_SIZE]; - let key_bytes = start.encode_key(&mut key_buf); + match spec { + ZenithHeaderSpecifier::Number(n) => { + let header: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &n, + )?; + Ok(header.into_iter().collect()) + } + ZenithHeaderSpecifier::Range { start, end } => { + let mut cursor = tx.new_cursor::()?; + let mut headers = Vec::new(); - if let Some((key, value)) = - KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? - { - let block_num = BlockNumber::decode_key(&key)?; - if block_num <= end { - headers.push(DbZenithHeader::decode_value(&value)?); - } + // Position at start + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = start.encode_key(&mut key_buf); - while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { - let block_num = BlockNumber::decode_key(&key)?; - if block_num > end { - break; - } - headers.push(DbZenithHeader::decode_value(&value)?); - } + if let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? { + let block_num = BlockNumber::decode_key(&key)?; + if block_num <= end { + headers.push(DbZenithHeader::decode_value(&value)?); } - Ok(headers) + while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { + let block_num = BlockNumber::decode_key(&key)?; + if block_num > end { + break; + } + headers.push(DbZenithHeader::decode_value(&value)?); + } } + + Ok(headers) } - })()) + } } - async fn get_latest_block(&self) -> ColdResult> { - to_cold_result((|| { - let tx = self.env.tx()?; - TableTraverse::::exact( - &mut tx.new_cursor::()?, - &MetadataKey::LatestBlock, - ) - .map_err(MdbxColdError::from) - })()) + /// Internal implementation of get_latest_block. + fn get_latest_block_inner(&self) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::LatestBlock, + )?) } - async fn append_block(&self, data: BlockData) -> ColdResult<()> { - to_cold_result((|| { - let tx = self.env.tx_rw()?; - let block = data.block_number(); - - // Store header - tx.queue_put::(&block, &data.header)?; - - // Store header hash index - let header_hash = data.header.hash_slow(); - tx.queue_put::(&header_hash, &block)?; - - // Store transactions and their hash index - for (idx, tx_signed) in data.transactions.iter().enumerate() { - let tx_idx = idx as u64; - tx.queue_put_dual::(&block, &tx_idx, tx_signed)?; - - // Store tx hash index - let tx_hash = *tx_signed.hash(); - let location = TxLocation::new(block, tx_idx); - tx.queue_put::(&tx_hash, &location)?; - } + /// Internal implementation of append_block. + fn append_block_inner(&self, data: BlockData) -> Result<(), MdbxColdError> { + let tx = self.env.tx_rw()?; + let block = data.block_number(); - // Store receipts - for (idx, receipt) in data.receipts.iter().enumerate() { - let receipt_idx = idx as u64; - tx.queue_put_dual::(&block, &receipt_idx, receipt)?; - } + // Store header + tx.queue_put::(&block, &data.header)?; - // Store signet events - for (idx, event) in data.signet_events.iter().enumerate() { - let event_idx = idx as u64; - tx.queue_put_dual::(&block, &event_idx, event)?; - } + // Store header hash index + let header_hash = data.header.hash_slow(); + tx.queue_put::(&header_hash, &block)?; - // Store zenith header if present - if let Some(zh) = &data.zenith_header { - tx.queue_put::(&block, zh)?; - } + // Store transactions and their hash index + for (idx, tx_signed) in data.transactions.iter().enumerate() { + let tx_idx = idx as u64; + tx.queue_put_dual::(&block, &tx_idx, tx_signed)?; - // Update latest block if this is higher - let current_latest: Option = TableTraverse::::exact( - &mut tx.new_cursor::()?, - &MetadataKey::LatestBlock, - )?; - let new_latest = current_latest.map_or(block, |prev: BlockNumber| prev.max(block)); - tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; + // Store tx hash index + let tx_hash = *tx_signed.hash(); + let location = TxLocation::new(block, tx_idx); + tx.queue_put::(&tx_hash, &location)?; + } - // Update earliest block if not set or if this is lower - let current_earliest: Option = TableTraverse::::exact( - &mut tx.new_cursor::()?, - &MetadataKey::EarliestBlock, - )?; - let new_earliest = current_earliest.map_or(block, |prev: BlockNumber| prev.min(block)); - tx.queue_put::(&MetadataKey::EarliestBlock, &new_earliest)?; + // Store receipts + for (idx, receipt) in data.receipts.iter().enumerate() { + let receipt_idx = idx as u64; + tx.queue_put_dual::(&block, &receipt_idx, receipt)?; + } - tx.raw_commit()?; - Ok(()) - })()) - } + // Store signet events + for (idx, event) in data.signet_events.iter().enumerate() { + let event_idx = idx as u64; + tx.queue_put_dual::(&block, &event_idx, event)?; + } - async fn append_blocks(&self, data: Vec) -> ColdResult<()> { - for block_data in data { - self.append_block(block_data).await?; + // Store zenith header if present + if let Some(zh) = &data.zenith_header { + tx.queue_put::(&block, zh)?; } + + // Update latest block if this is higher + let current_latest: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::LatestBlock, + )?; + let new_latest = current_latest.map_or(block, |prev: BlockNumber| prev.max(block)); + tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; + + // Update earliest block if not set or if this is lower + let current_earliest: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::EarliestBlock, + )?; + let new_earliest = current_earliest.map_or(block, |prev: BlockNumber| prev.min(block)); + tx.queue_put::(&MetadataKey::EarliestBlock, &new_earliest)?; + + tx.raw_commit()?; Ok(()) } - async fn truncate_above(&self, block: BlockNumber) -> ColdResult<()> { - to_cold_result((|| { - let tx = self.env.tx_rw()?; + /// Internal implementation of truncate_above. + fn truncate_above_inner(&self, block: BlockNumber) -> Result<(), MdbxColdError> { + let tx = self.env.tx_rw()?; - // Collect headers to remove for hash index cleanup - let mut headers_to_remove: Vec<(BlockNumber, Header)> = Vec::new(); - { - let mut cursor = tx.new_cursor::()?; + // Collect headers to remove for hash index cleanup + let mut headers_to_remove: Vec<(BlockNumber, Header)> = Vec::new(); + { + let mut cursor = tx.new_cursor::()?; - // Position after the block we want to keep - let mut key_buf = [0u8; MAX_KEY_SIZE]; - let start_block = block + 1; - let key_bytes = start_block.encode_key(&mut key_buf); + // Position after the block we want to keep + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let start_block = block + 1; + let key_bytes = start_block.encode_key(&mut key_buf); - if let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? { + if let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? { + let block_num = BlockNumber::decode_key(&key)?; + let header = Header::decode_value(&value)?; + headers_to_remove.push((block_num, header)); + + while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { let block_num = BlockNumber::decode_key(&key)?; let header = Header::decode_value(&value)?; headers_to_remove.push((block_num, header)); - - while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { - let block_num = BlockNumber::decode_key(&key)?; - let header = Header::decode_value(&value)?; - headers_to_remove.push((block_num, header)); - } } } + } - // Collect transactions to remove for hash index cleanup - let mut tx_hashes_to_remove: Vec = Vec::new(); - for (block_num, _) in &headers_to_remove { - let mut cursor = tx.new_cursor::()?; - for item in - DualTableTraverse::::iter_k2(&mut cursor, block_num)? - { - let (_, tx_signed): (u64, TransactionSigned) = item?; - tx_hashes_to_remove.push(*tx_signed.hash()); - } + // Collect transactions to remove for hash index cleanup + let mut tx_hashes_to_remove: Vec = Vec::new(); + for (block_num, _) in &headers_to_remove { + let mut cursor = tx.new_cursor::()?; + for item in DualTableTraverse::::iter_k2(&mut cursor, block_num)? { + let (_, tx_signed): (u64, TransactionSigned) = item?; + tx_hashes_to_remove.push(*tx_signed.hash()); } + } - // Delete headers and their hash index entries - for (block_num, header) in &headers_to_remove { - tx.queue_delete::(block_num)?; - tx.queue_delete::(&header.hash_slow())?; - } + // Delete headers and their hash index entries + for (block_num, header) in &headers_to_remove { + tx.queue_delete::(block_num)?; + tx.queue_delete::(&header.hash_slow())?; + } - // Delete transaction hash index entries - for tx_hash in &tx_hashes_to_remove { - tx.queue_delete::(tx_hash)?; - } + // Delete transaction hash index entries + for tx_hash in &tx_hashes_to_remove { + tx.queue_delete::(tx_hash)?; + } - // Delete transactions, receipts, and signet events for removed blocks - for (block_num, _) in &headers_to_remove { - // Delete all transactions for this block - tx.clear_k1_for::(block_num)?; + // Delete transactions, receipts, and signet events for removed blocks + for (block_num, _) in &headers_to_remove { + // Delete all transactions for this block + tx.clear_k1_for::(block_num)?; - // Delete all receipts for this block - tx.clear_k1_for::(block_num)?; + // Delete all receipts for this block + tx.clear_k1_for::(block_num)?; - // Delete all signet events for this block - tx.clear_k1_for::(block_num)?; + // Delete all signet events for this block + tx.clear_k1_for::(block_num)?; - // Delete zenith header for this block - tx.queue_delete::(block_num)?; - } + // Delete zenith header for this block + tx.queue_delete::(block_num)?; + } - // Update latest block metadata - if !headers_to_remove.is_empty() { - // Find the new latest block - let mut cursor = tx.new_cursor::()?; - if let Some((key, _)) = KvTraverse::<_>::last(&mut cursor)? { - let new_latest = BlockNumber::decode_key(&key)?; - tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; - } else { - // No blocks left, remove latest block metadata - tx.queue_delete::(&MetadataKey::LatestBlock)?; - } + // Update latest block metadata + if !headers_to_remove.is_empty() { + // Find the new latest block + let mut cursor = tx.new_cursor::()?; + if let Some((key, _)) = KvTraverse::<_>::last(&mut cursor)? { + let new_latest = BlockNumber::decode_key(&key)?; + tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; + } else { + // No blocks left, remove latest block metadata + tx.queue_delete::(&MetadataKey::LatestBlock)?; } + } + + tx.raw_commit()?; + Ok(()) + } +} + +impl ColdStorage for MdbxColdBackend { + async fn get_header(&self, spec: HeaderSpecifier) -> ColdResult> { + Ok(self.get_header_inner(spec)?) + } + + async fn get_headers(&self, specs: Vec) -> ColdResult>> { + let mut results = Vec::with_capacity(specs.len()); + for spec in specs { + results.push(self.get_header_inner(spec)?); + } + Ok(results) + } + + async fn get_transaction( + &self, + spec: TransactionSpecifier, + ) -> ColdResult> { + Ok(self.get_transaction_inner(spec)?) + } + + async fn get_transactions_in_block( + &self, + block: BlockNumber, + ) -> ColdResult> { + Ok(self.get_transactions_in_block_inner(block)?) + } + + async fn get_transaction_count(&self, block: BlockNumber) -> ColdResult { + Ok(self.get_transaction_count_inner(block)?) + } + + async fn get_receipt(&self, spec: ReceiptSpecifier) -> ColdResult> { + Ok(self.get_receipt_inner(spec)?) + } + + async fn get_receipts_in_block(&self, block: BlockNumber) -> ColdResult> { + Ok(self.get_receipts_in_block_inner(block)?) + } + + async fn get_signet_events( + &self, + spec: SignetEventsSpecifier, + ) -> ColdResult> { + Ok(self.get_signet_events_inner(spec)?) + } + + async fn get_zenith_header( + &self, + spec: ZenithHeaderSpecifier, + ) -> ColdResult> { + Ok(self.get_zenith_header_inner(spec)?) + } + + async fn get_zenith_headers( + &self, + spec: ZenithHeaderSpecifier, + ) -> ColdResult> { + Ok(self.get_zenith_headers_inner(spec)?) + } + + async fn get_latest_block(&self) -> ColdResult> { + Ok(self.get_latest_block_inner()?) + } + + async fn append_block(&self, data: BlockData) -> ColdResult<()> { + Ok(self.append_block_inner(data)?) + } + + async fn append_blocks(&self, data: Vec) -> ColdResult<()> { + for block_data in data { + self.append_block_inner(block_data)?; + } + Ok(()) + } - tx.raw_commit()?; - Ok(()) - })()) + async fn truncate_above(&self, block: BlockNumber) -> ColdResult<()> { + Ok(self.truncate_above_inner(block)?) } } diff --git a/crates/cold-mdbx/src/error.rs b/crates/cold-mdbx/src/error.rs index d3dccb2..3c8474e 100644 --- a/crates/cold-mdbx/src/error.rs +++ b/crates/cold-mdbx/src/error.rs @@ -18,3 +18,10 @@ pub enum MdbxColdError { #[error("database is read-only")] ReadOnly, } + +#[cfg(feature = "backend")] +impl From for signet_cold::ColdStorageError { + fn from(error: MdbxColdError) -> Self { + Self::Backend(Box::new(error)) + } +} From 22296b1ce909f16c42567938519a10456571204a Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 14:42:25 -0500 Subject: [PATCH 7/9] refactor: reduce nesting and simplify backend.rs - Extract specifier resolution into dedicated methods (resolve_header_spec, resolve_tx_spec, resolve_receipt_spec) - Replace large match blocks with function invocations - Use iterator collection patterns to avoid manual loop allocation - Simplify table creation with a loop over table metadata - Use scoped blocks to manage cursor lifetimes in truncate_above_inner Co-Authored-By: Claude Opus 4.5 --- crates/cold-mdbx/src/backend.rs | 578 +++++++++++++++----------------- 1 file changed, 271 insertions(+), 307 deletions(-) diff --git a/crates/cold-mdbx/src/backend.rs b/crates/cold-mdbx/src/backend.rs index 5dc07ef..1df1e45 100644 --- a/crates/cold-mdbx/src/backend.rs +++ b/crates/cold-mdbx/src/backend.rs @@ -62,82 +62,81 @@ impl MdbxColdBackend { Ok(backend) } - /// Create all required tables if they don't exist. fn create_tables(&self) -> Result<(), MdbxColdError> { let tx = self.env.tx_rw()?; - // Create single-key tables - tx.queue_raw_create( - ColdHeaders::NAME, - ColdHeaders::DUAL_KEY_SIZE, - ColdHeaders::FIXED_VAL_SIZE, - ColdHeaders::INT_KEY, - )?; - tx.queue_raw_create( - ColdZenithHeaders::NAME, - ColdZenithHeaders::DUAL_KEY_SIZE, - ColdZenithHeaders::FIXED_VAL_SIZE, - ColdZenithHeaders::INT_KEY, - )?; - tx.queue_raw_create( - ColdBlockHashIndex::NAME, - ColdBlockHashIndex::DUAL_KEY_SIZE, - ColdBlockHashIndex::FIXED_VAL_SIZE, - ColdBlockHashIndex::INT_KEY, - )?; - tx.queue_raw_create( - ColdTxHashIndex::NAME, - ColdTxHashIndex::DUAL_KEY_SIZE, - ColdTxHashIndex::FIXED_VAL_SIZE, - ColdTxHashIndex::INT_KEY, - )?; - tx.queue_raw_create( - ColdMetadata::NAME, - ColdMetadata::DUAL_KEY_SIZE, - ColdMetadata::FIXED_VAL_SIZE, - ColdMetadata::INT_KEY, - )?; - - // Create dual-key (DUPSORT) tables - tx.queue_raw_create( - ColdTransactions::NAME, - ColdTransactions::DUAL_KEY_SIZE, - ColdTransactions::FIXED_VAL_SIZE, - ColdTransactions::INT_KEY, - )?; - tx.queue_raw_create( - ColdReceipts::NAME, - ColdReceipts::DUAL_KEY_SIZE, - ColdReceipts::FIXED_VAL_SIZE, - ColdReceipts::INT_KEY, - )?; - tx.queue_raw_create( - ColdSignetEvents::NAME, - ColdSignetEvents::DUAL_KEY_SIZE, - ColdSignetEvents::FIXED_VAL_SIZE, - ColdSignetEvents::INT_KEY, - )?; + for (name, dual_key_size, fixed_val_size, int_key) in [ + ( + ColdHeaders::NAME, + ColdHeaders::DUAL_KEY_SIZE, + ColdHeaders::FIXED_VAL_SIZE, + ColdHeaders::INT_KEY, + ), + ( + ColdZenithHeaders::NAME, + ColdZenithHeaders::DUAL_KEY_SIZE, + ColdZenithHeaders::FIXED_VAL_SIZE, + ColdZenithHeaders::INT_KEY, + ), + ( + ColdBlockHashIndex::NAME, + ColdBlockHashIndex::DUAL_KEY_SIZE, + ColdBlockHashIndex::FIXED_VAL_SIZE, + ColdBlockHashIndex::INT_KEY, + ), + ( + ColdTxHashIndex::NAME, + ColdTxHashIndex::DUAL_KEY_SIZE, + ColdTxHashIndex::FIXED_VAL_SIZE, + ColdTxHashIndex::INT_KEY, + ), + ( + ColdMetadata::NAME, + ColdMetadata::DUAL_KEY_SIZE, + ColdMetadata::FIXED_VAL_SIZE, + ColdMetadata::INT_KEY, + ), + ( + ColdTransactions::NAME, + ColdTransactions::DUAL_KEY_SIZE, + ColdTransactions::FIXED_VAL_SIZE, + ColdTransactions::INT_KEY, + ), + ( + ColdReceipts::NAME, + ColdReceipts::DUAL_KEY_SIZE, + ColdReceipts::FIXED_VAL_SIZE, + ColdReceipts::INT_KEY, + ), + ( + ColdSignetEvents::NAME, + ColdSignetEvents::DUAL_KEY_SIZE, + ColdSignetEvents::FIXED_VAL_SIZE, + ColdSignetEvents::INT_KEY, + ), + ] { + tx.queue_raw_create(name, dual_key_size, fixed_val_size, int_key)?; + } tx.raw_commit()?; Ok(()) } - /// Resolve a block tag to a block number. fn resolve_tag(&self, tag: BlockTag) -> Result, MdbxColdError> { - let tx = self.env.tx()?; - let metadata_key = match tag { + let key = match tag { BlockTag::Latest => MetadataKey::LatestBlock, BlockTag::Finalized => MetadataKey::FinalizedBlock, BlockTag::Safe => MetadataKey::SafeBlock, BlockTag::Earliest => MetadataKey::EarliestBlock, }; - Ok(TableTraverse::::exact( - &mut tx.new_cursor::()?, - &metadata_key, - )?) + self.get_metadata(key) + } + + fn get_metadata(&self, key: MetadataKey) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(TableTraverse::::exact(&mut tx.new_cursor::()?, &key)?) } - /// Get the block number for a block hash. fn get_block_by_hash( &self, hash: alloy::primitives::B256, @@ -149,7 +148,6 @@ impl MdbxColdBackend { )?) } - /// Get transaction location by hash. fn get_tx_location( &self, hash: alloy::primitives::B256, @@ -161,18 +159,48 @@ impl MdbxColdBackend { )?) } - /// Internal implementation of get_header that returns MdbxColdError. - fn get_header_inner(&self, spec: HeaderSpecifier) -> Result, MdbxColdError> { - let block_num = match spec { - HeaderSpecifier::Number(n) => Some(n), - HeaderSpecifier::Hash(h) => self.get_block_by_hash(h)?, - HeaderSpecifier::Tag(tag) => self.resolve_tag(tag)?, - }; + fn resolve_header_spec( + &self, + spec: HeaderSpecifier, + ) -> Result, MdbxColdError> { + match spec { + HeaderSpecifier::Number(n) => Ok(Some(n)), + HeaderSpecifier::Hash(h) => self.get_block_by_hash(h), + HeaderSpecifier::Tag(tag) => self.resolve_tag(tag), + } + } - let Some(block_num) = block_num else { - return Ok(None); - }; + fn resolve_tx_spec( + &self, + spec: TransactionSpecifier, + ) -> Result, MdbxColdError> { + match spec { + TransactionSpecifier::Hash(h) => { + self.get_tx_location(h).map(|opt| opt.map(|loc| (loc.block, loc.index))) + } + TransactionSpecifier::BlockAndIndex { block, index } => Ok(Some((block, index))), + TransactionSpecifier::BlockHashAndIndex { block_hash, index } => { + self.get_block_by_hash(block_hash).map(|opt| opt.map(|b| (b, index))) + } + } + } + fn resolve_receipt_spec( + &self, + spec: ReceiptSpecifier, + ) -> Result, MdbxColdError> { + match spec { + ReceiptSpecifier::TxHash(h) => { + self.get_tx_location(h).map(|opt| opt.map(|loc| (loc.block, loc.index))) + } + ReceiptSpecifier::BlockAndIndex { block, index } => Ok(Some((block, index))), + } + } + + fn get_header_by_number( + &self, + block_num: BlockNumber, + ) -> Result, MdbxColdError> { let tx = self.env.tx()?; Ok(TableTraverse::::exact( &mut tx.new_cursor::()?, @@ -180,27 +208,11 @@ impl MdbxColdBackend { )?) } - /// Internal implementation of get_transaction that returns MdbxColdError. - fn get_transaction_inner( + fn get_transaction_by_location( &self, - spec: TransactionSpecifier, + block: BlockNumber, + index: u64, ) -> Result, MdbxColdError> { - let (block, index) = match spec { - TransactionSpecifier::Hash(h) => { - let Some(loc) = self.get_tx_location(h)? else { - return Ok(None); - }; - (loc.block, loc.index) - } - TransactionSpecifier::BlockAndIndex { block, index } => (block, index), - TransactionSpecifier::BlockHashAndIndex { block_hash, index } => { - let Some(block) = self.get_block_by_hash(block_hash)? else { - return Ok(None); - }; - (block, index) - } - }; - let tx = self.env.tx()?; Ok(DualTableTraverse::::exact_dual( &mut tx.new_cursor::()?, @@ -209,310 +221,230 @@ impl MdbxColdBackend { )?) } - /// Internal implementation of get_transactions_in_block. - fn get_transactions_in_block_inner( + fn get_receipt_by_location( &self, block: BlockNumber, - ) -> Result, MdbxColdError> { + index: u64, + ) -> Result, MdbxColdError> { let tx = self.env.tx()?; - let mut cursor = tx.new_cursor::()?; - - let mut transactions = Vec::new(); - for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { - let (_, tx_signed) = item?; - transactions.push(tx_signed); - } + Ok(DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + )?) + } - Ok(transactions) + fn get_zenith_header_by_number( + &self, + block: BlockNumber, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &block, + )?) } - /// Internal implementation of get_transaction_count. - fn get_transaction_count_inner(&self, block: BlockNumber) -> Result { + fn collect_transactions_in_block( + &self, + block: BlockNumber, + ) -> Result, MdbxColdError> { let tx = self.env.tx()?; let mut cursor = tx.new_cursor::()?; + DualTableTraverse::::iter_k2(&mut cursor, &block)? + .map(|item| item.map(|(_, v)| v)) + .collect::>() + .map_err(Into::into) + } + fn count_transactions_in_block(&self, block: BlockNumber) -> Result { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; let mut count = 0u64; for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { - let _ = item?; + item?; count += 1; } - Ok(count) } - /// Internal implementation of get_receipt. - fn get_receipt_inner(&self, spec: ReceiptSpecifier) -> Result, MdbxColdError> { - let (block, index) = match spec { - ReceiptSpecifier::TxHash(h) => { - let Some(loc) = self.get_tx_location(h)? else { - return Ok(None); - }; - (loc.block, loc.index) - } - ReceiptSpecifier::BlockAndIndex { block, index } => (block, index), - }; - + fn collect_receipts_in_block(&self, block: BlockNumber) -> Result, MdbxColdError> { let tx = self.env.tx()?; - Ok(DualTableTraverse::::exact_dual( - &mut tx.new_cursor::()?, - &block, - &index, - )?) + let mut cursor = tx.new_cursor::()?; + DualTableTraverse::::iter_k2(&mut cursor, &block)? + .map(|item| item.map(|(_, v)| v)) + .collect::>() + .map_err(Into::into) } - /// Internal implementation of get_receipts_in_block. - fn get_receipts_in_block_inner( + fn collect_signet_events_in_block( &self, block: BlockNumber, - ) -> Result, MdbxColdError> { + ) -> Result, MdbxColdError> { let tx = self.env.tx()?; - let mut cursor = tx.new_cursor::()?; - - let mut receipts = Vec::new(); - for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { - let (_, receipt) = item?; - receipts.push(receipt); - } - - Ok(receipts) + let mut cursor = tx.new_cursor::()?; + DualTableTraverse::::iter_k2(&mut cursor, &block)? + .map(|item| item.map(|(_, v)| v)) + .collect::>() + .map_err(Into::into) } - /// Internal implementation of get_signet_events. - fn get_signet_events_inner( + fn collect_signet_events_in_range( &self, - spec: SignetEventsSpecifier, + start: BlockNumber, + end: BlockNumber, ) -> Result, MdbxColdError> { let tx = self.env.tx()?; - - match spec { - SignetEventsSpecifier::Block(block) => { - let mut cursor = tx.new_cursor::()?; - let mut events = Vec::new(); - for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? - { - let (_, event) = item?; - events.push(event); - } - Ok(events) - } - SignetEventsSpecifier::BlockRange { start, end } => { - let mut events = Vec::new(); - for block in start..=end { - let mut cursor = tx.new_cursor::()?; - for item in - DualTableTraverse::::iter_k2(&mut cursor, &block)? - { - let (_, event) = item?; - events.push(event); - } - } - Ok(events) + let mut events = Vec::new(); + for block in start..=end { + let mut cursor = tx.new_cursor::()?; + for item in DualTableTraverse::::iter_k2(&mut cursor, &block)? { + events.push(item?.1); } } + Ok(events) } - /// Internal implementation of get_zenith_header. - fn get_zenith_header_inner( + fn collect_zenith_headers_in_range( &self, - spec: ZenithHeaderSpecifier, - ) -> Result, MdbxColdError> { - let block = match spec { - ZenithHeaderSpecifier::Number(n) => n, - ZenithHeaderSpecifier::Range { start, .. } => start, - }; - - let tx = self.env.tx()?; - Ok(TableTraverse::::exact( - &mut tx.new_cursor::()?, - &block, - )?) - } - - /// Internal implementation of get_zenith_headers. - fn get_zenith_headers_inner( - &self, - spec: ZenithHeaderSpecifier, + start: BlockNumber, + end: BlockNumber, ) -> Result, MdbxColdError> { let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; + let mut headers = Vec::new(); - match spec { - ZenithHeaderSpecifier::Number(n) => { - let header: Option = TableTraverse::::exact( - &mut tx.new_cursor::()?, - &n, - )?; - Ok(header.into_iter().collect()) - } - ZenithHeaderSpecifier::Range { start, end } => { - let mut cursor = tx.new_cursor::()?; - let mut headers = Vec::new(); - - // Position at start - let mut key_buf = [0u8; MAX_KEY_SIZE]; - let key_bytes = start.encode_key(&mut key_buf); - - if let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? { - let block_num = BlockNumber::decode_key(&key)?; - if block_num <= end { - headers.push(DbZenithHeader::decode_value(&value)?); - } - - while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { - let block_num = BlockNumber::decode_key(&key)?; - if block_num > end { - break; - } - headers.push(DbZenithHeader::decode_value(&value)?); - } - } + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = start.encode_key(&mut key_buf); - Ok(headers) + let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? else { + return Ok(headers); + }; + + let block_num = BlockNumber::decode_key(&key)?; + if block_num <= end { + headers.push(DbZenithHeader::decode_value(&value)?); + } + + while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { + let block_num = BlockNumber::decode_key(&key)?; + if block_num > end { + break; } + headers.push(DbZenithHeader::decode_value(&value)?); } - } - /// Internal implementation of get_latest_block. - fn get_latest_block_inner(&self) -> Result, MdbxColdError> { - let tx = self.env.tx()?; - Ok(TableTraverse::::exact( - &mut tx.new_cursor::()?, - &MetadataKey::LatestBlock, - )?) + Ok(headers) } - /// Internal implementation of append_block. fn append_block_inner(&self, data: BlockData) -> Result<(), MdbxColdError> { let tx = self.env.tx_rw()?; let block = data.block_number(); - // Store header tx.queue_put::(&block, &data.header)?; + tx.queue_put::(&data.header.hash_slow(), &block)?; - // Store header hash index - let header_hash = data.header.hash_slow(); - tx.queue_put::(&header_hash, &block)?; - - // Store transactions and their hash index for (idx, tx_signed) in data.transactions.iter().enumerate() { let tx_idx = idx as u64; tx.queue_put_dual::(&block, &tx_idx, tx_signed)?; - - // Store tx hash index - let tx_hash = *tx_signed.hash(); - let location = TxLocation::new(block, tx_idx); - tx.queue_put::(&tx_hash, &location)?; + tx.queue_put::(tx_signed.hash(), &TxLocation::new(block, tx_idx))?; } - // Store receipts for (idx, receipt) in data.receipts.iter().enumerate() { - let receipt_idx = idx as u64; - tx.queue_put_dual::(&block, &receipt_idx, receipt)?; + tx.queue_put_dual::(&block, &(idx as u64), receipt)?; } - // Store signet events for (idx, event) in data.signet_events.iter().enumerate() { - let event_idx = idx as u64; - tx.queue_put_dual::(&block, &event_idx, event)?; + tx.queue_put_dual::(&block, &(idx as u64), event)?; } - // Store zenith header if present if let Some(zh) = &data.zenith_header { tx.queue_put::(&block, zh)?; } - // Update latest block if this is higher + // Update block bounds let current_latest: Option = TableTraverse::::exact( &mut tx.new_cursor::()?, &MetadataKey::LatestBlock, )?; - let new_latest = current_latest.map_or(block, |prev: BlockNumber| prev.max(block)); - tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; + tx.queue_put::( + &MetadataKey::LatestBlock, + ¤t_latest.map_or(block, |prev| prev.max(block)), + )?; - // Update earliest block if not set or if this is lower let current_earliest: Option = TableTraverse::::exact( &mut tx.new_cursor::()?, &MetadataKey::EarliestBlock, )?; - let new_earliest = current_earliest.map_or(block, |prev: BlockNumber| prev.min(block)); - tx.queue_put::(&MetadataKey::EarliestBlock, &new_earliest)?; + tx.queue_put::( + &MetadataKey::EarliestBlock, + ¤t_earliest.map_or(block, |prev| prev.min(block)), + )?; tx.raw_commit()?; Ok(()) } - /// Internal implementation of truncate_above. fn truncate_above_inner(&self, block: BlockNumber) -> Result<(), MdbxColdError> { let tx = self.env.tx_rw()?; - // Collect headers to remove for hash index cleanup - let mut headers_to_remove: Vec<(BlockNumber, Header)> = Vec::new(); - { + // Collect headers above the cutoff + let headers_to_remove = { let mut cursor = tx.new_cursor::()?; + let mut headers: Vec<(BlockNumber, Header)> = Vec::new(); - // Position after the block we want to keep - let mut key_buf = [0u8; MAX_KEY_SIZE]; let start_block = block + 1; + let mut key_buf = [0u8; MAX_KEY_SIZE]; let key_bytes = start_block.encode_key(&mut key_buf); if let Some((key, value)) = KvTraverse::<_>::lower_bound(&mut cursor, key_bytes)? { - let block_num = BlockNumber::decode_key(&key)?; - let header = Header::decode_value(&value)?; - headers_to_remove.push((block_num, header)); + headers.push((BlockNumber::decode_key(&key)?, Header::decode_value(&value)?)); while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { - let block_num = BlockNumber::decode_key(&key)?; - let header = Header::decode_value(&value)?; - headers_to_remove.push((block_num, header)); + headers.push((BlockNumber::decode_key(&key)?, Header::decode_value(&value)?)); } } - } + headers + }; - // Collect transactions to remove for hash index cleanup - let mut tx_hashes_to_remove: Vec = Vec::new(); - for (block_num, _) in &headers_to_remove { - let mut cursor = tx.new_cursor::()?; - for item in DualTableTraverse::::iter_k2(&mut cursor, block_num)? { - let (_, tx_signed): (u64, TransactionSigned) = item?; - tx_hashes_to_remove.push(*tx_signed.hash()); - } + if headers_to_remove.is_empty() { + return Ok(()); } - // Delete headers and their hash index entries + // Delete each block's data for (block_num, header) in &headers_to_remove { + // Delete transaction hash indices + { + let mut tx_cursor = tx.new_cursor::()?; + for item in + DualTableTraverse::::iter_k2(&mut tx_cursor, block_num)? + { + let (_, tx_signed): (u64, TransactionSigned) = item?; + tx.queue_delete::(tx_signed.hash())?; + } + } + tx.queue_delete::(block_num)?; tx.queue_delete::(&header.hash_slow())?; - } - - // Delete transaction hash index entries - for tx_hash in &tx_hashes_to_remove { - tx.queue_delete::(tx_hash)?; - } - - // Delete transactions, receipts, and signet events for removed blocks - for (block_num, _) in &headers_to_remove { - // Delete all transactions for this block tx.clear_k1_for::(block_num)?; - - // Delete all receipts for this block tx.clear_k1_for::(block_num)?; - - // Delete all signet events for this block tx.clear_k1_for::(block_num)?; - - // Delete zenith header for this block tx.queue_delete::(block_num)?; } - // Update latest block metadata - if !headers_to_remove.is_empty() { - // Find the new latest block + // Update latest block + { let mut cursor = tx.new_cursor::()?; - if let Some((key, _)) = KvTraverse::<_>::last(&mut cursor)? { - let new_latest = BlockNumber::decode_key(&key)?; - tx.queue_put::(&MetadataKey::LatestBlock, &new_latest)?; - } else { - // No blocks left, remove latest block metadata - tx.queue_delete::(&MetadataKey::LatestBlock)?; + match KvTraverse::<_>::last(&mut cursor)? { + Some((key, _)) => { + tx.queue_put::( + &MetadataKey::LatestBlock, + &BlockNumber::decode_key(&key)?, + )?; + } + None => { + tx.queue_delete::(&MetadataKey::LatestBlock)?; + } } } @@ -523,66 +455,98 @@ impl MdbxColdBackend { impl ColdStorage for MdbxColdBackend { async fn get_header(&self, spec: HeaderSpecifier) -> ColdResult> { - Ok(self.get_header_inner(spec)?) + let Some(block_num) = self.resolve_header_spec(spec)? else { + return Ok(None); + }; + Ok(self.get_header_by_number(block_num)?) } async fn get_headers(&self, specs: Vec) -> ColdResult>> { - let mut results = Vec::with_capacity(specs.len()); - for spec in specs { - results.push(self.get_header_inner(spec)?); - } - Ok(results) + specs + .into_iter() + .map(|spec| { + self.resolve_header_spec(spec)? + .map(|n| self.get_header_by_number(n)) + .transpose() + .map(Option::flatten) + }) + .collect::>() + .map_err(Into::into) } async fn get_transaction( &self, spec: TransactionSpecifier, ) -> ColdResult> { - Ok(self.get_transaction_inner(spec)?) + let Some((block, index)) = self.resolve_tx_spec(spec)? else { + return Ok(None); + }; + Ok(self.get_transaction_by_location(block, index)?) } async fn get_transactions_in_block( &self, block: BlockNumber, ) -> ColdResult> { - Ok(self.get_transactions_in_block_inner(block)?) + Ok(self.collect_transactions_in_block(block)?) } async fn get_transaction_count(&self, block: BlockNumber) -> ColdResult { - Ok(self.get_transaction_count_inner(block)?) + Ok(self.count_transactions_in_block(block)?) } async fn get_receipt(&self, spec: ReceiptSpecifier) -> ColdResult> { - Ok(self.get_receipt_inner(spec)?) + let Some((block, index)) = self.resolve_receipt_spec(spec)? else { + return Ok(None); + }; + Ok(self.get_receipt_by_location(block, index)?) } async fn get_receipts_in_block(&self, block: BlockNumber) -> ColdResult> { - Ok(self.get_receipts_in_block_inner(block)?) + Ok(self.collect_receipts_in_block(block)?) } async fn get_signet_events( &self, spec: SignetEventsSpecifier, ) -> ColdResult> { - Ok(self.get_signet_events_inner(spec)?) + let events = match spec { + SignetEventsSpecifier::Block(block) => self.collect_signet_events_in_block(block)?, + SignetEventsSpecifier::BlockRange { start, end } => { + self.collect_signet_events_in_range(start, end)? + } + }; + Ok(events) } async fn get_zenith_header( &self, spec: ZenithHeaderSpecifier, ) -> ColdResult> { - Ok(self.get_zenith_header_inner(spec)?) + let block = match spec { + ZenithHeaderSpecifier::Number(n) => n, + ZenithHeaderSpecifier::Range { start, .. } => start, + }; + Ok(self.get_zenith_header_by_number(block)?) } async fn get_zenith_headers( &self, spec: ZenithHeaderSpecifier, ) -> ColdResult> { - Ok(self.get_zenith_headers_inner(spec)?) + let headers = match spec { + ZenithHeaderSpecifier::Number(n) => { + self.get_zenith_header_by_number(n)?.into_iter().collect() + } + ZenithHeaderSpecifier::Range { start, end } => { + self.collect_zenith_headers_in_range(start, end)? + } + }; + Ok(headers) } async fn get_latest_block(&self) -> ColdResult> { - Ok(self.get_latest_block_inner()?) + Ok(self.get_metadata(MetadataKey::LatestBlock)?) } async fn append_block(&self, data: BlockData) -> ColdResult<()> { From 06bca7b22cf8c30e5f5e35ca527fb3b5fb2a6660 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Feb 2026 17:04:57 -0500 Subject: [PATCH 8/9] fix: make signet-hot-mdbx and signet-cold required dependencies Remove the optional `backend` feature and make both `signet-hot-mdbx` and `signet-cold` required dependencies. The backend implementation is now always available. Gate conformance test behind `test-utils` feature since it requires `signet_cold::conformance`. Co-Authored-By: Claude Opus 4.5 --- crates/cold-mdbx/Cargo.toml | 8 +++----- crates/cold-mdbx/src/backend.rs | 2 +- crates/cold-mdbx/src/error.rs | 2 -- crates/cold-mdbx/src/lib.rs | 9 --------- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/crates/cold-mdbx/Cargo.toml b/crates/cold-mdbx/Cargo.toml index 43e8489..d947cb5 100644 --- a/crates/cold-mdbx/Cargo.toml +++ b/crates/cold-mdbx/Cargo.toml @@ -13,14 +13,13 @@ categories = ["database-implementations"] [dependencies] alloy.workspace = true -signet-cold = { version = "0.0.1", path = "../cold", optional = true } +signet-cold = { version = "0.0.1", path = "../cold" } signet-hot.workspace = true -signet-hot-mdbx = { workspace = true, optional = true } +signet-hot-mdbx.workspace = true signet-storage-types.workspace = true thiserror.workspace = true [dev-dependencies] -signet-cold = { version = "0.0.1", path = "../cold", features = ["test-utils"] } signet-hot-mdbx = { workspace = true, features = ["test-utils"] } signet-libmdbx.workspace = true tempfile.workspace = true @@ -28,5 +27,4 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [features] default = [] -backend = ["dep:signet-cold", "dep:signet-hot-mdbx"] -test-utils = ["backend", "signet-cold/test-utils"] +test-utils = ["signet-cold/test-utils"] diff --git a/crates/cold-mdbx/src/backend.rs b/crates/cold-mdbx/src/backend.rs index 1df1e45..158b7b6 100644 --- a/crates/cold-mdbx/src/backend.rs +++ b/crates/cold-mdbx/src/backend.rs @@ -565,7 +565,7 @@ impl ColdStorage for MdbxColdBackend { } } -#[cfg(test)] +#[cfg(all(test, feature = "test-utils"))] mod tests { use super::*; use signet_cold::conformance::conformance; diff --git a/crates/cold-mdbx/src/error.rs b/crates/cold-mdbx/src/error.rs index 3c8474e..8cc29fa 100644 --- a/crates/cold-mdbx/src/error.rs +++ b/crates/cold-mdbx/src/error.rs @@ -10,7 +10,6 @@ pub enum MdbxColdError { Ser(#[from] DeserError), /// An MDBX error occurred. - #[cfg(feature = "backend")] #[error("mdbx error: {0}")] Mdbx(#[from] signet_hot_mdbx::MdbxError), @@ -19,7 +18,6 @@ pub enum MdbxColdError { ReadOnly, } -#[cfg(feature = "backend")] impl From for signet_cold::ColdStorageError { fn from(error: MdbxColdError) -> Self { Self::Backend(Box::new(error)) diff --git a/crates/cold-mdbx/src/lib.rs b/crates/cold-mdbx/src/lib.rs index 12fc799..0093203 100644 --- a/crates/cold-mdbx/src/lib.rs +++ b/crates/cold-mdbx/src/lib.rs @@ -19,12 +19,6 @@ //! ## Metadata Tables //! //! - [`ColdMetadata`]: Storage metadata (latest block, finalized, safe, earliest). -//! -//! ## Backend Implementation -//! -//! When the `backend` feature is enabled, this crate also provides -//! [`MdbxColdBackend`], an MDBX-based implementation of the [`signet_cold::ColdStorage`] -//! trait. #![warn( missing_copy_implementations, @@ -47,10 +41,7 @@ pub use tables::{ ColdTransactions, ColdTxHashIndex, ColdZenithHeaders, MetadataKey, }; -#[cfg(feature = "backend")] mod backend; -#[cfg(feature = "backend")] pub use backend::MdbxColdBackend; -#[cfg(feature = "backend")] pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnvKind}; From 0259ea0dfa9fb429c0a1fd3e3844531c49d67ad5 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 3 Feb 2026 09:11:51 -0500 Subject: [PATCH 9/9] docs: fix broken intra-doc link in genesis_header Co-Authored-By: Claude Opus 4.5 --- crates/types/src/util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/types/src/util.rs b/crates/types/src/util.rs index 1916fd1..a1cd6b1 100644 --- a/crates/types/src/util.rs +++ b/crates/types/src/util.rs @@ -65,7 +65,7 @@ bitflags! { } } -/// Helper method building a [`Header`] given [`Genesis`] and [`ChainHardforks`]. +/// Helper method building a [`Header`] given [`Genesis`] and [`EthereumHardfork`]. pub fn genesis_header(genesis: &Genesis, hardforks: &EthereumHardfork) -> Header { // If London is activated at genesis, we set the initial base fee as per EIP-1559. let base_fee_per_gas = hardforks