From 90df32942ae4238e5ca7d113cfdc95e8a5a889df Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 18:43:45 +0400 Subject: [PATCH 01/12] Initial implementation with dashmaps --- src/payload/ext/cached_state.rs | 308 ++++++++++++++++++++++++++++++++ src/payload/ext/mod.rs | 1 + 2 files changed, 309 insertions(+) create mode 100644 src/payload/ext/cached_state.rs diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs new file mode 100644 index 0000000..1731d62 --- /dev/null +++ b/src/payload/ext/cached_state.rs @@ -0,0 +1,308 @@ +//! Database adapters for payload building. +// Originated from reth https://github.com/paradigmxyz/reth/blob/b72bb6790a5f7ada75282e52b80f986d9717e698/crates/revm/src/cached.rs +use { + crate::{ + alloy::primitives::{Address, B256, U256, map::HashMap}, + reth::revm::{ + Database, + DatabaseRef, + bytecode::Bytecode, + state::AccountInfo, + }, + }, + core::cell::RefCell, + dashmap::{DashMap, Entry, Map}, + std::sync::Arc, +}; + +/// A container type that caches reads from an underlying [`DatabaseRef`]. +/// +/// This is intended to be used in conjunction with `revm::db::State` +/// during payload building which repeatedly accesses the same data. +/// +/// [`CachedReads::as_db_mut`] transforms this type into a [`Database`] +/// implementation that uses [`CachedReads`] as a caching layer for operations, +/// and records any cache misses. +/// +/// # Example +/// +/// ``` +/// use reth_revm::{cached::CachedReads, DatabaseRef, db::State}; +/// +/// fn build_payload(db: DB) { +/// let mut cached_reads = CachedReads::default(); +/// let db = cached_reads.as_db_mut(db); +/// // this is `Database` and can be used to build a payload, it never commits to `CachedReads` or the underlying database, but all reads from the underlying database are cached in `CachedReads`. +/// // Subsequent payload build attempts can use cached reads and avoid hitting the underlying database. +/// let state = State::builder().with_database(db).build(); +/// } +/// ``` +#[derive(Debug, Clone, Default)] +pub struct CachedReads { + /// Block state account with storage. + pub accounts: DashMap, + /// Created contracts. + pub contracts: DashMap, + /// Block hash mapped to the block number. + pub block_hashes: DashMap, +} + +// === impl CachedReads === + +impl CachedReads { + /// Gets a [`DatabaseRef`] that will cache reads from the given database. + pub const fn as_db(&mut self, db: DB) -> CachedReadsDBRef<'_, DB> { + self.as_db_mut(db).into_db() + } + + /// Gets a mutable [`Database`] that will cache reads from the underlying + /// database. + pub const fn as_db_mut(&mut self, db: DB) -> CachedReadsDbMut<'_, DB> { + CachedReadsDbMut { cached: self, db } + } + + /// Inserts an account info into the cache. + pub fn insert_account( + &self, + address: Address, + info: AccountInfo, + storage: HashMap, + ) { + self.accounts.insert(address, CachedAccount { + info: Some(info), + storage, + }); + } + + /// Extends current cache with entries from another [`CachedReads`] instance. + /// + /// Note: It is expected that both instances are based on the exact same + /// state. + pub fn extend(&self, other: Self) { + for (k, v) in other.accounts.into_iter() { + self.accounts.insert(k, v); + } + for (k, v) in other.contracts.into_iter() { + self.contracts.insert(k, v); + } + for (k, v) in other.block_hashes.into_iter() { + self.block_hashes.insert(k, v); + } + } +} + +/// A [Database] that caches reads inside [`CachedReads`]. +#[derive(Debug)] +pub struct CachedReadsDbMut<'a, DB> { + /// The cache of reads. + pub cached: &'a CachedReads, + /// The underlying database. + pub db: DB, +} + +impl<'a, DB> CachedReadsDbMut<'a, DB> { + /// Converts this [`Database`] implementation into a [`DatabaseRef`] that will + /// still cache reads. + pub const fn into_db(self) -> CachedReadsDBRef<'a, DB> { + CachedReadsDBRef { + inner: RefCell::new(self), + } + } + + /// Returns access to wrapped [`DatabaseRef`]. + pub const fn inner(&self) -> &DB { + &self.db + } +} + +impl AsRef for CachedReadsDbMut<'_, DB> +where + DB: AsRef, +{ + fn as_ref(&self) -> &T { + self.inner().as_ref() + } +} + +impl Database for CachedReadsDbMut<'_, DB> { + type Error = ::Error; + + fn basic( + &mut self, + address: Address, + ) -> Result, Self::Error> { + let basic = match self.cached.accounts.entry(address) { + Entry::Occupied(entry) => entry.get().info.clone(), + Entry::Vacant(entry) => entry + .insert(CachedAccount::new(self.db.basic_ref(address)?)) + .info + .clone(), + }; + Ok(basic) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + let code = match self.cached.contracts.entry(code_hash) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + entry.insert(self.db.code_by_hash_ref(code_hash)?).clone() + } + }; + Ok(code) + } + + fn storage( + &mut self, + address: Address, + index: U256, + ) -> Result { + match self.cached.accounts.entry(address) { + Entry::Occupied(mut acc_entry) => { + match acc_entry.get_mut().storage.entry(index) { + std::collections::hash_map::Entry::Occupied(entry) => { + Ok(*entry.get()) + } + std::collections::hash_map::Entry::Vacant(entry) => { + Ok(*entry.insert(self.db.storage_ref(address, index)?)) + } + } + } + Entry::Vacant(acc_entry) => { + // acc needs to be loaded for us to access slots. + let info = self.db.basic_ref(address)?; + let (account, value) = if info.is_some() { + let value = self.db.storage_ref(address, index)?; + let mut account = CachedAccount::new(info); + account.storage.insert(index, value); + (account, value) + } else { + (CachedAccount::new(info), U256::ZERO) + }; + acc_entry.insert(account); + Ok(value) + } + } + } + + fn block_hash(&mut self, number: u64) -> Result { + let hash = match self.cached.block_hashes.entry(number) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => *entry.insert(self.db.block_hash_ref(number)?), + }; + Ok(hash) + } +} + +/// A [`DatabaseRef`] that caches reads inside [`CachedReads`]. +/// +/// This is intended to be used as the [`DatabaseRef`] for +/// `revm::db::State` for repeated payload build jobs. +#[derive(Debug)] +pub struct CachedReadsDBRef<'a, DB> { + /// The inner cache reads db mut. + pub inner: RefCell>, +} + +impl DatabaseRef for CachedReadsDBRef<'_, DB> { + type Error = ::Error; + + fn basic_ref( + &self, + address: Address, + ) -> Result, Self::Error> { + self.inner.borrow_mut().basic(address) + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + self.inner.borrow_mut().code_by_hash(code_hash) + } + + fn storage_ref( + &self, + address: Address, + index: U256, + ) -> Result { + self.inner.borrow_mut().storage(address, index) + } + + fn block_hash_ref(&self, number: u64) -> Result { + self.inner.borrow_mut().block_hash(number) + } +} + +/// Cached account contains the account state with storage +/// but lacks the account status. +#[derive(Debug, Clone)] +pub struct CachedAccount { + /// Account state. + pub info: Option, + /// Account's storage. + pub storage: HashMap, +} + +impl CachedAccount { + fn new(info: Option) -> Self { + Self { + info, + storage: HashMap::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extend_with_two_cached_reads() { + // Setup test data + let hash1 = B256::from_slice(&[1u8; 32]); + let hash2 = B256::from_slice(&[2u8; 32]); + let address1 = Address::from_slice(&[1u8; 20]); + let address2 = Address::from_slice(&[2u8; 20]); + + // Create primary cache + let primary = { + let cache = CachedReads::default(); + cache + .accounts + .insert(address1, CachedAccount::new(Some(AccountInfo::default()))); + cache.contracts.insert(hash1, Bytecode::default()); + cache.block_hashes.insert(1, hash1); + cache + }; + + // Create additional cache + let additional = { + let cache = CachedReads::default(); + cache + .accounts + .insert(address2, CachedAccount::new(Some(AccountInfo::default()))); + cache.contracts.insert(hash2, Bytecode::default()); + cache.block_hashes.insert(2, hash2); + cache + }; + + // Extending primary with additional cache + primary.extend(additional); + + // Verify the combined state + assert!( + primary.accounts.len() == 2 + && primary.contracts.len() == 2 + && primary.block_hashes.len() == 2, + "All maps should contain 2 entries" + ); + + // Verify specific entries + assert!( + primary.accounts.contains_key(&address1) + && primary.accounts.contains_key(&address2) + && primary.contracts.contains_key(&hash1) + && primary.contracts.contains_key(&hash2) + && primary.block_hashes.get(&1).as_deref() == Some(&hash1) + && primary.block_hashes.get(&2).as_deref() == Some(&hash2), + "All expected entries should be present" + ); + } +} diff --git a/src/payload/ext/mod.rs b/src/payload/ext/mod.rs index f524e7b..feebff8 100644 --- a/src/payload/ext/mod.rs +++ b/src/payload/ext/mod.rs @@ -6,6 +6,7 @@ //! but are not strictly necessary for the core functionality. mod block; +mod cached_state; mod checkpoint; mod span; From 6c73fd99f71feb2be26e10f327c326023c4998ab Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:11:17 +0400 Subject: [PATCH 02/12] Rework to use CachedStateProvider --- src/payload/block.rs | 7 +- src/payload/ext/cached_state.rs | 744 ++++++++++++++++++++++---------- src/payload/ext/mod.rs | 2 +- src/payload/mod.rs | 3 +- 4 files changed, 517 insertions(+), 239 deletions(-) diff --git a/src/payload/block.rs b/src/payload/block.rs index 31953aa..7bde566 100644 --- a/src/payload/block.rs +++ b/src/payload/block.rs @@ -81,8 +81,11 @@ impl BlockContext

