reth_engine_tree/tree/
cached_state.rs

1//! Execution cache implementation for block processing.
2use alloy_primitives::{Address, StorageKey, StorageValue, B256};
3use metrics::Gauge;
4use mini_moka::sync::CacheBuilder;
5use reth_errors::ProviderResult;
6use reth_metrics::Metrics;
7use reth_primitives_traits::{Account, Bytecode};
8use reth_provider::{
9    AccountReader, BlockHashReader, BytecodeReader, HashedPostStateProvider, StateProofProvider,
10    StateProvider, StateRootProvider, StorageRootProvider,
11};
12use reth_revm::db::BundleState;
13use reth_trie::{
14    updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof,
15    MultiProofTargets, StorageMultiProof, StorageProof, TrieInput,
16};
17use revm_primitives::map::DefaultHashBuilder;
18use std::{sync::Arc, time::Duration};
19use tracing::trace;
20
21pub(crate) type Cache<K, V> =
22    mini_moka::sync::Cache<K, V, alloy_primitives::map::DefaultHashBuilder>;
23
24/// A wrapper of a state provider and a shared cache.
25pub(crate) struct CachedStateProvider<S> {
26    /// The state provider
27    state_provider: S,
28
29    /// The caches used for the provider
30    caches: ExecutionCache,
31
32    /// Metrics for the cached state provider
33    metrics: CachedStateMetrics,
34}
35
36impl<S> CachedStateProvider<S>
37where
38    S: StateProvider,
39{
40    /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and
41    /// [`CachedStateMetrics`].
42    pub(crate) const fn new_with_caches(
43        state_provider: S,
44        caches: ExecutionCache,
45        metrics: CachedStateMetrics,
46    ) -> Self {
47        Self { state_provider, caches, metrics }
48    }
49}
50
51/// Metrics for the cached state provider, showing hits / misses for each cache
52#[derive(Metrics, Clone)]
53#[metrics(scope = "sync.caching")]
54pub(crate) struct CachedStateMetrics {
55    /// Code cache hits
56    code_cache_hits: Gauge,
57
58    /// Code cache misses
59    code_cache_misses: Gauge,
60
61    /// Code cache size
62    ///
63    /// NOTE: this uses the moka caches' `entry_count`, NOT the `weighted_size` method to calculate
64    /// size.
65    code_cache_size: Gauge,
66
67    /// Storage cache hits
68    storage_cache_hits: Gauge,
69
70    /// Storage cache misses
71    storage_cache_misses: Gauge,
72
73    /// Storage cache size
74    ///
75    /// NOTE: this uses the moka caches' `entry_count`, NOT the `weighted_size` method to calculate
76    /// size.
77    storage_cache_size: Gauge,
78
79    /// Account cache hits
80    account_cache_hits: Gauge,
81
82    /// Account cache misses
83    account_cache_misses: Gauge,
84
85    /// Account cache size
86    ///
87    /// NOTE: this uses the moka caches' `entry_count`, NOT the `weighted_size` method to calculate
88    /// size.
89    account_cache_size: Gauge,
90}
91
92impl CachedStateMetrics {
93    /// Sets all values to zero, indicating that a new block is being executed.
94    pub(crate) fn reset(&self) {
95        // code cache
96        self.code_cache_hits.set(0);
97        self.code_cache_misses.set(0);
98
99        // storage cache
100        self.storage_cache_hits.set(0);
101        self.storage_cache_misses.set(0);
102
103        // account cache
104        self.account_cache_hits.set(0);
105        self.account_cache_misses.set(0);
106    }
107
108    /// Returns a new zeroed-out instance of [`CachedStateMetrics`].
109    pub(crate) fn zeroed() -> Self {
110        let zeroed = Self::default();
111        zeroed.reset();
112        zeroed
113    }
114}
115
116impl<S: AccountReader> AccountReader for CachedStateProvider<S> {
117    fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
118        if let Some(res) = self.caches.account_cache.get(address) {
119            self.metrics.account_cache_hits.increment(1);
120            return Ok(res)
121        }
122
123        self.metrics.account_cache_misses.increment(1);
124
125        let res = self.state_provider.basic_account(address)?;
126        self.caches.account_cache.insert(*address, res);
127        Ok(res)
128    }
129}
130
131/// Represents the status of a storage slot in the cache.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub(crate) enum SlotStatus {
134    /// The account's storage cache doesn't exist.
135    NotCached,
136    /// The storage slot exists in cache and is empty (value is zero).
137    Empty,
138    /// The storage slot exists in cache and has a specific non-zero value.
139    Value(StorageValue),
140}
141
142impl<S: StateProvider> StateProvider for CachedStateProvider<S> {
143    fn storage(
144        &self,
145        account: Address,
146        storage_key: StorageKey,
147    ) -> ProviderResult<Option<StorageValue>> {
148        match self.caches.get_storage(&account, &storage_key) {
149            SlotStatus::NotCached => {
150                self.metrics.storage_cache_misses.increment(1);
151                let final_res = self.state_provider.storage(account, storage_key)?;
152                self.caches.insert_storage(account, storage_key, final_res);
153                Ok(final_res)
154            }
155            SlotStatus::Empty => {
156                self.metrics.storage_cache_hits.increment(1);
157                Ok(None)
158            }
159            SlotStatus::Value(value) => {
160                self.metrics.storage_cache_hits.increment(1);
161                Ok(Some(value))
162            }
163        }
164    }
165}
166
167impl<S: BytecodeReader> BytecodeReader for CachedStateProvider<S> {
168    fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
169        if let Some(res) = self.caches.code_cache.get(code_hash) {
170            self.metrics.code_cache_hits.increment(1);
171            return Ok(res)
172        }
173
174        self.metrics.code_cache_misses.increment(1);
175
176        let final_res = self.state_provider.bytecode_by_hash(code_hash)?;
177        self.caches.code_cache.insert(*code_hash, final_res.clone());
178        Ok(final_res)
179    }
180}
181
182impl<S: StateRootProvider> StateRootProvider for CachedStateProvider<S> {
183    fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult<B256> {
184        self.state_provider.state_root(hashed_state)
185    }
186
187    fn state_root_from_nodes(&self, input: TrieInput) -> ProviderResult<B256> {
188        self.state_provider.state_root_from_nodes(input)
189    }
190
191    fn state_root_with_updates(
192        &self,
193        hashed_state: HashedPostState,
194    ) -> ProviderResult<(B256, TrieUpdates)> {
195        self.state_provider.state_root_with_updates(hashed_state)
196    }
197
198    fn state_root_from_nodes_with_updates(
199        &self,
200        input: TrieInput,
201    ) -> ProviderResult<(B256, TrieUpdates)> {
202        self.state_provider.state_root_from_nodes_with_updates(input)
203    }
204}
205
206impl<S: StateProofProvider> StateProofProvider for CachedStateProvider<S> {
207    fn proof(
208        &self,
209        input: TrieInput,
210        address: Address,
211        slots: &[B256],
212    ) -> ProviderResult<AccountProof> {
213        self.state_provider.proof(input, address, slots)
214    }
215
216    fn multiproof(
217        &self,
218        input: TrieInput,
219        targets: MultiProofTargets,
220    ) -> ProviderResult<MultiProof> {
221        self.state_provider.multiproof(input, targets)
222    }
223
224    fn witness(
225        &self,
226        input: TrieInput,
227        target: HashedPostState,
228    ) -> ProviderResult<Vec<alloy_primitives::Bytes>> {
229        self.state_provider.witness(input, target)
230    }
231}
232
233impl<S: StorageRootProvider> StorageRootProvider for CachedStateProvider<S> {
234    fn storage_root(
235        &self,
236        address: Address,
237        hashed_storage: HashedStorage,
238    ) -> ProviderResult<B256> {
239        self.state_provider.storage_root(address, hashed_storage)
240    }
241
242    fn storage_proof(
243        &self,
244        address: Address,
245        slot: B256,
246        hashed_storage: HashedStorage,
247    ) -> ProviderResult<StorageProof> {
248        self.state_provider.storage_proof(address, slot, hashed_storage)
249    }
250
251    /// Generate a storage multiproof for multiple storage slots.
252    ///
253    /// A **storage multiproof** is a cryptographic proof that can verify the values
254    /// of multiple storage slots for a single account in a single verification step.
255    /// Instead of generating separate proofs for each slot (which would be inefficient),
256    /// a multiproof bundles the necessary trie nodes to prove all requested slots.
257    ///
258    /// ## How it works:
259    /// 1. Takes an account address and a list of storage slot keys
260    /// 2. Traverses the account's storage trie to collect proof nodes
261    /// 3. Returns a [`StorageMultiProof`] containing the minimal set of trie nodes needed to verify
262    ///    all the requested storage slots
263    fn storage_multiproof(
264        &self,
265        address: Address,
266        slots: &[B256],
267        hashed_storage: HashedStorage,
268    ) -> ProviderResult<StorageMultiProof> {
269        self.state_provider.storage_multiproof(address, slots, hashed_storage)
270    }
271}
272
273impl<S: BlockHashReader> BlockHashReader for CachedStateProvider<S> {
274    fn block_hash(&self, number: alloy_primitives::BlockNumber) -> ProviderResult<Option<B256>> {
275        self.state_provider.block_hash(number)
276    }
277
278    fn canonical_hashes_range(
279        &self,
280        start: alloy_primitives::BlockNumber,
281        end: alloy_primitives::BlockNumber,
282    ) -> ProviderResult<Vec<B256>> {
283        self.state_provider.canonical_hashes_range(start, end)
284    }
285}
286
287impl<S: HashedPostStateProvider> HashedPostStateProvider for CachedStateProvider<S> {
288    fn hashed_post_state(&self, bundle_state: &reth_revm::db::BundleState) -> HashedPostState {
289        self.state_provider.hashed_post_state(bundle_state)
290    }
291}
292
293/// Execution cache used during block processing.
294///
295/// Optimizes state access by maintaining in-memory copies of frequently accessed
296/// accounts, storage slots, and bytecode. Works in conjunction with prewarming
297/// to reduce database I/O during block execution.
298#[derive(Debug, Clone)]
299pub(crate) struct ExecutionCache {
300    /// Cache for contract bytecode, keyed by code hash.
301    code_cache: Cache<B256, Option<Bytecode>>,
302
303    /// Per-account storage cache: outer cache keyed by Address, inner cache tracks that account’s
304    /// storage slots.
305    storage_cache: Cache<Address, AccountStorageCache>,
306
307    /// Cache for basic account information (nonce, balance, code hash).
308    account_cache: Cache<Address, Option<Account>>,
309}
310
311impl ExecutionCache {
312    /// Get storage value from hierarchical cache.
313    ///
314    /// Returns a `SlotStatus` indicating whether:
315    /// - `NotCached`: The account's storage cache doesn't exist
316    /// - `Empty`: The slot exists in the account's cache but is empty
317    /// - `Value`: The slot exists and has a specific value
318    pub(crate) fn get_storage(&self, address: &Address, key: &StorageKey) -> SlotStatus {
319        match self.storage_cache.get(address) {
320            None => SlotStatus::NotCached,
321            Some(account_cache) => account_cache.get_storage(key),
322        }
323    }
324
325    /// Insert storage value into hierarchical cache
326    pub(crate) fn insert_storage(
327        &self,
328        address: Address,
329        key: StorageKey,
330        value: Option<StorageValue>,
331    ) {
332        self.insert_storage_bulk(address, [(key, value)]);
333    }
334
335    /// Insert multiple storage values into hierarchical cache for a single account
336    ///
337    /// This method is optimized for inserting multiple storage values for the same address
338    /// by doing the account cache lookup only once instead of for each key-value pair.
339    pub(crate) fn insert_storage_bulk<I>(&self, address: Address, storage_entries: I)
340    where
341        I: IntoIterator<Item = (StorageKey, Option<StorageValue>)>,
342    {
343        let account_cache = self.storage_cache.get(&address).unwrap_or_else(|| {
344            let account_cache = AccountStorageCache::default();
345            self.storage_cache.insert(address, account_cache.clone());
346            account_cache
347        });
348
349        for (key, value) in storage_entries {
350            account_cache.insert_storage(key, value);
351        }
352    }
353
354    /// Invalidate storage for specific account
355    pub(crate) fn invalidate_account_storage(&self, address: &Address) {
356        self.storage_cache.invalidate(address);
357    }
358
359    /// Returns the total number of storage slots cached across all accounts
360    pub(crate) fn total_storage_slots(&self) -> usize {
361        self.storage_cache.iter().map(|addr| addr.len()).sum()
362    }
363
364    /// Inserts the post-execution state changes into the cache.
365    ///
366    /// This method is called after transaction execution to update the cache with
367    /// the touched and modified state. The insertion order is critical:
368    ///
369    /// 1. Bytecodes: Insert contract code first
370    /// 2. Storage slots: Update storage values for each account
371    /// 3. Accounts: Update account info (nonce, balance, code hash)
372    ///
373    /// ## Why This Order Matters
374    ///
375    /// Account information references bytecode via code hash. If we update accounts
376    /// before bytecode, we might create cache entries pointing to non-existent code.
377    /// The current order ensures cache consistency.
378    ///
379    /// ## Error Handling
380    ///
381    /// Returns an error if the state updates are inconsistent and should be discarded.
382    pub(crate) fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> {
383        // Insert bytecodes
384        for (code_hash, bytecode) in &state_updates.contracts {
385            self.code_cache.insert(*code_hash, Some(Bytecode(bytecode.clone())));
386        }
387
388        for (addr, account) in &state_updates.state {
389            // If the account was not modified, as in not changed and not destroyed, then we have
390            // nothing to do w.r.t. this particular account and can move on
391            if account.status.is_not_modified() {
392                continue
393            }
394
395            // If the account was destroyed, invalidate from the account / storage caches
396            if account.was_destroyed() {
397                // Invalidate the account cache entry if destroyed
398                self.account_cache.invalidate(addr);
399
400                self.invalidate_account_storage(addr);
401                continue
402            }
403
404            // If we have an account that was modified, but it has a `None` account info, some wild
405            // error has occurred because this state should be unrepresentable. An account with
406            // `None` current info, should be destroyed.
407            let Some(ref account_info) = account.info else {
408                trace!(target: "engine::caching", ?account, "Account with None account info found in state updates");
409                return Err(())
410            };
411
412            // Now we iterate over all storage and make updates to the cached storage values
413            // Use bulk insertion to optimize cache lookups - only lookup the account cache once
414            // instead of for each storage key
415            let storage_entries = account.storage.iter().map(|(storage_key, slot)| {
416                // We convert the storage key from U256 to B256 because that is how it's represented
417                // in the cache
418                ((*storage_key).into(), Some(slot.present_value))
419            });
420            self.insert_storage_bulk(*addr, storage_entries);
421
422            // Insert will update if present, so we just use the new account info as the new value
423            // for the account cache
424            self.account_cache.insert(*addr, Some(Account::from(account_info)));
425        }
426
427        Ok(())
428    }
429}
430
431/// A builder for [`ExecutionCache`].
432#[derive(Debug)]
433pub(crate) struct ExecutionCacheBuilder {
434    /// Code cache entries
435    code_cache_entries: u64,
436
437    /// Storage cache entries
438    storage_cache_entries: u64,
439
440    /// Account cache entries
441    account_cache_entries: u64,
442}
443
444impl ExecutionCacheBuilder {
445    /// Build an [`ExecutionCache`] struct, so that execution caches can be easily cloned.
446    pub(crate) fn build_caches(self, total_cache_size: u64) -> ExecutionCache {
447        let storage_cache_size = (total_cache_size * 8888) / 10000; // 88.88% of total
448        let account_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
449        let code_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
450
451        const EXPIRY_TIME: Duration = Duration::from_secs(7200); // 2 hours
452        const TIME_TO_IDLE: Duration = Duration::from_secs(3600); // 1 hour
453
454        let storage_cache = CacheBuilder::new(self.storage_cache_entries)
455            .weigher(|_key: &Address, value: &AccountStorageCache| -> u32 {
456                // values based on results from measure_storage_cache_overhead test
457                let base_weight = 39_000;
458                let slots_weight = value.len() * 218;
459                (base_weight + slots_weight) as u32
460            })
461            .max_capacity(storage_cache_size)
462            .time_to_live(EXPIRY_TIME)
463            .time_to_idle(TIME_TO_IDLE)
464            .build_with_hasher(DefaultHashBuilder::default());
465
466        let account_cache = CacheBuilder::new(self.account_cache_entries)
467            .weigher(|_key: &Address, _value: &Option<Account>| -> u32 {
468                // Account has a fixed size (none, balance,code_hash)
469                size_of::<Option<Account>>() as u32
470            })
471            .max_capacity(account_cache_size)
472            .time_to_live(EXPIRY_TIME)
473            .time_to_idle(TIME_TO_IDLE)
474            .build_with_hasher(DefaultHashBuilder::default());
475
476        let code_cache = CacheBuilder::new(self.code_cache_entries)
477            .weigher(|_key: &B256, value: &Option<Bytecode>| -> u32 {
478                match value {
479                    Some(bytecode) => {
480                        // base weight + actual bytecode size
481                        (40 + bytecode.len()) as u32
482                    }
483                    None => 8, // size of None variant
484                }
485            })
486            .max_capacity(code_cache_size)
487            .time_to_live(EXPIRY_TIME)
488            .time_to_idle(TIME_TO_IDLE)
489            .build_with_hasher(DefaultHashBuilder::default());
490
491        ExecutionCache { code_cache, storage_cache, account_cache }
492    }
493}
494
495impl Default for ExecutionCacheBuilder {
496    fn default() -> Self {
497        // With weigher and max_capacity in place, these numbers represent
498        // the maximum number of entries that can be stored, not the actual
499        // memory usage which is controlled by max_capacity.
500        //
501        // Code cache: up to 10M entries but limited to 0.5GB
502        // Storage cache: up to 10M accounts but limited to 8GB
503        // Account cache: up to 10M accounts but limited to 0.5GB
504        Self {
505            code_cache_entries: 10_000_000,
506            storage_cache_entries: 10_000_000,
507            account_cache_entries: 10_000_000,
508        }
509    }
510}
511
512/// A saved cache that has been used for executing a specific block, which has been updated for its
513/// execution.
514#[derive(Debug, Clone)]
515pub(crate) struct SavedCache {
516    /// The hash of the block these caches were used to execute.
517    hash: B256,
518
519    /// The caches used for the provider.
520    caches: ExecutionCache,
521
522    /// Metrics for the cached state provider
523    metrics: CachedStateMetrics,
524
525    /// A guard to track in-flight usage of this cache.
526    /// The cache is considered available if the strong count is 1.
527    usage_guard: Arc<()>,
528}
529
530impl SavedCache {
531    /// Creates a new instance with the internals
532    pub(super) fn new(hash: B256, caches: ExecutionCache, metrics: CachedStateMetrics) -> Self {
533        Self { hash, caches, metrics, usage_guard: Arc::new(()) }
534    }
535
536    /// Returns the hash for this cache
537    pub(crate) const fn executed_block_hash(&self) -> B256 {
538        self.hash
539    }
540
541    /// Splits the cache into its caches and metrics, consuming it.
542    pub(crate) fn split(self) -> (ExecutionCache, CachedStateMetrics) {
543        (self.caches, self.metrics)
544    }
545
546    /// Returns true if the cache is available for use (no other tasks are currently using it).
547    pub(crate) fn is_available(&self) -> bool {
548        Arc::strong_count(&self.usage_guard) == 1
549    }
550
551    /// Returns the [`ExecutionCache`] belonging to the tracked hash.
552    pub(crate) const fn cache(&self) -> &ExecutionCache {
553        &self.caches
554    }
555
556    /// Returns the metrics associated with this cache.
557    pub(crate) const fn metrics(&self) -> &CachedStateMetrics {
558        &self.metrics
559    }
560
561    /// Updates the metrics for the [`ExecutionCache`].
562    pub(crate) fn update_metrics(&self) {
563        self.metrics.storage_cache_size.set(self.caches.total_storage_slots() as f64);
564        self.metrics.account_cache_size.set(self.caches.account_cache.entry_count() as f64);
565        self.metrics.code_cache_size.set(self.caches.code_cache.entry_count() as f64);
566    }
567}
568
569#[cfg(test)]
570impl SavedCache {
571    fn clone_guard_for_test(&self) -> Arc<()> {
572        self.usage_guard.clone()
573    }
574}
575
576/// Cache for an individual account's storage slots.
577///
578/// This represents the second level of the hierarchical storage cache.
579/// Each account gets its own `AccountStorageCache` to store accessed storage slots.
580#[derive(Debug, Clone)]
581pub(crate) struct AccountStorageCache {
582    /// Map of storage keys to their cached values.
583    slots: Cache<StorageKey, Option<StorageValue>>,
584}
585
586impl AccountStorageCache {
587    /// Create a new [`AccountStorageCache`]
588    pub(crate) fn new(max_slots: u64) -> Self {
589        Self {
590            slots: CacheBuilder::new(max_slots).build_with_hasher(DefaultHashBuilder::default()),
591        }
592    }
593
594    /// Get a storage value from this account's cache.
595    /// - `NotCached`: The slot is not in the cache
596    /// - `Empty`: The slot is empty
597    /// - `Value`: The slot has a specific value
598    pub(crate) fn get_storage(&self, key: &StorageKey) -> SlotStatus {
599        match self.slots.get(key) {
600            None => SlotStatus::NotCached,
601            Some(None) => SlotStatus::Empty,
602            Some(Some(value)) => SlotStatus::Value(value),
603        }
604    }
605
606    /// Insert a storage value
607    pub(crate) fn insert_storage(&self, key: StorageKey, value: Option<StorageValue>) {
608        self.slots.insert(key, value);
609    }
610
611    /// Returns the number of slots in the cache
612    pub(crate) fn len(&self) -> usize {
613        self.slots.entry_count() as usize
614    }
615}
616
617impl Default for AccountStorageCache {
618    fn default() -> Self {
619        // With weigher and max_capacity in place, this number represents
620        // the maximum number of entries that can be stored, not the actual
621        // memory usage which is controlled by storage cache's max_capacity.
622        Self::new(1_000_000)
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use alloy_primitives::{B256, U256};
630    use rand::Rng;
631    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
632    use std::mem::size_of;
633
634    mod tracking_allocator {
635        use std::{
636            alloc::{GlobalAlloc, Layout, System},
637            sync::atomic::{AtomicUsize, Ordering},
638        };
639
640        #[derive(Debug)]
641        pub(crate) struct TrackingAllocator {
642            allocated: AtomicUsize,
643            total_allocated: AtomicUsize,
644            inner: System,
645        }
646
647        impl TrackingAllocator {
648            pub(crate) const fn new() -> Self {
649                Self {
650                    allocated: AtomicUsize::new(0),
651                    total_allocated: AtomicUsize::new(0),
652                    inner: System,
653                }
654            }
655
656            pub(crate) fn reset(&self) {
657                self.allocated.store(0, Ordering::SeqCst);
658                self.total_allocated.store(0, Ordering::SeqCst);
659            }
660
661            pub(crate) fn total_allocated(&self) -> usize {
662                self.total_allocated.load(Ordering::SeqCst)
663            }
664        }
665
666        unsafe impl GlobalAlloc for TrackingAllocator {
667            unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
668                let ret = unsafe { self.inner.alloc(layout) };
669                if !ret.is_null() {
670                    self.allocated.fetch_add(layout.size(), Ordering::SeqCst);
671                    self.total_allocated.fetch_add(layout.size(), Ordering::SeqCst);
672                }
673                ret
674            }
675
676            unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
677                self.allocated.fetch_sub(layout.size(), Ordering::SeqCst);
678                unsafe { self.inner.dealloc(ptr, layout) }
679            }
680        }
681    }
682
683    use tracking_allocator::TrackingAllocator;
684
685    #[global_allocator]
686    static ALLOCATOR: TrackingAllocator = TrackingAllocator::new();
687
688    fn measure_allocation<T, F>(f: F) -> (usize, T)
689    where
690        F: FnOnce() -> T,
691    {
692        ALLOCATOR.reset();
693        let result = f();
694        let total = ALLOCATOR.total_allocated();
695        (total, result)
696    }
697
698    #[test]
699    fn measure_storage_cache_overhead() {
700        let (base_overhead, cache) = measure_allocation(|| AccountStorageCache::new(1000));
701        println!("Base AccountStorageCache overhead: {base_overhead} bytes");
702        let mut rng = rand::rng();
703
704        let key = StorageKey::random();
705        let value = StorageValue::from(rng.random::<u128>());
706        let (first_slot, _) = measure_allocation(|| {
707            cache.insert_storage(key, Some(value));
708        });
709        println!("First slot insertion overhead: {first_slot} bytes");
710
711        const TOTAL_SLOTS: usize = 10_000;
712        let (test_slots, _) = measure_allocation(|| {
713            for _ in 0..TOTAL_SLOTS {
714                let key = StorageKey::random();
715                let value = StorageValue::from(rng.random::<u128>());
716                cache.insert_storage(key, Some(value));
717            }
718        });
719        println!("Average overhead over {} slots: {} bytes", TOTAL_SLOTS, test_slots / TOTAL_SLOTS);
720
721        println!("\nTheoretical sizes:");
722        println!("StorageKey size: {} bytes", size_of::<StorageKey>());
723        println!("StorageValue size: {} bytes", size_of::<StorageValue>());
724        println!("Option<StorageValue> size: {} bytes", size_of::<Option<StorageValue>>());
725        println!("Option<B256> size: {} bytes", size_of::<Option<B256>>());
726    }
727
728    #[test]
729    fn test_empty_storage_cached_state_provider() {
730        // make sure when we have an empty value in storage, we return `Empty` and not `NotCached`
731        let address = Address::random();
732        let storage_key = StorageKey::random();
733        let account = ExtendedAccount::new(0, U256::ZERO);
734
735        // note there is no storage here
736        let provider = MockEthProvider::default();
737        provider.extend_accounts(vec![(address, account)]);
738
739        let caches = ExecutionCacheBuilder::default().build_caches(1000);
740        let state_provider =
741            CachedStateProvider::new_with_caches(provider, caches, CachedStateMetrics::zeroed());
742
743        // check that the storage is empty
744        let res = state_provider.storage(address, storage_key);
745        assert!(res.is_ok());
746        assert_eq!(res.unwrap(), None);
747    }
748
749    #[test]
750    fn test_uncached_storage_cached_state_provider() {
751        // make sure when we have something uncached, we get the cached value
752        let address = Address::random();
753        let storage_key = StorageKey::random();
754        let storage_value = U256::from(1);
755        let account =
756            ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]);
757
758        // note that we extend storage here with one value
759        let provider = MockEthProvider::default();
760        provider.extend_accounts(vec![(address, account)]);
761
762        let caches = ExecutionCacheBuilder::default().build_caches(1000);
763        let state_provider =
764            CachedStateProvider::new_with_caches(provider, caches, CachedStateMetrics::zeroed());
765
766        // check that the storage returns the expected value
767        let res = state_provider.storage(address, storage_key);
768        assert!(res.is_ok());
769        assert_eq!(res.unwrap(), Some(storage_value));
770    }
771
772    #[test]
773    fn test_get_storage_populated() {
774        // make sure when we have something cached, we get the cached value in the `SlotStatus`
775        let address = Address::random();
776        let storage_key = StorageKey::random();
777        let storage_value = U256::from(1);
778
779        // insert into caches directly
780        let caches = ExecutionCacheBuilder::default().build_caches(1000);
781        caches.insert_storage(address, storage_key, Some(storage_value));
782
783        // check that the storage returns the cached value
784        let slot_status = caches.get_storage(&address, &storage_key);
785        assert_eq!(slot_status, SlotStatus::Value(storage_value));
786    }
787
788    #[test]
789    fn test_get_storage_not_cached() {
790        // make sure when we have nothing cached, we get the `NotCached` value in the `SlotStatus`
791        let storage_key = StorageKey::random();
792        let address = Address::random();
793
794        // just create empty caches
795        let caches = ExecutionCacheBuilder::default().build_caches(1000);
796
797        // check that the storage is not cached
798        let slot_status = caches.get_storage(&address, &storage_key);
799        assert_eq!(slot_status, SlotStatus::NotCached);
800    }
801
802    #[test]
803    fn test_get_storage_empty() {
804        // make sure when we insert an empty value to the cache, we get the `Empty` value in the
805        // `SlotStatus`
806        let address = Address::random();
807        let storage_key = StorageKey::random();
808
809        // insert into caches directly
810        let caches = ExecutionCacheBuilder::default().build_caches(1000);
811        caches.insert_storage(address, storage_key, None);
812
813        // check that the storage is empty
814        let slot_status = caches.get_storage(&address, &storage_key);
815        assert_eq!(slot_status, SlotStatus::Empty);
816    }
817
818    // Tests for SavedCache locking mechanism
819    #[test]
820    fn test_saved_cache_is_available() {
821        let execution_cache = ExecutionCacheBuilder::default().build_caches(1000);
822        let cache = SavedCache::new(B256::ZERO, execution_cache, CachedStateMetrics::zeroed());
823
824        // Initially, the cache should be available (only one reference)
825        assert!(cache.is_available(), "Cache should be available initially");
826
827        // Clone the usage guard (simulating it being handed out)
828        let _guard = cache.clone_guard_for_test();
829
830        // Now the cache should not be available (two references)
831        assert!(!cache.is_available(), "Cache should not be available with active guard");
832    }
833
834    #[test]
835    fn test_saved_cache_multiple_references() {
836        let execution_cache = ExecutionCacheBuilder::default().build_caches(1000);
837        let cache =
838            SavedCache::new(B256::from([2u8; 32]), execution_cache, CachedStateMetrics::zeroed());
839
840        // Create multiple references to the usage guard
841        let guard1 = cache.clone_guard_for_test();
842        let guard2 = cache.clone_guard_for_test();
843        let guard3 = guard1.clone();
844
845        // Cache should not be available with multiple guards
846        assert!(!cache.is_available());
847
848        // Drop guards one by one
849        drop(guard1);
850        assert!(!cache.is_available()); // Still not available
851
852        drop(guard2);
853        assert!(!cache.is_available()); // Still not available
854
855        drop(guard3);
856        assert!(cache.is_available()); // Now available
857    }
858}