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..d947cb5 --- /dev/null +++ b/crates/cold-mdbx/Cargo.toml @@ -0,0 +1,30 @@ +[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-cold = { version = "0.0.1", path = "../cold" } +signet-hot.workspace = true +signet-hot-mdbx.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 +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[features] +default = [] +test-utils = ["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..158b7b6 --- /dev/null +++ b/crates/cold-mdbx/src/backend.rs @@ -0,0 +1,580 @@ +//! 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, 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) + } + + fn create_tables(&self) -> Result<(), MdbxColdError> { + let tx = self.env.tx_rw()?; + + 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(()) + } + + fn resolve_tag(&self, tag: BlockTag) -> Result, MdbxColdError> { + let key = match tag { + BlockTag::Latest => MetadataKey::LatestBlock, + BlockTag::Finalized => MetadataKey::FinalizedBlock, + BlockTag::Safe => MetadataKey::SafeBlock, + BlockTag::Earliest => MetadataKey::EarliestBlock, + }; + 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)?) + } + + fn get_block_by_hash( + &self, + hash: alloy::primitives::B256, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &hash, + )?) + } + + fn get_tx_location( + &self, + hash: alloy::primitives::B256, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &hash, + )?) + } + + 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), + } + } + + 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::()?, + &block_num, + )?) + } + + fn get_transaction_by_location( + &self, + block: BlockNumber, + index: u64, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + )?) + } + + fn get_receipt_by_location( + &self, + block: BlockNumber, + index: u64, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(DualTableTraverse::::exact_dual( + &mut tx.new_cursor::()?, + &block, + &index, + )?) + } + + fn get_zenith_header_by_number( + &self, + block: BlockNumber, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + Ok(TableTraverse::::exact( + &mut tx.new_cursor::()?, + &block, + )?) + } + + 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)? { + item?; + count += 1; + } + Ok(count) + } + + fn collect_receipts_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 collect_signet_events_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 collect_signet_events_in_range( + &self, + start: BlockNumber, + end: BlockNumber, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + 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) + } + + fn collect_zenith_headers_in_range( + &self, + start: BlockNumber, + end: BlockNumber, + ) -> Result, MdbxColdError> { + let tx = self.env.tx()?; + let mut cursor = tx.new_cursor::()?; + let mut headers = Vec::new(); + + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = start.encode_key(&mut key_buf); + + 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)?); + } + + Ok(headers) + } + + fn append_block_inner(&self, data: BlockData) -> Result<(), MdbxColdError> { + let tx = self.env.tx_rw()?; + let block = data.block_number(); + + tx.queue_put::(&block, &data.header)?; + tx.queue_put::(&data.header.hash_slow(), &block)?; + + for (idx, tx_signed) in data.transactions.iter().enumerate() { + let tx_idx = idx as u64; + tx.queue_put_dual::(&block, &tx_idx, tx_signed)?; + tx.queue_put::(tx_signed.hash(), &TxLocation::new(block, tx_idx))?; + } + + for (idx, receipt) in data.receipts.iter().enumerate() { + tx.queue_put_dual::(&block, &(idx as u64), receipt)?; + } + + for (idx, event) in data.signet_events.iter().enumerate() { + tx.queue_put_dual::(&block, &(idx as u64), event)?; + } + + if let Some(zh) = &data.zenith_header { + tx.queue_put::(&block, zh)?; + } + + // Update block bounds + let current_latest: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::LatestBlock, + )?; + tx.queue_put::( + &MetadataKey::LatestBlock, + ¤t_latest.map_or(block, |prev| prev.max(block)), + )?; + + let current_earliest: Option = TableTraverse::::exact( + &mut tx.new_cursor::()?, + &MetadataKey::EarliestBlock, + )?; + tx.queue_put::( + &MetadataKey::EarliestBlock, + ¤t_earliest.map_or(block, |prev| prev.min(block)), + )?; + + tx.raw_commit()?; + Ok(()) + } + + fn truncate_above_inner(&self, block: BlockNumber) -> Result<(), MdbxColdError> { + let tx = self.env.tx_rw()?; + + // Collect headers above the cutoff + let headers_to_remove = { + let mut cursor = tx.new_cursor::()?; + let mut headers: Vec<(BlockNumber, Header)> = Vec::new(); + + 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)? { + headers.push((BlockNumber::decode_key(&key)?, Header::decode_value(&value)?)); + + while let Some((key, value)) = KvTraverse::<_>::read_next(&mut cursor)? { + headers.push((BlockNumber::decode_key(&key)?, Header::decode_value(&value)?)); + } + } + headers + }; + + if headers_to_remove.is_empty() { + return Ok(()); + } + + // 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())?; + tx.clear_k1_for::(block_num)?; + tx.clear_k1_for::(block_num)?; + tx.clear_k1_for::(block_num)?; + tx.queue_delete::(block_num)?; + } + + // Update latest block + { + let mut cursor = tx.new_cursor::()?; + match KvTraverse::<_>::last(&mut cursor)? { + Some((key, _)) => { + tx.queue_put::( + &MetadataKey::LatestBlock, + &BlockNumber::decode_key(&key)?, + )?; + } + None => { + tx.queue_delete::(&MetadataKey::LatestBlock)?; + } + } + } + + tx.raw_commit()?; + Ok(()) + } +} + +impl ColdStorage for MdbxColdBackend { + async fn get_header(&self, spec: HeaderSpecifier) -> ColdResult> { + 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>> { + 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> { + 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.collect_transactions_in_block(block)?) + } + + async fn get_transaction_count(&self, block: BlockNumber) -> ColdResult { + Ok(self.count_transactions_in_block(block)?) + } + + async fn get_receipt(&self, spec: ReceiptSpecifier) -> ColdResult> { + 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.collect_receipts_in_block(block)?) + } + + async fn get_signet_events( + &self, + spec: SignetEventsSpecifier, + ) -> ColdResult> { + 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> { + 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> { + 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_metadata(MetadataKey::LatestBlock)?) + } + + 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(()) + } + + async fn truncate_above(&self, block: BlockNumber) -> ColdResult<()> { + Ok(self.truncate_above_inner(block)?) + } +} + +#[cfg(all(test, feature = "test-utils"))] +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 new file mode 100644 index 0000000..8cc29fa --- /dev/null +++ b/crates/cold-mdbx/src/error.rs @@ -0,0 +1,25 @@ +//! 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), + + /// An MDBX error occurred. + #[error("mdbx error: {0}")] + Mdbx(#[from] signet_hot_mdbx::MdbxError), + + /// Database is read-only. + #[error("database is read-only")] + ReadOnly, +} + +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 new file mode 100644 index 0000000..0093203 --- /dev/null +++ b/crates/cold-mdbx/src/lib.rs @@ -0,0 +1,47 @@ +//! MDBX table definitions and backend 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, +}; + +mod backend; +pub use backend::MdbxColdBackend; + +pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnvKind}; diff --git a/crates/cold-mdbx/src/tables.rs b/crates/cold-mdbx/src/tables.rs new file mode 100644 index 0000000..9fa4951 --- /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 + 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 + const { assert!(!ColdBlockHashIndex::INT_KEY) }; + const { assert!(!ColdTxHashIndex::INT_KEY) }; + const { 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..1e45607 --- /dev/null +++ b/crates/hot/src/ser/cold_impls.rs @@ -0,0 +1,603 @@ +//! 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 +// ============================================================================ + +const 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) +// ============================================================================ + +const 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); + } +} 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