diff --git a/Cargo.toml b/Cargo.toml index cc89e52..856dba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,9 @@ alloy-contract = { version = "1.4.0", features = ["pubsub"] } reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } reth-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } reth-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } +reth-codecs = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } +reth-db-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } reth-eth-wire-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml new file mode 100644 index 0000000..0384e1d --- /dev/null +++ b/crates/storage/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "signet-storage" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +alloy.workspace = true +bytes = "1.11.0" +reth.workspace = true +reth-db.workspace = true +reth-db-api.workspace = true +thiserror.workspace = true diff --git a/crates/storage/README.md b/crates/storage/README.md new file mode 100644 index 0000000..a7d6488 --- /dev/null +++ b/crates/storage/README.md @@ -0,0 +1,17 @@ +# Signet Storage + +High-level API for Signet's storage layer + +This library contains the following: + +- Traits for serializing and deserializing Signet data structures as DB keys/ + value. +- Traits for hot and cold storage operations. +- Relevant KV table definitions. + +## Significant Traits + +- `HotKv` - Encapsulates logic for reading and writing to hot storage. +- `ColdKv` - Encapsulates logic for reading and writing to cold storage. +- `KeySer` - Provides methods for serializing a type as a DB key. +- `ValueSer` - Provides methods for serializing a type as a DB value. diff --git a/crates/storage/src/cold/mod.rs b/crates/storage/src/cold/mod.rs new file mode 100644 index 0000000..9b9d9be --- /dev/null +++ b/crates/storage/src/cold/mod.rs @@ -0,0 +1 @@ +//! Placeholder module for cold storage implementation. diff --git a/crates/storage/src/hot/db.rs b/crates/storage/src/hot/db.rs new file mode 100644 index 0000000..a4911da --- /dev/null +++ b/crates/storage/src/hot/db.rs @@ -0,0 +1,87 @@ +use crate::hot::{HotKv, HotKvError, HotKvRead, HotKvWrite}; +use std::{ + borrow::Cow, + sync::atomic::{AtomicBool, Ordering}, +}; + +/// Hot database wrapper around a key-value storage. +#[derive(Debug)] +pub struct HotDb { + inner: Inner, + + write_locked: AtomicBool, +} + +impl HotDb { + /// Create a new HotDb wrapping the given inner KV storage. + pub const fn new(inner: Inner) -> Self { + Self { inner, write_locked: AtomicBool::new(false) } + } + + /// Get a read-only handle. + pub fn reader(&self) -> Result + where + Inner: HotKv, + { + self.inner.reader() + } + + /// Get a write handle, if available. If not available, returns + /// [`HotKvError::WriteLocked`]. + pub fn writer(&self) -> Result, HotKvError> + where + Inner: HotKv, + { + if self.write_locked.swap(true, Ordering::AcqRel) { + return Err(HotKvError::WriteLocked); + } + self.inner.writer().map(Some).map(|tx| WriteGuard { tx, db: self }) + } +} + +/// Write guard for a write transaction. +#[derive(Debug)] +pub struct WriteGuard<'a, Inner> +where + Inner: HotKv, +{ + tx: Option, + db: &'a HotDb, +} + +impl Drop for WriteGuard<'_, Inner> +where + Inner: HotKv, +{ + fn drop(&mut self) { + self.db.write_locked.store(false, Ordering::Release); + } +} + +impl HotKvRead for WriteGuard<'_, Inner> +where + Inner: HotKv, +{ + type Error = <::RwTx as HotKvRead>::Error; + + fn get_raw<'a>( + &'a self, + table: &str, + key: &[u8], + ) -> Result>, Self::Error> { + self.tx.as_ref().expect("present until drop").get_raw(table, key) + } +} + +impl HotKvWrite for WriteGuard<'_, Inner> +where + Inner: HotKv, +{ + fn queue_raw_put(&mut self, table: &str, key: &[u8], value: &[u8]) -> Result<(), Self::Error> { + self.tx.as_mut().expect("present until drop").queue_raw_put(table, key, value) + } + + fn raw_commit(mut self) -> Result<(), Self::Error> { + self.tx.take().expect("present until drop").raw_commit() + } +} diff --git a/crates/storage/src/hot/db_traits.rs b/crates/storage/src/hot/db_traits.rs new file mode 100644 index 0000000..b71b637 --- /dev/null +++ b/crates/storage/src/hot/db_traits.rs @@ -0,0 +1,167 @@ +use crate::{ + hot::{HotKvRead, HotKvWrite}, + tables::hot::{self as tables, AccountStorageKey}, +}; +use alloy::primitives::{Address, B256, U256}; +use reth::primitives::{Account, Bytecode, Header, SealedHeader, StorageEntry}; +use std::borrow::Cow; + +/// Trait for database read operations. +pub trait HotDbReader: sealed::Sealed { + /// The error type for read operations + type Error: std::error::Error + Send + Sync + 'static + From; + + /// Read a block header by its number. + fn get_header(&self, number: u64) -> Result, Self::Error>; + + /// Read a block number by its hash. + fn get_header_number(&self, hash: &B256) -> Result, Self::Error>; + + /// Read the canonical hash by block number. + fn get_canonical_hash(&self, number: u64) -> Result, Self::Error>; + + /// Read contract Bytecode by its hash. + fn get_bytecode(&self, code_hash: &B256) -> Result, Self::Error>; + + /// Read an account by its address. + fn get_account(&self, address: &Address) -> Result, Self::Error>; + + /// Read a storage slot by its address and key. + fn get_storage(&self, address: &Address, key: &B256) -> Result, Self::Error>; + + /// Read a [`StorageEntry`] by its address and key. + fn get_storage_entry( + &self, + address: &Address, + key: &B256, + ) -> Result, Self::Error> { + let opt = self.get_storage(address, key)?; + Ok(opt.map(|value| StorageEntry { key: *key, value })) + } +} + +impl HotDbReader for T +where + T: HotKvRead, +{ + type Error = ::Error; + + fn get_header(&self, number: u64) -> Result, Self::Error> { + self.get::(&number) + } + + fn get_header_number(&self, hash: &B256) -> Result, Self::Error> { + self.get::(hash) + } + + fn get_canonical_hash(&self, number: u64) -> Result, Self::Error> { + self.get::(&number) + } + + fn get_bytecode(&self, code_hash: &B256) -> Result, Self::Error> { + self.get::(code_hash) + } + + fn get_account(&self, address: &Address) -> Result, Self::Error> { + self.get::(address) + } + + fn get_storage(&self, address: &Address, key: &B256) -> Result, Self::Error> { + let storage_key = AccountStorageKey { + address: std::borrow::Cow::Borrowed(address), + key: std::borrow::Cow::Borrowed(key), + }; + let key = storage_key.encode_key(); + self.get::(&key) + } +} + +/// Trait for database write operations. +pub trait HotDbWriter: sealed::Sealed { + /// The error type for write operations + type Error: std::error::Error + Send + Sync + 'static + From; + + /// Read the latest block header. + fn put_header(&mut self, header: &Header) -> Result<(), Self::Error>; + + /// Write a block number by its hash. + fn put_header_number(&mut self, hash: &B256, number: u64) -> Result<(), Self::Error>; + + /// Write the canonical hash by block number. + fn put_canonical_hash(&mut self, number: u64, hash: &B256) -> Result<(), Self::Error>; + + /// Write contract Bytecode by its hash. + fn put_bytecode(&mut self, code_hash: &B256, bytecode: &Bytecode) -> Result<(), Self::Error>; + + /// Write an account by its address. + fn put_account(&mut self, address: &Address, account: &Account) -> Result<(), Self::Error>; + + /// Write a storage entry by its address and key. + fn put_storage( + &mut self, + address: &Address, + key: &B256, + entry: &U256, + ) -> Result<(), Self::Error>; + + /// Commit the write transaction. + fn commit(self) -> Result<(), Self::Error>; + + /// Write a canonical header (header, number mapping, and canonical hash). + fn put_canonical(&mut self, header: &SealedHeader) -> Result<(), Self::Error> { + self.put_header(header)?; + self.put_header_number(&header.hash(), header.number)?; + self.put_canonical_hash(header.number, &header.hash()) + } +} + +impl HotDbWriter for T +where + T: HotKvWrite, +{ + type Error = ::Error; + + fn put_header(&mut self, header: &Header) -> Result<(), Self::Error> { + self.queue_put::(&header.number, header) + } + + fn put_header_number(&mut self, hash: &B256, number: u64) -> Result<(), Self::Error> { + self.queue_put::(hash, &number) + } + + fn put_canonical_hash(&mut self, number: u64, hash: &B256) -> Result<(), Self::Error> { + self.queue_put::(&number, hash) + } + + fn put_bytecode(&mut self, code_hash: &B256, bytecode: &Bytecode) -> Result<(), Self::Error> { + self.queue_put::(code_hash, bytecode) + } + + fn put_account(&mut self, address: &Address, account: &Account) -> Result<(), Self::Error> { + self.queue_put::(address, account) + } + + fn put_storage( + &mut self, + address: &Address, + key: &B256, + entry: &U256, + ) -> Result<(), Self::Error> { + let storage_key = + AccountStorageKey { address: Cow::Borrowed(address), key: Cow::Borrowed(key) }; + self.queue_put::(&storage_key.encode_key(), entry) + } + + fn commit(self) -> Result<(), Self::Error> { + HotKvWrite::raw_commit(self) + } +} + +mod sealed { + use crate::hot::HotKvRead; + + /// Sealed trait to prevent external implementations of HotDbReader and HotDbWriter. + #[allow(dead_code, unreachable_pub)] + pub trait Sealed {} + impl Sealed for T where T: HotKvRead {} +} diff --git a/crates/storage/src/hot/error.rs b/crates/storage/src/hot/error.rs new file mode 100644 index 0000000..a5331ce --- /dev/null +++ b/crates/storage/src/hot/error.rs @@ -0,0 +1,28 @@ +/// Trait for hot storage read/write errors. +#[derive(thiserror::Error, Debug)] +pub enum HotKvError { + /// Boxed error. Indicates an issue with the DB backend. + #[error(transparent)] + Inner(#[from] Box), + + /// Deserialization error. Indicates an issue deserializing a key or value. + #[error("Deserialization error: {0}")] + DeserError(#[from] crate::ser::DeserError), + + /// Indicates that a write transaction is already in progress. + #[error("A write transaction is already in progress")] + WriteLocked, +} + +impl HotKvError { + /// Internal helper to create a `HotKvError::Inner` from any error. + pub fn from_err(err: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + HotKvError::Inner(Box::new(err)) + } +} + +/// Result type for hot storage operations. +pub type HotKvResult = Result; diff --git a/crates/storage/src/hot/mod.rs b/crates/storage/src/hot/mod.rs new file mode 100644 index 0000000..2055543 --- /dev/null +++ b/crates/storage/src/hot/mod.rs @@ -0,0 +1,13 @@ +mod db; +pub use db::{HotDb, WriteGuard}; + +mod db_traits; +pub use db_traits::{HotDbReader, HotDbWriter}; + +mod error; +pub use error::{HotKvError, HotKvResult}; + +mod reth_impl; + +mod traits; +pub use traits::{HotKv, HotKvRead, HotKvWrite}; diff --git a/crates/storage/src/hot/reth_impl.rs b/crates/storage/src/hot/reth_impl.rs new file mode 100644 index 0000000..706fad6 --- /dev/null +++ b/crates/storage/src/hot/reth_impl.rs @@ -0,0 +1,32 @@ +use std::borrow::Cow; + +use crate::{hot::HotKvRead, ser::DeserError}; +use reth_db::mdbx::{TransactionKind, tx::Tx}; +use reth_db_api::DatabaseError; + +impl From for DatabaseError { + fn from(value: DeserError) -> Self { + DatabaseError::Other(value.to_string()) + } +} + +impl HotKvRead for Tx +where + K: TransactionKind, +{ + type Error = DatabaseError; + + fn get_raw<'a>( + &'a self, + table: &str, + key: &[u8], + ) -> Result>, Self::Error> { + let dbi = self + .inner + .open_db(Some(table)) + .map(|db| db.dbi()) + .map_err(|e| DatabaseError::Open(e.into()))?; + + self.inner.get(dbi, key.as_ref()).map_err(|err| DatabaseError::Read(err.into())) + } +} diff --git a/crates/storage/src/hot/traits.rs b/crates/storage/src/hot/traits.rs new file mode 100644 index 0000000..11f03c8 --- /dev/null +++ b/crates/storage/src/hot/traits.rs @@ -0,0 +1,127 @@ +use std::borrow::Cow; + +use crate::{ + hot::HotKvError, + ser::{KeySer, MAX_KEY_SIZE, ValSer}, + tables::Table, +}; + +/// Trait for hot storage. This is a KV store with read/write transactions. +pub trait HotKv { + /// The read-only transaction type. + type RoTx: HotKvRead; + /// The read-write transaction type. + type RwTx: HotKvWrite; + + /// Create a read-only transaction. + fn reader(&self) -> Result; + + /// Create a read-write transaction. + /// + /// # Returns + /// + /// - `Ok(Some(tx))` if the write transaction was created successfully. + /// - [`Err(HotKvError::WriteLocked)`] if there is already a write + /// transaction in progress. + /// - [`Err(HotKvError::Inner)`] if there was an error creating the + /// transaction. + /// + /// [`Err(HotKvError::Inner)`]: HotKvError::Inner + /// [`Err(HotKvError::WriteLocked)`]: HotKvError::WriteLocked + fn writer(&self) -> Result; +} + +/// Trait for hot storage read transactions. +pub trait HotKvRead { + /// Error type for read operations. + type Error: std::error::Error + From + Send + Sync + 'static; + + /// Get a raw value from a specific table. + fn get_raw<'a>(&'a self, table: &str, key: &[u8]) + -> Result>, Self::Error>; + + /// Get a value from a specific table. + fn get(&self, key: &T::Key) -> Result, Self::Error> { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = key.encode_key(&mut key_buf); + debug_assert!( + key_bytes.len() == T::Key::SIZE, + "Encoded key length does not match expected size" + ); + + let Some(value_bytes) = self.get_raw(T::NAME, key_bytes)? else { + return Ok(None); + }; + let data = &value_bytes[..]; + T::Value::decode_value(data).map(Some).map_err(Into::into) + } + + /// Get many values from a specific table. + /// + /// # Arguments + /// + /// * `keys` - An iterator over keys to retrieve. + /// + /// # Returns + /// + /// A vector of `Option`, where each element corresponds to the + /// value for the respective key in the input iterator. If a key does not + /// exist in the table, the corresponding element will be `None`. + /// + /// If any error occurs during retrieval or deserialization, the entire + /// operation will return an error. + fn get_many<'a, T, I>(&self, keys: I) -> Result>, Self::Error> + where + T::Key: 'a, + T: Table, + I: IntoIterator, + { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + + keys.into_iter() + .map(|key| self.get_raw(T::NAME, key.encode_key(&mut key_buf))) + .map(|maybe_val| { + maybe_val + .and_then(|val| ValSer::maybe_decode_value(val.as_deref()).map_err(Into::into)) + }) + .collect() + } +} + +/// Trait for hot storage write transactions. +pub trait HotKvWrite: HotKvRead { + /// Queue a raw put operation. + fn queue_raw_put(&mut self, table: &str, key: &[u8], value: &[u8]) -> Result<(), Self::Error>; + + /// Queue a put operation for a specific table. + fn queue_put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), Self::Error> { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = key.encode_key(&mut key_buf); + let value_bytes = value.encoded(); + + self.queue_raw_put(T::NAME, key_bytes, &value_bytes) + } + + /// Queue many put operations for a specific table. + fn queue_put_many<'a, 'b, T, I>(&mut self, entries: I) -> Result<(), Self::Error> + where + T: Table, + T::Key: 'a, + T::Value: 'b, + I: IntoIterator, + { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + + for (key, value) in entries { + let key_bytes = key.encode_key(&mut key_buf); + let value_bytes = value.encoded(); + + self.queue_raw_put(T::NAME, key_bytes, &value_bytes)?; + } + + Ok(()) + } + + /// Commit the queued operations. + fn raw_commit(self) -> Result<(), Self::Error>; +} diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs new file mode 100644 index 0000000..381014c --- /dev/null +++ b/crates/storage/src/lib.rs @@ -0,0 +1,24 @@ +#![doc = include_str!("../README.md")] +#![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, doc_auto_cfg))] + +/// Cold storage module. +pub mod cold; + +/// Hot storage module. +pub mod hot; + +/// Serialization module. +pub mod ser; + +/// Predefined tables module. +pub mod tables; diff --git a/crates/storage/src/ser/error.rs b/crates/storage/src/ser/error.rs new file mode 100644 index 0000000..de83f75 --- /dev/null +++ b/crates/storage/src/ser/error.rs @@ -0,0 +1,46 @@ +/// Error type for deserialization errors. +/// +/// Erases the underlying error type to a boxed trait object or a string +/// message, for convenience. +#[derive(thiserror::Error, Debug)] +pub enum DeserError { + /// Boxed error. + #[error(transparent)] + Boxed(Box), + + /// String error message. + #[error("{0}")] + String(String), + + /// Deserialization ended with extra bytes remaining. + #[error("inexact deserialization: {extra_bytes} extra bytes remaining")] + InexactDeser { + /// Number of extra bytes remaining after deserialization. + extra_bytes: usize, + }, + + /// Not enough data to complete deserialization. + #[error("insufficient data: needed {needed} bytes, but only {available} available")] + InsufficientData { + /// Number of bytes needed. + needed: usize, + /// Number of bytes available. + available: usize, + }, +} + +impl From<&str> for DeserError { + fn from(err: &str) -> Self { + DeserError::String(err.to_string()) + } +} + +impl DeserError { + /// Box an error into a `DeserError`. + pub fn from(err: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + DeserError::Boxed(Box::new(err)) + } +} diff --git a/crates/storage/src/ser/impls.rs b/crates/storage/src/ser/impls.rs new file mode 100644 index 0000000..96537e8 --- /dev/null +++ b/crates/storage/src/ser/impls.rs @@ -0,0 +1,534 @@ +use crate::ser::{DeserError, KeySer, MAX_KEY_SIZE, ValSer}; +use alloy::primitives::Bloom; +use bytes::BufMut; + +macro_rules! delegate_val_to_key { + ($ty:ty) => { + impl ValSer for $ty { + fn encoded_size(&self) -> usize { + ::SIZE + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = KeySer::encode_key(self, &mut key_buf); + buf.put_slice(key_bytes); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + KeySer::decode_key(&data) + } + } + }; +} + +macro_rules! ser_alloy_fixed { + ($size:expr) => { + impl KeySer for alloy::primitives::FixedBytes<$size> { + const SIZE: usize = $size; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, _buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + self.as_ref() + } + + fn decode_key(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < $size { + return Err(DeserError::InsufficientData { + needed: $size, + available: data.len(), + }); + } + let mut this = Self::default(); + this.as_mut_slice().copy_from_slice(&data[..$size]); + Ok(this) + } + } + + delegate_val_to_key!(alloy::primitives::FixedBytes<$size>); + }; + + ($($size:expr),* $(,)?) => { + $( + ser_alloy_fixed!($size); + )+ + }; +} + +macro_rules! ser_be_num { + ($ty:ty, $size:expr) => { + impl KeySer for $ty { + const SIZE: usize = $size; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + let be_bytes: [u8; $size] = self.to_be_bytes(); + buf[..$size].copy_from_slice(&be_bytes); + &buf[..$size] + } + + fn decode_key(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < $size { + return Err(DeserError::InsufficientData { + needed: $size, + available: data.len(), + }); + } + let bytes: [u8; $size] = data[..$size].try_into().map_err(DeserError::from)?; + Ok(<$ty>::from_be_bytes(bytes)) + } + } + + delegate_val_to_key!($ty); + }; + ($($ty:ty, $size:expr);* $(;)?) => { + $( + ser_be_num!($ty, $size); + )+ + }; +} + +ser_be_num!( + u8, 1; + i8, 1; + u16, 2; + u32, 4; + u64, 8; + u128, 16; + i16, 2; + i32, 4; + i64, 8; + i128, 16; + usize, std::mem::size_of::(); + isize, std::mem::size_of::(); + alloy::primitives::U160, 20; + alloy::primitives::U256, 32; +); + +// NB: 52 is for AccountStorageKey which is (20 + 32). +// 65 is for Signature, which is (1 + 32 + 32). +ser_alloy_fixed!(8, 16, 20, 32, 52, 65, 256); +delegate_val_to_key!(alloy::primitives::Address); + +impl KeySer for alloy::primitives::Address { + const SIZE: usize = 20; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, _buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + self.as_ref() + } + + fn decode_key(data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(DeserError::InsufficientData { needed: Self::SIZE, available: data.len() }); + } + let mut addr = Self::default(); + addr.copy_from_slice(&data[..Self::SIZE]); + Ok(addr) + } +} + +impl ValSer for Bloom { + fn encoded_size(&self) -> usize { + self.as_slice().len() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + buf.put_slice(self.as_ref()); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < 256 { + return Err(DeserError::InsufficientData { needed: 256, available: data.len() }); + } + let mut bloom = Self::default(); + bloom.as_mut_slice().copy_from_slice(&data[..256]); + Ok(bloom) + } +} + +impl ValSer for bytes::Bytes { + fn encoded_size(&self) -> usize { + 2 + self.len() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + buf.put_u16(self.len() as u16); + buf.put_slice(self); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < 2 { + return Err(DeserError::InsufficientData { needed: 2, available: data.len() }); + } + let len = u16::from_be_bytes(data[0..2].try_into().unwrap()) as usize; + Ok(bytes::Bytes::copy_from_slice(&data[2..2 + len])) + } +} + +impl ValSer for alloy::primitives::Bytes { + fn encoded_size(&self) -> usize { + self.0.encoded_size() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: BufMut + AsMut<[u8]>, + { + self.0.encode_value_to(buf); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + bytes::Bytes::decode_value(data).map(alloy::primitives::Bytes) + } +} + +impl ValSer for Option +where + T: ValSer, +{ + fn encoded_size(&self) -> usize { + 1 + match self { + Some(inner) => inner.encoded_size(), + None => 0, + } + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + // Simple presence flag + if let Some(inner) = self { + buf.put_u8(1); + inner.encode_value_to(buf); + } else { + buf.put_u8(0); + } + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let flag = data + .first() + .ok_or(DeserError::InsufficientData { needed: 1, available: data.len() })?; + match flag { + 0 => Ok(None), + 1 => Ok(Some(T::decode_value(&data[1..])?)), + _ => Err(DeserError::String(format!("Invalid Option flag: {}", flag))), + } + } +} + +impl ValSer for Vec +where + T: ValSer, +{ + fn encoded_size(&self) -> usize { + // 2 bytes for length prefix + 2 + self.iter().map(|item| item.encoded_size()).sum::() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + buf.put_u16(self.len() as u16); + self.iter().for_each(|item| item.encode_value_to(buf)); + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + if data.len() < 2 { + return Err(DeserError::InsufficientData { needed: 2, available: data.len() }); + } + + let items = u16::from_be_bytes(data[0..2].try_into().unwrap()) as usize; + data = &data[2..]; + + // Preallocate the vector + let mut vec = Vec::with_capacity(items); + + vec.spare_capacity_mut().iter_mut().try_for_each(|slot| { + // Decode the item and advance the data slice + let item = slot.write(T::decode_value(data)?); + // Advance data slice by the size of the decoded item + data = &data[item.encoded_size()..]; + Ok::<_, DeserError>(()) + })?; + + // SAFETY: + // If we did not shortcut return, we have initialized all `items` + // elements. + unsafe { + vec.set_len(items); + } + Ok(vec) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::{Address, Bloom, Bytes as AlloBytes, FixedBytes, U160, U256}; + use bytes::Bytes; + + /// Generic roundtrip test for any ValSer type + #[track_caller] + fn test_roundtrip(original: &T) + where + T: ValSer + PartialEq + std::fmt::Debug, + { + // Encode + let mut buf = bytes::BytesMut::new(); + original.encode_value_to(&mut buf); + let encoded = buf.freeze(); + + // Assert that the encoded size matches + assert_eq!( + original.encoded_size(), + encoded.len(), + "Encoded size mismatch: expected {}, got {}", + original.encoded_size(), + encoded.len() + ); + + // Decode + let decoded = T::decode_value(&encoded).expect("Failed to decode value"); + + // Assert equality + assert_eq!(*original, decoded, "Roundtrip failed"); + } + + #[test] + fn test_integer_roundtrips() { + // Test boundary values for all integer types + test_roundtrip(&0u8); + test_roundtrip(&255u8); + test_roundtrip(&127i8); + test_roundtrip(&-128i8); + + test_roundtrip(&0u16); + test_roundtrip(&65535u16); + test_roundtrip(&32767i16); + test_roundtrip(&-32768i16); + + test_roundtrip(&0u32); + test_roundtrip(&4294967295u32); + test_roundtrip(&2147483647i32); + test_roundtrip(&-2147483648i32); + + test_roundtrip(&0u64); + test_roundtrip(&18446744073709551615u64); + test_roundtrip(&9223372036854775807i64); + test_roundtrip(&-9223372036854775808i64); + + test_roundtrip(&0u128); + test_roundtrip(&340282366920938463463374607431768211455u128); + test_roundtrip(&170141183460469231731687303715884105727i128); + test_roundtrip(&-170141183460469231731687303715884105728i128); + + test_roundtrip(&0usize); + test_roundtrip(&usize::MAX); + test_roundtrip(&0isize); + test_roundtrip(&isize::MAX); + test_roundtrip(&isize::MIN); + } + + #[test] + fn test_u256_roundtrips() { + test_roundtrip(&U256::ZERO); + test_roundtrip(&U256::from(1u64)); + test_roundtrip(&U256::from(255u64)); + test_roundtrip(&U256::from(65535u64)); + test_roundtrip(&U256::from(u64::MAX)); + test_roundtrip(&U256::MAX); + } + + #[test] + fn test_u160_roundtrips() { + test_roundtrip(&U160::ZERO); + test_roundtrip(&U160::from(1u64)); + test_roundtrip(&U160::from(u64::MAX)); + // Create a maxed U160 (20 bytes = 160 bits) + let max_u160 = U160::from_be_bytes([0xFF; 20]); + test_roundtrip(&max_u160); + } + + #[test] + fn test_address_roundtrips() { + test_roundtrip(&Address::ZERO); + // Create a test address with known pattern + let test_addr = Address::from([ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, + 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, + ]); + test_roundtrip(&test_addr); + } + + #[test] + fn test_fixedbytes_roundtrips() { + // Test various FixedBytes sizes + test_roundtrip(&FixedBytes::<8>::ZERO); + test_roundtrip(&FixedBytes::<16>::ZERO); + test_roundtrip(&FixedBytes::<20>::ZERO); + test_roundtrip(&FixedBytes::<32>::ZERO); + test_roundtrip(&FixedBytes::<52>::ZERO); + test_roundtrip(&FixedBytes::<65>::ZERO); + test_roundtrip(&FixedBytes::<256>::ZERO); + + // Test with non-zero patterns + let pattern_32 = FixedBytes::<32>::from([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, + 0x1D, 0x1E, 0x1F, 0x20, + ]); + test_roundtrip(&pattern_32); + } + + #[test] + fn test_bloom_roundtrips() { + test_roundtrip(&Bloom::ZERO); + // Create a bloom with some bits set + let mut bloom_data = [0u8; 256]; + bloom_data[0] = 0xFF; + bloom_data[127] = 0xAA; + bloom_data[255] = 0x55; + let bloom = Bloom::from(bloom_data); + test_roundtrip(&bloom); + } + + #[test] + fn test_bytes_roundtrips() { + // Test bytes::Bytes + test_roundtrip(&Bytes::new()); + test_roundtrip(&Bytes::from_static(b"hello world")); + test_roundtrip(&Bytes::from(vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD])); + + // Test alloy::primitives::Bytes + test_roundtrip(&AlloBytes::new()); + test_roundtrip(&AlloBytes::from_static(b"hello alloy")); + test_roundtrip(&AlloBytes::copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF])); + } + + #[test] + fn test_option_roundtrips() { + // None variants + test_roundtrip(&None::); + test_roundtrip(&None::
); + test_roundtrip(&None::); + + // Some variants + test_roundtrip(&Some(42u32)); + test_roundtrip(&Some(u64::MAX)); + test_roundtrip(&Some(Address::ZERO)); + test_roundtrip(&Some(U256::from(12345u64))); + test_roundtrip(&Some(AlloBytes::from_static(b"test"))); + + // Nested options + test_roundtrip(&Some(Some(123u32))); + test_roundtrip(&Some(None::)); + test_roundtrip(&None::>); + } + + #[test] + fn test_vec_roundtrips() { + // Empty vectors + test_roundtrip(&Vec::::new()); + test_roundtrip(&Vec::
::new()); + + // Single element vectors + test_roundtrip(&vec![42u32]); + test_roundtrip(&vec![Address::ZERO]); + + // Multiple element vectors + test_roundtrip(&vec![1u32, 2, 3, 4, 5]); + test_roundtrip(&vec![U256::ZERO, U256::from(1u64), U256::MAX]); + + // Vector of bytes + test_roundtrip(&vec![ + AlloBytes::from_static(b"first"), + AlloBytes::from_static(b"second"), + AlloBytes::new(), + AlloBytes::from_static(b"last"), + ]); + + // Nested vectors + test_roundtrip(&vec![vec![1u32, 2, 3], vec![], vec![4, 5]]); + + // Vector of options + test_roundtrip(&vec![Some(1u32), None, Some(2u32), Some(3u32), None]); + } + + #[test] + fn test_complex_combinations() { + // Option of Vec + test_roundtrip(&Some(vec![1u32, 2, 3, 4])); + test_roundtrip(&None::>); + + // Vec of Options + test_roundtrip(&vec![Some(Address::ZERO), None, Some(Address::ZERO)]); + + // Option of Option + test_roundtrip(&Some(Some(42u32))); + test_roundtrip(&Some(None::)); + + // Complex nested structure + let complex = vec![ + Some(vec![AlloBytes::from_static(b"hello")]), + None, + Some(vec![ + AlloBytes::from_static(b"world"), + AlloBytes::new(), + AlloBytes::from_static(b"!"), + ]), + ]; + test_roundtrip(&complex); + } + + #[test] + fn test_edge_cases() { + // Maximum values that should still work + test_roundtrip(&vec![0u8; 65535]); // Max length for Vec + + // Large FixedBytes + let large_fixed = FixedBytes::<256>::from([0xFF; 256]); + test_roundtrip(&large_fixed); + + // Very large U256 + let large_u256 = U256::from_str_radix( + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + 10, + ) + .unwrap(); + test_roundtrip(&large_u256); + } +} diff --git a/crates/storage/src/ser/mod.rs b/crates/storage/src/ser/mod.rs new file mode 100644 index 0000000..2f34e88 --- /dev/null +++ b/crates/storage/src/ser/mod.rs @@ -0,0 +1,9 @@ +mod error; +pub use error::DeserError; + +mod traits; +pub use traits::{KeySer, MAX_KEY_SIZE, ValSer}; + +mod impls; + +mod reth_impls; diff --git a/crates/storage/src/ser/reth_impls.rs b/crates/storage/src/ser/reth_impls.rs new file mode 100644 index 0000000..2ee25d4 --- /dev/null +++ b/crates/storage/src/ser/reth_impls.rs @@ -0,0 +1,1822 @@ +use crate::ser::{DeserError, KeySer, MAX_KEY_SIZE, ValSer}; +use alloy::{ + consensus::{EthereumTxEnvelope, Signed, TxEip1559, TxEip2930, TxEip4844, TxEip7702, TxLegacy}, + eips::{ + eip2930::{AccessList, AccessListItem}, + eip7702::{Authorization, SignedAuthorization}, + }, + primitives::{Address, B256, FixedBytes, Signature, TxKind, U256}, +}; +use reth::{ + primitives::{Account, Bytecode, Header, Log, LogData, TransactionSigned, TxType}, + revm::bytecode::{JumpTable, LegacyAnalyzedBytecode, eip7702::Eip7702Bytecode}, +}; +use reth_db_api::{ + BlockNumberList, + models::{ + AccountBeforeTx, ShardedKey, StoredBlockBodyIndices, storage_sharded_key::StorageShardedKey, + }, +}; + +impl KeySer for ShardedKey { + const SIZE: usize = T::SIZE + u64::SIZE; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + let mut scratch = [0u8; MAX_KEY_SIZE]; + + T::encode_key(&self.key, &mut scratch); + scratch[T::SIZE..Self::SIZE].copy_from_slice(&self.highest_block_number.to_be_bytes()); + *buf = scratch; + + &buf[0..Self::SIZE] + } + + fn decode_key(data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(DeserError::InsufficientData { needed: Self::SIZE, available: data.len() }); + } + + let key = T::decode_key(&data[0..T::SIZE])?; + let highest_block_number = u64::decode_key(&data[T::SIZE..T::SIZE + 8])?; + Ok(Self { key, highest_block_number }) + } +} + +impl KeySer for StorageShardedKey { + const SIZE: usize = Address::SIZE + B256::SIZE + u64::SIZE; + + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8] { + buf[0..Address::SIZE].copy_from_slice(self.address.as_slice()); + buf[Address::SIZE..Address::SIZE + B256::SIZE] + .copy_from_slice(self.sharded_key.key.as_slice()); + buf[Address::SIZE + B256::SIZE..Self::SIZE] + .copy_from_slice(&self.sharded_key.highest_block_number.to_be_bytes()); + + &buf[0..Self::SIZE] + } + + fn decode_key(mut data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(DeserError::InsufficientData { needed: Self::SIZE, available: data.len() }); + } + + let address = Address::from_slice(&data[0..Address::SIZE]); + data = &data[Address::SIZE..]; + + let storage_key = B256::from_slice(&data[0..B256::SIZE]); + data = &data[B256::SIZE..]; + + let highest_block_number = u64::from_be_bytes(data[0..8].try_into().unwrap()); + + Ok(Self { address, sharded_key: ShardedKey { key: storage_key, highest_block_number } }) + } +} + +macro_rules! by_props { + (@size $($prop:ident),* $(,)?) => { + { + 0 $( + + $prop.encoded_size() + )+ + } + }; + (@encode $buf:ident; $($prop:ident),* $(,)?) => { + { + $( + $prop.encode_value_to($buf); + )+ + } + }; + (@decode $data:ident; $($prop:ident),* $(,)?) => { + { + $( + *$prop = ValSer::decode_value($data)?; + $data = &$data[$prop.encoded_size()..]; + )* + } + }; +} + +impl ValSer for BlockNumberList { + fn encoded_size(&self) -> usize { + 2 + self.serialized_size() + } + + fn encode_value_to(&self, mut buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + use std::io::Write; + let mut writer: bytes::buf::Writer<&mut B> = bytes::BufMut::writer(&mut buf); + + debug_assert!( + self.serialized_size() <= u16::MAX as usize, + "BlockNumberList too large to encode" + ); + + writer.write_all(&(self.serialized_size() as u16).to_be_bytes()).unwrap(); + self.serialize_into(writer).unwrap(); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let size = u16::decode_value(&data[..2])? as usize; + BlockNumberList::from_bytes(&data[2..2 + size]) + .map_err(|err| DeserError::String(format!("Failed to decode BlockNumberList {err}"))) + } +} + +impl ValSer for Header { + fn encoded_size(&self) -> usize { + // NB: Destructure to ensure changes are compile errors and mistakes + // are unused var warnings. + let Header { + parent_hash, + ommers_hash, + beneficiary, + state_root, + transactions_root, + receipts_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + mix_hash, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + requests_hash, + } = self; + + by_props!( + @size + parent_hash, + ommers_hash, + beneficiary, + state_root, + transactions_root, + receipts_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + mix_hash, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + requests_hash, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + // NB: Destructure to ensure changes are compile errors and mistakes + // are unused var warnings. + let Header { + parent_hash, + ommers_hash, + beneficiary, + state_root, + transactions_root, + receipts_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + mix_hash, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + requests_hash, + } = self; + + by_props!( + @encode buf; + parent_hash, + ommers_hash, + beneficiary, + state_root, + transactions_root, + receipts_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + mix_hash, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + requests_hash, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + // NB: Destructure to ensure changes are compile errors and mistakes + // are unused var warnings. + let mut h = Header::default(); + let Header { + parent_hash, + ommers_hash, + beneficiary, + state_root, + transactions_root, + receipts_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + mix_hash, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + requests_hash, + } = &mut h; + + by_props!( + @decode data; + parent_hash, + ommers_hash, + beneficiary, + state_root, + transactions_root, + receipts_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + mix_hash, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + requests_hash, + ); + Ok(h) + } +} + +impl ValSer for Account { + fn encoded_size(&self) -> usize { + // NB: Destructure to ensure changes are compile errors and mistakes + // are unused var warnings. + let Account { nonce, balance, bytecode_hash } = self; + by_props!( + @size + nonce, + balance, + bytecode_hash, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + // NB: Destructure to ensure changes are compile errors and mistakes + // are unused var warnings. + let Account { nonce, balance, bytecode_hash } = self; + by_props!( + @encode buf; + nonce, + balance, + bytecode_hash, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + // NB: Destructure to ensure changes are compile errors and mistakes + // are unused var warnings. + let mut account = Account::default(); + let Account { nonce, balance, bytecode_hash } = &mut account; + + let mut data = data; + by_props!( + @decode data; + nonce, + balance, + bytecode_hash, + ); + Ok(account) + } +} + +impl ValSer for LogData { + fn encoded_size(&self) -> usize { + let LogData { data, .. } = self; + let topics = self.topics(); + 2 + topics.iter().map(|t| t.encoded_size()).sum::() + data.encoded_size() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let LogData { data, .. } = self; + let topics = self.topics(); + buf.put_u16(topics.len() as u16); + for topic in topics { + topic.encode_value_to(buf); + } + data.encode_value_to(buf); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut data = data; + let topics_len = u16::decode_value(&data[..2])? as usize; + data = &data[2..]; + + if topics_len > 4 { + return Err(DeserError::String("LogData topics length exceeds maximum of 4".into())); + } + + let mut topics = Vec::with_capacity(topics_len); + for _ in 0..topics_len { + let topic = B256::decode_value(data)?; + data = &data[topic.encoded_size()..]; + topics.push(topic); + } + + let log_data = alloy::primitives::Bytes::decode_value(data)?; + + Ok(LogData::new_unchecked(topics, log_data)) + } +} + +impl ValSer for Log { + fn encoded_size(&self) -> usize { + let Log { address, data } = self; + by_props!( + @size + address, + data, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let Log { address, data } = self; + by_props!( + @encode buf; + address, + data, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut log = Log::::default(); + let Log { address, data: log_data } = &mut log; + + let mut data = data; + by_props!( + @decode data; + address, + log_data, + ); + Ok(log) + } +} + +impl ValSer for TxType { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + buf.put_u8(*self as u8); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let byte = u8::decode_value(data)?; + TxType::try_from(byte) + .map_err(|_| DeserError::String(format!("Invalid TxType value: {}", byte))) + } +} + +impl ValSer for StoredBlockBodyIndices { + fn encoded_size(&self) -> usize { + let StoredBlockBodyIndices { first_tx_num, tx_count } = self; + by_props!( + @size + first_tx_num, + tx_count, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let StoredBlockBodyIndices { first_tx_num, tx_count } = self; + by_props!( + @encode buf; + first_tx_num, + tx_count, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut indices = StoredBlockBodyIndices::default(); + let StoredBlockBodyIndices { first_tx_num, tx_count } = &mut indices; + + let mut data = data; + by_props!( + @decode data; + first_tx_num, + tx_count, + ); + Ok(indices) + } +} + +impl ValSer for Eip7702Bytecode { + fn encoded_size(&self) -> usize { + let Eip7702Bytecode { delegated_address, version, raw } = self; + by_props!( + @size + delegated_address, + version, + raw, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let Eip7702Bytecode { delegated_address, version, raw } = self; + by_props!( + @encode buf; + delegated_address, + version, + raw, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut eip7702 = Eip7702Bytecode { + delegated_address: Address::ZERO, + version: 0, + raw: alloy::primitives::Bytes::new(), + }; + let Eip7702Bytecode { delegated_address, version, raw } = &mut eip7702; + + let mut data = data; + by_props!( + @decode data; + delegated_address, + version, + raw, + ); + Ok(eip7702) + } +} + +impl ValSer for JumpTable { + fn encoded_size(&self) -> usize { + 2 + 2 + self.as_slice().len() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + debug_assert!(self.len() <= u16::MAX as usize, "JumpTable bitlen too large to encode"); + debug_assert!(self.as_slice().len() <= u16::MAX as usize, "JumpTable too large to encode"); + buf.put_u16(self.len() as u16); + buf.put_u16(self.as_slice().len() as u16); + buf.put_slice(self.as_slice()); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let bit_len = u16::decode_value(&data[..2])? as usize; + let slice_len = u16::decode_value(&data[2..4])? as usize; + Ok(JumpTable::from_slice(&data[4..4 + slice_len], bit_len)) + } +} + +impl ValSer for LegacyAnalyzedBytecode { + fn encoded_size(&self) -> usize { + let bytecode = self.bytecode(); + let original_len = self.original_len(); + let jump_table = self.jump_table(); + by_props!( + @size + bytecode, + original_len, + jump_table, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let bytecode = self.bytecode(); + let original_len = self.original_len(); + let jump_table = self.jump_table(); + by_props!( + @encode buf; + bytecode, + original_len, + jump_table, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + let mut bytecode = alloy::primitives::Bytes::new(); + let mut original_len = 0usize; + let mut jump_table = JumpTable::default(); + + let bc = &mut bytecode; + let ol = &mut original_len; + let jt = &mut jump_table; + by_props!( + @decode data; + bc, + ol, + jt, + ); + Ok(LegacyAnalyzedBytecode::new(bytecode, original_len, jump_table)) + } +} + +impl ValSer for Bytecode { + fn encoded_size(&self) -> usize { + 1 + match &self.0 { + reth::revm::state::Bytecode::Eip7702(code) => code.encoded_size(), + reth::revm::state::Bytecode::LegacyAnalyzed(code) => code.encoded_size(), + } + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + match &self.0 { + reth::revm::state::Bytecode::Eip7702(code) => { + buf.put_u8(1); + code.encode_value_to(buf); + } + reth::revm::state::Bytecode::LegacyAnalyzed(code) => { + buf.put_u8(0); + code.encode_value_to(buf); + } + } + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let ty = u8::decode_value(&data[..1])?; + let data = &data[1..]; + match ty { + 0 => { + let analyzed = LegacyAnalyzedBytecode::decode_value(data)?; + Ok(Bytecode(reth::revm::state::Bytecode::LegacyAnalyzed(analyzed))) + } + 1 => { + let eip7702 = Eip7702Bytecode::decode_value(data)?; + Ok(Bytecode(reth::revm::state::Bytecode::Eip7702(eip7702))) + } + _ => Err(DeserError::String(format!("Invalid Bytecode type value: {}. Max is 1.", ty))), + } + } +} + +impl ValSer for AccountBeforeTx { + fn encoded_size(&self) -> usize { + let AccountBeforeTx { address, info } = self; + by_props!( + @size + address, + info, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let AccountBeforeTx { address, info } = self; + by_props!( + @encode buf; + address, + info, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + let mut abt = AccountBeforeTx::default(); + let AccountBeforeTx { address, info } = &mut abt; + + by_props!( + @decode data; + address, + info, + ); + Ok(abt) + } +} + +impl ValSer for Signature { + fn encoded_size(&self) -> usize { + 65 + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + FixedBytes(self.as_bytes()).encode_value_to(buf); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let bytes = FixedBytes::<65>::decode_value(data)?; + Self::from_raw_array(bytes.as_ref()) + .map_err(|e| DeserError::String(format!("Invalid signature bytes: {}", e))) + } +} + +impl ValSer for TxKind { + fn encoded_size(&self) -> usize { + 1 + match self { + TxKind::Create => 0, + TxKind::Call(_) => 20, + } + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + match self { + TxKind::Create => { + buf.put_u8(0); + } + TxKind::Call(address) => { + buf.put_u8(1); + address.encode_value_to(buf); + } + } + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let ty = u8::decode_value(&data[..1])?; + let data = &data[1..]; + match ty { + 0 => Ok(TxKind::Create), + 1 => { + let address = Address::decode_value(data)?; + Ok(TxKind::Call(address)) + } + _ => Err(DeserError::String(format!("Invalid TxKind type value: {}. Max is 1.", ty))), + } + } +} + +impl ValSer for AccessListItem { + fn encoded_size(&self) -> usize { + let AccessListItem { address, storage_keys } = self; + by_props!( + @size + address, + storage_keys, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let AccessListItem { address, storage_keys } = self; + by_props!( + @encode buf; + address, + storage_keys, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut item = AccessListItem::default(); + let AccessListItem { address, storage_keys } = &mut item; + + let mut data = data; + by_props!( + @decode data; + address, + storage_keys, + ); + Ok(item) + } +} + +impl ValSer for AccessList { + fn encoded_size(&self) -> usize { + self.0.encoded_size() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + self.0.encode_value_to(buf); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + Vec::::decode_value(data).map(AccessList) + } +} + +impl ValSer for Authorization { + fn encoded_size(&self) -> usize { + let Authorization { chain_id, address, nonce } = self; + by_props!( + @size + chain_id, + address, + nonce, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let Authorization { chain_id, address, nonce } = self; + by_props!( + @encode buf; + chain_id, + address, + nonce, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut auth = Authorization { chain_id: U256::ZERO, address: Address::ZERO, nonce: 0 }; + let Authorization { chain_id, address, nonce } = &mut auth; + + let mut data = data; + by_props!( + @decode data; + chain_id, + address, + nonce, + ); + Ok(auth) + } +} + +impl ValSer for SignedAuthorization { + fn encoded_size(&self) -> usize { + let auth = self.inner(); + let y_parity = self.y_parity(); + let r = self.r(); + let s = self.s(); + by_props!( + @size + auth, + y_parity, + r, + s, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let auth = self.inner(); + let y_parity = &self.y_parity(); + let r = &self.r(); + let s = &self.s(); + by_props!( + @encode buf; + auth, + y_parity, + r, + s, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + let mut auth = Authorization { chain_id: U256::ZERO, address: Address::ZERO, nonce: 0 }; + let mut y_parity = 0u8; + let mut r = U256::ZERO; + let mut s = U256::ZERO; + + let ap = &mut auth; + let yp = &mut y_parity; + let rr = &mut r; + let ss = &mut s; + + by_props!( + @decode data; + ap, + yp, + rr, + ss, + ); + Ok(SignedAuthorization::new_unchecked(auth, y_parity, r, s)) + } +} + +impl ValSer for TxLegacy { + fn encoded_size(&self) -> usize { + let TxLegacy { chain_id, nonce, gas_price, gas_limit, to, value, input } = self; + by_props!( + @size + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let TxLegacy { chain_id, nonce, gas_price, gas_limit, to, value, input } = self; + by_props!( + @encode buf; + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + let mut tx = TxLegacy::default(); + let TxLegacy { chain_id, nonce, gas_price, gas_limit, to, value, input } = &mut tx; + + by_props!( + @decode data; + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + ); + Ok(tx) + } +} + +impl ValSer for TxEip2930 { + fn encoded_size(&self) -> usize { + let TxEip2930 { chain_id, nonce, gas_price, gas_limit, to, value, input, access_list } = + self; + by_props!( + @size + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + access_list, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let TxEip2930 { chain_id, nonce, gas_price, gas_limit, to, value, input, access_list } = + self; + by_props!( + @encode buf; + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + access_list, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + let mut tx = TxEip2930::default(); + let TxEip2930 { chain_id, nonce, gas_price, gas_limit, to, value, input, access_list } = + &mut tx; + + by_props!( + @decode data; + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + access_list, + ); + Ok(tx) + } +} + +impl ValSer for TxEip1559 { + fn encoded_size(&self) -> usize { + let TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input, + } = self; + by_props!( + @size + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input, + } = self; + by_props!( + @encode buf; + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut tx = TxEip1559::default(); + let TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input, + } = &mut tx; + + let mut data = data; + by_props!( + @decode data; + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input + ); + Ok(tx) + } +} + +impl ValSer for TxEip4844 { + fn encoded_size(&self) -> usize { + let TxEip4844 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + blob_versioned_hashes, + max_fee_per_blob_gas, + input, + } = self; + by_props!( + @size + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + blob_versioned_hashes, + max_fee_per_blob_gas, + input, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let TxEip4844 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + blob_versioned_hashes, + max_fee_per_blob_gas, + input, + } = self; + by_props!( + @encode buf; + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + blob_versioned_hashes, + max_fee_per_blob_gas, + input, + ) + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut tx = TxEip4844::default(); + let TxEip4844 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + blob_versioned_hashes, + max_fee_per_blob_gas, + input, + } = &mut tx; + + let mut data = data; + by_props!( + @decode data; + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + blob_versioned_hashes, + max_fee_per_blob_gas, + input, + ); + Ok(tx) + } +} + +impl ValSer for TxEip7702 { + fn encoded_size(&self) -> usize { + let TxEip7702 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + authorization_list, + input, + } = self; + by_props!( + @size + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + authorization_list, + input, + ) + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + let TxEip7702 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + authorization_list, + input, + } = self; + by_props!( + @encode buf; + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + authorization_list, + input, + ) + } + + fn decode_value(mut data: &[u8]) -> Result + where + Self: Sized, + { + let mut tx = TxEip7702::default(); + let TxEip7702 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + authorization_list, + input, + } = &mut tx; + by_props!( + @decode data; + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + authorization_list, + input, + ); + Ok(tx) + } +} + +impl ValSer for Signed +where + T: ValSer, + Sig: ValSer, +{ + fn encoded_size(&self) -> usize { + self.signature().encoded_size() + self.tx().encoded_size() + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + self.signature().encode_value_to(buf); + self.tx().encode_value_to(buf); + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let mut data = data; + + let signature = Sig::decode_value(data)?; + data = &data[signature.encoded_size()..]; + + let tx = T::decode_value(data)?; + + Ok(Signed::new_unhashed(tx, signature)) + } +} + +impl ValSer for TransactionSigned { + fn encoded_size(&self) -> usize { + self.tx_type().encoded_size() + + match self { + EthereumTxEnvelope::Legacy(signed) => signed.encoded_size(), + EthereumTxEnvelope::Eip2930(signed) => signed.encoded_size(), + EthereumTxEnvelope::Eip1559(signed) => signed.encoded_size(), + EthereumTxEnvelope::Eip4844(signed) => signed.encoded_size(), + EthereumTxEnvelope::Eip7702(signed) => signed.encoded_size(), + } + } + + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>, + { + self.tx_type().encode_value_to(buf); + match self { + EthereumTxEnvelope::Legacy(signed) => { + signed.encode_value_to(buf); + } + EthereumTxEnvelope::Eip2930(signed) => { + signed.encode_value_to(buf); + } + EthereumTxEnvelope::Eip1559(signed) => { + signed.encode_value_to(buf); + } + EthereumTxEnvelope::Eip4844(signed) => { + signed.encode_value_to(buf); + } + EthereumTxEnvelope::Eip7702(signed) => { + signed.encode_value_to(buf); + } + } + } + + fn decode_value(data: &[u8]) -> Result + where + Self: Sized, + { + let ty = TxType::decode_value(data)?; + let data = &data[ty.encoded_size()..]; + match ty { + TxType::Legacy => ValSer::decode_value(data).map(EthereumTxEnvelope::Legacy), + TxType::Eip2930 => ValSer::decode_value(data).map(EthereumTxEnvelope::Eip2930), + TxType::Eip1559 => ValSer::decode_value(data).map(EthereumTxEnvelope::Eip1559), + TxType::Eip4844 => ValSer::decode_value(data).map(EthereumTxEnvelope::Eip4844), + TxType::Eip7702 => ValSer::decode_value(data).map(EthereumTxEnvelope::Eip7702), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::{ + Address, B256, Bloom, Bytes as AlloBytes, Signature, TxKind, U256, keccak256, + }; + use alloy::{ + consensus::{TxEip1559, TxEip2930, TxEip4844, TxEip7702, TxLegacy}, + eips::{ + eip2930::{AccessList, AccessListItem}, + eip7702::{Authorization, SignedAuthorization}, + }, + }; + use reth::primitives::{Account, Header, Log, LogData, TxType}; + use reth::revm::bytecode::JumpTable; + use reth_db_api::{BlockNumberList, models::StoredBlockBodyIndices}; + + /// Generic roundtrip test for any ValSer type + #[track_caller] + fn test_roundtrip(original: &T) + where + T: ValSer + PartialEq + std::fmt::Debug, + { + // Encode + let mut buf = bytes::BytesMut::new(); + original.encode_value_to(&mut buf); + let encoded = buf.freeze(); + + // Assert that the encoded size matches + assert_eq!( + original.encoded_size(), + encoded.len(), + "Encoded size mismatch: expected {}, got {}", + original.encoded_size(), + encoded.len() + ); + + // Decode + let decoded = T::decode_value(&encoded).expect("Failed to decode value"); + + // Assert equality + assert_eq!(*original, decoded, "Roundtrip failed"); + } + + #[test] + fn test_blocknumberlist_roundtrips() { + // Empty list + test_roundtrip(&BlockNumberList::empty()); + + // Single item + let mut single = BlockNumberList::empty(); + single.push(42u64).unwrap(); + test_roundtrip(&single); + + // Multiple items + let mut multiple = BlockNumberList::empty(); + for i in [0, 1, 255, 256, 65535, 65536, u64::MAX] { + multiple.push(i).unwrap(); + } + test_roundtrip(&multiple); + } + + #[test] + fn test_account_roundtrips() { + // Default account + test_roundtrip(&Account::default()); + + // Account with values + let account = Account { + nonce: 42, + balance: U256::from(123456789u64), + bytecode_hash: Some(keccak256(b"hello world")), + }; + test_roundtrip(&account); + + // Account with max values + let max_account = Account { + nonce: u64::MAX, + balance: U256::MAX, + bytecode_hash: Some(B256::from([0xFF; 32])), + }; + test_roundtrip(&max_account); + } + + #[test] + fn test_header_roundtrips() { + // Default header + test_roundtrip(&Header::default()); + + // Header with some values + let header = Header { + number: 12345, + gas_limit: 8000000, + timestamp: 1234567890, + difficulty: U256::from(1000000u64), + ..Default::default() + }; + test_roundtrip(&header); + } + + #[test] + fn test_logdata_roundtrips() { + // Empty log data + test_roundtrip(&LogData::new_unchecked(vec![], AlloBytes::new())); + + // Log data with one topic + test_roundtrip(&LogData::new_unchecked( + vec![B256::from([1; 32])], + AlloBytes::from_static(b"hello"), + )); + + // Log data with multiple topics + test_roundtrip(&LogData::new_unchecked( + vec![ + B256::from([1; 32]), + B256::from([2; 32]), + B256::from([3; 32]), + B256::from([4; 32]), + ], + AlloBytes::from_static(b"world"), + )); + } + + #[test] + fn test_log_roundtrips() { + let log_data = LogData::new_unchecked( + vec![B256::from([1; 32]), B256::from([2; 32])], + AlloBytes::from_static(b"test log data"), + ); + let log = Log { address: Address::from([0x42; 20]), data: log_data }; + test_roundtrip(&log); + } + + #[test] + fn test_txtype_roundtrips() { + test_roundtrip(&TxType::Legacy); + test_roundtrip(&TxType::Eip2930); + test_roundtrip(&TxType::Eip1559); + test_roundtrip(&TxType::Eip4844); + test_roundtrip(&TxType::Eip7702); + } + + #[test] + fn test_stored_block_body_indices_roundtrips() { + test_roundtrip(&StoredBlockBodyIndices { first_tx_num: 0, tx_count: 0 }); + + test_roundtrip(&StoredBlockBodyIndices { first_tx_num: 12345, tx_count: 67890 }); + + test_roundtrip(&StoredBlockBodyIndices { first_tx_num: u64::MAX, tx_count: u64::MAX }); + } + + #[test] + fn test_signature_roundtrips() { + test_roundtrip(&Signature::test_signature()); + + // Zero signature + let zero_sig = Signature::new(U256::ZERO, U256::ZERO, false); + test_roundtrip(&zero_sig); + + // Max signature + let max_sig = Signature::new(U256::MAX, U256::MAX, true); + test_roundtrip(&max_sig); + } + + #[test] + fn test_txkind_roundtrips() { + test_roundtrip(&TxKind::Create); + test_roundtrip(&TxKind::Call(Address::ZERO)); + test_roundtrip(&TxKind::Call(Address::from([0xFF; 20]))); + } + + #[test] + fn test_accesslist_roundtrips() { + // Empty access list + test_roundtrip(&AccessList::default()); + + // Access list with one item + let item = AccessListItem { + address: Address::from([0x12; 20]), + storage_keys: vec![B256::from([0x34; 32])], + }; + test_roundtrip(&AccessList(vec![item])); + + // Access list with multiple items and keys + let items = vec![ + AccessListItem { + address: Address::repeat_byte(11), + storage_keys: vec![B256::from([0x22; 32]), B256::from([0x33; 32])], + }, + AccessListItem { address: Address::from([0x44; 20]), storage_keys: vec![] }, + AccessListItem { + address: Address::from([0x55; 20]), + storage_keys: vec![B256::from([0x66; 32])], + }, + ]; + test_roundtrip(&AccessList(items)); + } + + #[test] + fn test_authorization_roundtrips() { + test_roundtrip(&Authorization { + chain_id: U256::from(1u64), + address: Address::repeat_byte(11), + nonce: 0, + }); + + test_roundtrip(&Authorization { + chain_id: U256::MAX, + address: Address::from([0xFF; 20]), + nonce: u64::MAX, + }); + } + + #[test] + fn test_signed_authorization_roundtrips() { + let auth = Authorization { + chain_id: U256::from(1u64), + address: Address::repeat_byte(11), + nonce: 42, + }; + let signed_auth = + SignedAuthorization::new_unchecked(auth, 1, U256::from(12345u64), U256::from(67890u64)); + test_roundtrip(&signed_auth); + } + + #[test] + fn test_tx_legacy_roundtrips() { + test_roundtrip(&TxLegacy::default()); + + let tx = TxLegacy { + chain_id: Some(1), + nonce: 42, + gas_price: 20_000_000_000, + gas_limit: 21000u64, + to: TxKind::Call(Address::repeat_byte(11)), + value: U256::from(1000000000000000000u64), // 1 ETH in wei + input: AlloBytes::from_static(b"hello world"), + }; + test_roundtrip(&tx); + } + + #[test] + fn test_tx_eip2930_roundtrips() { + test_roundtrip(&TxEip2930::default()); + + let access_list = AccessList(vec![AccessListItem { + address: Address::from([0x22; 20]), + storage_keys: vec![B256::from([0x33; 32])], + }]); + + let tx = TxEip2930 { + chain_id: 1, + nonce: 42, + gas_price: 20_000_000_000, + gas_limit: 21000u64, + to: TxKind::Call(Address::repeat_byte(11)), + value: U256::from(1000000000000000000u64), + input: AlloBytes::from_static(b"eip2930 tx"), + access_list, + }; + test_roundtrip(&tx); + } + + #[test] + fn test_tx_eip1559_roundtrips() { + test_roundtrip(&TxEip1559::default()); + + let tx = TxEip1559 { + chain_id: 1, + nonce: 42, + gas_limit: 21000u64, + max_fee_per_gas: 30_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(Address::repeat_byte(11)), + value: U256::from(1000000000000000000u64), + input: AlloBytes::from_static(b"eip1559 tx"), + access_list: AccessList::default(), + }; + test_roundtrip(&tx); + } + + #[test] + fn test_tx_eip4844_roundtrips() { + test_roundtrip(&TxEip4844::default()); + + let tx = TxEip4844 { + chain_id: 1, + nonce: 42, + gas_limit: 21000u64, + max_fee_per_gas: 30_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: Address::repeat_byte(11), + value: U256::from(1000000000000000000u64), + input: AlloBytes::from_static(b"eip4844 tx"), + access_list: AccessList::default(), + blob_versioned_hashes: vec![B256::from([0x44; 32])], + max_fee_per_blob_gas: 1_000_000, + }; + test_roundtrip(&tx); + } + + #[test] + fn test_tx_eip7702_roundtrips() { + test_roundtrip(&TxEip7702::default()); + + let auth = SignedAuthorization::new_unchecked( + Authorization { + chain_id: U256::from(1u64), + address: Address::from([0x77; 20]), + nonce: 0, + }, + 1, + U256::from(12345u64), + U256::from(67890u64), + ); + + let tx = TxEip7702 { + chain_id: 1, + nonce: 42, + gas_limit: 21000u64, + max_fee_per_gas: 30_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: Address::repeat_byte(11), + value: U256::from(1000000000000000000u64), + input: AlloBytes::from_static(b"eip7702 tx"), + access_list: AccessList::default(), + authorization_list: vec![auth], + }; + test_roundtrip(&tx); + } + + #[test] + fn test_jump_table_roundtrips() { + // Empty jump table + test_roundtrip(&JumpTable::default()); + + // Jump table with some jumps + let jump_table = JumpTable::from_slice(&[0b10101010, 0b01010101], 16); + test_roundtrip(&jump_table); + } + + #[test] + fn test_complex_combinations() { + // Test a complex Header with all fields populated + let header = Header { + number: 12345, + gas_limit: 8000000, + timestamp: 1234567890, + difficulty: U256::from(1000000u64), + parent_hash: keccak256(b"parent"), + ommers_hash: keccak256(b"ommers"), + beneficiary: Address::from([0xBE; 20]), + state_root: keccak256(b"state"), + transactions_root: keccak256(b"txs"), + receipts_root: keccak256(b"receipts"), + logs_bloom: Bloom::default(), + gas_used: 7999999, + mix_hash: keccak256(b"mix"), + nonce: [0x42; 8].into(), + extra_data: AlloBytes::from_static(b"extra data"), + base_fee_per_gas: Some(1000000000), + withdrawals_root: Some(keccak256(b"withdrawals_root")), + blob_gas_used: Some(500000), + excess_blob_gas: Some(10000), + parent_beacon_block_root: Some(keccak256(b"parent_beacon_block_root")), + requests_hash: Some(keccak256(b"requests_hash")), + }; + test_roundtrip(&header); + + // Test a complex EIP-1559 transaction + let access_list = AccessList(vec![ + AccessListItem { + address: Address::repeat_byte(11), + storage_keys: vec![B256::from([0x22; 32]), B256::from([0x33; 32])], + }, + AccessListItem { address: Address::from([0x44; 20]), storage_keys: vec![] }, + ]); + + let complex_tx = TxEip1559 { + chain_id: 1, + nonce: 123456, + gas_limit: 500000u64, + max_fee_per_gas: 50_000_000_000, + max_priority_fee_per_gas: 3_000_000_000, + to: TxKind::Create, + value: U256::ZERO, + input: AlloBytes::copy_from_slice(&[0xFF; 1000]), // Large input + access_list, + }; + test_roundtrip(&complex_tx); + } + + #[test] + fn test_edge_cases() { + // Very large access list + let large_storage_keys: Vec = + (0..1000).map(|i| B256::from(U256::from(i).to_be_bytes::<32>())).collect(); + let large_access_list = AccessList(vec![AccessListItem { + address: Address::from([0xAA; 20]), + storage_keys: large_storage_keys, + }]); + test_roundtrip(&large_access_list); + + // Transaction with maximum values + let max_tx = TxEip1559 { + chain_id: u64::MAX, + nonce: u64::MAX, + gas_limit: u64::MAX, + max_fee_per_gas: u128::MAX, + max_priority_fee_per_gas: u128::MAX, + to: TxKind::Call(Address::repeat_byte(0xFF)), + value: U256::MAX, + input: AlloBytes::copy_from_slice(&[0xFF; 10000]), // Very large input + access_list: AccessList::default(), + }; + test_roundtrip(&max_tx); + + // BlockNumberList with many numbers + let mut large_list = BlockNumberList::empty(); + for i in 0..10000u64 { + large_list.push(i).unwrap(); + } + test_roundtrip(&large_list); + } +} diff --git a/crates/storage/src/ser/traits.rs b/crates/storage/src/ser/traits.rs new file mode 100644 index 0000000..88e1fef --- /dev/null +++ b/crates/storage/src/ser/traits.rs @@ -0,0 +1,117 @@ +use crate::ser::error::DeserError; +use alloy::primitives::Bytes; + +/// Maximum allowed key size in bytes. +pub const MAX_KEY_SIZE: usize = 64; + +/// Trait for key serialization with fixed-size keys of size no greater than 32 +/// bytes. +/// +/// Keys must be FIXED SIZE, of size no greater than `MAX_KEY_SIZE` (64), and +/// no less than 1. The serialization must preserve ordering, i.e., for any two +/// keys `k1` and `k2`, if `k1 > k2`, then the byte representation of `k1` +/// must be lexicographically greater than that of `k2`. +/// +/// In practice, keys are often hashes, addresses, numbers, or composites +/// of these. +pub trait KeySer: Ord + Sized { + /// The fixed size of the serialized key in bytes. + /// Must satisfy `SIZE <= MAX_KEY_SIZE`. + const SIZE: usize; + + /// Compile-time assertion to ensure SIZE is within limits. + #[doc(hidden)] + const ASSERT: () = { + assert!( + Self::SIZE <= MAX_KEY_SIZE, + "KeySer implementations must have SIZE <= MAX_KEY_SIZE" + ); + assert!(Self::SIZE > 0, "KeySer implementations must have SIZE > 0"); + }; + + /// Encode the key, optionally using the provided buffer. + /// + /// # Returns + /// + /// A slice containing the encoded key. This may be a slice of `buf`, or may + /// be borrowed from the key itself. This slice must be <= `SIZE` bytes. + fn encode_key<'a: 'c, 'b: 'c, 'c>(&'a self, buf: &'b mut [u8; MAX_KEY_SIZE]) -> &'c [u8]; + + /// Decode a key from a byte slice. + /// + /// # Arguments + /// * `data` - Exactly `SIZE` bytes to decode from. + /// + /// # Errors + /// Returns an error if `data.len() != SIZE` or decoding fails. + fn decode_key(data: &[u8]) -> Result; + + /// Decode an optional key from an optional byte slice. + /// + /// Useful in DB decoding, where the absence of a key is represented by + /// `None`. + fn maybe_decode_key(data: Option<&[u8]>) -> Result, DeserError> { + match data { + Some(d) => Ok(Some(Self::decode_key(d)?)), + None => Ok(None), + } + } +} + +/// Trait for value serialization. +/// +/// Values can be of variable size, but must implement accurate size reporting. +/// When serialized, value sizes must be self-describing. I.e. the value must +/// tolerate being deserialized from a byte slice of arbitrary length, consuming +/// only as many bytes as needed. +/// +/// E.g. a correct implementation for an array serializes the length of the +/// array first, so that the deserializer knows how many items to expect. +pub trait ValSer { + /// The encoded size of the value in bytes. This MUST be accurate, as it is + /// used to allocate buffers for serialization. Inaccurate sizes may result + /// in panics or incorrect behavior. + fn encoded_size(&self) -> usize; + + /// Serialize the value into bytes. + fn encode_value_to(&self, buf: &mut B) + where + B: bytes::BufMut + AsMut<[u8]>; + + /// Serialize the value into bytes and return them. + fn encoded(&self) -> Bytes { + let mut buf = bytes::BytesMut::new(); + self.encode_value_to(&mut buf); + buf.freeze().into() + } + + /// Deserialize the value from bytes. + fn decode_value(data: &[u8]) -> Result + where + Self: Sized; + + /// Deserialize an optional value from an optional byte slice. + /// + /// Useful in DB decoding, where the absence of a value is represented by + /// `None`. + fn maybe_decode_value(data: Option<&[u8]>) -> Result, DeserError> + where + Self: Sized, + { + match data { + Some(d) => Ok(Some(Self::decode_value(d)?)), + None => Ok(None), + } + } + + /// Deserialize the value from bytes, ensuring all bytes are consumed. + fn decode_value_exact(data: &[u8]) -> Result + where + Self: Sized, + { + let val = Self::decode_value(data)?; + (val.encoded_size() == data.len()) + .then_some(val) + .ok_or(DeserError::InexactDeser { extra_bytes: data.len() }) + } +} diff --git a/crates/storage/src/tables/cold.rs b/crates/storage/src/tables/cold.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/storage/src/tables/hot.rs b/crates/storage/src/tables/hot.rs new file mode 100644 index 0000000..ec19884 --- /dev/null +++ b/crates/storage/src/tables/hot.rs @@ -0,0 +1,86 @@ +use std::borrow::Cow; + +use crate::tables::Table; +use alloy::primitives::{Address, B256, BlockNumber, FixedBytes, U256}; +use reth::primitives::{Account, Bytecode, Header}; +use reth_db_api::{ + BlockNumberList, + models::{AccountBeforeTx, ShardedKey, storage_sharded_key::StorageShardedKey}, +}; + +tables! { + /// Records recent block Headers, by their number. + Headers, + + /// Records block numbers by hash. + HeaderNumbers, + + /// Records the canonical chain header hashes, by height. + CanonicalHeaders, + + /// Records contract Bytecode, by its hash. + Bytecodes, + + /// Records plain account states, keyed by address. + PlainAccountState, + + /// Records account state change history, keyed by address. + AccountsHistory, BlockNumberList>, + + /// Records storage state change history, keyed by address and storage key. + StorageHistory, + + /// Records account change sets, keyed by block number. + AccountChangeSets, +} + +/// Records plain storage states, keyed by address and storage key. +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct PlainStorageState; + +impl Table for PlainStorageState { + const NAME: &'static str = "PlainStorageState"; + + type Key = FixedBytes<52>; + + type Value = U256; +} + +/// Key for the [`PlainStorageState`] table. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct AccountStorageKey<'a, 'b> { + /// Address of the account. + pub address: Cow<'a, Address>, + /// Storage key. + pub key: Cow<'b, B256>, +} + +impl AccountStorageKey<'static, 'static> { + /// Decode the key from the provided data. + pub fn decode_key(data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(crate::ser::DeserError::InsufficientData { + needed: Self::SIZE, + available: data.len(), + }); + } + + let address = Address::from_slice(&data[0..20]); + let key = B256::from_slice(&data[20..52]); + + Ok(Self { address: Cow::Owned(address), key: Cow::Owned(key) }) + } +} + +impl<'a, 'b> AccountStorageKey<'a, 'b> { + /// Size in bytes. + pub const SIZE: usize = 20 + 32; + + /// Encode the key into the provided buffer. + pub fn encode_key(&self) -> FixedBytes<52> { + let mut buf = [0u8; Self::SIZE]; + buf[0..20].copy_from_slice(self.address.as_slice()); + buf[20..52].copy_from_slice(self.key.as_slice()); + buf.into() + } +} diff --git a/crates/storage/src/tables/macros.rs b/crates/storage/src/tables/macros.rs new file mode 100644 index 0000000..89dc412 --- /dev/null +++ b/crates/storage/src/tables/macros.rs @@ -0,0 +1,21 @@ +macro_rules! tables { + ( + #[doc = $doc:expr] + $name:ident<$key:ty, $value:ty>) => { + #[doc = $doc] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct $name; + + impl crate::tables::Table for $name { + const NAME: &'static str = stringify!($name); + type Key = $key; + type Value = $value; + } + }; + + ($(#[doc = $doc:expr] $name:ident<$key:ty, $value:ty>),* $(,)?) => { + $( + tables!(#[doc = $doc] $name<$key, $value>); + )* + }; +} diff --git a/crates/storage/src/tables/mod.rs b/crates/storage/src/tables/mod.rs new file mode 100644 index 0000000..bbd6263 --- /dev/null +++ b/crates/storage/src/tables/mod.rs @@ -0,0 +1,21 @@ +#[macro_use] +mod macros; + +/// Tables that are not hot. +pub mod cold; + +/// Tables that are hot, or conditionally hot. +pub mod hot; + +use crate::ser::{KeySer, ValSer}; + +/// Trait for table definitions. +pub trait Table { + /// A Human-readable name for the table. + const NAME: &'static str; + + /// The key type. + type Key: KeySer; + /// The value type. + type Value: ValSer; +}