Skip to main content

reth_execution_cache/
cached_state.rs

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