{ .next_evm_env(&parent, &block_env) .map_err(Error::EvmEnv)?; + // TODO: Prefill execution_cached with bundle state from the previous block + let execution_cached = ExecutionCache::default(); + let provider = CachedStateProvider::new_with_caches(base_state, execution_cached); let mut base_state = State::builder() - .with_database(StateProviderDatabase(base_state)) + .with_database(StateProviderDatabase(provider)) .with_bundle_update() .build(); @@ -202,7 +205,7 @@ struct BlockContextInner { /// /// This state has no changes made to it during the payload building process /// through any of the created checkpoints. - base_state: State>, + base_state: State>>, /// The EVM factory configured for the environment in which we are building /// the payload. This type is used to create individual EVM instances that diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 1731d62..65a4e7b 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -1,308 +1,582 @@ //! Database adapters for payload building. // Originated from reth https://github.com/paradigmxyz/reth/blob/b72bb6790a5f7ada75282e52b80f986d9717e698/crates/revm/src/cached.rs + +use reth_ethereum::evm::revm::db::BundleState; +use reth_ethereum::primitives::{Account, Bytecode}; +use reth_ethereum::trie::{AccountProof, HashedPostState, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, StorageProof, TrieInput}; +use reth_ethereum::trie::updates::TrieUpdates; use { crate::{ - alloy::primitives::{Address, B256, U256, map::HashMap}, - reth::revm::{ - Database, - DatabaseRef, - bytecode::Bytecode, - state::AccountInfo, - }, + alloy::primitives::{Address, B256}, + reth::ethereum::provider::{AccountReader, BlockHashReader, BytecodeReader, HashedPostStateProvider, StateProofProvider, StateRootProvider, StorageRootProvider, StateProvider}, + reth::errors::ProviderResult, }, - core::cell::RefCell, - dashmap::{DashMap, Entry, Map}, + dashmap::DashMap, std::sync::Arc, }; +use crate::alloy::primitives::{StorageKey, StorageValue}; -/// A container type that caches reads from an underlying [`DatabaseRef`]. -/// -/// This is intended to be used in conjunction with `revm::db::State` -/// during payload building which repeatedly accesses the same data. -/// -/// [`CachedReads::as_db_mut`] transforms this type into a [`Database`] -/// implementation that uses [`CachedReads`] as a caching layer for operations, -/// and records any cache misses. -/// -/// # Example -/// -/// ``` -/// use reth_revm::{cached::CachedReads, DatabaseRef, db::State}; -/// -/// fn build_payload(db: DB) { -/// let mut cached_reads = CachedReads::default(); -/// let db = cached_reads.as_db_mut(db); -/// // this is `Database` and can be used to build a payload, it never commits to `CachedReads` or the underlying database, but all reads from the underlying database are cached in `CachedReads`. -/// // Subsequent payload build attempts can use cached reads and avoid hitting the underlying database. -/// let state = State::builder().with_database(db).build(); -/// } -/// ``` -#[derive(Debug, Clone, Default)] -pub struct CachedReads { - /// Block state account with storage. - pub accounts: DashMap, - /// Created contracts. - pub contracts: DashMap, - /// Block hash mapped to the block number. - pub block_hashes: DashMap, -} +/// A wrapper of a state provider and a shared cache. +pub struct CachedStateProvider { + /// The state provider + state_provider: S, -// === impl CachedReads === + /// The caches used for the provider + caches: ExecutionCache, +} -impl CachedReads { - /// Gets a [`DatabaseRef`] that will cache reads from the given database. - pub const fn as_db(&mut self, db: DB) -> CachedReadsDBRef<'_, DB> { - self.as_db_mut(db).into_db() +impl CachedStateProvider +where + S: StateProvider, +{ + /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and + /// [`CachedStateMetrics`]. + pub const fn new_with_caches( + state_provider: S, + caches: ExecutionCache, + ) -> Self { + Self { state_provider, caches } } +} + +impl AccountReader for CachedStateProvider { + fn basic_account(&self, address: &Address) -> ProviderResult> { + if let Some(res) = self.caches.account_cache.get(address) { + return Ok(*res) + } - /// Gets a mutable [`Database`] that will cache reads from the underlying - /// database. - pub const fn as_db_mut(&mut self, db: DB) -> CachedReadsDbMut<'_, DB> { - CachedReadsDbMut { cached: self, db } + let res = self.state_provider.basic_account(address)?; + self.caches.account_cache.insert(*address, res); + Ok(res) } +} + +/// Represents the status of a storage slot in the cache. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SlotStatus { + /// The account's storage cache doesn't exist. + NotCached, + /// The storage slot exists in cache and is empty (value is zero). + Empty, + /// The storage slot exists in cache and has a specific non-zero value. + Value(StorageValue), +} - /// Inserts an account info into the cache. - pub fn insert_account( +impl StateProvider for CachedStateProvider { + fn storage( &self, - address: Address, - info: AccountInfo, - storage: HashMap, - ) { - self.accounts.insert(address, CachedAccount { - info: Some(info), - storage, - }); + account: Address, + storage_key: StorageKey, + ) -> ProviderResult> { + match self.caches.get_storage(&account, &storage_key) { + SlotStatus::NotCached => { + let final_res = self.state_provider.storage(account, storage_key)?; + self.caches.insert_storage(account, storage_key, final_res); + Ok(final_res) + } + SlotStatus::Empty => { + Ok(None) + } + SlotStatus::Value(value) => { + Ok(Some(value)) + } + } } +} - /// Extends current cache with entries from another [`CachedReads`] instance. - /// - /// Note: It is expected that both instances are based on the exact same - /// state. - pub fn extend(&self, other: Self) { - for (k, v) in other.accounts.into_iter() { - self.accounts.insert(k, v); - } - for (k, v) in other.contracts.into_iter() { - self.contracts.insert(k, v); - } - for (k, v) in other.block_hashes.into_iter() { - self.block_hashes.insert(k, v); +impl BytecodeReader for CachedStateProvider { + fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult> { + if let Some(res) = self.caches.code_cache.get(code_hash).as_deref() { + return Ok(res.clone()) } + + let final_res = self.state_provider.bytecode_by_hash(code_hash)?; + self.caches.code_cache.insert(*code_hash, final_res.clone()); + Ok(final_res) } } -/// A [Database] that caches reads inside [`CachedReads`]. -#[derive(Debug)] -pub struct CachedReadsDbMut<'a, DB> { - /// The cache of reads. - pub cached: &'a CachedReads, - /// The underlying database. - pub db: DB, -} +impl StateRootProvider for CachedStateProvider { + fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult { + self.state_provider.state_root(hashed_state) + } -impl<'a, DB> CachedReadsDbMut<'a, DB> { - /// Converts this [`Database`] implementation into a [`DatabaseRef`] that will - /// still cache reads. - pub const fn into_db(self) -> CachedReadsDBRef<'a, DB> { - CachedReadsDBRef { - inner: RefCell::new(self), - } + fn state_root_from_nodes(&self, input: TrieInput) -> ProviderResult { + self.state_provider.state_root_from_nodes(input) } - /// Returns access to wrapped [`DatabaseRef`]. - pub const fn inner(&self) -> &DB { - &self.db + fn state_root_with_updates( + &self, + hashed_state: HashedPostState, + ) -> ProviderResult<(B256, TrieUpdates)> { + self.state_provider.state_root_with_updates(hashed_state) } -} -impl AsRef for CachedReadsDbMut<'_, DB> -where - DB: AsRef, -{ - fn as_ref(&self) -> &T { - self.inner().as_ref() + fn state_root_from_nodes_with_updates( + &self, + input: TrieInput, + ) -> ProviderResult<(B256, TrieUpdates)> { + self.state_provider.state_root_from_nodes_with_updates(input) } } -impl Database for CachedReadsDbMut<'_, DB> { - type Error = ::Error; +impl StateProofProvider for CachedStateProvider { + fn proof( + &self, + input: TrieInput, + address: Address, + slots: &[B256], + ) -> ProviderResult { + self.state_provider.proof(input, address, slots) + } + + fn multiproof( + &self, + input: TrieInput, + targets: MultiProofTargets, + ) -> ProviderResult { + self.state_provider.multiproof(input, targets) + } + + fn witness( + &self, + input: TrieInput, + target: HashedPostState, + ) -> ProviderResult> { + self.state_provider.witness(input, target) + } +} - fn basic( - &mut self, +impl StorageRootProvider for CachedStateProvider { + fn storage_root( + &self, address: Address, - ) -> Result, Self::Error> { - let basic = match self.cached.accounts.entry(address) { - Entry::Occupied(entry) => entry.get().info.clone(), - Entry::Vacant(entry) => entry - .insert(CachedAccount::new(self.db.basic_ref(address)?)) - .info - .clone(), - }; - Ok(basic) + hashed_storage: HashedStorage, + ) -> ProviderResult { + self.state_provider.storage_root(address, hashed_storage) } - fn code_by_hash(&mut self, code_hash: B256) -> Result { - let code = match self.cached.contracts.entry(code_hash) { - Entry::Occupied(entry) => entry.get().clone(), - Entry::Vacant(entry) => { - entry.insert(self.db.code_by_hash_ref(code_hash)?).clone() - } - }; - Ok(code) + fn storage_proof( + &self, + address: Address, + slot: B256, + hashed_storage: HashedStorage, + ) -> ProviderResult { + self.state_provider.storage_proof(address, slot, hashed_storage) } - fn storage( - &mut self, + /// Generate a storage multiproof for multiple storage slots. + /// + /// A **storage multiproof** is a cryptographic proof that can verify the values + /// of multiple storage slots for a single account in a single verification step. + /// Instead of generating separate proofs for each slot (which would be inefficient), + /// a multiproof bundles the necessary trie nodes to prove all requested slots. + /// + /// ## How it works: + /// 1. Takes an account address and a list of storage slot keys + /// 2. Traverses the account's storage trie to collect proof nodes + /// 3. Returns a [`StorageMultiProof`] containing the minimal set of trie nodes needed to verify + /// all the requested storage slots + fn storage_multiproof( + &self, address: Address, - index: U256, - ) -> Result { - match self.cached.accounts.entry(address) { - Entry::Occupied(mut acc_entry) => { - match acc_entry.get_mut().storage.entry(index) { - std::collections::hash_map::Entry::Occupied(entry) => { - Ok(*entry.get()) - } - std::collections::hash_map::Entry::Vacant(entry) => { - Ok(*entry.insert(self.db.storage_ref(address, index)?)) - } - } - } - Entry::Vacant(acc_entry) => { - // acc needs to be loaded for us to access slots. - let info = self.db.basic_ref(address)?; - let (account, value) = if info.is_some() { - let value = self.db.storage_ref(address, index)?; - let mut account = CachedAccount::new(info); - account.storage.insert(index, value); - (account, value) - } else { - (CachedAccount::new(info), U256::ZERO) - }; - acc_entry.insert(account); - Ok(value) - } - } + slots: &[B256], + hashed_storage: HashedStorage, + ) -> ProviderResult { + self.state_provider.storage_multiproof(address, slots, hashed_storage) + } +} + +impl BlockHashReader for CachedStateProvider { + fn block_hash(&self, number: crate::alloy::primitives::BlockNumber) -> ProviderResult> { + self.state_provider.block_hash(number) } - fn block_hash(&mut self, number: u64) -> Result { - let hash = match self.cached.block_hashes.entry(number) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => *entry.insert(self.db.block_hash_ref(number)?), - }; - Ok(hash) + fn canonical_hashes_range( + &self, + start: crate::alloy::primitives::BlockNumber, + end: crate::alloy::primitives::BlockNumber, + ) -> ProviderResult> { + self.state_provider.canonical_hashes_range(start, end) } } -/// A [`DatabaseRef`] that caches reads inside [`CachedReads`]. +impl HashedPostStateProvider for CachedStateProvider { + fn hashed_post_state(&self, bundle_state: &BundleState) -> HashedPostState { + self.state_provider.hashed_post_state(bundle_state) + } +} + +/// Execution cache used during block processing. /// -/// This is intended to be used as the [`DatabaseRef`] for -/// `revm::db::State` for repeated payload build jobs. -#[derive(Debug)] -pub struct CachedReadsDBRef<'a, DB> { - /// The inner cache reads db mut. - pub inner: RefCell>, +/// Optimizes state access by maintaining in-memory copies of frequently accessed +/// accounts, storage slots, and bytecode. Works in conjunction with prewarming +/// to reduce database I/O during block execution. +#[derive(Debug, Clone, Default)] +pub struct ExecutionCache { + /// Cache for contract bytecode, keyed by code hash. + code_cache: DashMap>, + + /// Per-account storage cache: outer cache keyed by Address, inner cache tracks that account’s + /// storage slots. + storage_cache: DashMap>, + + /// Cache for basic account information (nonce, balance, code hash). + account_cache: DashMap>, } -impl DatabaseRef for CachedReadsDBRef<'_, DB> { - type Error = ::Error; +impl ExecutionCache { + /// Get storage value from hierarchical cache. + /// + /// Returns a `SlotStatus` indicating whether: + /// - `NotCached`: The account's storage cache doesn't exist + /// - `Empty`: The slot exists in the account's cache but is empty + /// - `Value`: The slot exists and has a specific value + pub fn get_storage(&self, address: &Address, key: &StorageKey) -> SlotStatus { + match self.storage_cache.get(address) { + None => SlotStatus::NotCached, + Some(account_cache) => account_cache.get_storage(key), + } + } - fn basic_ref( + /// Insert storage value into hierarchical cache + pub fn insert_storage( &self, address: Address, - ) -> Result, Self::Error> { - self.inner.borrow_mut().basic(address) + key: StorageKey, + value: Option, + ) { + self.insert_storage_bulk(address, [(key, value)]); + } + + /// Insert multiple storage values into hierarchical cache for a single account + /// + /// This method is optimized for inserting multiple storage values for the same address + /// by doing the account cache lookup only once instead of for each key-value pair. + pub fn insert_storage_bulk(&self, address: Address, storage_entries: I) + where + I: IntoIterator)>, + { + let account_cache = self.storage_cache.entry(address).or_insert_with(Default::default); + + for (key, value) in storage_entries { + account_cache.slots.insert(key, value); + } } - fn code_by_hash_ref(&self, code_hash: B256) -> Result { - self.inner.borrow_mut().code_by_hash(code_hash) + /// Invalidate storage for specific account + pub fn invalidate_account_storage(&self, address: &Address) { + self.storage_cache.remove(address); } - fn storage_ref( - &self, - address: Address, - index: U256, - ) -> Result { - self.inner.borrow_mut().storage(address, index) + /// Returns the total number of storage slots cached across all accounts + pub fn total_storage_slots(&self) -> usize { + self.storage_cache.iter().map(|addr| addr.len()).sum() } - fn block_hash_ref(&self, number: u64) -> Result { - self.inner.borrow_mut().block_hash(number) + /// Inserts the post-execution state changes into the cache. + /// + /// This method is called after transaction execution to update the cache with + /// the touched and modified state. The insertion order is critical: + /// + /// 1. Bytecodes: Insert contract code first + /// 2. Storage slots: Update storage values for each account + /// 3. Accounts: Update account info (nonce, balance, code hash) + /// + /// ## Why This Order Matters + /// + /// Account information references bytecode via code hash. If we update accounts + /// before bytecode, we might create cache entries pointing to non-existent code. + /// The current order ensures cache consistency. + /// + /// ## Error Handling + /// + /// Returns an error if the state updates are inconsistent and should be discarded. + pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> { + // Insert bytecodes + for (code_hash, bytecode) in &state_updates.contracts { + self.code_cache.insert(*code_hash, Some(Bytecode(bytecode.clone()))); + } + + for (addr, account) in &state_updates.state { + // If the account was not modified, as in not changed and not destroyed, then we have + // nothing to do w.r.t. this particular account and can move on + if account.status.is_not_modified() { + continue + } + + // If the account was destroyed, invalidate from the account / storage caches + if account.was_destroyed() { + // Invalidate the account cache entry if destroyed + self.account_cache.remove(addr); + + self.invalidate_account_storage(addr); + continue + } + + // If we have an account that was modified, but it has a `None` account info, some wild + // error has occurred because this state should be unrepresentable. An account with + // `None` current info, should be destroyed. + let Some(ref account_info) = account.info else { + tracing::trace!(?account, "Account with None account info found in state updates"); + return Err(()) + }; + + // Now we iterate over all storage and make updates to the cached storage values + // Use bulk insertion to optimize cache lookups - only lookup the account cache once + // instead of for each storage key + let storage_entries = account.storage.iter().map(|(storage_key, slot)| { + // We convert the storage key from U256 to B256 because that is how it's represented + // in the cache + ((*storage_key).into(), Some(slot.present_value)) + }); + self.insert_storage_bulk(*addr, storage_entries); + + // Insert will update if present, so we just use the new account info as the new value + // for the account cache + self.account_cache.insert(*addr, Some(Account::from(account_info))); + } + + Ok(()) } } -/// Cached account contains the account state with storage -/// but lacks the account status. +/// Cache for an individual account's storage slots. +/// +/// This represents the second level of the hierarchical storage cache. +/// Each account gets its own `AccountStorageCache` to store accessed storage slots. #[derive(Debug, Clone)] -pub struct CachedAccount { - /// Account state. - pub info: Option, - /// Account's storage. - pub storage: HashMap, +pub struct AccountStorageCache { + /// Map of storage keys to their cached values. + slots: DashMap>, } -impl CachedAccount { - fn new(info: Option) -> Self { +impl AccountStorageCache { + /// Create a new [`AccountStorageCache`] + pub(crate) fn new() -> Self { Self { - info, - storage: HashMap::default(), + slots: DashMap::new(), + } + } + + /// Get a storage value from this account's cache. + /// - `NotCached`: The slot is not in the cache + /// - `Empty`: The slot is empty + /// - `Value`: The slot has a specific value + pub(crate) fn get_storage(&self, key: &StorageKey) -> SlotStatus { + match self.slots.get(key).as_deref() { + None => SlotStatus::NotCached, + Some(None) => SlotStatus::Empty, + Some(Some(value)) => SlotStatus::Value(*value), } } + + /// Insert a storage value + pub(crate) fn insert_storage(&self, key: StorageKey, value: Option) { + self.slots.insert(key, value); + } + + /// Returns the number of slots in the cache + pub(crate) fn len(&self) -> usize { + self.slots.len() + } +} + +impl Default for AccountStorageCache { + fn default() -> Self { + Self::new() + } } #[cfg(test)] mod tests { use super::*; + use crate::alloy::primitives::{B256, U256}; + use rand::Rng; + use crate::reth::providers::test_utils::{ExtendedAccount, MockEthProvider}; + use std::mem::size_of; - #[test] - fn test_extend_with_two_cached_reads() { - // Setup test data - let hash1 = B256::from_slice(&[1u8; 32]); - let hash2 = B256::from_slice(&[2u8; 32]); - let address1 = Address::from_slice(&[1u8; 20]); - let address2 = Address::from_slice(&[2u8; 20]); - - // Create primary cache - let primary = { - let cache = CachedReads::default(); - cache - .accounts - .insert(address1, CachedAccount::new(Some(AccountInfo::default()))); - cache.contracts.insert(hash1, Bytecode::default()); - cache.block_hashes.insert(1, hash1); - cache + mod tracking_allocator { + use std::{ + alloc::{GlobalAlloc, Layout, System}, + sync::atomic::{AtomicUsize, Ordering}, }; - // Create additional cache - let additional = { - let cache = CachedReads::default(); - cache - .accounts - .insert(address2, CachedAccount::new(Some(AccountInfo::default()))); - cache.contracts.insert(hash2, Bytecode::default()); - cache.block_hashes.insert(2, hash2); - cache - }; + #[derive(Debug)] + pub(crate) struct TrackingAllocator { + allocated: AtomicUsize, + total_allocated: AtomicUsize, + inner: System, + } + + impl TrackingAllocator { + pub(crate) const fn new() -> Self { + Self { + allocated: AtomicUsize::new(0), + total_allocated: AtomicUsize::new(0), + inner: System, + } + } + + pub(crate) fn reset(&self) { + self.allocated.store(0, Ordering::SeqCst); + self.total_allocated.store(0, Ordering::SeqCst); + } + + pub(crate) fn total_allocated(&self) -> usize { + self.total_allocated.load(Ordering::SeqCst) + } + } + + unsafe impl GlobalAlloc for TrackingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let ret = unsafe { self.inner.alloc(layout) }; + if !ret.is_null() { + self.allocated.fetch_add(layout.size(), Ordering::SeqCst); + self.total_allocated.fetch_add(layout.size(), Ordering::SeqCst); + } + ret + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + self.allocated.fetch_sub(layout.size(), Ordering::SeqCst); + unsafe { self.inner.dealloc(ptr, layout) } + } + } + } + + use tracking_allocator::TrackingAllocator; + + #[global_allocator] + static ALLOCATOR: TrackingAllocator = TrackingAllocator::new(); + + fn measure_allocation(f: F) -> (usize, T) + where + F: FnOnce() -> T, + { + ALLOCATOR.reset(); + let result = f(); + let total = ALLOCATOR.total_allocated(); + (total, result) + } + + #[test] + fn measure_storage_cache_overhead() { + let (base_overhead, cache) = measure_allocation(|| AccountStorageCache::new()); + println!("Base AccountStorageCache overhead: {base_overhead} bytes"); + let mut rng = rand::rng(); + + let key = StorageKey::random(); + let value = StorageValue::from(rng.random::()); + let (first_slot, _) = measure_allocation(|| { + cache.insert_storage(key, Some(value)); + }); + println!("First slot insertion overhead: {first_slot} bytes"); + + const TOTAL_SLOTS: usize = 10_000; + let (test_slots, _) = measure_allocation(|| { + for _ in 0..TOTAL_SLOTS { + let key = StorageKey::random(); + let value = StorageValue::from(rng.random::()); + cache.insert_storage(key, Some(value)); + } + }); + println!("Average overhead over {} slots: {} bytes", TOTAL_SLOTS, test_slots / TOTAL_SLOTS); + + println!("\nTheoretical sizes:"); + println!("StorageKey size: {} bytes", size_of::()); + println!("StorageValue size: {} bytes", size_of::()); + println!("Option size: {} bytes", size_of::>()); + println!("Option size: {} bytes", size_of::>()); + } + + #[test] + fn test_empty_storage_cached_state_provider() { + // make sure when we have an empty value in storage, we return `Empty` and not `NotCached` + let address = Address::random(); + let storage_key = StorageKey::random(); + let account = ExtendedAccount::new(0, U256::ZERO); + + // note there is no storage here + let provider = MockEthProvider::default(); + provider.extend_accounts(vec![(address, account)]); + + let caches = ExecutionCache::default(); + let state_provider = + CachedStateProvider::new_with_caches(provider, caches); + + // check that the storage is empty + let res = state_provider.storage(address, storage_key); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), None); + } + + #[test] + fn test_uncached_storage_cached_state_provider() { + // make sure when we have something uncached, we get the cached value + let address = Address::random(); + let storage_key = StorageKey::random(); + let storage_value = U256::from(1); + let account = + ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]); + + // note that we extend storage here with one value + let provider = MockEthProvider::default(); + provider.extend_accounts(vec![(address, account)]); + + let caches = ExecutionCache::default(); + let state_provider = + CachedStateProvider::new_with_caches(provider, caches); + + // check that the storage returns the expected value + let res = state_provider.storage(address, storage_key); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), Some(storage_value)); + } + + #[test] + fn test_get_storage_populated() { + // make sure when we have something cached, we get the cached value in the `SlotStatus` + let address = Address::random(); + let storage_key = StorageKey::random(); + let storage_value = U256::from(1); + + // insert into caches directly + let caches = ExecutionCache::default(); + caches.insert_storage(address, storage_key, Some(storage_value)); + + // check that the storage returns the cached value + let slot_status = caches.get_storage(&address, &storage_key); + assert_eq!(slot_status, SlotStatus::Value(storage_value)); + } + + #[test] + fn test_get_storage_not_cached() { + // make sure when we have nothing cached, we get the `NotCached` value in the `SlotStatus` + let storage_key = StorageKey::random(); + let address = Address::random(); + + // just create empty caches + let caches = ExecutionCache::default(); + + // check that the storage is not cached + let slot_status = caches.get_storage(&address, &storage_key); + assert_eq!(slot_status, SlotStatus::NotCached); + } + + #[test] + fn test_get_storage_empty() { + // make sure when we insert an empty value to the cache, we get the `Empty` value in the + // `SlotStatus` + let address = Address::random(); + let storage_key = StorageKey::random(); + + // insert into caches directly + let caches = ExecutionCache::default(); + caches.insert_storage(address, storage_key, None); - // Extending primary with additional cache - primary.extend(additional); - - // Verify the combined state - assert!( - primary.accounts.len() == 2 - && primary.contracts.len() == 2 - && primary.block_hashes.len() == 2, - "All maps should contain 2 entries" - ); - - // Verify specific entries - assert!( - primary.accounts.contains_key(&address1) - && primary.accounts.contains_key(&address2) - && primary.contracts.contains_key(&hash1) - && primary.contracts.contains_key(&hash2) - && primary.block_hashes.get(&1).as_deref() == Some(&hash1) - && primary.block_hashes.get(&2).as_deref() == Some(&hash2), - "All expected entries should be present" - ); + // check that the storage is empty + let slot_status = caches.get_storage(&address, &storage_key); + assert_eq!(slot_status, SlotStatus::Empty); } } diff --git a/src/payload/ext/mod.rs b/src/payload/ext/mod.rs index feebff8..66ef7ac 100644 --- a/src/payload/ext/mod.rs +++ b/src/payload/ext/mod.rs @@ -10,7 +10,7 @@ mod cached_state; mod checkpoint; mod span; -pub use {block::BlockExt, checkpoint::CheckpointExt, span::SpanExt}; +pub use {block::BlockExt, checkpoint::CheckpointExt, span::SpanExt, cached_state::{CachedStateProvider, ExecutionCache}}; mod sealed { /// This pattern is used to prevent external implementations of the extension diff --git a/src/payload/mod.rs b/src/payload/mod.rs index 48f226f..69f4bc4 100644 --- a/src/payload/mod.rs +++ b/src/payload/mod.rs @@ -12,6 +12,7 @@ pub use { block::{BlockContext, Error as BlockError}, checkpoint::{Checkpoint, Error as CheckpointError}, exec::{Executable, ExecutionError, ExecutionResult, IntoExecutable}, - ext::{BlockExt, CheckpointExt, SpanExt}, + ext::{BlockExt, CheckpointExt, SpanExt, CachedStateProvider, ExecutionCache}, span::{Error as SpanError, Span}, + }; From 11ddd41a9f48ccaaa6511e83b20809595e4eb38c Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:11:31 +0400 Subject: [PATCH 03/12] fmt --- src/payload/block.rs | 6 +- src/payload/ext/cached_state.rs | 265 ++++++++++++++++++++------------ src/payload/ext/mod.rs | 7 +- src/payload/mod.rs | 9 +- 4 files changed, 187 insertions(+), 100 deletions(-) diff --git a/src/payload/block.rs b/src/payload/block.rs index 7bde566..fc5b5a7 100644 --- a/src/payload/block.rs +++ b/src/payload/block.rs @@ -83,7 +83,8 @@ impl BlockContext

{ // TODO: Prefill execution_cached with bundle state from the previous block let execution_cached = ExecutionCache::default(); - let provider = CachedStateProvider::new_with_caches(base_state, execution_cached); + let provider = + CachedStateProvider::new_with_caches(base_state, execution_cached); let mut base_state = State::builder() .with_database(StateProviderDatabase(provider)) .with_bundle_update() @@ -205,7 +206,8 @@ struct BlockContextInner { /// /// This state has no changes made to it during the payload building process /// through any of the created checkpoints. - base_state: State>>, + base_state: + State>>, /// The EVM factory configured for the environment in which we are building /// the payload. This type is used to create individual EVM instances that diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 65a4e7b..d38525d 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -1,20 +1,41 @@ //! Database adapters for payload building. // Originated from reth https://github.com/paradigmxyz/reth/blob/b72bb6790a5f7ada75282e52b80f986d9717e698/crates/revm/src/cached.rs -use reth_ethereum::evm::revm::db::BundleState; -use reth_ethereum::primitives::{Account, Bytecode}; -use reth_ethereum::trie::{AccountProof, HashedPostState, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, StorageProof, TrieInput}; -use reth_ethereum::trie::updates::TrieUpdates; use { crate::{ - alloy::primitives::{Address, B256}, - reth::ethereum::provider::{AccountReader, BlockHashReader, BytecodeReader, HashedPostStateProvider, StateProofProvider, StateRootProvider, StorageRootProvider, StateProvider}, - reth::errors::ProviderResult, + alloy::primitives::{Address, B256, StorageKey, StorageValue}, + reth::{ + errors::ProviderResult, + ethereum::provider::{ + AccountReader, + BlockHashReader, + BytecodeReader, + HashedPostStateProvider, + StateProofProvider, + StateProvider, + StateRootProvider, + StorageRootProvider, + }, + }, }, dashmap::DashMap, + reth_ethereum::{ + evm::revm::db::BundleState, + primitives::{Account, Bytecode}, + trie::{ + AccountProof, + HashedPostState, + HashedStorage, + MultiProof, + MultiProofTargets, + StorageMultiProof, + StorageProof, + TrieInput, + updates::TrieUpdates, + }, + }, std::sync::Arc, }; -use crate::alloy::primitives::{StorageKey, StorageValue}; /// A wrapper of a state provider and a shared cache. pub struct CachedStateProvider { @@ -29,20 +50,26 @@ impl CachedStateProvider where S: StateProvider, { - /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and - /// [`CachedStateMetrics`]. + /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state + /// provider, and [`CachedStateMetrics`]. pub const fn new_with_caches( state_provider: S, caches: ExecutionCache, ) -> Self { - Self { state_provider, caches } + Self { + state_provider, + caches, + } } } impl AccountReader for CachedStateProvider { - fn basic_account(&self, address: &Address) -> ProviderResult> { + fn basic_account( + &self, + address: &Address, + ) -> ProviderResult> { if let Some(res) = self.caches.account_cache.get(address) { - return Ok(*res) + return Ok(*res); } let res = self.state_provider.basic_account(address)?; @@ -74,20 +101,19 @@ impl StateProvider for CachedStateProvider { self.caches.insert_storage(account, storage_key, final_res); Ok(final_res) } - SlotStatus::Empty => { - Ok(None) - } - SlotStatus::Value(value) => { - Ok(Some(value)) - } + SlotStatus::Empty => Ok(None), + SlotStatus::Value(value) => Ok(Some(value)), } } } impl BytecodeReader for CachedStateProvider { - fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult> { + fn bytecode_by_hash( + &self, + code_hash: &B256, + ) -> ProviderResult> { if let Some(res) = self.caches.code_cache.get(code_hash).as_deref() { - return Ok(res.clone()) + return Ok(res.clone()); } let final_res = self.state_provider.bytecode_by_hash(code_hash)?; @@ -116,7 +142,9 @@ impl StateRootProvider for CachedStateProvider { &self, input: TrieInput, ) -> ProviderResult<(B256, TrieUpdates)> { - self.state_provider.state_root_from_nodes_with_updates(input) + self + .state_provider + .state_root_from_nodes_with_updates(input) } } @@ -162,33 +190,41 @@ impl StorageRootProvider for CachedStateProvider { slot: B256, hashed_storage: HashedStorage, ) -> ProviderResult { - self.state_provider.storage_proof(address, slot, hashed_storage) + self + .state_provider + .storage_proof(address, slot, hashed_storage) } /// Generate a storage multiproof for multiple storage slots. /// - /// A **storage multiproof** is a cryptographic proof that can verify the values - /// of multiple storage slots for a single account in a single verification step. - /// Instead of generating separate proofs for each slot (which would be inefficient), - /// a multiproof bundles the necessary trie nodes to prove all requested slots. + /// A **storage multiproof** is a cryptographic proof that can verify the + /// values of multiple storage slots for a single account in a single + /// verification step. Instead of generating separate proofs for each slot + /// (which would be inefficient), a multiproof bundles the necessary trie + /// nodes to prove all requested slots. /// /// ## How it works: /// 1. Takes an account address and a list of storage slot keys /// 2. Traverses the account's storage trie to collect proof nodes - /// 3. Returns a [`StorageMultiProof`] containing the minimal set of trie nodes needed to verify - /// all the requested storage slots + /// 3. Returns a [`StorageMultiProof`] containing the minimal set of trie + /// nodes needed to verify all the requested storage slots fn storage_multiproof( &self, address: Address, slots: &[B256], hashed_storage: HashedStorage, ) -> ProviderResult { - self.state_provider.storage_multiproof(address, slots, hashed_storage) + self + .state_provider + .storage_multiproof(address, slots, hashed_storage) } } impl BlockHashReader for CachedStateProvider { - fn block_hash(&self, number: crate::alloy::primitives::BlockNumber) -> ProviderResult> { + fn block_hash( + &self, + number: crate::alloy::primitives::BlockNumber, + ) -> ProviderResult> { self.state_provider.block_hash(number) } @@ -201,7 +237,9 @@ impl BlockHashReader for CachedStateProvider { } } -impl HashedPostStateProvider for CachedStateProvider { +impl HashedPostStateProvider + for CachedStateProvider +{ fn hashed_post_state(&self, bundle_state: &BundleState) -> HashedPostState { self.state_provider.hashed_post_state(bundle_state) } @@ -209,16 +247,16 @@ impl HashedPostStateProvider for CachedStateProvider /// Execution cache used during block processing. /// -/// Optimizes state access by maintaining in-memory copies of frequently accessed -/// accounts, storage slots, and bytecode. Works in conjunction with prewarming -/// to reduce database I/O during block execution. +/// Optimizes state access by maintaining in-memory copies of frequently +/// accessed accounts, storage slots, and bytecode. Works in conjunction with +/// prewarming to reduce database I/O during block execution. #[derive(Debug, Clone, Default)] pub struct ExecutionCache { /// Cache for contract bytecode, keyed by code hash. code_cache: DashMap>, - /// Per-account storage cache: outer cache keyed by Address, inner cache tracks that account’s - /// storage slots. + /// Per-account storage cache: outer cache keyed by Address, inner cache + /// tracks that account’s storage slots. storage_cache: DashMap>, /// Cache for basic account information (nonce, balance, code hash). @@ -232,7 +270,7 @@ impl ExecutionCache { /// - `NotCached`: The account's storage cache doesn't exist /// - `Empty`: The slot exists in the account's cache but is empty /// - `Value`: The slot exists and has a specific value - pub fn get_storage(&self, address: &Address, key: &StorageKey) -> SlotStatus { + pub fn get_storage(&self, address: &Address, key: &StorageKey) -> SlotStatus { match self.storage_cache.get(address) { None => SlotStatus::NotCached, Some(account_cache) => account_cache.get_storage(key), @@ -240,7 +278,7 @@ impl ExecutionCache { } /// Insert storage value into hierarchical cache - pub fn insert_storage( + pub fn insert_storage( &self, address: Address, key: StorageKey, @@ -249,15 +287,20 @@ impl ExecutionCache { self.insert_storage_bulk(address, [(key, value)]); } - /// Insert multiple storage values into hierarchical cache for a single account + /// Insert multiple storage values into hierarchical cache for a single + /// account /// - /// This method is optimized for inserting multiple storage values for the same address - /// by doing the account cache lookup only once instead of for each key-value pair. - pub fn insert_storage_bulk(&self, address: Address, storage_entries: I) + /// This method is optimized for inserting multiple storage values for the + /// same address by doing the account cache lookup only once instead of for + /// each key-value pair. + pub fn insert_storage_bulk(&self, address: Address, storage_entries: I) where I: IntoIterator)>, { - let account_cache = self.storage_cache.entry(address).or_insert_with(Default::default); + let account_cache = self + .storage_cache + .entry(address) + .or_insert_with(Default::default); for (key, value) in storage_entries { account_cache.slots.insert(key, value); @@ -265,12 +308,12 @@ impl ExecutionCache { } /// Invalidate storage for specific account - pub fn invalidate_account_storage(&self, address: &Address) { + pub fn invalidate_account_storage(&self, address: &Address) { self.storage_cache.remove(address); } /// Returns the total number of storage slots cached across all accounts - pub fn total_storage_slots(&self) -> usize { + pub fn total_storage_slots(&self) -> usize { self.storage_cache.iter().map(|addr| addr.len()).sum() } @@ -285,56 +328,68 @@ impl ExecutionCache { /// /// ## Why This Order Matters /// - /// Account information references bytecode via code hash. If we update accounts - /// before bytecode, we might create cache entries pointing to non-existent code. - /// The current order ensures cache consistency. + /// Account information references bytecode via code hash. If we update + /// accounts before bytecode, we might create cache entries pointing to + /// non-existent code. The current order ensures cache consistency. /// /// ## Error Handling /// - /// Returns an error if the state updates are inconsistent and should be discarded. - pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> { + /// Returns an error if the state updates are inconsistent and should be + /// discarded. + pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> { // Insert bytecodes for (code_hash, bytecode) in &state_updates.contracts { - self.code_cache.insert(*code_hash, Some(Bytecode(bytecode.clone()))); + self + .code_cache + .insert(*code_hash, Some(Bytecode(bytecode.clone()))); } for (addr, account) in &state_updates.state { - // If the account was not modified, as in not changed and not destroyed, then we have - // nothing to do w.r.t. this particular account and can move on + // If the account was not modified, as in not changed and not destroyed, + // then we have nothing to do w.r.t. this particular account and can + // move on if account.status.is_not_modified() { - continue + continue; } - // If the account was destroyed, invalidate from the account / storage caches + // If the account was destroyed, invalidate from the account / storage + // caches if account.was_destroyed() { // Invalidate the account cache entry if destroyed self.account_cache.remove(addr); self.invalidate_account_storage(addr); - continue + continue; } - // If we have an account that was modified, but it has a `None` account info, some wild - // error has occurred because this state should be unrepresentable. An account with - // `None` current info, should be destroyed. + // If we have an account that was modified, but it has a `None` account + // info, some wild error has occurred because this state should be + // unrepresentable. An account with `None` current info, should be + // destroyed. let Some(ref account_info) = account.info else { - tracing::trace!(?account, "Account with None account info found in state updates"); - return Err(()) + tracing::trace!( + ?account, + "Account with None account info found in state updates" + ); + return Err(()); }; - // Now we iterate over all storage and make updates to the cached storage values - // Use bulk insertion to optimize cache lookups - only lookup the account cache once - // instead of for each storage key - let storage_entries = account.storage.iter().map(|(storage_key, slot)| { - // We convert the storage key from U256 to B256 because that is how it's represented - // in the cache - ((*storage_key).into(), Some(slot.present_value)) - }); + // Now we iterate over all storage and make updates to the cached storage + // values Use bulk insertion to optimize cache lookups - only lookup + // the account cache once instead of for each storage key + let storage_entries = + account.storage.iter().map(|(storage_key, slot)| { + // We convert the storage key from U256 to B256 because that is how + // it's represented in the cache + ((*storage_key).into(), Some(slot.present_value)) + }); self.insert_storage_bulk(*addr, storage_entries); - // Insert will update if present, so we just use the new account info as the new value - // for the account cache - self.account_cache.insert(*addr, Some(Account::from(account_info))); + // Insert will update if present, so we just use the new account info as + // the new value for the account cache + self + .account_cache + .insert(*addr, Some(Account::from(account_info))); } Ok(()) @@ -344,7 +399,8 @@ impl ExecutionCache { /// Cache for an individual account's storage slots. /// /// This represents the second level of the hierarchical storage cache. -/// Each account gets its own `AccountStorageCache` to store accessed storage slots. +/// Each account gets its own `AccountStorageCache` to store accessed storage +/// slots. #[derive(Debug, Clone)] pub struct AccountStorageCache { /// Map of storage keys to their cached values. @@ -372,7 +428,11 @@ impl AccountStorageCache { } /// Insert a storage value - pub(crate) fn insert_storage(&self, key: StorageKey, value: Option) { + pub(crate) fn insert_storage( + &self, + key: StorageKey, + value: Option, + ) { self.slots.insert(key, value); } @@ -390,11 +450,15 @@ impl Default for AccountStorageCache { #[cfg(test)] mod tests { - use super::*; - use crate::alloy::primitives::{B256, U256}; - use rand::Rng; - use crate::reth::providers::test_utils::{ExtendedAccount, MockEthProvider}; - use std::mem::size_of; + use { + super::*, + crate::{ + alloy::primitives::{B256, U256}, + reth::providers::test_utils::{ExtendedAccount, MockEthProvider}, + }, + rand::Rng, + std::mem::size_of, + }; mod tracking_allocator { use std::{ @@ -433,7 +497,9 @@ mod tests { let ret = unsafe { self.inner.alloc(layout) }; if !ret.is_null() { self.allocated.fetch_add(layout.size(), Ordering::SeqCst); - self.total_allocated.fetch_add(layout.size(), Ordering::SeqCst); + self + .total_allocated + .fetch_add(layout.size(), Ordering::SeqCst); } ret } @@ -462,7 +528,8 @@ mod tests { #[test] fn measure_storage_cache_overhead() { - let (base_overhead, cache) = measure_allocation(|| AccountStorageCache::new()); + let (base_overhead, cache) = + measure_allocation(|| AccountStorageCache::new()); println!("Base AccountStorageCache overhead: {base_overhead} bytes"); let mut rng = rand::rng(); @@ -481,18 +548,26 @@ mod tests { cache.insert_storage(key, Some(value)); } }); - println!("Average overhead over {} slots: {} bytes", TOTAL_SLOTS, test_slots / TOTAL_SLOTS); + println!( + "Average overhead over {} slots: {} bytes", + TOTAL_SLOTS, + test_slots / TOTAL_SLOTS + ); println!("\nTheoretical sizes:"); println!("StorageKey size: {} bytes", size_of::()); println!("StorageValue size: {} bytes", size_of::()); - println!("Option size: {} bytes", size_of::>()); + println!( + "Option size: {} bytes", + size_of::>() + ); println!("Option size: {} bytes", size_of::>()); } #[test] fn test_empty_storage_cached_state_provider() { - // make sure when we have an empty value in storage, we return `Empty` and not `NotCached` + // make sure when we have an empty value in storage, we return `Empty` and + // not `NotCached` let address = Address::random(); let storage_key = StorageKey::random(); let account = ExtendedAccount::new(0, U256::ZERO); @@ -502,8 +577,7 @@ mod tests { provider.extend_accounts(vec![(address, account)]); let caches = ExecutionCache::default(); - let state_provider = - CachedStateProvider::new_with_caches(provider, caches); + let state_provider = CachedStateProvider::new_with_caches(provider, caches); // check that the storage is empty let res = state_provider.storage(address, storage_key); @@ -517,16 +591,15 @@ mod tests { let address = Address::random(); let storage_key = StorageKey::random(); let storage_value = U256::from(1); - let account = - ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]); + let account = ExtendedAccount::new(0, U256::ZERO) + .extend_storage(vec![(storage_key, storage_value)]); // note that we extend storage here with one value let provider = MockEthProvider::default(); provider.extend_accounts(vec![(address, account)]); let caches = ExecutionCache::default(); - let state_provider = - CachedStateProvider::new_with_caches(provider, caches); + let state_provider = CachedStateProvider::new_with_caches(provider, caches); // check that the storage returns the expected value let res = state_provider.storage(address, storage_key); @@ -536,7 +609,8 @@ mod tests { #[test] fn test_get_storage_populated() { - // make sure when we have something cached, we get the cached value in the `SlotStatus` + // make sure when we have something cached, we get the cached value in the + // `SlotStatus` let address = Address::random(); let storage_key = StorageKey::random(); let storage_value = U256::from(1); @@ -552,7 +626,8 @@ mod tests { #[test] fn test_get_storage_not_cached() { - // make sure when we have nothing cached, we get the `NotCached` value in the `SlotStatus` + // make sure when we have nothing cached, we get the `NotCached` value in + // the `SlotStatus` let storage_key = StorageKey::random(); let address = Address::random(); @@ -566,8 +641,8 @@ mod tests { #[test] fn test_get_storage_empty() { - // make sure when we insert an empty value to the cache, we get the `Empty` value in the - // `SlotStatus` + // make sure when we insert an empty value to the cache, we get the `Empty` + // value in the `SlotStatus` let address = Address::random(); let storage_key = StorageKey::random(); diff --git a/src/payload/ext/mod.rs b/src/payload/ext/mod.rs index 66ef7ac..c3e7022 100644 --- a/src/payload/ext/mod.rs +++ b/src/payload/ext/mod.rs @@ -10,7 +10,12 @@ mod cached_state; mod checkpoint; mod span; -pub use {block::BlockExt, checkpoint::CheckpointExt, span::SpanExt, cached_state::{CachedStateProvider, ExecutionCache}}; +pub use { + block::BlockExt, + cached_state::{CachedStateProvider, ExecutionCache}, + checkpoint::CheckpointExt, + span::SpanExt, +}; mod sealed { /// This pattern is used to prevent external implementations of the extension diff --git a/src/payload/mod.rs b/src/payload/mod.rs index 69f4bc4..7600cb4 100644 --- a/src/payload/mod.rs +++ b/src/payload/mod.rs @@ -12,7 +12,12 @@ pub use { block::{BlockContext, Error as BlockError}, checkpoint::{Checkpoint, Error as CheckpointError}, exec::{Executable, ExecutionError, ExecutionResult, IntoExecutable}, - ext::{BlockExt, CheckpointExt, SpanExt, CachedStateProvider, ExecutionCache}, + ext::{ + BlockExt, + CachedStateProvider, + CheckpointExt, + ExecutionCache, + SpanExt, + }, span::{Error as SpanError, Span}, - }; From 4c40ebdd2a7276272b2ab267cc89876de74b1111 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:17:48 +0400 Subject: [PATCH 04/12] clippy --- src/payload/ext/cached_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index d38525d..fe2da2a 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -402,7 +402,7 @@ impl ExecutionCache { /// Each account gets its own `AccountStorageCache` to store accessed storage /// slots. #[derive(Debug, Clone)] -pub struct AccountStorageCache { +pub(super) struct AccountStorageCache { /// Map of storage keys to their cached values. slots: DashMap>, } From fd0d41e737158392586c7e7d936f91df70d10673 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:22:50 +0400 Subject: [PATCH 05/12] clippy --- src/payload/ext/cached_state.rs | 37 ++++++++++++++------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index fe2da2a..447ef2c 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -68,19 +68,19 @@ impl AccountReader for CachedStateProvider { &self, address: &Address, ) -> ProviderResult> { - if let Some(res) = self.caches.account_cache.get(address) { + if let Some(res) = self.caches.account.get(address) { return Ok(*res); } let res = self.state_provider.basic_account(address)?; - self.caches.account_cache.insert(*address, res); + self.caches.account.insert(*address, res); Ok(res) } } /// Represents the status of a storage slot in the cache. #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum SlotStatus { +pub enum SlotStatus { /// The account's storage cache doesn't exist. NotCached, /// The storage slot exists in cache and is empty (value is zero). @@ -112,12 +112,12 @@ impl BytecodeReader for CachedStateProvider { &self, code_hash: &B256, ) -> ProviderResult> { - if let Some(res) = self.caches.code_cache.get(code_hash).as_deref() { + if let Some(res) = self.caches.code.get(code_hash).as_deref() { return Ok(res.clone()); } let final_res = self.state_provider.bytecode_by_hash(code_hash)?; - self.caches.code_cache.insert(*code_hash, final_res.clone()); + self.caches.code.insert(*code_hash, final_res.clone()); Ok(final_res) } } @@ -253,14 +253,14 @@ impl HashedPostStateProvider #[derive(Debug, Clone, Default)] pub struct ExecutionCache { /// Cache for contract bytecode, keyed by code hash. - code_cache: DashMap>, + code: DashMap>, /// Per-account storage cache: outer cache keyed by Address, inner cache /// tracks that account’s storage slots. - storage_cache: DashMap>, + storage: DashMap>, /// Cache for basic account information (nonce, balance, code hash). - account_cache: DashMap>, + account: DashMap>, } impl ExecutionCache { @@ -271,7 +271,7 @@ impl ExecutionCache { /// - `Empty`: The slot exists in the account's cache but is empty /// - `Value`: The slot exists and has a specific value pub fn get_storage(&self, address: &Address, key: &StorageKey) -> SlotStatus { - match self.storage_cache.get(address) { + match self.storage.get(address) { None => SlotStatus::NotCached, Some(account_cache) => account_cache.get_storage(key), } @@ -298,7 +298,7 @@ impl ExecutionCache { I: IntoIterator)>, { let account_cache = self - .storage_cache + .storage .entry(address) .or_insert_with(Default::default); @@ -307,14 +307,9 @@ impl ExecutionCache { } } - /// Invalidate storage for specific account - pub fn invalidate_account_storage(&self, address: &Address) { - self.storage_cache.remove(address); - } - /// Returns the total number of storage slots cached across all accounts pub fn total_storage_slots(&self) -> usize { - self.storage_cache.iter().map(|addr| addr.len()).sum() + self.storage.iter().map(|addr| addr.len()).sum() } /// Inserts the post-execution state changes into the cache. @@ -340,7 +335,7 @@ impl ExecutionCache { // Insert bytecodes for (code_hash, bytecode) in &state_updates.contracts { self - .code_cache + .code .insert(*code_hash, Some(Bytecode(bytecode.clone()))); } @@ -356,9 +351,8 @@ impl ExecutionCache { // caches if account.was_destroyed() { // Invalidate the account cache entry if destroyed - self.account_cache.remove(addr); - - self.invalidate_account_storage(addr); + self.account.remove(addr); + self.storage.remove(addr); continue; } @@ -388,7 +382,7 @@ impl ExecutionCache { // Insert will update if present, so we just use the new account info as // the new value for the account cache self - .account_cache + .account .insert(*addr, Some(Account::from(account_info))); } @@ -428,6 +422,7 @@ impl AccountStorageCache { } /// Insert a storage value + #[expect(dead_code)] pub(crate) fn insert_storage( &self, key: StorageKey, From 8fd8a5c1a0b0d2a9e07719adb19468aaee7f2d6e Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:26:53 +0400 Subject: [PATCH 06/12] remove deadcode --- src/payload/ext/cached_state.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 447ef2c..e116c81 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -422,7 +422,6 @@ impl AccountStorageCache { } /// Insert a storage value - #[expect(dead_code)] pub(crate) fn insert_storage( &self, key: StorageKey, From fbf716b9005be6d327de168c31bd846a9668664c Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:35:41 +0400 Subject: [PATCH 07/12] fix clippy --- src/payload/ext/cached_state.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index e116c81..1d6e6fd 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -278,6 +278,7 @@ impl ExecutionCache { } /// Insert storage value into hierarchical cache + #[expect(dead_code)] pub fn insert_storage( &self, address: Address, @@ -331,6 +332,7 @@ impl ExecutionCache { /// /// Returns an error if the state updates are inconsistent and should be /// discarded. + #[expect(clippy::result_unit_err)] pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> { // Insert bytecodes for (code_hash, bytecode) in &state_updates.contracts { From dc6b54ffd4ed0a9f958d604d6ed9df00d49586f8 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:35:54 +0400 Subject: [PATCH 08/12] fmt --- src/payload/ext/cached_state.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 1d6e6fd..8b511a9 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -298,10 +298,8 @@ impl ExecutionCache { where I: IntoIterator)>, { - let account_cache = self - .storage - .entry(address) - .or_insert_with(Default::default); + let account_cache = + self.storage.entry(address).or_insert_with(Default::default); for (key, value) in storage_entries { account_cache.slots.insert(key, value); From d56a074e9e345f71cf7117ecb1d5124f58dac839 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:44:29 +0400 Subject: [PATCH 09/12] info! --- src/payload/ext/cached_state.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 8b511a9..6c53c41 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -452,6 +452,7 @@ mod tests { }, rand::Rng, std::mem::size_of, + tracing::info, }; mod tracking_allocator { @@ -524,7 +525,7 @@ mod tests { fn measure_storage_cache_overhead() { let (base_overhead, cache) = measure_allocation(|| AccountStorageCache::new()); - println!("Base AccountStorageCache overhead: {base_overhead} bytes"); + info!("Base AccountStorageCache overhead: {base_overhead} bytes"); let mut rng = rand::rng(); let key = StorageKey::random(); @@ -532,7 +533,7 @@ mod tests { let (first_slot, _) = measure_allocation(|| { cache.insert_storage(key, Some(value)); }); - println!("First slot insertion overhead: {first_slot} bytes"); + info!("First slot insertion overhead: {first_slot} bytes"); const TOTAL_SLOTS: usize = 10_000; let (test_slots, _) = measure_allocation(|| { @@ -542,20 +543,20 @@ mod tests { cache.insert_storage(key, Some(value)); } }); - println!( + info!( "Average overhead over {} slots: {} bytes", TOTAL_SLOTS, test_slots / TOTAL_SLOTS ); - println!("\nTheoretical sizes:"); - println!("StorageKey size: {} bytes", size_of::()); - println!("StorageValue size: {} bytes", size_of::()); - println!( + info!("\nTheoretical sizes:"); + info!("StorageKey size: {} bytes", size_of::()); + info!("StorageValue size: {} bytes", size_of::()); + info!( "Option size: {} bytes", size_of::>() ); - println!("Option size: {} bytes", size_of::>()); + info!("Option size: {} bytes", size_of::>()); } #[test] From 67a62cdc397e920198ad34e96f05801cd3f81202 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Thu, 20 Nov 2025 20:45:24 +0400 Subject: [PATCH 10/12] info! --- src/payload/ext/cached_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 6c53c41..18b10a2 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -278,7 +278,6 @@ impl ExecutionCache { } /// Insert storage value into hierarchical cache - #[expect(dead_code)] pub fn insert_storage( &self, address: Address, @@ -422,6 +421,7 @@ impl AccountStorageCache { } /// Insert a storage value + #[expect(dead_code)] pub(crate) fn insert_storage( &self, key: StorageKey, From 0ea2ecbd3cacc9a4ae9d03b33e205a1051da4661 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Fri, 21 Nov 2025 12:20:11 +0400 Subject: [PATCH 11/12] Preload caches --- src/payload/block.rs | 4 +-- src/pipelines/service.rs | 59 +++++++++++++++++++++++++++++++++++-- src/test_utils/exts/mock.rs | 2 ++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/payload/block.rs b/src/payload/block.rs index fc5b5a7..2c5b609 100644 --- a/src/payload/block.rs +++ b/src/payload/block.rs @@ -69,6 +69,7 @@ impl BlockContext

{ attribs: types::PayloadBuilderAttributes

, base_state: StateProviderBox, chainspec: Arc>, + cached: Option, ) -> Result> { let block_env = P::next_block_environment_context::

( &chainspec, @@ -81,8 +82,7 @@ impl BlockContext

{ .next_evm_env(&parent, &block_env) .map_err(Error::EvmEnv)?; - // TODO: Prefill execution_cached with bundle state from the previous block - let execution_cached = ExecutionCache::default(); + let execution_cached = cached.unwrap_or_default(); let provider = CachedStateProvider::new_with_caches(base_state, execution_cached); let mut base_state = State::builder() diff --git a/src/pipelines/service.rs b/src/pipelines/service.rs index fee78bd..7230ef5 100644 --- a/src/pipelines/service.rs +++ b/src/pipelines/service.rs @@ -4,6 +4,7 @@ use { super::{job::PayloadJob, metrics}, crate::{ + alloy::primitives::B256, prelude::*, reth::{ api::PayloadBuilderAttributes, @@ -12,10 +13,12 @@ use { NodeConfig, components::PayloadServiceBuilder, }, + node::builder::NodePrimitives, payload::builder::{PayloadBuilderHandle, PayloadBuilderService, *}, providers::{CanonStateSubscriptions, StateProviderFactory}, }, }, + reth_ethereum::provider::CanonStateNotification, std::sync::Arc, tracing::debug, }; @@ -146,6 +149,7 @@ where { pipeline: Arc>, service: Arc>, + pre_cached: Option, } impl JobGenerator @@ -160,7 +164,24 @@ where let pipeline = Arc::new(pipeline); let service = Arc::new(service); - Self { pipeline, service } + Self { + pipeline, + service, + pre_cached: None, + } + } + + /// Returns the pre-cached reads for the given parent header if it matches the + /// cached state's block. + fn maybe_pre_cached(&self, parent: B256) -> Option { + // TODO: probably possible to remove .clone() here, even tho it shouldn't be + // too heavy + if let Some(cached) = self.pre_cached.clone() + && cached.block == parent + { + return Some(cached.cached); + } + None } } @@ -183,8 +204,8 @@ where PayloadBuilderError::MissingParentHeader(attribs.parent()) })?; - let base_state = - self.service.provider().state_by_block_hash(header.hash())?; + let hash = header.hash(); + let base_state = self.service.provider().state_by_block_hash(hash)?; // This is the beginning of the state manipulation API usage from within // the pipelines API. @@ -193,9 +214,41 @@ where attribs, base_state, self.service.chain_spec().clone(), + self.maybe_pre_cached(hash), ) .map_err(PayloadBuilderError::other)?; Ok(PayloadJob::new(&self.pipeline, block_ctx, &self.service)) } + + fn on_new_state( + &mut self, + new_state: CanonStateNotification, + ) { + let cached = ExecutionCache::default(); + if let Err(()) = + cached.insert_state(&new_state.committed().execution_outcome().bundle) + { + tracing::error!( + "Failed to insert committed state bundle for block {}", + new_state.tip().hash() + ); + return; + } + self.pre_cached = Some(PrecachedState { + block: new_state.tip().hash(), + cached, + }); + } +} + +/// Pre-filled [`ExecutionCache`] for a specific block. +/// +/// This is extracted from the [`CanonStateNotification`] for the tip block. +#[derive(Debug, Clone)] +pub(super) struct PrecachedState { + /// The block for which the state is pre-cached. + pub block: B256, + /// Cached state for the block. + pub cached: ExecutionCache, } diff --git a/src/test_utils/exts/mock.rs b/src/test_utils/exts/mock.rs index c711e2b..725e927 100644 --- a/src/test_utils/exts/mock.rs +++ b/src/test_utils/exts/mock.rs @@ -127,6 +127,7 @@ where payload_attribs, base_state, chainspec.clone(), + None, ) .expect("Failed to create mocked block context") } @@ -164,6 +165,7 @@ where payload_attributes, base_state, chainspec.clone(), + None, ) .expect("Failed to create mocked block context") } From 5ca7d12deb637eafc66c12e69d9c173e29803256 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Fri, 21 Nov 2025 12:22:23 +0400 Subject: [PATCH 12/12] Clean up test --- src/payload/ext/cached_state.rs | 109 +------------------------------- 1 file changed, 1 insertion(+), 108 deletions(-) diff --git a/src/payload/ext/cached_state.rs b/src/payload/ext/cached_state.rs index 18b10a2..15c65a9 100644 --- a/src/payload/ext/cached_state.rs +++ b/src/payload/ext/cached_state.rs @@ -447,118 +447,11 @@ mod tests { use { super::*, crate::{ - alloy::primitives::{B256, U256}, + alloy::primitives::U256, reth::providers::test_utils::{ExtendedAccount, MockEthProvider}, }, - rand::Rng, - std::mem::size_of, - tracing::info, }; - mod tracking_allocator { - use std::{ - alloc::{GlobalAlloc, Layout, System}, - sync::atomic::{AtomicUsize, Ordering}, - }; - - #[derive(Debug)] - pub(crate) struct TrackingAllocator { - allocated: AtomicUsize, - total_allocated: AtomicUsize, - inner: System, - } - - impl TrackingAllocator { - pub(crate) const fn new() -> Self { - Self { - allocated: AtomicUsize::new(0), - total_allocated: AtomicUsize::new(0), - inner: System, - } - } - - pub(crate) fn reset(&self) { - self.allocated.store(0, Ordering::SeqCst); - self.total_allocated.store(0, Ordering::SeqCst); - } - - pub(crate) fn total_allocated(&self) -> usize { - self.total_allocated.load(Ordering::SeqCst) - } - } - - unsafe impl GlobalAlloc for TrackingAllocator { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let ret = unsafe { self.inner.alloc(layout) }; - if !ret.is_null() { - self.allocated.fetch_add(layout.size(), Ordering::SeqCst); - self - .total_allocated - .fetch_add(layout.size(), Ordering::SeqCst); - } - ret - } - - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - self.allocated.fetch_sub(layout.size(), Ordering::SeqCst); - unsafe { self.inner.dealloc(ptr, layout) } - } - } - } - - use tracking_allocator::TrackingAllocator; - - #[global_allocator] - static ALLOCATOR: TrackingAllocator = TrackingAllocator::new(); - - fn measure_allocation(f: F) -> (usize, T) - where - F: FnOnce() -> T, - { - ALLOCATOR.reset(); - let result = f(); - let total = ALLOCATOR.total_allocated(); - (total, result) - } - - #[test] - fn measure_storage_cache_overhead() { - let (base_overhead, cache) = - measure_allocation(|| AccountStorageCache::new()); - info!("Base AccountStorageCache overhead: {base_overhead} bytes"); - let mut rng = rand::rng(); - - let key = StorageKey::random(); - let value = StorageValue::from(rng.random::()); - let (first_slot, _) = measure_allocation(|| { - cache.insert_storage(key, Some(value)); - }); - info!("First slot insertion overhead: {first_slot} bytes"); - - const TOTAL_SLOTS: usize = 10_000; - let (test_slots, _) = measure_allocation(|| { - for _ in 0..TOTAL_SLOTS { - let key = StorageKey::random(); - let value = StorageValue::from(rng.random::()); - cache.insert_storage(key, Some(value)); - } - }); - info!( - "Average overhead over {} slots: {} bytes", - TOTAL_SLOTS, - test_slots / TOTAL_SLOTS - ); - - info!("\nTheoretical sizes:"); - info!("StorageKey size: {} bytes", size_of::()); - info!("StorageValue size: {} bytes", size_of::()); - info!( - "Option size: {} bytes", - size_of::>() - ); - info!("Option size: {} bytes", size_of::>()); - } - #[test] fn test_empty_storage_cached_state_provider() { // make sure when we have an empty value in storage, we return `Empty` and