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