Skip to main content

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