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 witness(
1002        &self,
1003        input: TrieInput,
1004        target: HashedPostState,
1005        mode: reth_trie::ExecutionWitnessMode,
1006    ) -> ProviderResult<Vec<alloy_primitives::Bytes>> {
1007        self.state_provider.witness(input, target, mode)
1008    }
1009}
1010
1011impl<S: StorageRootProvider> StorageRootProvider for CachedStateProvider<S> {
1012    fn storage_root(
1013        &self,
1014        address: Address,
1015        hashed_storage: HashedStorage,
1016    ) -> ProviderResult<B256> {
1017        self.state_provider.storage_root(address, hashed_storage)
1018    }
1019
1020    fn storage_proof(
1021        &self,
1022        address: Address,
1023        slot: B256,
1024        hashed_storage: HashedStorage,
1025    ) -> ProviderResult<StorageProof> {
1026        self.state_provider.storage_proof(address, slot, hashed_storage)
1027    }
1028
1029    fn storage_multiproof(
1030        &self,
1031        address: Address,
1032        slots: &[B256],
1033        hashed_storage: HashedStorage,
1034    ) -> ProviderResult<StorageMultiProof> {
1035        self.state_provider.storage_multiproof(address, slots, hashed_storage)
1036    }
1037}
1038
1039impl<S: BlockHashReader> BlockHashReader for CachedStateProvider<S> {
1040    fn block_hash(&self, number: alloy_primitives::BlockNumber) -> ProviderResult<Option<B256>> {
1041        self.state_provider.block_hash(number)
1042    }
1043
1044    fn canonical_hashes_range(
1045        &self,
1046        start: alloy_primitives::BlockNumber,
1047        end: alloy_primitives::BlockNumber,
1048    ) -> ProviderResult<Vec<B256>> {
1049        self.state_provider.canonical_hashes_range(start, end)
1050    }
1051}
1052
1053impl<S: HashedPostStateProvider> HashedPostStateProvider for CachedStateProvider<S> {
1054    fn hashed_post_state(&self, bundle_state: &reth_revm::db::BundleState) -> HashedPostState {
1055        self.state_provider.hashed_post_state(bundle_state)
1056    }
1057}
1058
1059/// Execution cache used during block processing.
1060///
1061/// Optimizes state access by maintaining in-memory copies of frequently accessed
1062/// accounts, storage slots, and bytecode. Works in conjunction with prewarming
1063/// to reduce database I/O during block execution.
1064///
1065/// ## Storage Invalidation
1066///
1067/// Since EIP-6780, SELFDESTRUCT only works within the same transaction where the
1068/// contract was created, so we don't need to handle clearing the storage.
1069#[derive(Debug, Clone)]
1070pub struct ExecutionCache(Arc<ExecutionCacheInner>);
1071
1072/// Inner state of the [`ExecutionCache`], wrapped in a single [`Arc`].
1073#[derive(Debug)]
1074struct ExecutionCacheInner {
1075    /// Cache for contract bytecode, keyed by code hash.
1076    code_cache: FixedCache<B256, Option<Bytecode>, FbBuildHasher<32>>,
1077
1078    /// Flat storage cache: maps `(Address, StorageKey)` to storage value.
1079    storage_cache: FixedCache<(Address, StorageKey), StorageValue>,
1080
1081    /// Cache for basic account information (nonce, balance, code hash).
1082    account_cache: FixedCache<Address, Option<Account>, FbBuildHasher<20>>,
1083
1084    /// Stats handler for the code cache (shared with the cache via [`Stats`]).
1085    code_stats: Arc<CacheStatsHandler>,
1086
1087    /// Stats handler for the storage cache (shared with the cache via [`Stats`]).
1088    storage_stats: Arc<CacheStatsHandler>,
1089
1090    /// Stats handler for the account cache (shared with the cache via [`Stats`]).
1091    account_stats: Arc<CacheStatsHandler>,
1092
1093    /// One-time notification when SELFDESTRUCT is encountered
1094    selfdestruct_encountered: Once,
1095}
1096
1097impl ExecutionCache {
1098    /// Minimum cache size required when epochs are enabled.
1099    /// With EPOCHS=true, fixed-cache requires 12 bottom bits to be zero (2 needed + 10 epoch).
1100    const MIN_CACHE_SIZE_WITH_EPOCHS: usize = 1 << 12; // 4096
1101
1102    /// Converts a byte size to number of cache entries, rounding down to a power of two.
1103    ///
1104    /// Fixed-cache requires power-of-two sizes for efficient indexing.
1105    /// With epochs enabled, the minimum size is 4096 entries.
1106    pub const fn bytes_to_entries(size_bytes: usize, entry_size: usize) -> usize {
1107        let entries = size_bytes / entry_size;
1108        // Round down to nearest power of two
1109        let rounded = if entries == 0 { 1 } else { (entries + 1).next_power_of_two() >> 1 };
1110        // Ensure minimum size for epoch tracking
1111        if rounded < Self::MIN_CACHE_SIZE_WITH_EPOCHS {
1112            Self::MIN_CACHE_SIZE_WITH_EPOCHS
1113        } else {
1114            rounded
1115        }
1116    }
1117
1118    /// Build an [`ExecutionCache`] struct, so that execution caches can be easily cloned.
1119    pub fn new(total_cache_size: usize) -> Self {
1120        let code_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
1121        let storage_cache_size = (total_cache_size * 8888) / 10000; // 88.88% of total
1122        let account_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
1123
1124        let code_capacity = Self::bytes_to_entries(code_cache_size, CODE_CACHE_ENTRY_SIZE);
1125        let storage_capacity = Self::bytes_to_entries(storage_cache_size, STORAGE_CACHE_ENTRY_SIZE);
1126        let account_capacity = Self::bytes_to_entries(account_cache_size, ACCOUNT_CACHE_ENTRY_SIZE);
1127
1128        let code_stats = Arc::new(CacheStatsHandler::new(code_capacity));
1129        let storage_stats = Arc::new(CacheStatsHandler::new(storage_capacity));
1130        let account_stats = Arc::new(CacheStatsHandler::new(account_capacity));
1131
1132        Self(Arc::new(ExecutionCacheInner {
1133            code_cache: FixedCache::new(code_capacity, FbBuildHasher::<32>::default())
1134                .with_stats(Some(Stats::new(code_stats.clone()))),
1135            storage_cache: FixedCache::new(storage_capacity, DefaultHashBuilder::default())
1136                .with_stats(Some(Stats::new(storage_stats.clone()))),
1137            account_cache: FixedCache::new(account_capacity, FbBuildHasher::<20>::default())
1138                .with_stats(Some(Stats::new(account_stats.clone()))),
1139            code_stats,
1140            storage_stats,
1141            account_stats,
1142            selfdestruct_encountered: Once::new(),
1143        }))
1144    }
1145
1146    /// Returns the number of active handles to the shared cache.
1147    fn usage_count(&self) -> usize {
1148        Arc::strong_count(&self.0)
1149    }
1150
1151    /// Gets code from cache, or inserts using the provided function.
1152    pub fn get_or_try_insert_code_with<E>(
1153        &self,
1154        hash: B256,
1155        f: impl FnOnce() -> Result<Option<Bytecode>, E>,
1156    ) -> Result<CachedStatus<Option<Bytecode>>, E> {
1157        let mut miss = false;
1158        let result = self.0.code_cache.get_or_try_insert_with(hash, |_| {
1159            miss = true;
1160            f()
1161        })?;
1162
1163        if miss {
1164            Ok(CachedStatus::NotCached(result))
1165        } else {
1166            Ok(CachedStatus::Cached(result))
1167        }
1168    }
1169
1170    /// Gets storage from cache, or inserts using the provided function.
1171    pub fn get_or_try_insert_storage_with<E>(
1172        &self,
1173        address: Address,
1174        key: StorageKey,
1175        f: impl FnOnce() -> Result<StorageValue, E>,
1176    ) -> Result<CachedStatus<StorageValue>, E> {
1177        let mut miss = false;
1178        let result = self.0.storage_cache.get_or_try_insert_with((address, key), |_| {
1179            miss = true;
1180            f()
1181        })?;
1182
1183        if miss {
1184            Ok(CachedStatus::NotCached(result))
1185        } else {
1186            Ok(CachedStatus::Cached(result))
1187        }
1188    }
1189
1190    /// Gets account from cache, or inserts using the provided function.
1191    pub fn get_or_try_insert_account_with<E>(
1192        &self,
1193        address: Address,
1194        f: impl FnOnce() -> Result<Option<Account>, E>,
1195    ) -> Result<CachedStatus<Option<Account>>, E> {
1196        let mut miss = false;
1197        let result = self.0.account_cache.get_or_try_insert_with(address, |_| {
1198            miss = true;
1199            f()
1200        })?;
1201
1202        if miss {
1203            Ok(CachedStatus::NotCached(result))
1204        } else {
1205            Ok(CachedStatus::Cached(result))
1206        }
1207    }
1208
1209    /// Insert storage value into cache.
1210    pub fn insert_storage(&self, address: Address, key: StorageKey, value: Option<StorageValue>) {
1211        self.0.storage_cache.insert((address, key), value.unwrap_or_default());
1212    }
1213
1214    /// Insert code into cache.
1215    pub fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
1216        self.0.code_cache.insert(hash, code);
1217    }
1218
1219    /// Insert account into cache.
1220    pub fn insert_account(&self, address: Address, account: Option<Account>) {
1221        self.0.account_cache.insert(address, account);
1222    }
1223
1224    /// Inserts the post-execution state changes into the cache.
1225    ///
1226    /// This method is called after transaction execution to update the cache with
1227    /// the touched and modified state. The insertion order is critical:
1228    ///
1229    /// 1. Bytecodes: Insert contract code first
1230    /// 2. Storage slots: Update storage values for each account
1231    /// 3. Accounts: Update account info (nonce, balance, code hash)
1232    ///
1233    /// ## Why This Order Matters
1234    ///
1235    /// Account information references bytecode via code hash. If we update accounts
1236    /// before bytecode, we might create cache entries pointing to non-existent code.
1237    /// The current order ensures cache consistency.
1238    ///
1239    /// ## Error Handling
1240    ///
1241    /// Returns an error if the state updates are inconsistent and should be discarded.
1242    #[instrument(level = "debug", target = "engine::caching", skip_all)]
1243    #[expect(clippy::result_unit_err)]
1244    pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> {
1245        let _enter =
1246            debug_span!(target: "engine::tree", "contracts", len = state_updates.contracts.len())
1247                .entered();
1248        // Insert bytecodes
1249        for (code_hash, bytecode) in &state_updates.contracts {
1250            self.insert_code(*code_hash, Some(Bytecode(bytecode.clone())));
1251        }
1252        drop(_enter);
1253
1254        let _enter = debug_span!(
1255            target: "engine::tree",
1256            "accounts",
1257            accounts = state_updates.state.len(),
1258            storages =
1259                state_updates.state.values().map(|account| account.storage.len()).sum::<usize>()
1260        )
1261        .entered();
1262        for (addr, account) in &state_updates.state {
1263            // If the account was not modified, as in not changed and not destroyed, then we have
1264            // nothing to do w.r.t. this particular account and can move on
1265            if account.status.is_not_modified() {
1266                continue
1267            }
1268
1269            // If the original account had code (was a contract), we must clear the entire cache
1270            // because we can't efficiently invalidate all storage slots for a single address.
1271            // This should only happen on pre-Dencun networks.
1272            //
1273            // If the original account had no code (was an EOA or a not yet deployed contract), we
1274            // just remove the account from cache - no storage exists for it.
1275            if account.was_destroyed() {
1276                let had_code =
1277                    account.original_info.as_ref().is_some_and(|info| !info.is_empty_code_hash());
1278                if had_code {
1279                    self.0.selfdestruct_encountered.call_once(|| {
1280                        warn!(
1281                            target: "engine::caching",
1282                            address = ?addr,
1283                            info = ?account.info,
1284                            original_info = ?account.original_info,
1285                            "Encountered an inter-transaction SELFDESTRUCT that reset the storage cache. Are you running a pre-Dencun network?"
1286                        );
1287                    });
1288                    self.clear();
1289                    return Ok(())
1290                }
1291
1292                self.0.account_cache.remove(addr);
1293                continue;
1294            }
1295
1296            // If we have an account that was modified, but it has a `None` account info, some wild
1297            // error has occurred because this state should be unrepresentable. An account with
1298            // `None` current info, should be destroyed.
1299            let Some(ref account_info) = account.info else {
1300                trace!(target: "engine::caching", ?account, "Account with None account info found in state updates");
1301                return Err(())
1302            };
1303
1304            // Now we iterate over all storage and make updates to the cached storage values
1305            for (key, slot) in &account.storage {
1306                self.insert_storage(*addr, (*key).into(), Some(slot.present_value));
1307            }
1308
1309            // Insert will update if present, so we just use the new account info as the new value
1310            // for the account cache
1311            self.insert_account(*addr, Some(Account::from(account_info)));
1312        }
1313
1314        Ok(())
1315    }
1316
1317    /// Clears storage and account caches, resetting them to empty state.
1318    ///
1319    /// We do not clear the bytecodes cache, because its mapping can never change, as it's
1320    /// `keccak256(bytecode) => bytecode`.
1321    pub fn clear(&self) {
1322        self.0.storage_cache.clear();
1323        self.0.account_cache.clear();
1324
1325        self.0.storage_stats.reset_size();
1326        self.0.account_stats.reset_size();
1327    }
1328
1329    /// Updates the provided metrics with the current stats from the cache's stats handlers,
1330    /// and resets the hit/miss/collision counters.
1331    pub fn update_metrics(&self, metrics: &CachedStateCacheMetrics) {
1332        metrics.code_cache_size.set(self.0.code_stats.size() as f64);
1333        metrics.code_cache_capacity.set(self.0.code_stats.capacity() as f64);
1334        metrics.code_cache_collisions.set(self.0.code_stats.collisions() as f64);
1335        self.0.code_stats.reset_stats();
1336
1337        metrics.storage_cache_size.set(self.0.storage_stats.size() as f64);
1338        metrics.storage_cache_capacity.set(self.0.storage_stats.capacity() as f64);
1339        metrics.storage_cache_collisions.set(self.0.storage_stats.collisions() as f64);
1340        self.0.storage_stats.reset_stats();
1341
1342        metrics.account_cache_size.set(self.0.account_stats.size() as f64);
1343        metrics.account_cache_capacity.set(self.0.account_stats.capacity() as f64);
1344        metrics.account_cache_collisions.set(self.0.account_stats.collisions() as f64);
1345        self.0.account_stats.reset_stats();
1346    }
1347}
1348
1349/// A saved cache that has been used for executing a specific block, which has been updated for its
1350/// execution.
1351#[derive(Debug, Clone)]
1352pub struct SavedCache {
1353    /// The hash of the block these caches were used to execute.
1354    hash: B256,
1355
1356    /// The caches used for the provider.
1357    caches: ExecutionCache,
1358}
1359
1360impl SavedCache {
1361    /// Creates a new instance with the internals
1362    pub const fn new(hash: B256, caches: ExecutionCache) -> Self {
1363        Self { hash, caches }
1364    }
1365
1366    /// Returns the hash for this cache
1367    pub const fn executed_block_hash(&self) -> B256 {
1368        self.hash
1369    }
1370
1371    /// Returns true if the cache is available for use (no other tasks are currently using it).
1372    pub fn is_available(&self) -> bool {
1373        self.caches.usage_count() == 1
1374    }
1375
1376    /// Returns the current number of active handles to the shared cache.
1377    pub fn usage_count(&self) -> usize {
1378        self.caches.usage_count()
1379    }
1380
1381    /// Returns the [`ExecutionCache`] belonging to the tracked hash.
1382    pub const fn cache(&self) -> &ExecutionCache {
1383        &self.caches
1384    }
1385
1386    /// Updates the cache metrics (size/capacity/collisions) from the stats handlers.
1387    pub fn update_metrics(&self, metrics: Option<&CachedStateCacheMetrics>) {
1388        if let Some(metrics) = metrics {
1389            self.caches.update_metrics(metrics);
1390        }
1391    }
1392
1393    /// Clears all caches, resetting them to empty state,
1394    /// and updates the hash of the block this cache belongs to.
1395    pub fn clear_with_hash(&mut self, hash: B256) {
1396        self.hash = hash;
1397        self.caches.clear();
1398    }
1399}
1400
1401#[cfg(any(test, feature = "test-utils"))]
1402impl SavedCache {
1403    /// Clones the cache handle that acts as the availability guard.
1404    pub fn clone_guard_for_test(&self) -> ExecutionCache {
1405        self.caches.clone()
1406    }
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411    use super::*;
1412    use alloy_primitives::{map::HashMap, U256};
1413    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1414    use reth_revm::db::{AccountStatus, BundleAccount};
1415    use revm::state::AccountInfo;
1416
1417    #[test]
1418    fn test_empty_storage_cached_state_provider() {
1419        let address = Address::random();
1420        let storage_key = StorageKey::random();
1421        let account = ExtendedAccount::new(0, U256::ZERO);
1422
1423        let provider = MockEthProvider::default();
1424        provider.extend_accounts(vec![(address, account)]);
1425
1426        let caches = ExecutionCache::new(1000);
1427        let state_provider = CachedStateProvider::new(
1428            provider,
1429            caches,
1430            Some(CachedStateMetrics::zeroed(CachedStateMetricsSource::Test)),
1431        );
1432
1433        let res = state_provider.storage(address, storage_key);
1434        assert!(res.is_ok());
1435        assert_eq!(res.unwrap(), None);
1436    }
1437
1438    #[test]
1439    fn test_uncached_storage_cached_state_provider() {
1440        let address = Address::random();
1441        let storage_key = StorageKey::random();
1442        let storage_value = U256::from(1);
1443        let account =
1444            ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]);
1445
1446        let provider = MockEthProvider::default();
1447        provider.extend_accounts(vec![(address, account)]);
1448
1449        let caches = ExecutionCache::new(1000);
1450        let state_provider = CachedStateProvider::new(
1451            provider,
1452            caches,
1453            Some(CachedStateMetrics::zeroed(CachedStateMetricsSource::Test)),
1454        );
1455
1456        let res = state_provider.storage(address, storage_key);
1457        assert!(res.is_ok());
1458        assert_eq!(res.unwrap(), Some(storage_value));
1459    }
1460
1461    #[test]
1462    fn test_get_storage_populated() {
1463        let address = Address::random();
1464        let storage_key = StorageKey::random();
1465        let storage_value = U256::from(1);
1466
1467        let caches = ExecutionCache::new(1000);
1468        caches.insert_storage(address, storage_key, Some(storage_value));
1469
1470        let result = caches
1471            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
1472        assert_eq!(result.unwrap(), CachedStatus::Cached(storage_value));
1473    }
1474
1475    #[test]
1476    fn test_get_storage_empty() {
1477        let address = Address::random();
1478        let storage_key = StorageKey::random();
1479
1480        let caches = ExecutionCache::new(1000);
1481        caches.insert_storage(address, storage_key, None);
1482
1483        let result = caches
1484            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
1485        assert_eq!(result.unwrap(), CachedStatus::Cached(U256::ZERO));
1486    }
1487
1488    #[test]
1489    fn test_saved_cache_is_available() {
1490        let execution_cache = ExecutionCache::new(1000);
1491        let cache = SavedCache::new(B256::ZERO, execution_cache);
1492
1493        assert!(cache.is_available(), "Cache should be available initially");
1494
1495        let _cache = cache.clone_guard_for_test();
1496
1497        assert!(!cache.is_available(), "Cache should not be available with active handle");
1498    }
1499
1500    #[test]
1501    fn test_saved_cache_multiple_references() {
1502        let execution_cache = ExecutionCache::new(1000);
1503        let cache = SavedCache::new(B256::from([2u8; 32]), execution_cache);
1504
1505        let cache1 = cache.clone_guard_for_test();
1506        let cache2 = cache.clone_guard_for_test();
1507        let cache3 = cache1.clone();
1508
1509        assert!(!cache.is_available());
1510
1511        drop(cache1);
1512        assert!(!cache.is_available());
1513
1514        drop(cache2);
1515        assert!(!cache.is_available());
1516
1517        drop(cache3);
1518        assert!(cache.is_available());
1519    }
1520
1521    #[test]
1522    fn test_insert_state_destroyed_account_with_code_clears_cache() {
1523        let caches = ExecutionCache::new(1000);
1524
1525        // Pre-populate caches with some data
1526        let addr1 = Address::random();
1527        let addr2 = Address::random();
1528        let storage_key = StorageKey::random();
1529        caches.insert_account(addr1, Some(Account::default()));
1530        caches.insert_account(addr2, Some(Account::default()));
1531        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1532
1533        // Verify caches are populated
1534        assert!(caches.0.account_cache.get(&addr1).is_some());
1535        assert!(caches.0.account_cache.get(&addr2).is_some());
1536        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1537
1538        let bundle = BundleState {
1539            // BundleState with a destroyed contract (had code)
1540            state: HashMap::from_iter([(
1541                Address::random(),
1542                BundleAccount::new(
1543                    Some(AccountInfo {
1544                        balance: U256::ZERO,
1545                        nonce: 1,
1546                        code_hash: B256::random(), // Non-empty code hash
1547                        code: None,
1548                        account_id: None,
1549                    }),
1550                    None, // Destroyed, so no current info
1551                    Default::default(),
1552                    AccountStatus::Destroyed,
1553                ),
1554            )]),
1555            contracts: Default::default(),
1556            reverts: Default::default(),
1557            state_size: 0,
1558            reverts_size: 0,
1559        };
1560
1561        // Insert state should clear all caches because a contract was destroyed
1562        let result = caches.insert_state(&bundle);
1563        assert!(result.is_ok());
1564
1565        // Verify all caches were cleared
1566        assert!(caches.0.account_cache.get(&addr1).is_none());
1567        assert!(caches.0.account_cache.get(&addr2).is_none());
1568        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_none());
1569    }
1570
1571    #[test]
1572    fn test_insert_state_destroyed_account_without_code_removes_only_account() {
1573        let caches = ExecutionCache::new(1000);
1574
1575        // Pre-populate caches with some data
1576        let addr1 = Address::random();
1577        let addr2 = Address::random();
1578        let storage_key = StorageKey::random();
1579        caches.insert_account(addr1, Some(Account::default()));
1580        caches.insert_account(addr2, Some(Account::default()));
1581        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1582
1583        let bundle = BundleState {
1584            // BundleState with a destroyed EOA (no code)
1585            state: HashMap::from_iter([(
1586                addr1,
1587                BundleAccount::new(
1588                    Some(AccountInfo {
1589                        balance: U256::from(100),
1590                        nonce: 1,
1591                        code_hash: alloy_primitives::KECCAK256_EMPTY, // Empty code hash = EOA
1592                        code: None,
1593                        account_id: None,
1594                    }),
1595                    None, // Destroyed
1596                    Default::default(),
1597                    AccountStatus::Destroyed,
1598                ),
1599            )]),
1600            contracts: Default::default(),
1601            reverts: Default::default(),
1602            state_size: 0,
1603            reverts_size: 0,
1604        };
1605
1606        // Insert state should only remove the destroyed account
1607        assert!(caches.insert_state(&bundle).is_ok());
1608
1609        // Verify only addr1 was removed, other data is still present
1610        assert!(caches.0.account_cache.get(&addr1).is_none());
1611        assert!(caches.0.account_cache.get(&addr2).is_some());
1612        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1613    }
1614
1615    #[test]
1616    fn test_insert_state_destroyed_account_no_original_info_removes_only_account() {
1617        let caches = ExecutionCache::new(1000);
1618
1619        // Pre-populate caches
1620        let addr1 = Address::random();
1621        let addr2 = Address::random();
1622        caches.insert_account(addr1, Some(Account::default()));
1623        caches.insert_account(addr2, Some(Account::default()));
1624
1625        let bundle = BundleState {
1626            // BundleState with a destroyed account (has no original info)
1627            state: HashMap::from_iter([(
1628                addr1,
1629                BundleAccount::new(
1630                    None, // No original info
1631                    None, // Destroyed
1632                    Default::default(),
1633                    AccountStatus::Destroyed,
1634                ),
1635            )]),
1636            contracts: Default::default(),
1637            reverts: Default::default(),
1638            state_size: 0,
1639            reverts_size: 0,
1640        };
1641
1642        // Insert state should only remove the destroyed account (no code = no full clear)
1643        assert!(caches.insert_state(&bundle).is_ok());
1644
1645        // Verify only addr1 was removed
1646        assert!(caches.0.account_cache.get(&addr1).is_none());
1647        assert!(caches.0.account_cache.get(&addr2).is_some());
1648    }
1649
1650    #[test]
1651    fn test_insert_state_destroyed_uncached_account_keeps_size_zero() {
1652        let caches = ExecutionCache::new(1000);
1653        assert_eq!(caches.0.account_stats.size(), 0);
1654
1655        let addr = Address::random();
1656        let bundle = BundleState {
1657            state: HashMap::from_iter([(
1658                addr,
1659                BundleAccount::new(
1660                    None, // No original info
1661                    None, // Destroyed
1662                    Default::default(),
1663                    AccountStatus::Destroyed,
1664                ),
1665            )]),
1666            contracts: Default::default(),
1667            reverts: Default::default(),
1668            state_size: 0,
1669            reverts_size: 0,
1670        };
1671
1672        assert!(caches.insert_state(&bundle).is_ok());
1673        assert_eq!(caches.0.account_stats.size(), 0);
1674        assert!(caches.0.account_cache.get(&addr).is_none());
1675    }
1676
1677    #[test]
1678    fn test_code_cache_capacity_with_default_budget() {
1679        // Default cross-block cache is 4 GB; code gets 5.56% = ~228 MB.
1680        let total_cache_size = 4 * 1024 * 1024 * 1024; // 4 GB
1681        let code_budget = (total_cache_size * 556) / 10000; // 228 MB
1682
1683        let capacity = ExecutionCache::bytes_to_entries(code_budget, CODE_CACHE_ENTRY_SIZE);
1684
1685        // With ESTIMATED_AVG_CODE_SIZE (8 KiB) we expect 16384 entries.
1686        // If someone accidentally reverts to MAX_CODE_SIZE (48 KiB), this would drop to 4096.
1687        assert_eq!(
1688            capacity, 16384,
1689            "code cache should have 16384 entries with default 4 GB budget"
1690        );
1691    }
1692}