Skip to main content

reth_execution_cache/
cached_state.rs

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