Skip to main content

reth_execution_cache/
cached_state.rs

1//! Execution cache implementation for block processing.
2use alloy_primitives::{
3    map::{DefaultHashBuilder, FbBuildHasher},
4    Address, StorageKey, StorageValue, B256,
5};
6use fixed_cache::{AnyRef, CacheConfig, Stats, StatsHandler};
7use metrics::{Counter, Gauge, Histogram};
8use parking_lot::Once;
9use reth_errors::ProviderResult;
10use reth_metrics::Metrics;
11use reth_primitives_traits::{Account, Bytecode};
12use reth_provider::{
13    AccountReader, BlockHashReader, BytecodeReader, HashedPostStateProvider, StateProofProvider,
14    StateProvider, StateRootProvider, StorageRootProvider,
15};
16use reth_revm::db::BundleState;
17use reth_trie::{
18    updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof,
19    MultiProofTargets, StorageMultiProof, StorageProof, TrieInput,
20};
21use std::{
22    sync::{
23        atomic::{AtomicU64, AtomicUsize, Ordering},
24        Arc,
25    },
26    time::Duration,
27};
28use tracing::{debug_span, instrument, trace, warn};
29
30/// Alignment in bytes for entries in the fixed-cache.
31///
32/// Each bucket in `fixed-cache` is aligned to 128 bytes (cache line) due to
33/// `#[repr(C, align(128))]` on the internal `Bucket` struct.
34const FIXED_CACHE_ALIGNMENT: usize = 128;
35
36/// Overhead per entry in the fixed-cache (the `AtomicUsize` tag field).
37const FIXED_CACHE_ENTRY_OVERHEAD: usize = size_of::<usize>();
38
39/// Calculates the actual size of a fixed-cache entry for a given key-value pair.
40///
41/// The entry size is `overhead + size_of::<K>() + size_of::<V>()`, rounded up to the
42/// next multiple of [`FIXED_CACHE_ALIGNMENT`] (128 bytes).
43const fn fixed_cache_entry_size<K, V>() -> usize {
44    fixed_cache_key_size_with_value::<K>(size_of::<V>())
45}
46
47/// Calculates the actual size of a fixed-cache entry for a given key-value pair.
48///
49/// The entry size is `overhead + size_of::<K>() + size_of::<V>()`, rounded up to the
50/// next multiple of [`FIXED_CACHE_ALIGNMENT`] (128 bytes).
51const fn fixed_cache_key_size_with_value<K>(value: usize) -> usize {
52    let raw_size = FIXED_CACHE_ENTRY_OVERHEAD + size_of::<K>() + value;
53    // Round up to next multiple of alignment
54    raw_size.div_ceil(FIXED_CACHE_ALIGNMENT) * FIXED_CACHE_ALIGNMENT
55}
56
57/// Estimated average bytecode size for cache budget calculation.
58///
59/// The fixed-cache stores `Option<Bytecode>` inline (pointer-sized), but each cached contract
60/// also holds bytecode on the heap. For budget estimation we use 8 KiB, which is close to the
61/// observed mainnet average (~7 KiB). Using `MAX_CODE_SIZE` (48 KiB) overestimates by ~7x,
62/// yielding only 4096 entries for a 228 MB code-cache budget when 16384 fit comfortably.
63const ESTIMATED_AVG_CODE_SIZE: usize = 8 * 1024;
64
65/// Size in bytes of a single code cache entry (inline metadata + estimated heap).
66const CODE_CACHE_ENTRY_SIZE: usize =
67    fixed_cache_key_size_with_value::<Address>(ESTIMATED_AVG_CODE_SIZE);
68
69/// Size in bytes of a single storage cache entry.
70const STORAGE_CACHE_ENTRY_SIZE: usize =
71    fixed_cache_entry_size::<(Address, StorageKey), StorageValue>();
72
73/// Size in bytes of a single account cache entry.
74const ACCOUNT_CACHE_ENTRY_SIZE: usize = fixed_cache_entry_size::<Address, Option<Account>>();
75
76/// Cache configuration with epoch tracking enabled for O(1) cache invalidation.
77struct EpochCacheConfig;
78impl CacheConfig for EpochCacheConfig {
79    const EPOCHS: bool = true;
80}
81
82/// Type alias for the fixed-cache used for accounts and storage.
83type FixedCache<K, V, H = DefaultHashBuilder> = fixed_cache::Cache<K, V, H, EpochCacheConfig>;
84
85/// A wrapper of a state provider and a shared cache.
86///
87/// The const generic `PREWARM` controls whether every cache miss is populated. This is only
88/// relevant for pre-warm transaction execution with the intention to pre-populate the cache with
89/// data for regular block execution. During regular block execution the cache doesn't need to be
90/// populated because the actual EVM database `State` also caches
91/// internally during block execution and the cache is then updated after the block with the entire
92/// [`BundleState`] output of that block which contains all accessed accounts, code, storage. See
93/// also [`ExecutionCache::insert_state`].
94#[derive(Debug)]
95pub struct CachedStateProvider<S, const PREWARM: bool = false> {
96    /// The state provider
97    state_provider: S,
98
99    /// The caches used for the provider
100    caches: ExecutionCache,
101
102    /// Metrics for the cached state provider
103    metrics: CachedStateMetrics,
104
105    /// Optional cache statistics for detailed block logging. Only tracked when slow block
106    /// threshold is configured.
107    cache_stats: Option<Arc<CacheStats>>,
108}
109
110impl<S> CachedStateProvider<S> {
111    /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and
112    /// [`CachedStateMetrics`].
113    pub const fn new(
114        state_provider: S,
115        caches: ExecutionCache,
116        metrics: CachedStateMetrics,
117    ) -> Self {
118        Self { state_provider, caches, metrics, cache_stats: None }
119    }
120}
121
122impl<S> CachedStateProvider<S, true> {
123    /// Creates a new [`CachedStateProvider`] with prewarming enabled.
124    pub const fn new_prewarm(
125        state_provider: S,
126        caches: ExecutionCache,
127        metrics: CachedStateMetrics,
128    ) -> Self {
129        Self { state_provider, caches, metrics, cache_stats: None }
130    }
131}
132
133impl<S, const PREWARM: bool> CachedStateProvider<S, PREWARM> {
134    /// Enables cache statistics tracking for detailed block logging.
135    pub fn with_cache_stats(mut self, stats: Option<Arc<CacheStats>>) -> Self {
136        self.cache_stats = stats;
137        self
138    }
139}
140
141/// Represents the status of a key in the cache.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum CachedStatus<T> {
144    /// The key is not in the cache (or was invalidated). The value was recalculated.
145    NotCached(T),
146    /// The key exists in cache and has a specific value.
147    Cached(T),
148}
149
150/// Metrics for the cached state provider, showing hits / misses for each cache
151#[derive(Metrics, Clone)]
152#[metrics(scope = "sync.caching")]
153pub struct CachedStateMetrics {
154    /// Number of times a new execution cache was created
155    execution_cache_created_total: Counter,
156
157    /// Duration of execution cache creation in seconds
158    execution_cache_creation_duration_seconds: Histogram,
159
160    /// Code cache hits
161    code_cache_hits: Gauge,
162
163    /// Code cache misses
164    code_cache_misses: Gauge,
165
166    /// Code cache size (number of entries)
167    code_cache_size: Gauge,
168
169    /// Code cache capacity (maximum entries)
170    code_cache_capacity: Gauge,
171
172    /// Code cache collisions (hash collisions causing eviction)
173    code_cache_collisions: Gauge,
174
175    /// Storage cache hits
176    storage_cache_hits: Gauge,
177
178    /// Storage cache misses
179    storage_cache_misses: Gauge,
180
181    /// Storage cache size (number of entries)
182    storage_cache_size: Gauge,
183
184    /// Storage cache capacity (maximum entries)
185    storage_cache_capacity: Gauge,
186
187    /// Storage cache collisions (hash collisions causing eviction)
188    storage_cache_collisions: Gauge,
189
190    /// Account cache hits
191    account_cache_hits: Gauge,
192
193    /// Account cache misses
194    account_cache_misses: Gauge,
195
196    /// Account cache size (number of entries)
197    account_cache_size: Gauge,
198
199    /// Account cache capacity (maximum entries)
200    account_cache_capacity: Gauge,
201
202    /// Account cache collisions (hash collisions causing eviction)
203    account_cache_collisions: Gauge,
204}
205
206impl CachedStateMetrics {
207    /// Sets all values to zero, indicating that a new block is being executed.
208    pub fn reset(&self) {
209        // code cache
210        self.code_cache_hits.set(0);
211        self.code_cache_misses.set(0);
212        self.code_cache_collisions.set(0);
213
214        // storage cache
215        self.storage_cache_hits.set(0);
216        self.storage_cache_misses.set(0);
217        self.storage_cache_collisions.set(0);
218
219        // account cache
220        self.account_cache_hits.set(0);
221        self.account_cache_misses.set(0);
222        self.account_cache_collisions.set(0);
223    }
224
225    /// Returns a new zeroed-out instance of [`CachedStateMetrics`].
226    pub fn zeroed() -> Self {
227        let zeroed = Self::default();
228        zeroed.reset();
229        zeroed
230    }
231
232    /// Records a new execution cache creation with its duration.
233    pub fn record_cache_creation(&self, duration: Duration) {
234        self.execution_cache_created_total.increment(1);
235        self.execution_cache_creation_duration_seconds.record(duration.as_secs_f64());
236    }
237}
238
239/// Cache hit/miss statistics for detailed block logging.
240#[derive(Debug, Default)]
241pub struct CacheStats {
242    /// Account cache hits
243    account_hits: AtomicUsize,
244    /// Account cache misses
245    account_misses: AtomicUsize,
246    /// Storage cache hits
247    storage_hits: AtomicUsize,
248    /// Storage cache misses
249    storage_misses: AtomicUsize,
250    /// Code cache hits
251    code_hits: AtomicUsize,
252    /// Code cache misses
253    code_misses: AtomicUsize,
254}
255
256impl CacheStats {
257    /// Records an account cache hit.
258    pub fn record_account_hit(&self) {
259        self.account_hits.fetch_add(1, Ordering::Relaxed);
260    }
261
262    /// Records an account cache miss.
263    pub fn record_account_miss(&self) {
264        self.account_misses.fetch_add(1, Ordering::Relaxed);
265    }
266
267    /// Returns the number of account cache hits.
268    pub fn account_hits(&self) -> usize {
269        self.account_hits.load(Ordering::Relaxed)
270    }
271
272    /// Returns the number of account cache misses.
273    pub fn account_misses(&self) -> usize {
274        self.account_misses.load(Ordering::Relaxed)
275    }
276
277    /// Records a storage cache hit.
278    pub fn record_storage_hit(&self) {
279        self.storage_hits.fetch_add(1, Ordering::Relaxed);
280    }
281
282    /// Records a storage cache miss.
283    pub fn record_storage_miss(&self) {
284        self.storage_misses.fetch_add(1, Ordering::Relaxed);
285    }
286
287    /// Returns the number of storage cache hits.
288    pub fn storage_hits(&self) -> usize {
289        self.storage_hits.load(Ordering::Relaxed)
290    }
291
292    /// Returns the number of storage cache misses.
293    pub fn storage_misses(&self) -> usize {
294        self.storage_misses.load(Ordering::Relaxed)
295    }
296
297    /// Records a code cache hit.
298    pub fn record_code_hit(&self) {
299        self.code_hits.fetch_add(1, Ordering::Relaxed);
300    }
301
302    /// Records a code cache miss.
303    pub fn record_code_miss(&self) {
304        self.code_misses.fetch_add(1, Ordering::Relaxed);
305    }
306
307    /// Returns the number of code cache hits.
308    pub fn code_hits(&self) -> usize {
309        self.code_hits.load(Ordering::Relaxed)
310    }
311
312    /// Returns the number of code cache misses.
313    pub fn code_misses(&self) -> usize {
314        self.code_misses.load(Ordering::Relaxed)
315    }
316}
317
318/// A stats handler for fixed-cache that tracks collisions and size.
319///
320/// Note: Hits and misses are tracked directly by the [`CachedStateProvider`] via
321/// [`CachedStateMetrics`], not here. The stats handler is used for:
322/// - Collision detection (hash collisions causing eviction of a different key)
323/// - Size tracking
324///
325/// ## Size Tracking
326///
327/// Size is tracked via `on_insert` and `on_remove` callbacks:
328/// - `on_insert`: increment size only when inserting into an empty bucket (no eviction)
329/// - `on_remove`: always decrement size
330///
331/// Collisions (evicting a different key) don't change size since they replace an existing entry.
332#[derive(Debug)]
333pub struct CacheStatsHandler {
334    collisions: AtomicU64,
335    size: AtomicUsize,
336    capacity: usize,
337}
338
339impl CacheStatsHandler {
340    /// Creates a new stats handler with all counters initialized to zero.
341    pub const fn new(capacity: usize) -> Self {
342        Self { collisions: AtomicU64::new(0), size: AtomicUsize::new(0), capacity }
343    }
344
345    /// Returns the number of cache collisions.
346    pub fn collisions(&self) -> u64 {
347        self.collisions.load(Ordering::Relaxed)
348    }
349
350    /// Returns the current size (number of entries).
351    pub fn size(&self) -> usize {
352        self.size.load(Ordering::Relaxed)
353    }
354
355    /// Returns the capacity (maximum number of entries).
356    pub const fn capacity(&self) -> usize {
357        self.capacity
358    }
359
360    /// Increments the size counter. Called on cache insert.
361    pub fn increment_size(&self) {
362        let _ = self.size.fetch_add(1, Ordering::Relaxed);
363    }
364
365    /// Decrements the size counter. Called on cache remove.
366    pub fn decrement_size(&self) {
367        let _ = self.size.fetch_sub(1, Ordering::Relaxed);
368    }
369
370    /// Resets size to zero. Called on cache clear.
371    pub fn reset_size(&self) {
372        self.size.store(0, Ordering::Relaxed);
373    }
374
375    /// Resets collision counter to zero (but not size).
376    pub fn reset_stats(&self) {
377        self.collisions.store(0, Ordering::Relaxed);
378    }
379}
380
381impl<K: PartialEq, V> StatsHandler<K, V> for CacheStatsHandler {
382    fn on_hit(&self, _key: &K, _value: &V) {}
383
384    fn on_miss(&self, _key: AnyRef<'_>) {}
385
386    fn on_insert(&self, key: &K, _value: &V, evicted: Option<(&K, &V)>) {
387        match evicted {
388            None => {
389                // Inserting into an empty bucket
390                self.increment_size();
391            }
392            Some((evicted_key, _)) if evicted_key != key => {
393                // Collision: evicting a different key
394                self.collisions.fetch_add(1, Ordering::Relaxed);
395            }
396            Some(_) => {
397                // Updating the same key, size unchanged
398            }
399        }
400    }
401
402    fn on_remove(&self, _key: &K, _value: &V) {
403        self.decrement_size();
404    }
405}
406
407impl<S: AccountReader, const PREWARM: bool> AccountReader for CachedStateProvider<S, PREWARM> {
408    fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
409        if PREWARM {
410            match self.caches.get_or_try_insert_account_with(*address, || {
411                self.state_provider.basic_account(address)
412            })? {
413                // During prewarm we only record stats (not prometheus metrics)
414                CachedStatus::NotCached(value) => {
415                    if let Some(stats) = &self.cache_stats {
416                        stats.record_account_miss();
417                    }
418                    Ok(value)
419                }
420                CachedStatus::Cached(value) => {
421                    if let Some(stats) = &self.cache_stats {
422                        stats.record_account_hit();
423                    }
424                    Ok(value)
425                }
426            }
427        } else if let Some(account) = self.caches.0.account_cache.get(address) {
428            self.metrics.account_cache_hits.increment(1);
429            if let Some(stats) = &self.cache_stats {
430                stats.record_account_hit();
431            }
432            Ok(account)
433        } else {
434            self.metrics.account_cache_misses.increment(1);
435            if let Some(stats) = &self.cache_stats {
436                stats.record_account_miss();
437            }
438            self.state_provider.basic_account(address)
439        }
440    }
441}
442
443impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvider<S, PREWARM> {
444    fn storage(
445        &self,
446        account: Address,
447        storage_key: StorageKey,
448    ) -> ProviderResult<Option<StorageValue>> {
449        if PREWARM {
450            match self.caches.get_or_try_insert_storage_with(account, storage_key, || {
451                self.state_provider.storage(account, storage_key).map(Option::unwrap_or_default)
452            })? {
453                // During prewarm we only record stats (not prometheus metrics)
454                CachedStatus::NotCached(value) => {
455                    if let Some(stats) = &self.cache_stats {
456                        stats.record_storage_miss();
457                    }
458                    Ok(Some(value).filter(|v| !v.is_zero()))
459                }
460                CachedStatus::Cached(value) => {
461                    if let Some(stats) = &self.cache_stats {
462                        stats.record_storage_hit();
463                    }
464                    Ok(Some(value).filter(|v| !v.is_zero()))
465                }
466            }
467        } else if let Some(value) = self.caches.0.storage_cache.get(&(account, storage_key)) {
468            self.metrics.storage_cache_hits.increment(1);
469            if let Some(stats) = &self.cache_stats {
470                stats.record_storage_hit();
471            }
472            Ok(Some(value).filter(|v| !v.is_zero()))
473        } else {
474            self.metrics.storage_cache_misses.increment(1);
475            if let Some(stats) = &self.cache_stats {
476                stats.record_storage_miss();
477            }
478            self.state_provider.storage(account, storage_key)
479        }
480    }
481}
482
483impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvider<S, PREWARM> {
484    fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
485        if PREWARM {
486            match self.caches.get_or_try_insert_code_with(*code_hash, || {
487                self.state_provider.bytecode_by_hash(code_hash)
488            })? {
489                // During prewarm we only record stats (not prometheus metrics)
490                CachedStatus::NotCached(code) => {
491                    if let Some(stats) = &self.cache_stats {
492                        stats.record_code_miss();
493                    }
494                    Ok(code)
495                }
496                CachedStatus::Cached(code) => {
497                    if let Some(stats) = &self.cache_stats {
498                        stats.record_code_hit();
499                    }
500                    Ok(code)
501                }
502            }
503        } else if let Some(code) = self.caches.0.code_cache.get(code_hash) {
504            self.metrics.code_cache_hits.increment(1);
505            if let Some(stats) = &self.cache_stats {
506                stats.record_code_hit();
507            }
508            Ok(code)
509        } else {
510            self.metrics.code_cache_misses.increment(1);
511            if let Some(stats) = &self.cache_stats {
512                stats.record_code_miss();
513            }
514            self.state_provider.bytecode_by_hash(code_hash)
515        }
516    }
517}
518
519impl<S: StateRootProvider, const PREWARM: bool> StateRootProvider
520    for CachedStateProvider<S, PREWARM>
521{
522    fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult<B256> {
523        self.state_provider.state_root(hashed_state)
524    }
525
526    fn state_root_from_nodes(&self, input: TrieInput) -> ProviderResult<B256> {
527        self.state_provider.state_root_from_nodes(input)
528    }
529
530    fn state_root_with_updates(
531        &self,
532        hashed_state: HashedPostState,
533    ) -> ProviderResult<(B256, TrieUpdates)> {
534        self.state_provider.state_root_with_updates(hashed_state)
535    }
536
537    fn state_root_from_nodes_with_updates(
538        &self,
539        input: TrieInput,
540    ) -> ProviderResult<(B256, TrieUpdates)> {
541        self.state_provider.state_root_from_nodes_with_updates(input)
542    }
543}
544
545impl<S: StateProofProvider, const PREWARM: bool> StateProofProvider
546    for CachedStateProvider<S, PREWARM>
547{
548    fn proof(
549        &self,
550        input: TrieInput,
551        address: Address,
552        slots: &[B256],
553    ) -> ProviderResult<AccountProof> {
554        self.state_provider.proof(input, address, slots)
555    }
556
557    fn multiproof(
558        &self,
559        input: TrieInput,
560        targets: MultiProofTargets,
561    ) -> ProviderResult<MultiProof> {
562        self.state_provider.multiproof(input, targets)
563    }
564
565    fn witness(
566        &self,
567        input: TrieInput,
568        target: HashedPostState,
569    ) -> ProviderResult<Vec<alloy_primitives::Bytes>> {
570        self.state_provider.witness(input, target)
571    }
572}
573
574impl<S: StorageRootProvider, const PREWARM: bool> StorageRootProvider
575    for CachedStateProvider<S, PREWARM>
576{
577    fn storage_root(
578        &self,
579        address: Address,
580        hashed_storage: HashedStorage,
581    ) -> ProviderResult<B256> {
582        self.state_provider.storage_root(address, hashed_storage)
583    }
584
585    fn storage_proof(
586        &self,
587        address: Address,
588        slot: B256,
589        hashed_storage: HashedStorage,
590    ) -> ProviderResult<StorageProof> {
591        self.state_provider.storage_proof(address, slot, hashed_storage)
592    }
593
594    fn storage_multiproof(
595        &self,
596        address: Address,
597        slots: &[B256],
598        hashed_storage: HashedStorage,
599    ) -> ProviderResult<StorageMultiProof> {
600        self.state_provider.storage_multiproof(address, slots, hashed_storage)
601    }
602}
603
604impl<S: BlockHashReader, const PREWARM: bool> BlockHashReader for CachedStateProvider<S, PREWARM> {
605    fn block_hash(&self, number: alloy_primitives::BlockNumber) -> ProviderResult<Option<B256>> {
606        self.state_provider.block_hash(number)
607    }
608
609    fn canonical_hashes_range(
610        &self,
611        start: alloy_primitives::BlockNumber,
612        end: alloy_primitives::BlockNumber,
613    ) -> ProviderResult<Vec<B256>> {
614        self.state_provider.canonical_hashes_range(start, end)
615    }
616}
617
618impl<S: HashedPostStateProvider, const PREWARM: bool> HashedPostStateProvider
619    for CachedStateProvider<S, PREWARM>
620{
621    fn hashed_post_state(&self, bundle_state: &reth_revm::db::BundleState) -> HashedPostState {
622        self.state_provider.hashed_post_state(bundle_state)
623    }
624}
625
626/// Execution cache used during block processing.
627///
628/// Optimizes state access by maintaining in-memory copies of frequently accessed
629/// accounts, storage slots, and bytecode. Works in conjunction with prewarming
630/// to reduce database I/O during block execution.
631///
632/// ## Storage Invalidation
633///
634/// Since EIP-6780, SELFDESTRUCT only works within the same transaction where the
635/// contract was created, so we don't need to handle clearing the storage.
636#[derive(Debug, Clone)]
637pub struct ExecutionCache(Arc<ExecutionCacheInner>);
638
639/// Inner state of the [`ExecutionCache`], wrapped in a single [`Arc`].
640#[derive(Debug)]
641struct ExecutionCacheInner {
642    /// Cache for contract bytecode, keyed by code hash.
643    code_cache: FixedCache<B256, Option<Bytecode>, FbBuildHasher<32>>,
644
645    /// Flat storage cache: maps `(Address, StorageKey)` to storage value.
646    storage_cache: FixedCache<(Address, StorageKey), StorageValue>,
647
648    /// Cache for basic account information (nonce, balance, code hash).
649    account_cache: FixedCache<Address, Option<Account>, FbBuildHasher<20>>,
650
651    /// Stats handler for the code cache (shared with the cache via [`Stats`]).
652    code_stats: Arc<CacheStatsHandler>,
653
654    /// Stats handler for the storage cache (shared with the cache via [`Stats`]).
655    storage_stats: Arc<CacheStatsHandler>,
656
657    /// Stats handler for the account cache (shared with the cache via [`Stats`]).
658    account_stats: Arc<CacheStatsHandler>,
659
660    /// One-time notification when SELFDESTRUCT is encountered
661    selfdestruct_encountered: Once,
662}
663
664impl ExecutionCache {
665    /// Minimum cache size required when epochs are enabled.
666    /// With EPOCHS=true, fixed-cache requires 12 bottom bits to be zero (2 needed + 10 epoch).
667    const MIN_CACHE_SIZE_WITH_EPOCHS: usize = 1 << 12; // 4096
668
669    /// Converts a byte size to number of cache entries, rounding down to a power of two.
670    ///
671    /// Fixed-cache requires power-of-two sizes for efficient indexing.
672    /// With epochs enabled, the minimum size is 4096 entries.
673    pub const fn bytes_to_entries(size_bytes: usize, entry_size: usize) -> usize {
674        let entries = size_bytes / entry_size;
675        // Round down to nearest power of two
676        let rounded = if entries == 0 { 1 } else { (entries + 1).next_power_of_two() >> 1 };
677        // Ensure minimum size for epoch tracking
678        if rounded < Self::MIN_CACHE_SIZE_WITH_EPOCHS {
679            Self::MIN_CACHE_SIZE_WITH_EPOCHS
680        } else {
681            rounded
682        }
683    }
684
685    /// Build an [`ExecutionCache`] struct, so that execution caches can be easily cloned.
686    pub fn new(total_cache_size: usize) -> Self {
687        let code_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
688        let storage_cache_size = (total_cache_size * 8888) / 10000; // 88.88% of total
689        let account_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
690
691        let code_capacity = Self::bytes_to_entries(code_cache_size, CODE_CACHE_ENTRY_SIZE);
692        let storage_capacity = Self::bytes_to_entries(storage_cache_size, STORAGE_CACHE_ENTRY_SIZE);
693        let account_capacity = Self::bytes_to_entries(account_cache_size, ACCOUNT_CACHE_ENTRY_SIZE);
694
695        let code_stats = Arc::new(CacheStatsHandler::new(code_capacity));
696        let storage_stats = Arc::new(CacheStatsHandler::new(storage_capacity));
697        let account_stats = Arc::new(CacheStatsHandler::new(account_capacity));
698
699        Self(Arc::new(ExecutionCacheInner {
700            code_cache: FixedCache::new(code_capacity, FbBuildHasher::<32>::default())
701                .with_stats(Some(Stats::new(code_stats.clone()))),
702            storage_cache: FixedCache::new(storage_capacity, DefaultHashBuilder::default())
703                .with_stats(Some(Stats::new(storage_stats.clone()))),
704            account_cache: FixedCache::new(account_capacity, FbBuildHasher::<20>::default())
705                .with_stats(Some(Stats::new(account_stats.clone()))),
706            code_stats,
707            storage_stats,
708            account_stats,
709            selfdestruct_encountered: Once::new(),
710        }))
711    }
712
713    /// Gets code from cache, or inserts using the provided function.
714    pub fn get_or_try_insert_code_with<E>(
715        &self,
716        hash: B256,
717        f: impl FnOnce() -> Result<Option<Bytecode>, E>,
718    ) -> Result<CachedStatus<Option<Bytecode>>, E> {
719        let mut miss = false;
720        let result = self.0.code_cache.get_or_try_insert_with(hash, |_| {
721            miss = true;
722            f()
723        })?;
724
725        if miss {
726            Ok(CachedStatus::NotCached(result))
727        } else {
728            Ok(CachedStatus::Cached(result))
729        }
730    }
731
732    /// Gets storage from cache, or inserts using the provided function.
733    pub fn get_or_try_insert_storage_with<E>(
734        &self,
735        address: Address,
736        key: StorageKey,
737        f: impl FnOnce() -> Result<StorageValue, E>,
738    ) -> Result<CachedStatus<StorageValue>, E> {
739        let mut miss = false;
740        let result = self.0.storage_cache.get_or_try_insert_with((address, key), |_| {
741            miss = true;
742            f()
743        })?;
744
745        if miss {
746            Ok(CachedStatus::NotCached(result))
747        } else {
748            Ok(CachedStatus::Cached(result))
749        }
750    }
751
752    /// Gets account from cache, or inserts using the provided function.
753    pub fn get_or_try_insert_account_with<E>(
754        &self,
755        address: Address,
756        f: impl FnOnce() -> Result<Option<Account>, E>,
757    ) -> Result<CachedStatus<Option<Account>>, E> {
758        let mut miss = false;
759        let result = self.0.account_cache.get_or_try_insert_with(address, |_| {
760            miss = true;
761            f()
762        })?;
763
764        if miss {
765            Ok(CachedStatus::NotCached(result))
766        } else {
767            Ok(CachedStatus::Cached(result))
768        }
769    }
770
771    /// Insert storage value into cache.
772    pub fn insert_storage(&self, address: Address, key: StorageKey, value: Option<StorageValue>) {
773        self.0.storage_cache.insert((address, key), value.unwrap_or_default());
774    }
775
776    /// Insert code into cache.
777    pub fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
778        self.0.code_cache.insert(hash, code);
779    }
780
781    /// Insert account into cache.
782    pub fn insert_account(&self, address: Address, account: Option<Account>) {
783        self.0.account_cache.insert(address, account);
784    }
785
786    /// Inserts the post-execution state changes into the cache.
787    ///
788    /// This method is called after transaction execution to update the cache with
789    /// the touched and modified state. The insertion order is critical:
790    ///
791    /// 1. Bytecodes: Insert contract code first
792    /// 2. Storage slots: Update storage values for each account
793    /// 3. Accounts: Update account info (nonce, balance, code hash)
794    ///
795    /// ## Why This Order Matters
796    ///
797    /// Account information references bytecode via code hash. If we update accounts
798    /// before bytecode, we might create cache entries pointing to non-existent code.
799    /// The current order ensures cache consistency.
800    ///
801    /// ## Error Handling
802    ///
803    /// Returns an error if the state updates are inconsistent and should be discarded.
804    #[instrument(level = "debug", target = "engine::caching", skip_all)]
805    #[expect(clippy::result_unit_err)]
806    pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> {
807        let _enter =
808            debug_span!(target: "engine::tree", "contracts", len = state_updates.contracts.len())
809                .entered();
810        // Insert bytecodes
811        for (code_hash, bytecode) in &state_updates.contracts {
812            self.insert_code(*code_hash, Some(Bytecode(bytecode.clone())));
813        }
814        drop(_enter);
815
816        let _enter = debug_span!(
817            target: "engine::tree",
818            "accounts",
819            accounts = state_updates.state.len(),
820            storages =
821                state_updates.state.values().map(|account| account.storage.len()).sum::<usize>()
822        )
823        .entered();
824        for (addr, account) in &state_updates.state {
825            // If the account was not modified, as in not changed and not destroyed, then we have
826            // nothing to do w.r.t. this particular account and can move on
827            if account.status.is_not_modified() {
828                continue
829            }
830
831            // If the original account had code (was a contract), we must clear the entire cache
832            // because we can't efficiently invalidate all storage slots for a single address.
833            // This should only happen on pre-Dencun networks.
834            //
835            // If the original account had no code (was an EOA or a not yet deployed contract), we
836            // just remove the account from cache - no storage exists for it.
837            if account.was_destroyed() {
838                let had_code =
839                    account.original_info.as_ref().is_some_and(|info| !info.is_empty_code_hash());
840                if had_code {
841                    self.0.selfdestruct_encountered.call_once(|| {
842                        warn!(
843                            target: "engine::caching",
844                            address = ?addr,
845                            info = ?account.info,
846                            original_info = ?account.original_info,
847                            "Encountered an inter-transaction SELFDESTRUCT that reset the storage cache. Are you running a pre-Dencun network?"
848                        );
849                    });
850                    self.clear();
851                    return Ok(())
852                }
853
854                self.0.account_cache.remove(addr);
855                self.0.account_stats.decrement_size();
856                continue;
857            }
858
859            // If we have an account that was modified, but it has a `None` account info, some wild
860            // error has occurred because this state should be unrepresentable. An account with
861            // `None` current info, should be destroyed.
862            let Some(ref account_info) = account.info else {
863                trace!(target: "engine::caching", ?account, "Account with None account info found in state updates");
864                return Err(())
865            };
866
867            // Now we iterate over all storage and make updates to the cached storage values
868            for (key, slot) in &account.storage {
869                self.insert_storage(*addr, (*key).into(), Some(slot.present_value));
870            }
871
872            // Insert will update if present, so we just use the new account info as the new value
873            // for the account cache
874            self.insert_account(*addr, Some(Account::from(account_info)));
875        }
876
877        Ok(())
878    }
879
880    /// Clears storage and account caches, resetting them to empty state.
881    ///
882    /// We do not clear the bytecodes cache, because its mapping can never change, as it's
883    /// `keccak256(bytecode) => bytecode`.
884    pub fn clear(&self) {
885        self.0.storage_cache.clear();
886        self.0.account_cache.clear();
887
888        self.0.storage_stats.reset_size();
889        self.0.account_stats.reset_size();
890    }
891
892    /// Updates the provided metrics with the current stats from the cache's stats handlers,
893    /// and resets the hit/miss/collision counters.
894    pub fn update_metrics(&self, metrics: &CachedStateMetrics) {
895        metrics.code_cache_size.set(self.0.code_stats.size() as f64);
896        metrics.code_cache_capacity.set(self.0.code_stats.capacity() as f64);
897        metrics.code_cache_collisions.set(self.0.code_stats.collisions() as f64);
898        self.0.code_stats.reset_stats();
899
900        metrics.storage_cache_size.set(self.0.storage_stats.size() as f64);
901        metrics.storage_cache_capacity.set(self.0.storage_stats.capacity() as f64);
902        metrics.storage_cache_collisions.set(self.0.storage_stats.collisions() as f64);
903        self.0.storage_stats.reset_stats();
904
905        metrics.account_cache_size.set(self.0.account_stats.size() as f64);
906        metrics.account_cache_capacity.set(self.0.account_stats.capacity() as f64);
907        metrics.account_cache_collisions.set(self.0.account_stats.collisions() as f64);
908        self.0.account_stats.reset_stats();
909    }
910}
911
912/// A saved cache that has been used for executing a specific block, which has been updated for its
913/// execution.
914#[derive(Debug, Clone)]
915pub struct SavedCache {
916    /// The hash of the block these caches were used to execute.
917    hash: B256,
918
919    /// The caches used for the provider.
920    caches: ExecutionCache,
921
922    /// Metrics for the cached state provider (includes size/capacity/collisions from fixed-cache)
923    metrics: CachedStateMetrics,
924
925    /// A guard to track in-flight usage of this cache.
926    /// The cache is considered available if the strong count is 1.
927    usage_guard: Arc<()>,
928
929    /// Whether to skip cache metrics recording (can be expensive with large cached state).
930    disable_cache_metrics: bool,
931}
932
933impl SavedCache {
934    /// Creates a new instance with the internals
935    pub fn new(hash: B256, caches: ExecutionCache, metrics: CachedStateMetrics) -> Self {
936        Self { hash, caches, metrics, usage_guard: Arc::new(()), disable_cache_metrics: false }
937    }
938
939    /// Sets whether to disable cache metrics recording.
940    pub const fn with_disable_cache_metrics(mut self, disable: bool) -> Self {
941        self.disable_cache_metrics = disable;
942        self
943    }
944
945    /// Returns the hash for this cache
946    pub const fn executed_block_hash(&self) -> B256 {
947        self.hash
948    }
949
950    /// Splits the cache into its caches, metrics, and `disable_cache_metrics` flag, consuming it.
951    pub fn split(self) -> (ExecutionCache, CachedStateMetrics, bool) {
952        (self.caches, self.metrics, self.disable_cache_metrics)
953    }
954
955    /// Returns true if the cache is available for use (no other tasks are currently using it).
956    pub fn is_available(&self) -> bool {
957        Arc::strong_count(&self.usage_guard) == 1
958    }
959
960    /// Returns the current strong count of the usage guard.
961    pub fn usage_count(&self) -> usize {
962        Arc::strong_count(&self.usage_guard)
963    }
964
965    /// Returns the [`ExecutionCache`] belonging to the tracked hash.
966    pub const fn cache(&self) -> &ExecutionCache {
967        &self.caches
968    }
969
970    /// Returns the metrics associated with this cache.
971    pub const fn metrics(&self) -> &CachedStateMetrics {
972        &self.metrics
973    }
974
975    /// Updates the cache metrics (size/capacity/collisions) from the stats handlers.
976    ///
977    /// Note: This can be expensive with large cached state. Use
978    /// `with_disable_cache_metrics(true)` to skip.
979    pub fn update_metrics(&self) {
980        if self.disable_cache_metrics {
981            return
982        }
983        self.caches.update_metrics(&self.metrics);
984    }
985
986    /// Clears all caches, resetting them to empty state,
987    /// and updates the hash of the block this cache belongs to.
988    pub fn clear_with_hash(&mut self, hash: B256) {
989        self.hash = hash;
990        self.caches.clear();
991    }
992}
993
994#[cfg(any(test, feature = "test-utils"))]
995impl SavedCache {
996    /// Clones the usage guard for testing availability tracking.
997    pub fn clone_guard_for_test(&self) -> Arc<()> {
998        self.usage_guard.clone()
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005    use alloy_primitives::{map::HashMap, U256};
1006    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1007    use reth_revm::db::{AccountStatus, BundleAccount};
1008    use revm_state::AccountInfo;
1009
1010    #[test]
1011    fn test_empty_storage_cached_state_provider() {
1012        let address = Address::random();
1013        let storage_key = StorageKey::random();
1014        let account = ExtendedAccount::new(0, U256::ZERO);
1015
1016        let provider = MockEthProvider::default();
1017        provider.extend_accounts(vec![(address, account)]);
1018
1019        let caches = ExecutionCache::new(1000);
1020        let state_provider =
1021            CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
1022
1023        let res = state_provider.storage(address, storage_key);
1024        assert!(res.is_ok());
1025        assert_eq!(res.unwrap(), None);
1026    }
1027
1028    #[test]
1029    fn test_uncached_storage_cached_state_provider() {
1030        let address = Address::random();
1031        let storage_key = StorageKey::random();
1032        let storage_value = U256::from(1);
1033        let account =
1034            ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]);
1035
1036        let provider = MockEthProvider::default();
1037        provider.extend_accounts(vec![(address, account)]);
1038
1039        let caches = ExecutionCache::new(1000);
1040        let state_provider =
1041            CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
1042
1043        let res = state_provider.storage(address, storage_key);
1044        assert!(res.is_ok());
1045        assert_eq!(res.unwrap(), Some(storage_value));
1046    }
1047
1048    #[test]
1049    fn test_get_storage_populated() {
1050        let address = Address::random();
1051        let storage_key = StorageKey::random();
1052        let storage_value = U256::from(1);
1053
1054        let caches = ExecutionCache::new(1000);
1055        caches.insert_storage(address, storage_key, Some(storage_value));
1056
1057        let result = caches
1058            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
1059        assert_eq!(result.unwrap(), CachedStatus::Cached(storage_value));
1060    }
1061
1062    #[test]
1063    fn test_get_storage_empty() {
1064        let address = Address::random();
1065        let storage_key = StorageKey::random();
1066
1067        let caches = ExecutionCache::new(1000);
1068        caches.insert_storage(address, storage_key, None);
1069
1070        let result = caches
1071            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
1072        assert_eq!(result.unwrap(), CachedStatus::Cached(U256::ZERO));
1073    }
1074
1075    #[test]
1076    fn test_saved_cache_is_available() {
1077        let execution_cache = ExecutionCache::new(1000);
1078        let cache = SavedCache::new(B256::ZERO, execution_cache, CachedStateMetrics::zeroed());
1079
1080        assert!(cache.is_available(), "Cache should be available initially");
1081
1082        let _guard = cache.clone_guard_for_test();
1083
1084        assert!(!cache.is_available(), "Cache should not be available with active guard");
1085    }
1086
1087    #[test]
1088    fn test_saved_cache_multiple_references() {
1089        let execution_cache = ExecutionCache::new(1000);
1090        let cache =
1091            SavedCache::new(B256::from([2u8; 32]), execution_cache, CachedStateMetrics::zeroed());
1092
1093        let guard1 = cache.clone_guard_for_test();
1094        let guard2 = cache.clone_guard_for_test();
1095        let guard3 = guard1.clone();
1096
1097        assert!(!cache.is_available());
1098
1099        drop(guard1);
1100        assert!(!cache.is_available());
1101
1102        drop(guard2);
1103        assert!(!cache.is_available());
1104
1105        drop(guard3);
1106        assert!(cache.is_available());
1107    }
1108
1109    #[test]
1110    fn test_insert_state_destroyed_account_with_code_clears_cache() {
1111        let caches = ExecutionCache::new(1000);
1112
1113        // Pre-populate caches with some data
1114        let addr1 = Address::random();
1115        let addr2 = Address::random();
1116        let storage_key = StorageKey::random();
1117        caches.insert_account(addr1, Some(Account::default()));
1118        caches.insert_account(addr2, Some(Account::default()));
1119        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1120
1121        // Verify caches are populated
1122        assert!(caches.0.account_cache.get(&addr1).is_some());
1123        assert!(caches.0.account_cache.get(&addr2).is_some());
1124        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1125
1126        let bundle = BundleState {
1127            // BundleState with a destroyed contract (had code)
1128            state: HashMap::from_iter([(
1129                Address::random(),
1130                BundleAccount::new(
1131                    Some(AccountInfo {
1132                        balance: U256::ZERO,
1133                        nonce: 1,
1134                        code_hash: B256::random(), // Non-empty code hash
1135                        code: None,
1136                        account_id: None,
1137                    }),
1138                    None, // Destroyed, so no current info
1139                    Default::default(),
1140                    AccountStatus::Destroyed,
1141                ),
1142            )]),
1143            contracts: Default::default(),
1144            reverts: Default::default(),
1145            state_size: 0,
1146            reverts_size: 0,
1147        };
1148
1149        // Insert state should clear all caches because a contract was destroyed
1150        let result = caches.insert_state(&bundle);
1151        assert!(result.is_ok());
1152
1153        // Verify all caches were cleared
1154        assert!(caches.0.account_cache.get(&addr1).is_none());
1155        assert!(caches.0.account_cache.get(&addr2).is_none());
1156        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_none());
1157    }
1158
1159    #[test]
1160    fn test_insert_state_destroyed_account_without_code_removes_only_account() {
1161        let caches = ExecutionCache::new(1000);
1162
1163        // Pre-populate caches with some data
1164        let addr1 = Address::random();
1165        let addr2 = Address::random();
1166        let storage_key = StorageKey::random();
1167        caches.insert_account(addr1, Some(Account::default()));
1168        caches.insert_account(addr2, Some(Account::default()));
1169        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1170
1171        let bundle = BundleState {
1172            // BundleState with a destroyed EOA (no code)
1173            state: HashMap::from_iter([(
1174                addr1,
1175                BundleAccount::new(
1176                    Some(AccountInfo {
1177                        balance: U256::from(100),
1178                        nonce: 1,
1179                        code_hash: alloy_primitives::KECCAK256_EMPTY, // Empty code hash = EOA
1180                        code: None,
1181                        account_id: None,
1182                    }),
1183                    None, // Destroyed
1184                    Default::default(),
1185                    AccountStatus::Destroyed,
1186                ),
1187            )]),
1188            contracts: Default::default(),
1189            reverts: Default::default(),
1190            state_size: 0,
1191            reverts_size: 0,
1192        };
1193
1194        // Insert state should only remove the destroyed account
1195        assert!(caches.insert_state(&bundle).is_ok());
1196
1197        // Verify only addr1 was removed, other data is still present
1198        assert!(caches.0.account_cache.get(&addr1).is_none());
1199        assert!(caches.0.account_cache.get(&addr2).is_some());
1200        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1201    }
1202
1203    #[test]
1204    fn test_insert_state_destroyed_account_no_original_info_removes_only_account() {
1205        let caches = ExecutionCache::new(1000);
1206
1207        // Pre-populate caches
1208        let addr1 = Address::random();
1209        let addr2 = Address::random();
1210        caches.insert_account(addr1, Some(Account::default()));
1211        caches.insert_account(addr2, Some(Account::default()));
1212
1213        let bundle = BundleState {
1214            // BundleState with a destroyed account (has no original info)
1215            state: HashMap::from_iter([(
1216                addr1,
1217                BundleAccount::new(
1218                    None, // No original info
1219                    None, // Destroyed
1220                    Default::default(),
1221                    AccountStatus::Destroyed,
1222                ),
1223            )]),
1224            contracts: Default::default(),
1225            reverts: Default::default(),
1226            state_size: 0,
1227            reverts_size: 0,
1228        };
1229
1230        // Insert state should only remove the destroyed account (no code = no full clear)
1231        assert!(caches.insert_state(&bundle).is_ok());
1232
1233        // Verify only addr1 was removed
1234        assert!(caches.0.account_cache.get(&addr1).is_none());
1235        assert!(caches.0.account_cache.get(&addr2).is_some());
1236    }
1237
1238    #[test]
1239    fn test_code_cache_capacity_with_default_budget() {
1240        // Default cross-block cache is 4 GB; code gets 5.56% = ~228 MB.
1241        let total_cache_size = 4 * 1024 * 1024 * 1024; // 4 GB
1242        let code_budget = (total_cache_size * 556) / 10000; // 228 MB
1243
1244        let capacity = ExecutionCache::bytes_to_entries(code_budget, CODE_CACHE_ENTRY_SIZE);
1245
1246        // With ESTIMATED_AVG_CODE_SIZE (8 KiB) we expect 16384 entries.
1247        // If someone accidentally reverts to MAX_CODE_SIZE (48 KiB), this would drop to 4096.
1248        assert_eq!(
1249            capacity, 16384,
1250            "code cache should have 16384 entries with default 4 GB budget"
1251        );
1252    }
1253}