Skip to main content

reth_execution_cache/
cached_state.rs

1//! Execution cache implementation for block processing.
2use crate::TxPoolPrewarmCacheSnapshot;
3use alloy_primitives::{
4    map::{DefaultHashBuilder, FbBuildHasher},
5    Address, StorageKey, StorageValue, B256,
6};
7use fixed_cache::{AnyRef, CacheConfig, Stats, StatsHandler};
8use metrics::{Counter, Gauge, Histogram};
9use parking_lot::Once;
10use reth_errors::ProviderResult;
11use reth_metrics::Metrics;
12use reth_primitives_traits::{Account, Bytecode};
13use reth_revm::db::BundleState;
14use reth_storage_api::EvmStateProvider;
15use std::{
16    cell::Cell,
17    fmt,
18    sync::{
19        atomic::{AtomicU64, AtomicUsize, Ordering},
20        Arc,
21    },
22    time::Duration,
23};
24use tracing::{debug_span, instrument, trace, warn};
25
26/// Alignment in bytes for entries in the fixed-cache.
27///
28/// Each bucket in `fixed-cache` is aligned to 128 bytes (cache line) due to
29/// `#[repr(C, align(128))]` on the internal `Bucket` struct.
30const FIXED_CACHE_ALIGNMENT: usize = 128;
31
32/// Overhead per entry in the fixed-cache (the `AtomicUsize` tag field).
33const FIXED_CACHE_ENTRY_OVERHEAD: usize = size_of::<usize>();
34
35/// Calculates the actual size of a fixed-cache entry for a given key-value pair.
36///
37/// The entry size is `overhead + size_of::<K>() + size_of::<V>()`, rounded up to the
38/// next multiple of [`FIXED_CACHE_ALIGNMENT`] (128 bytes).
39const fn fixed_cache_entry_size<K, V>() -> usize {
40    fixed_cache_key_size_with_value::<K>(size_of::<V>())
41}
42
43/// Calculates the actual size of a fixed-cache entry for a given key-value pair.
44///
45/// The entry size is `overhead + size_of::<K>() + size_of::<V>()`, rounded up to the
46/// next multiple of [`FIXED_CACHE_ALIGNMENT`] (128 bytes).
47const fn fixed_cache_key_size_with_value<K>(value: usize) -> usize {
48    let raw_size = FIXED_CACHE_ENTRY_OVERHEAD + size_of::<K>() + value;
49    // Round up to next multiple of alignment
50    raw_size.div_ceil(FIXED_CACHE_ALIGNMENT) * FIXED_CACHE_ALIGNMENT
51}
52
53/// Estimated average bytecode size for cache budget calculation.
54///
55/// The fixed-cache stores `Option<Bytecode>` inline (pointer-sized), but each cached contract
56/// also holds bytecode on the heap. For budget estimation we use 8 KiB, which is close to the
57/// observed mainnet average (~7 KiB). Using `MAX_CODE_SIZE` (48 KiB) overestimates by ~7x,
58/// yielding only 4096 entries for a 228 MB code-cache budget when 16384 fit comfortably.
59const ESTIMATED_AVG_CODE_SIZE: usize = 8 * 1024;
60
61/// Size in bytes of a single code cache entry (inline metadata + estimated heap).
62const CODE_CACHE_ENTRY_SIZE: usize =
63    fixed_cache_key_size_with_value::<Address>(ESTIMATED_AVG_CODE_SIZE);
64
65/// Size in bytes of a single storage cache entry.
66const STORAGE_CACHE_ENTRY_SIZE: usize =
67    fixed_cache_entry_size::<(Address, StorageKey), StorageValue>();
68
69/// Size in bytes of a single account cache entry.
70const ACCOUNT_CACHE_ENTRY_SIZE: usize = fixed_cache_entry_size::<Address, Option<Account>>();
71
72/// Cache configuration with epoch tracking enabled for O(1) cache invalidation.
73struct EpochCacheConfig;
74impl CacheConfig for EpochCacheConfig {
75    const EPOCHS: bool = true;
76}
77
78/// Type alias for the fixed-cache used for accounts and storage.
79type FixedCache<K, V, H = DefaultHashBuilder> = fixed_cache::Cache<K, V, H, EpochCacheConfig>;
80
81/// A wrapper of a state provider and a shared cache.
82///
83/// [`CacheFillMode`] controls whether misses populate the shared cache. This is used by background
84/// prewarmers and speculative execution workers that intentionally seed the cache for other
85/// readers. Canonical execution usually leaves this disabled because the EVM database `State`
86/// already caches reads during the block, and the shared cache is updated after the block from the
87/// final [`BundleState`]. See also [`ExecutionCache::insert_state`].
88///
89/// Execution-cache and txpool-snapshot hit/miss metrics are recorded separately when
90/// [`CachedStateMetrics`] is provided. Slow-block [`CacheStats`] are controlled separately by
91/// [`Self::new_with_mode`].
92#[derive(Debug)]
93pub struct CachedStateProvider<S> {
94    /// The state provider
95    state_provider: S,
96
97    /// The caches used for the provider
98    caches: ExecutionCache,
99
100    /// Optional immutable txpool-prewarm snapshot consulted before the regular execution cache.
101    txpool_snapshot: Option<TxPoolPrewarmCacheSnapshot>,
102
103    /// Metrics for the cached state provider.
104    metrics: Option<CachedStateMetrics>,
105
106    /// Provider-local execution-cache hit/miss counters flushed when the provider is dropped.
107    execution_metric_counts: CacheMetricCounts,
108
109    /// Provider-local txpool-cache hit/miss counters flushed when the provider is dropped.
110    txpool_metric_counts: CacheMetricCounts,
111
112    /// Whether cache misses should populate the shared execution cache.
113    fill_mode: CacheFillMode,
114
115    /// Optional cache statistics for detailed block logging. Only tracked when slow block
116    /// threshold is configured.
117    cache_stats: Option<Arc<CacheStats>>,
118}
119
120impl<S> CachedStateProvider<S> {
121    /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and
122    /// optional [`CachedStateMetrics`].
123    pub const fn new(
124        state_provider: S,
125        caches: ExecutionCache,
126        metrics: Option<CachedStateMetrics>,
127    ) -> Self {
128        Self::new_with_mode(state_provider, caches, CacheFillMode::LookupOnly, metrics, None)
129    }
130
131    /// Creates a cache-filling [`CachedStateProvider`].
132    ///
133    /// Doesn't accept metrics because prewarming path does not need to report hit/misses.
134    pub const fn new_prewarm(state_provider: S, caches: ExecutionCache) -> Self {
135        Self::new_with_mode(state_provider, caches, CacheFillMode::FillOnMiss, None, None)
136    }
137
138    /// Creates a [`CachedStateProvider`] with explicit cache fill behavior and optional
139    /// block-local cache stats.
140    pub const fn new_with_mode(
141        state_provider: S,
142        caches: ExecutionCache,
143        fill_mode: CacheFillMode,
144        metrics: Option<CachedStateMetrics>,
145        cache_stats: Option<Arc<CacheStats>>,
146    ) -> Self {
147        Self {
148            state_provider,
149            caches,
150            txpool_snapshot: None,
151            metrics,
152            execution_metric_counts: CacheMetricCounts::new(),
153            txpool_metric_counts: CacheMetricCounts::new(),
154            fill_mode,
155            cache_stats,
156        }
157    }
158
159    /// Adds an immutable txpool-prewarm snapshot as the first cache lookup tier.
160    pub fn with_txpool_snapshot(mut self, snapshot: Option<TxPoolPrewarmCacheSnapshot>) -> Self {
161        self.txpool_snapshot = snapshot;
162        self
163    }
164
165    fn record_account_hit(&self) {
166        self.record_metric(CacheMetricKind::AccountHit);
167        if let Some(stats) = &self.cache_stats {
168            stats.record_account_hit();
169        }
170    }
171
172    fn record_account_miss(&self) {
173        self.record_metric(CacheMetricKind::AccountMiss);
174        if let Some(stats) = &self.cache_stats {
175            stats.record_account_miss();
176        }
177    }
178
179    fn record_storage_hit(&self) {
180        self.record_metric(CacheMetricKind::StorageHit);
181        if let Some(stats) = &self.cache_stats {
182            stats.record_storage_hit();
183        }
184    }
185
186    fn record_storage_miss(&self) {
187        self.record_metric(CacheMetricKind::StorageMiss);
188        if let Some(stats) = &self.cache_stats {
189            stats.record_storage_miss();
190        }
191    }
192
193    fn record_code_hit(&self) {
194        self.record_metric(CacheMetricKind::CodeHit);
195        if let Some(stats) = &self.cache_stats {
196            stats.record_code_hit();
197        }
198    }
199
200    fn record_code_miss(&self) {
201        self.record_metric(CacheMetricKind::CodeMiss);
202        if let Some(stats) = &self.cache_stats {
203            stats.record_code_miss();
204        }
205    }
206
207    fn record_txpool_account_hit(&self) {
208        self.record_txpool_metric(CacheMetricKind::AccountHit);
209        if let Some(stats) = &self.cache_stats {
210            stats.record_txpool_snapshot_account_hit();
211        }
212    }
213
214    fn record_txpool_account_miss(&self) {
215        self.record_txpool_metric(CacheMetricKind::AccountMiss);
216        if let Some(stats) = &self.cache_stats {
217            stats.record_txpool_snapshot_account_miss();
218        }
219    }
220
221    fn record_txpool_storage_hit(&self) {
222        self.record_txpool_metric(CacheMetricKind::StorageHit);
223        if let Some(stats) = &self.cache_stats {
224            stats.record_txpool_snapshot_storage_hit();
225        }
226    }
227
228    fn record_txpool_storage_miss(&self) {
229        self.record_txpool_metric(CacheMetricKind::StorageMiss);
230        if let Some(stats) = &self.cache_stats {
231            stats.record_txpool_snapshot_storage_miss();
232        }
233    }
234
235    fn record_txpool_code_hit(&self) {
236        self.record_txpool_metric(CacheMetricKind::CodeHit);
237        if let Some(stats) = &self.cache_stats {
238            stats.record_txpool_snapshot_code_hit();
239        }
240    }
241
242    fn record_txpool_code_miss(&self) {
243        self.record_txpool_metric(CacheMetricKind::CodeMiss);
244        if let Some(stats) = &self.cache_stats {
245            stats.record_txpool_snapshot_code_miss();
246        }
247    }
248
249    #[inline]
250    fn record_metric(&self, kind: CacheMetricKind) {
251        if self.metrics.is_some() {
252            self.execution_metric_counts.record(kind);
253        }
254    }
255
256    #[inline]
257    fn record_txpool_metric(&self, kind: CacheMetricKind) {
258        if self.metrics.is_some() {
259            self.txpool_metric_counts.record(kind);
260        }
261    }
262
263    fn flush_buffered_metrics(&self) {
264        let execution_counts = self.execution_metric_counts.take();
265        let txpool_counts = self.txpool_metric_counts.take();
266        if execution_counts.is_empty() && txpool_counts.is_empty() {
267            return;
268        }
269
270        if let Some(metrics) = &self.metrics {
271            metrics.record_access_counts(execution_counts);
272            metrics.record_txpool_access_counts(txpool_counts);
273        }
274    }
275
276    const fn should_fill_on_miss(&self) -> bool {
277        matches!(self.fill_mode, CacheFillMode::FillOnMiss)
278    }
279}
280
281impl<S> Drop for CachedStateProvider<S> {
282    fn drop(&mut self) {
283        self.flush_buffered_metrics();
284    }
285}
286
287/// Whether cache misses should populate the shared execution cache.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum CacheFillMode {
290    /// Only read existing cache entries.
291    LookupOnly,
292    /// Insert values loaded from the underlying provider.
293    FillOnMiss,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297enum CacheMetricKind {
298    AccountHit,
299    AccountMiss,
300    StorageHit,
301    StorageMiss,
302    CodeHit,
303    CodeMiss,
304}
305
306#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
307struct CacheMetricSnapshot {
308    account_hits: u64,
309    account_misses: u64,
310    storage_hits: u64,
311    storage_misses: u64,
312    code_hits: u64,
313    code_misses: u64,
314}
315
316impl CacheMetricSnapshot {
317    const fn is_empty(&self) -> bool {
318        self.account_hits == 0 &&
319            self.account_misses == 0 &&
320            self.storage_hits == 0 &&
321            self.storage_misses == 0 &&
322            self.code_hits == 0 &&
323            self.code_misses == 0
324    }
325}
326
327#[derive(Debug, Default)]
328struct CacheMetricCounts {
329    account_hits: Cell<u64>,
330    account_misses: Cell<u64>,
331    storage_hits: Cell<u64>,
332    storage_misses: Cell<u64>,
333    code_hits: Cell<u64>,
334    code_misses: Cell<u64>,
335}
336
337impl CacheMetricCounts {
338    const fn new() -> Self {
339        Self {
340            account_hits: Cell::new(0),
341            account_misses: Cell::new(0),
342            storage_hits: Cell::new(0),
343            storage_misses: Cell::new(0),
344            code_hits: Cell::new(0),
345            code_misses: Cell::new(0),
346        }
347    }
348
349    #[inline]
350    fn record(&self, kind: CacheMetricKind) {
351        let counter = match kind {
352            CacheMetricKind::AccountHit => &self.account_hits,
353            CacheMetricKind::AccountMiss => &self.account_misses,
354            CacheMetricKind::StorageHit => &self.storage_hits,
355            CacheMetricKind::StorageMiss => &self.storage_misses,
356            CacheMetricKind::CodeHit => &self.code_hits,
357            CacheMetricKind::CodeMiss => &self.code_misses,
358        };
359        counter.set(counter.get() + 1);
360    }
361
362    const fn take(&self) -> CacheMetricSnapshot {
363        CacheMetricSnapshot {
364            account_hits: self.account_hits.replace(0),
365            account_misses: self.account_misses.replace(0),
366            storage_hits: self.storage_hits.replace(0),
367            storage_misses: self.storage_misses.replace(0),
368            code_hits: self.code_hits.replace(0),
369            code_misses: self.code_misses.replace(0),
370        }
371    }
372}
373
374/// Represents the status of a key in the cache.
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub enum CachedStatus<T> {
377    /// The key is not in the cache (or was invalidated). The value was recalculated.
378    NotCached(T),
379    /// The key exists in cache and has a specific value.
380    Cached(T),
381}
382
383/// The source that is using the execution cache.
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum CachedStateMetricsSource {
386    /// Engine (validation).
387    Engine,
388    /// Payload builder.
389    Builder,
390    /// Tests.
391    #[cfg(any(test, feature = "test-utils"))]
392    Test,
393}
394
395impl fmt::Display for CachedStateMetricsSource {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        match self {
398            Self::Engine => f.write_str("engine"),
399            Self::Builder => f.write_str("builder"),
400            #[cfg(any(test, feature = "test-utils"))]
401            Self::Test => f.write_str("test"),
402        }
403    }
404}
405
406/// Metrics for the cached state provider, showing hits and misses for each cache tier.
407#[derive(Metrics, Clone)]
408#[metrics(scope = "sync.caching")]
409pub struct CachedStateMetrics {
410    /// Number of times a new execution cache was created
411    execution_cache_created_total: Counter,
412
413    /// Duration of execution cache creation in seconds
414    execution_cache_creation_duration_seconds: Histogram,
415
416    /// Execution-cache code hits
417    code_cache_hits: Gauge,
418
419    /// Execution-cache code misses
420    code_cache_misses: Gauge,
421
422    /// Execution-cache storage hits
423    storage_cache_hits: Gauge,
424
425    /// Execution-cache storage misses
426    storage_cache_misses: Gauge,
427
428    /// Execution-cache account hits
429    account_cache_hits: Gauge,
430
431    /// Execution-cache account misses
432    account_cache_misses: Gauge,
433
434    /// Txpool-prewarm snapshot code hits
435    txpool_snapshot_code_hits: Gauge,
436
437    /// Txpool-prewarm snapshot code misses
438    txpool_snapshot_code_misses: Gauge,
439
440    /// Txpool-prewarm snapshot storage hits
441    txpool_snapshot_storage_hits: Gauge,
442
443    /// Txpool-prewarm snapshot storage misses
444    txpool_snapshot_storage_misses: Gauge,
445
446    /// Txpool-prewarm snapshot account hits
447    txpool_snapshot_account_hits: Gauge,
448
449    /// Txpool-prewarm snapshot account misses
450    txpool_snapshot_account_misses: Gauge,
451}
452
453/// Metrics for shared execution cache state.
454#[derive(Metrics, Clone)]
455#[metrics(scope = "sync.caching")]
456pub struct CachedStateCacheMetrics {
457    /// Code cache size (number of entries)
458    code_cache_size: Gauge,
459
460    /// Code cache capacity (maximum entries)
461    code_cache_capacity: Gauge,
462
463    /// Code cache collisions (hash collisions causing eviction)
464    code_cache_collisions: Gauge,
465
466    /// Storage cache size (number of entries)
467    storage_cache_size: Gauge,
468
469    /// Storage cache capacity (maximum entries)
470    storage_cache_capacity: Gauge,
471
472    /// Storage cache collisions (hash collisions causing eviction)
473    storage_cache_collisions: Gauge,
474
475    /// Account cache size (number of entries)
476    account_cache_size: Gauge,
477
478    /// Account cache capacity (maximum entries)
479    account_cache_capacity: Gauge,
480
481    /// Account cache collisions (hash collisions causing eviction)
482    account_cache_collisions: Gauge,
483}
484
485impl CachedStateMetrics {
486    /// Sets all values to zero, indicating that a new block is being executed.
487    pub fn reset(&self) {
488        // code cache
489        self.code_cache_hits.set(0);
490        self.code_cache_misses.set(0);
491
492        // storage cache
493        self.storage_cache_hits.set(0);
494        self.storage_cache_misses.set(0);
495
496        // account cache
497        self.account_cache_hits.set(0);
498        self.account_cache_misses.set(0);
499
500        // txpool-prewarm code cache
501        self.txpool_snapshot_code_hits.set(0);
502        self.txpool_snapshot_code_misses.set(0);
503
504        // txpool-prewarm storage cache
505        self.txpool_snapshot_storage_hits.set(0);
506        self.txpool_snapshot_storage_misses.set(0);
507
508        // txpool-prewarm account cache
509        self.txpool_snapshot_account_hits.set(0);
510        self.txpool_snapshot_account_misses.set(0);
511    }
512
513    /// Returns a new zeroed-out instance of [`CachedStateMetrics`] with a `source` label
514    /// to distinguish between different callers (e.g., engine vs builder).
515    pub fn zeroed(source: CachedStateMetricsSource) -> Self {
516        let zeroed = Self::new_with_labels(&[("source", source.to_string())]);
517        zeroed.reset();
518        zeroed
519    }
520
521    fn record_access(&self, kind: CacheMetricKind, count: u64) {
522        match kind {
523            CacheMetricKind::AccountHit => self.account_cache_hits.increment(count as f64),
524            CacheMetricKind::AccountMiss => self.account_cache_misses.increment(count as f64),
525            CacheMetricKind::StorageHit => self.storage_cache_hits.increment(count as f64),
526            CacheMetricKind::StorageMiss => self.storage_cache_misses.increment(count as f64),
527            CacheMetricKind::CodeHit => self.code_cache_hits.increment(count as f64),
528            CacheMetricKind::CodeMiss => self.code_cache_misses.increment(count as f64),
529        }
530    }
531
532    fn record_access_counts(&self, counts: CacheMetricSnapshot) {
533        if counts.account_hits != 0 {
534            self.record_access(CacheMetricKind::AccountHit, counts.account_hits);
535        }
536        if counts.account_misses != 0 {
537            self.record_access(CacheMetricKind::AccountMiss, counts.account_misses);
538        }
539        if counts.storage_hits != 0 {
540            self.record_access(CacheMetricKind::StorageHit, counts.storage_hits);
541        }
542        if counts.storage_misses != 0 {
543            self.record_access(CacheMetricKind::StorageMiss, counts.storage_misses);
544        }
545        if counts.code_hits != 0 {
546            self.record_access(CacheMetricKind::CodeHit, counts.code_hits);
547        }
548        if counts.code_misses != 0 {
549            self.record_access(CacheMetricKind::CodeMiss, counts.code_misses);
550        }
551    }
552
553    fn record_txpool_access(&self, kind: CacheMetricKind, count: u64) {
554        match kind {
555            CacheMetricKind::AccountHit => {
556                self.txpool_snapshot_account_hits.increment(count as f64)
557            }
558            CacheMetricKind::AccountMiss => {
559                self.txpool_snapshot_account_misses.increment(count as f64)
560            }
561            CacheMetricKind::StorageHit => {
562                self.txpool_snapshot_storage_hits.increment(count as f64)
563            }
564            CacheMetricKind::StorageMiss => {
565                self.txpool_snapshot_storage_misses.increment(count as f64)
566            }
567            CacheMetricKind::CodeHit => self.txpool_snapshot_code_hits.increment(count as f64),
568            CacheMetricKind::CodeMiss => self.txpool_snapshot_code_misses.increment(count as f64),
569        }
570    }
571
572    fn record_txpool_access_counts(&self, counts: CacheMetricSnapshot) {
573        if counts.account_hits != 0 {
574            self.record_txpool_access(CacheMetricKind::AccountHit, counts.account_hits);
575        }
576        if counts.account_misses != 0 {
577            self.record_txpool_access(CacheMetricKind::AccountMiss, counts.account_misses);
578        }
579        if counts.storage_hits != 0 {
580            self.record_txpool_access(CacheMetricKind::StorageHit, counts.storage_hits);
581        }
582        if counts.storage_misses != 0 {
583            self.record_txpool_access(CacheMetricKind::StorageMiss, counts.storage_misses);
584        }
585        if counts.code_hits != 0 {
586            self.record_txpool_access(CacheMetricKind::CodeHit, counts.code_hits);
587        }
588        if counts.code_misses != 0 {
589            self.record_txpool_access(CacheMetricKind::CodeMiss, counts.code_misses);
590        }
591    }
592
593    /// Records a new execution cache creation with its duration.
594    pub fn record_cache_creation(&self, duration: Duration) {
595        self.execution_cache_created_total.increment(1);
596        self.execution_cache_creation_duration_seconds.record(duration.as_secs_f64());
597    }
598}
599
600/// Cache hit/miss statistics for detailed block logging.
601#[derive(Debug, Default)]
602pub struct CacheStats {
603    /// Execution-cache account hits
604    account_hits: AtomicUsize,
605    /// Execution-cache account misses
606    account_misses: AtomicUsize,
607    /// Execution-cache storage hits
608    storage_hits: AtomicUsize,
609    /// Execution-cache storage misses
610    storage_misses: AtomicUsize,
611    /// Execution-cache code hits
612    code_hits: AtomicUsize,
613    /// Execution-cache code misses
614    code_misses: AtomicUsize,
615    /// Txpool-prewarm snapshot account hits
616    txpool_snapshot_account_hits: AtomicUsize,
617    /// Txpool-prewarm snapshot account misses
618    txpool_snapshot_account_misses: AtomicUsize,
619    /// Txpool-prewarm snapshot storage hits
620    txpool_snapshot_storage_hits: AtomicUsize,
621    /// Txpool-prewarm snapshot storage misses
622    txpool_snapshot_storage_misses: AtomicUsize,
623    /// Txpool-prewarm snapshot code hits
624    txpool_snapshot_code_hits: AtomicUsize,
625    /// Txpool-prewarm snapshot code misses
626    txpool_snapshot_code_misses: AtomicUsize,
627}
628
629impl CacheStats {
630    /// Records an account cache hit.
631    pub fn record_account_hit(&self) {
632        self.account_hits.fetch_add(1, Ordering::Relaxed);
633    }
634
635    /// Records an account cache miss.
636    pub fn record_account_miss(&self) {
637        self.account_misses.fetch_add(1, Ordering::Relaxed);
638    }
639
640    /// Returns the number of account cache hits.
641    pub fn account_hits(&self) -> usize {
642        self.account_hits.load(Ordering::Relaxed)
643    }
644
645    /// Returns the number of account cache misses.
646    pub fn account_misses(&self) -> usize {
647        self.account_misses.load(Ordering::Relaxed)
648    }
649
650    /// Records a storage cache hit.
651    pub fn record_storage_hit(&self) {
652        self.storage_hits.fetch_add(1, Ordering::Relaxed);
653    }
654
655    /// Records a storage cache miss.
656    pub fn record_storage_miss(&self) {
657        self.storage_misses.fetch_add(1, Ordering::Relaxed);
658    }
659
660    /// Returns the number of storage cache hits.
661    pub fn storage_hits(&self) -> usize {
662        self.storage_hits.load(Ordering::Relaxed)
663    }
664
665    /// Returns the number of storage cache misses.
666    pub fn storage_misses(&self) -> usize {
667        self.storage_misses.load(Ordering::Relaxed)
668    }
669
670    /// Records a code cache hit.
671    pub fn record_code_hit(&self) {
672        self.code_hits.fetch_add(1, Ordering::Relaxed);
673    }
674
675    /// Records a code cache miss.
676    pub fn record_code_miss(&self) {
677        self.code_misses.fetch_add(1, Ordering::Relaxed);
678    }
679
680    /// Returns the number of code cache hits.
681    pub fn code_hits(&self) -> usize {
682        self.code_hits.load(Ordering::Relaxed)
683    }
684
685    /// Returns the number of code cache misses.
686    pub fn code_misses(&self) -> usize {
687        self.code_misses.load(Ordering::Relaxed)
688    }
689
690    /// Records a txpool-prewarm snapshot account hit.
691    pub fn record_txpool_snapshot_account_hit(&self) {
692        self.txpool_snapshot_account_hits.fetch_add(1, Ordering::Relaxed);
693    }
694
695    /// Records a txpool-prewarm snapshot account miss.
696    pub fn record_txpool_snapshot_account_miss(&self) {
697        self.txpool_snapshot_account_misses.fetch_add(1, Ordering::Relaxed);
698    }
699
700    /// Returns the number of txpool-prewarm snapshot account hits.
701    pub fn txpool_snapshot_account_hits(&self) -> usize {
702        self.txpool_snapshot_account_hits.load(Ordering::Relaxed)
703    }
704
705    /// Returns the number of txpool-prewarm snapshot account misses.
706    pub fn txpool_snapshot_account_misses(&self) -> usize {
707        self.txpool_snapshot_account_misses.load(Ordering::Relaxed)
708    }
709
710    /// Records a txpool-prewarm snapshot storage hit.
711    pub fn record_txpool_snapshot_storage_hit(&self) {
712        self.txpool_snapshot_storage_hits.fetch_add(1, Ordering::Relaxed);
713    }
714
715    /// Records a txpool-prewarm snapshot storage miss.
716    pub fn record_txpool_snapshot_storage_miss(&self) {
717        self.txpool_snapshot_storage_misses.fetch_add(1, Ordering::Relaxed);
718    }
719
720    /// Returns the number of txpool-prewarm snapshot storage hits.
721    pub fn txpool_snapshot_storage_hits(&self) -> usize {
722        self.txpool_snapshot_storage_hits.load(Ordering::Relaxed)
723    }
724
725    /// Returns the number of txpool-prewarm snapshot storage misses.
726    pub fn txpool_snapshot_storage_misses(&self) -> usize {
727        self.txpool_snapshot_storage_misses.load(Ordering::Relaxed)
728    }
729
730    /// Records a txpool-prewarm snapshot code hit.
731    pub fn record_txpool_snapshot_code_hit(&self) {
732        self.txpool_snapshot_code_hits.fetch_add(1, Ordering::Relaxed);
733    }
734
735    /// Records a txpool-prewarm snapshot code miss.
736    pub fn record_txpool_snapshot_code_miss(&self) {
737        self.txpool_snapshot_code_misses.fetch_add(1, Ordering::Relaxed);
738    }
739
740    /// Returns the number of txpool-prewarm snapshot code hits.
741    pub fn txpool_snapshot_code_hits(&self) -> usize {
742        self.txpool_snapshot_code_hits.load(Ordering::Relaxed)
743    }
744
745    /// Returns the number of txpool-prewarm snapshot code misses.
746    pub fn txpool_snapshot_code_misses(&self) -> usize {
747        self.txpool_snapshot_code_misses.load(Ordering::Relaxed)
748    }
749}
750
751/// A stats handler for fixed-cache that tracks collisions and size.
752///
753/// Note: Hits and misses are tracked directly by the [`CachedStateProvider`] via
754/// [`CachedStateMetrics`], not here. The stats handler is used for:
755/// - Collision detection (hash collisions causing eviction of a different key)
756/// - Size tracking
757///
758/// ## Size Tracking
759///
760/// Size is tracked via `on_insert` and `on_remove` callbacks:
761/// - `on_insert`: increment size only when inserting into an empty bucket (no eviction)
762/// - `on_remove`: always decrement size
763///
764/// Collisions (evicting a different key) don't change size since they replace an existing entry.
765#[derive(Debug)]
766pub struct CacheStatsHandler {
767    collisions: AtomicU64,
768    size: AtomicUsize,
769    capacity: usize,
770}
771
772impl CacheStatsHandler {
773    /// Creates a new stats handler with all counters initialized to zero.
774    pub const fn new(capacity: usize) -> Self {
775        Self { collisions: AtomicU64::new(0), size: AtomicUsize::new(0), capacity }
776    }
777
778    /// Returns the number of cache collisions.
779    pub fn collisions(&self) -> u64 {
780        self.collisions.load(Ordering::Relaxed)
781    }
782
783    /// Returns the current size (number of entries).
784    pub fn size(&self) -> usize {
785        self.size.load(Ordering::Relaxed)
786    }
787
788    /// Returns the capacity (maximum number of entries).
789    pub const fn capacity(&self) -> usize {
790        self.capacity
791    }
792
793    /// Increments the size counter. Called on cache insert.
794    pub fn increment_size(&self) {
795        let _ = self.size.fetch_add(1, Ordering::Relaxed);
796    }
797
798    /// Decrements the size counter. Called on cache remove.
799    pub fn decrement_size(&self) {
800        let _ = self.size.fetch_sub(1, Ordering::Relaxed);
801    }
802
803    /// Resets size to zero. Called on cache clear.
804    pub fn reset_size(&self) {
805        self.size.store(0, Ordering::Relaxed);
806    }
807
808    /// Resets collision counter to zero (but not size).
809    pub fn reset_stats(&self) {
810        self.collisions.store(0, Ordering::Relaxed);
811    }
812}
813
814impl<K: PartialEq, V> StatsHandler<K, V> for CacheStatsHandler {
815    fn on_hit(&self, _key: &K, _value: &V) {}
816
817    fn on_miss(&self, _key: AnyRef<'_>) {}
818
819    fn on_insert(&self, key: &K, _value: &V, evicted: Option<(&K, &V)>) {
820        match evicted {
821            None => {
822                // Inserting into an empty bucket
823                self.increment_size();
824            }
825            Some((evicted_key, _)) if evicted_key != key => {
826                // Collision: evicting a different key
827                self.collisions.fetch_add(1, Ordering::Relaxed);
828            }
829            Some(_) => {
830                // Updating the same key, size unchanged
831            }
832        }
833    }
834
835    fn on_remove(&self, _key: &K, _value: &V) {
836        self.decrement_size();
837    }
838}
839
840#[inline]
841fn nonzero_storage_value(value: StorageValue) -> Option<StorageValue> {
842    (!value.is_zero()).then_some(value)
843}
844
845impl<S: EvmStateProvider> EvmStateProvider for CachedStateProvider<S> {
846    fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
847        if let Some(snapshot) = &self.txpool_snapshot {
848            if let Some(account) = snapshot.account(address) {
849                self.record_txpool_account_hit();
850                return Ok(account)
851            }
852            self.record_txpool_account_miss();
853        }
854
855        if self.should_fill_on_miss() {
856            match self.caches.get_or_try_insert_account_with(*address, || {
857                self.state_provider.basic_account(address)
858            })? {
859                CachedStatus::NotCached(value) => {
860                    self.record_account_miss();
861                    Ok(value)
862                }
863                CachedStatus::Cached(value) => {
864                    self.record_account_hit();
865                    Ok(value)
866                }
867            }
868        } else if let Some(account) = self.caches.0.account_cache.get(address) {
869            self.record_account_hit();
870            Ok(account)
871        } else {
872            self.record_account_miss();
873            self.state_provider.basic_account(address)
874        }
875    }
876
877    fn storage(
878        &self,
879        account: Address,
880        storage_key: StorageKey,
881    ) -> ProviderResult<Option<StorageValue>> {
882        if let Some(snapshot) = &self.txpool_snapshot {
883            if let Some(value) = snapshot.storage(account, storage_key) {
884                self.record_txpool_storage_hit();
885                return Ok(nonzero_storage_value(value))
886            }
887            self.record_txpool_storage_miss();
888        }
889
890        if self.should_fill_on_miss() {
891            match self.caches.get_or_try_insert_storage_with(account, storage_key, || {
892                self.state_provider.storage(account, storage_key).map(Option::unwrap_or_default)
893            })? {
894                CachedStatus::NotCached(value) => {
895                    self.record_storage_miss();
896                    Ok(nonzero_storage_value(value))
897                }
898                CachedStatus::Cached(value) => {
899                    self.record_storage_hit();
900                    Ok(nonzero_storage_value(value))
901                }
902            }
903        } else if let Some(value) = self.caches.0.storage_cache.get(&(account, storage_key)) {
904            self.record_storage_hit();
905            Ok(nonzero_storage_value(value))
906        } else {
907            self.record_storage_miss();
908            self.state_provider.storage(account, storage_key)
909        }
910    }
911
912    fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
913        if let Some(snapshot) = &self.txpool_snapshot {
914            if let Some(code) = snapshot.bytecode(code_hash) {
915                self.record_txpool_code_hit();
916                return Ok(code)
917            }
918            self.record_txpool_code_miss();
919        }
920
921        if self.should_fill_on_miss() {
922            match self.caches.get_or_try_insert_code_with(*code_hash, || {
923                self.state_provider.bytecode_by_hash(code_hash)
924            })? {
925                CachedStatus::NotCached(code) => {
926                    self.record_code_miss();
927                    Ok(code)
928                }
929                CachedStatus::Cached(code) => {
930                    self.record_code_hit();
931                    Ok(code)
932                }
933            }
934        } else if let Some(code) = self.caches.0.code_cache.get(code_hash) {
935            self.record_code_hit();
936            Ok(code)
937        } else {
938            self.record_code_miss();
939            self.state_provider.bytecode_by_hash(code_hash)
940        }
941    }
942
943    fn block_hash(&self, number: alloy_primitives::BlockNumber) -> ProviderResult<Option<B256>> {
944        self.state_provider.block_hash(number)
945    }
946}
947
948/// Execution cache used during block processing.
949///
950/// Optimizes state access by maintaining in-memory copies of frequently accessed
951/// accounts, storage slots, and bytecode. Works in conjunction with prewarming
952/// to reduce database I/O during block execution.
953///
954/// ## Storage Invalidation
955///
956/// Since EIP-6780, SELFDESTRUCT only works within the same transaction where the
957/// contract was created, so we don't need to handle clearing the storage.
958#[derive(Debug, Clone)]
959pub struct ExecutionCache(Arc<ExecutionCacheInner>);
960
961/// Inner state of the [`ExecutionCache`], wrapped in a single [`Arc`].
962#[derive(Debug)]
963struct ExecutionCacheInner {
964    /// Cache for contract bytecode, keyed by code hash.
965    code_cache: FixedCache<B256, Option<Bytecode>, FbBuildHasher<32>>,
966
967    /// Flat storage cache: maps `(Address, StorageKey)` to storage value.
968    storage_cache: FixedCache<(Address, StorageKey), StorageValue>,
969
970    /// Cache for basic account information (nonce, balance, code hash).
971    account_cache: FixedCache<Address, Option<Account>, FbBuildHasher<20>>,
972
973    /// Stats handler for the code cache (shared with the cache via [`Stats`]).
974    code_stats: Arc<CacheStatsHandler>,
975
976    /// Stats handler for the storage cache (shared with the cache via [`Stats`]).
977    storage_stats: Arc<CacheStatsHandler>,
978
979    /// Stats handler for the account cache (shared with the cache via [`Stats`]).
980    account_stats: Arc<CacheStatsHandler>,
981
982    /// One-time notification when SELFDESTRUCT is encountered
983    selfdestruct_encountered: Once,
984}
985
986impl ExecutionCache {
987    /// Minimum cache size required when epochs are enabled.
988    /// With EPOCHS=true, fixed-cache requires 12 bottom bits to be zero (2 needed + 10 epoch).
989    const MIN_CACHE_SIZE_WITH_EPOCHS: usize = 1 << 12; // 4096
990
991    /// Converts a byte size to number of cache entries, rounding down to a power of two.
992    ///
993    /// Fixed-cache requires power-of-two sizes for efficient indexing.
994    /// With epochs enabled, the minimum size is 4096 entries.
995    pub const fn bytes_to_entries(size_bytes: usize, entry_size: usize) -> usize {
996        let entries = size_bytes / entry_size;
997        // Round down to nearest power of two
998        let rounded = if entries == 0 { 1 } else { (entries + 1).next_power_of_two() >> 1 };
999        // Ensure minimum size for epoch tracking
1000        if rounded < Self::MIN_CACHE_SIZE_WITH_EPOCHS {
1001            Self::MIN_CACHE_SIZE_WITH_EPOCHS
1002        } else {
1003            rounded
1004        }
1005    }
1006
1007    /// Build an [`ExecutionCache`] struct, so that execution caches can be easily cloned.
1008    pub fn new(total_cache_size: usize) -> Self {
1009        let code_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
1010        let storage_cache_size = (total_cache_size * 8888) / 10000; // 88.88% of total
1011        let account_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
1012
1013        let code_capacity = Self::bytes_to_entries(code_cache_size, CODE_CACHE_ENTRY_SIZE);
1014        let storage_capacity = Self::bytes_to_entries(storage_cache_size, STORAGE_CACHE_ENTRY_SIZE);
1015        let account_capacity = Self::bytes_to_entries(account_cache_size, ACCOUNT_CACHE_ENTRY_SIZE);
1016
1017        let code_stats = Arc::new(CacheStatsHandler::new(code_capacity));
1018        let storage_stats = Arc::new(CacheStatsHandler::new(storage_capacity));
1019        let account_stats = Arc::new(CacheStatsHandler::new(account_capacity));
1020
1021        Self(Arc::new(ExecutionCacheInner {
1022            code_cache: FixedCache::new(code_capacity, FbBuildHasher::<32>::default())
1023                .with_stats(Some(Stats::new(code_stats.clone()))),
1024            storage_cache: FixedCache::new(storage_capacity, DefaultHashBuilder::default())
1025                .with_stats(Some(Stats::new(storage_stats.clone()))),
1026            account_cache: FixedCache::new(account_capacity, FbBuildHasher::<20>::default())
1027                .with_stats(Some(Stats::new(account_stats.clone()))),
1028            code_stats,
1029            storage_stats,
1030            account_stats,
1031            selfdestruct_encountered: Once::new(),
1032        }))
1033    }
1034
1035    /// Returns the number of active handles to the shared cache.
1036    fn usage_count(&self) -> usize {
1037        Arc::strong_count(&self.0)
1038    }
1039
1040    /// Gets code from cache, or inserts using the provided function.
1041    pub fn get_or_try_insert_code_with<E>(
1042        &self,
1043        hash: B256,
1044        f: impl FnOnce() -> Result<Option<Bytecode>, E>,
1045    ) -> Result<CachedStatus<Option<Bytecode>>, E> {
1046        let mut miss = false;
1047        let result = self.0.code_cache.get_or_try_insert_with(hash, |_| {
1048            miss = true;
1049            f()
1050        })?;
1051
1052        if miss {
1053            Ok(CachedStatus::NotCached(result))
1054        } else {
1055            Ok(CachedStatus::Cached(result))
1056        }
1057    }
1058
1059    /// Gets storage from cache, or inserts using the provided function.
1060    pub fn get_or_try_insert_storage_with<E>(
1061        &self,
1062        address: Address,
1063        key: StorageKey,
1064        f: impl FnOnce() -> Result<StorageValue, E>,
1065    ) -> Result<CachedStatus<StorageValue>, E> {
1066        let mut miss = false;
1067        let result = self.0.storage_cache.get_or_try_insert_with((address, key), |_| {
1068            miss = true;
1069            f()
1070        })?;
1071
1072        if miss {
1073            Ok(CachedStatus::NotCached(result))
1074        } else {
1075            Ok(CachedStatus::Cached(result))
1076        }
1077    }
1078
1079    /// Gets account from cache, or inserts using the provided function.
1080    pub fn get_or_try_insert_account_with<E>(
1081        &self,
1082        address: Address,
1083        f: impl FnOnce() -> Result<Option<Account>, E>,
1084    ) -> Result<CachedStatus<Option<Account>>, E> {
1085        let mut miss = false;
1086        let result = self.0.account_cache.get_or_try_insert_with(address, |_| {
1087            miss = true;
1088            f()
1089        })?;
1090
1091        if miss {
1092            Ok(CachedStatus::NotCached(result))
1093        } else {
1094            Ok(CachedStatus::Cached(result))
1095        }
1096    }
1097
1098    /// Insert storage value into cache.
1099    pub fn insert_storage(&self, address: Address, key: StorageKey, value: Option<StorageValue>) {
1100        self.0.storage_cache.insert((address, key), value.unwrap_or_default());
1101    }
1102
1103    /// Insert code into cache.
1104    pub fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
1105        self.0.code_cache.insert(hash, code);
1106    }
1107
1108    /// Insert account into cache.
1109    pub fn insert_account(&self, address: Address, account: Option<Account>) {
1110        self.0.account_cache.insert(address, account);
1111    }
1112
1113    /// Inserts the post-execution state changes into the cache.
1114    ///
1115    /// This method is called after transaction execution to update the cache with
1116    /// the touched and modified state. The insertion order is critical:
1117    ///
1118    /// 1. Bytecodes: Insert contract code first
1119    /// 2. Storage slots: Update storage values for each account
1120    /// 3. Accounts: Update account info (nonce, balance, code hash)
1121    ///
1122    /// ## Why This Order Matters
1123    ///
1124    /// Account information references bytecode via code hash. If we update accounts
1125    /// before bytecode, we might create cache entries pointing to non-existent code.
1126    /// The current order ensures cache consistency.
1127    ///
1128    /// ## Error Handling
1129    ///
1130    /// Returns an error if the state updates are inconsistent and should be discarded.
1131    #[instrument(level = "debug", target = "engine::caching", skip_all)]
1132    #[expect(clippy::result_unit_err)]
1133    pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> {
1134        let _enter =
1135            debug_span!(target: "engine::tree", "contracts", len = state_updates.contracts.len())
1136                .entered();
1137        // Insert bytecodes
1138        for (code_hash, bytecode) in &state_updates.contracts {
1139            self.insert_code(*code_hash, Some(Bytecode(bytecode.clone())));
1140        }
1141        drop(_enter);
1142
1143        let _enter = debug_span!(
1144            target: "engine::tree",
1145            "accounts",
1146            accounts = state_updates.state.len(),
1147            storages =
1148                state_updates.state.values().map(|account| account.storage.len()).sum::<usize>()
1149        )
1150        .entered();
1151        for (addr, account) in &state_updates.state {
1152            // If the account was not modified, as in not changed and not destroyed, then we have
1153            // nothing to do w.r.t. this particular account and can move on
1154            if account.status.is_not_modified() {
1155                continue
1156            }
1157
1158            // If the original account had code (was a contract), we must clear the entire cache
1159            // because we can't efficiently invalidate all storage slots for a single address.
1160            // This should only happen on pre-Dencun networks.
1161            //
1162            // If the original account had no code (was an EOA or a not yet deployed contract), we
1163            // just remove the account from cache - no storage exists for it.
1164            if account.was_destroyed() {
1165                let had_code =
1166                    account.original_info.as_ref().is_some_and(|info| !info.is_empty_code_hash());
1167                if had_code {
1168                    self.0.selfdestruct_encountered.call_once(|| {
1169                        warn!(
1170                            target: "engine::caching",
1171                            address = ?addr,
1172                            info = ?account.info,
1173                            original_info = ?account.original_info,
1174                            "Encountered an inter-transaction SELFDESTRUCT that reset the storage cache. Are you running a pre-Dencun network?"
1175                        );
1176                    });
1177                    self.clear();
1178                    return Ok(())
1179                }
1180
1181                self.0.account_cache.remove(addr);
1182                continue;
1183            }
1184
1185            // If we have an account that was modified, but it has a `None` account info, some wild
1186            // error has occurred because this state should be unrepresentable. An account with
1187            // `None` current info, should be destroyed.
1188            let Some(ref account_info) = account.info else {
1189                trace!(target: "engine::caching", ?account, "Account with None account info found in state updates");
1190                return Err(())
1191            };
1192
1193            // Now we iterate over all storage and make updates to the cached storage values
1194            for (key, slot) in &account.storage {
1195                self.insert_storage(*addr, (*key).into(), Some(slot.present_value));
1196            }
1197
1198            // Insert will update if present, so we just use the new account info as the new value
1199            // for the account cache
1200            self.insert_account(*addr, Some(Account::from(account_info)));
1201        }
1202
1203        Ok(())
1204    }
1205
1206    /// Clears storage and account caches, resetting them to empty state.
1207    ///
1208    /// We do not clear the bytecodes cache, because its mapping can never change, as it's
1209    /// `keccak256(bytecode) => bytecode`.
1210    pub fn clear(&self) {
1211        self.0.storage_cache.clear();
1212        self.0.account_cache.clear();
1213
1214        self.0.storage_stats.reset_size();
1215        self.0.account_stats.reset_size();
1216    }
1217
1218    /// Updates the provided metrics with the current stats from the cache's stats handlers,
1219    /// and resets the hit/miss/collision counters.
1220    pub fn update_metrics(&self, metrics: &CachedStateCacheMetrics) {
1221        metrics.code_cache_size.set(self.0.code_stats.size() as f64);
1222        metrics.code_cache_capacity.set(self.0.code_stats.capacity() as f64);
1223        metrics.code_cache_collisions.set(self.0.code_stats.collisions() as f64);
1224        self.0.code_stats.reset_stats();
1225
1226        metrics.storage_cache_size.set(self.0.storage_stats.size() as f64);
1227        metrics.storage_cache_capacity.set(self.0.storage_stats.capacity() as f64);
1228        metrics.storage_cache_collisions.set(self.0.storage_stats.collisions() as f64);
1229        self.0.storage_stats.reset_stats();
1230
1231        metrics.account_cache_size.set(self.0.account_stats.size() as f64);
1232        metrics.account_cache_capacity.set(self.0.account_stats.capacity() as f64);
1233        metrics.account_cache_collisions.set(self.0.account_stats.collisions() as f64);
1234        self.0.account_stats.reset_stats();
1235    }
1236}
1237
1238/// A saved cache that has been used for executing a specific block, which has been updated for its
1239/// execution.
1240#[derive(Debug, Clone)]
1241pub struct SavedCache {
1242    /// The hash of the block these caches were used to execute.
1243    hash: B256,
1244
1245    /// The caches used for the provider.
1246    caches: ExecutionCache,
1247}
1248
1249impl SavedCache {
1250    /// Creates a new instance with the internals
1251    pub const fn new(hash: B256, caches: ExecutionCache) -> Self {
1252        Self { hash, caches }
1253    }
1254
1255    /// Returns the hash for this cache
1256    pub const fn executed_block_hash(&self) -> B256 {
1257        self.hash
1258    }
1259
1260    /// Returns true if the cache is available for use (no other tasks are currently using it).
1261    pub fn is_available(&self) -> bool {
1262        self.caches.usage_count() == 1
1263    }
1264
1265    /// Returns the current number of active handles to the shared cache.
1266    pub fn usage_count(&self) -> usize {
1267        self.caches.usage_count()
1268    }
1269
1270    /// Returns the [`ExecutionCache`] belonging to the tracked hash.
1271    pub const fn cache(&self) -> &ExecutionCache {
1272        &self.caches
1273    }
1274
1275    /// Consumes the saved cache and returns its [`ExecutionCache`].
1276    pub fn into_cache(self) -> ExecutionCache {
1277        self.caches
1278    }
1279
1280    /// Returns whether `self` and `other` refer to the same [`ExecutionCache`] data,
1281    /// regardless of their block hashes.
1282    pub fn shares_cache_with(&self, other: &Self) -> bool {
1283        Arc::ptr_eq(&self.caches.0, &other.caches.0)
1284    }
1285
1286    /// Updates the cache metrics (size/capacity/collisions) from the stats handlers.
1287    pub fn update_metrics(&self, metrics: Option<&CachedStateCacheMetrics>) {
1288        if let Some(metrics) = metrics {
1289            self.caches.update_metrics(metrics);
1290        }
1291    }
1292
1293    /// Clears all caches, resetting them to empty state,
1294    /// and updates the hash of the block this cache belongs to.
1295    pub fn clear_with_hash(&mut self, hash: B256) {
1296        self.hash = hash;
1297        self.caches.clear();
1298    }
1299}
1300
1301#[cfg(any(test, feature = "test-utils"))]
1302impl SavedCache {
1303    /// Clones the cache handle that acts as the availability guard.
1304    pub fn clone_guard_for_test(&self) -> ExecutionCache {
1305        self.caches.clone()
1306    }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311    use super::*;
1312    use alloy_primitives::{map::HashMap, U256};
1313    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1314    use reth_revm::db::{AccountStatus, BundleAccount};
1315    use reth_storage_api::StateProvider;
1316    use revm::state::AccountInfo;
1317
1318    #[test]
1319    fn test_empty_storage_cached_state_provider() {
1320        let address = Address::random();
1321        let storage_key = StorageKey::random();
1322        let account = ExtendedAccount::new(0, U256::ZERO);
1323
1324        let provider = MockEthProvider::default();
1325        provider.extend_accounts(vec![(address, account)]);
1326
1327        let caches = ExecutionCache::new(1000);
1328        let state_provider = CachedStateProvider::new(
1329            provider.into_evm_state_provider(),
1330            caches,
1331            Some(CachedStateMetrics::zeroed(CachedStateMetricsSource::Test)),
1332        );
1333
1334        let res = state_provider.storage(address, storage_key);
1335        assert!(res.is_ok());
1336        assert_eq!(res.unwrap(), None);
1337    }
1338
1339    #[test]
1340    fn test_uncached_storage_cached_state_provider() {
1341        let address = Address::random();
1342        let storage_key = StorageKey::random();
1343        let storage_value = U256::from(1);
1344        let account =
1345            ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]);
1346
1347        let provider = MockEthProvider::default();
1348        provider.extend_accounts(vec![(address, account)]);
1349
1350        let caches = ExecutionCache::new(1000);
1351        let state_provider = CachedStateProvider::new(
1352            provider.into_evm_state_provider(),
1353            caches,
1354            Some(CachedStateMetrics::zeroed(CachedStateMetricsSource::Test)),
1355        );
1356
1357        let res = state_provider.storage(address, storage_key);
1358        assert!(res.is_ok());
1359        assert_eq!(res.unwrap(), Some(storage_value));
1360    }
1361
1362    #[test]
1363    fn test_get_storage_populated() {
1364        let address = Address::random();
1365        let storage_key = StorageKey::random();
1366        let storage_value = U256::from(1);
1367
1368        let caches = ExecutionCache::new(1000);
1369        caches.insert_storage(address, storage_key, Some(storage_value));
1370
1371        let result = caches
1372            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
1373        assert_eq!(result.unwrap(), CachedStatus::Cached(storage_value));
1374    }
1375
1376    #[test]
1377    fn test_get_storage_empty() {
1378        let address = Address::random();
1379        let storage_key = StorageKey::random();
1380
1381        let caches = ExecutionCache::new(1000);
1382        caches.insert_storage(address, storage_key, None);
1383
1384        let result = caches
1385            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
1386        assert_eq!(result.unwrap(), CachedStatus::Cached(U256::ZERO));
1387    }
1388
1389    #[test]
1390    fn test_saved_cache_is_available() {
1391        let execution_cache = ExecutionCache::new(1000);
1392        let cache = SavedCache::new(B256::ZERO, execution_cache);
1393
1394        assert!(cache.is_available(), "Cache should be available initially");
1395
1396        let _cache = cache.clone_guard_for_test();
1397
1398        assert!(!cache.is_available(), "Cache should not be available with active handle");
1399    }
1400
1401    #[test]
1402    fn test_saved_cache_multiple_references() {
1403        let execution_cache = ExecutionCache::new(1000);
1404        let cache = SavedCache::new(B256::from([2u8; 32]), execution_cache);
1405
1406        let cache1 = cache.clone_guard_for_test();
1407        let cache2 = cache.clone_guard_for_test();
1408        let cache3 = cache1.clone();
1409
1410        assert!(!cache.is_available());
1411
1412        drop(cache1);
1413        assert!(!cache.is_available());
1414
1415        drop(cache2);
1416        assert!(!cache.is_available());
1417
1418        drop(cache3);
1419        assert!(cache.is_available());
1420    }
1421
1422    #[test]
1423    fn test_insert_state_destroyed_account_with_code_clears_cache() {
1424        let caches = ExecutionCache::new(1000);
1425
1426        // Pre-populate caches with some data
1427        let addr1 = Address::random();
1428        let addr2 = Address::random();
1429        let storage_key = StorageKey::random();
1430        caches.insert_account(addr1, Some(Account::default()));
1431        caches.insert_account(addr2, Some(Account::default()));
1432        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1433
1434        // Verify caches are populated
1435        assert!(caches.0.account_cache.get(&addr1).is_some());
1436        assert!(caches.0.account_cache.get(&addr2).is_some());
1437        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1438
1439        let bundle = BundleState {
1440            // BundleState with a destroyed contract (had code)
1441            state: HashMap::from_iter([(
1442                Address::random(),
1443                BundleAccount::new(
1444                    Some(AccountInfo {
1445                        nonce: 1,
1446                        code_hash: B256::random(), // Non-empty code hash
1447                        code: None,
1448                        ..Default::default()
1449                    }),
1450                    None, // Destroyed, so no current info
1451                    Default::default(),
1452                    AccountStatus::Destroyed,
1453                ),
1454            )]),
1455            contracts: Default::default(),
1456            reverts: Default::default(),
1457            state_size: 0,
1458            reverts_size: 0,
1459        };
1460
1461        // Insert state should clear all caches because a contract was destroyed
1462        let result = caches.insert_state(&bundle);
1463        assert!(result.is_ok());
1464
1465        // Verify all caches were cleared
1466        assert!(caches.0.account_cache.get(&addr1).is_none());
1467        assert!(caches.0.account_cache.get(&addr2).is_none());
1468        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_none());
1469    }
1470
1471    #[test]
1472    fn test_insert_state_destroyed_account_without_code_removes_only_account() {
1473        let caches = ExecutionCache::new(1000);
1474
1475        // Pre-populate caches with some data
1476        let addr1 = Address::random();
1477        let addr2 = Address::random();
1478        let storage_key = StorageKey::random();
1479        caches.insert_account(addr1, Some(Account::default()));
1480        caches.insert_account(addr2, Some(Account::default()));
1481        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1482
1483        let bundle = BundleState {
1484            // BundleState with a destroyed EOA (no code)
1485            state: HashMap::from_iter([(
1486                addr1,
1487                BundleAccount::new(
1488                    Some(AccountInfo {
1489                        balance: U256::from(100),
1490                        nonce: 1,
1491                        code_hash: alloy_primitives::KECCAK256_EMPTY, // Empty code hash = EOA
1492                        code: None,
1493                        ..Default::default()
1494                    }),
1495                    None, // Destroyed
1496                    Default::default(),
1497                    AccountStatus::Destroyed,
1498                ),
1499            )]),
1500            contracts: Default::default(),
1501            reverts: Default::default(),
1502            state_size: 0,
1503            reverts_size: 0,
1504        };
1505
1506        // Insert state should only remove the destroyed account
1507        assert!(caches.insert_state(&bundle).is_ok());
1508
1509        // Verify only addr1 was removed, other data is still present
1510        assert!(caches.0.account_cache.get(&addr1).is_none());
1511        assert!(caches.0.account_cache.get(&addr2).is_some());
1512        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1513    }
1514
1515    #[test]
1516    fn test_insert_state_destroyed_account_no_original_info_removes_only_account() {
1517        let caches = ExecutionCache::new(1000);
1518
1519        // Pre-populate caches
1520        let addr1 = Address::random();
1521        let addr2 = Address::random();
1522        caches.insert_account(addr1, Some(Account::default()));
1523        caches.insert_account(addr2, Some(Account::default()));
1524
1525        let bundle = BundleState {
1526            // BundleState with a destroyed account (has no original info)
1527            state: HashMap::from_iter([(
1528                addr1,
1529                BundleAccount::new(
1530                    None, // No original info
1531                    None, // Destroyed
1532                    Default::default(),
1533                    AccountStatus::Destroyed,
1534                ),
1535            )]),
1536            contracts: Default::default(),
1537            reverts: Default::default(),
1538            state_size: 0,
1539            reverts_size: 0,
1540        };
1541
1542        // Insert state should only remove the destroyed account (no code = no full clear)
1543        assert!(caches.insert_state(&bundle).is_ok());
1544
1545        // Verify only addr1 was removed
1546        assert!(caches.0.account_cache.get(&addr1).is_none());
1547        assert!(caches.0.account_cache.get(&addr2).is_some());
1548    }
1549
1550    #[test]
1551    fn test_insert_state_destroyed_uncached_account_keeps_size_zero() {
1552        let caches = ExecutionCache::new(1000);
1553        assert_eq!(caches.0.account_stats.size(), 0);
1554
1555        let addr = Address::random();
1556        let bundle = BundleState {
1557            state: HashMap::from_iter([(
1558                addr,
1559                BundleAccount::new(
1560                    None, // No original info
1561                    None, // Destroyed
1562                    Default::default(),
1563                    AccountStatus::Destroyed,
1564                ),
1565            )]),
1566            contracts: Default::default(),
1567            reverts: Default::default(),
1568            state_size: 0,
1569            reverts_size: 0,
1570        };
1571
1572        assert!(caches.insert_state(&bundle).is_ok());
1573        assert_eq!(caches.0.account_stats.size(), 0);
1574        assert!(caches.0.account_cache.get(&addr).is_none());
1575    }
1576
1577    #[test]
1578    fn test_code_cache_capacity_with_default_budget() {
1579        // Default cross-block cache is 4 GB; code gets 5.56% = ~228 MB.
1580        let total_cache_size = 4 * 1024 * 1024 * 1024; // 4 GB
1581        let code_budget = (total_cache_size * 556) / 10000; // 228 MB
1582
1583        let capacity = ExecutionCache::bytes_to_entries(code_budget, CODE_CACHE_ENTRY_SIZE);
1584
1585        // With ESTIMATED_AVG_CODE_SIZE (8 KiB) we expect 16384 entries.
1586        // If someone accidentally reverts to MAX_CODE_SIZE (48 KiB), this would drop to 4096.
1587        assert_eq!(
1588            capacity, 16384,
1589            "code cache should have 16384 entries with default 4 GB budget"
1590        );
1591    }
1592}