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