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