Skip to main content

reth_engine_tree/tree/
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 revm_primitives::eip7907::MAX_CODE_SIZE;
22use std::{
23    mem::size_of,
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/// Size in bytes of a single code cache entry.
60const CODE_CACHE_ENTRY_SIZE: usize = fixed_cache_key_size_with_value::<Address>(MAX_CODE_SIZE);
61
62/// Size in bytes of a single storage cache entry.
63const STORAGE_CACHE_ENTRY_SIZE: usize =
64    fixed_cache_entry_size::<(Address, StorageKey), StorageValue>();
65
66/// Size in bytes of a single account cache entry.
67const ACCOUNT_CACHE_ENTRY_SIZE: usize = fixed_cache_entry_size::<Address, Option<Account>>();
68
69/// Cache configuration with epoch tracking enabled for O(1) cache invalidation.
70struct EpochCacheConfig;
71impl CacheConfig for EpochCacheConfig {
72    const EPOCHS: bool = true;
73}
74
75/// Type alias for the fixed-cache used for accounts and storage.
76type FixedCache<K, V, H = DefaultHashBuilder> = fixed_cache::Cache<K, V, H, EpochCacheConfig>;
77
78/// A wrapper of a state provider and a shared cache.
79///
80/// The const generic `PREWARM` controls whether every cache miss is populated. This is only
81/// relevant for pre-warm transaction execution with the intention to pre-populate the cache with
82/// data for regular block execution. During regular block execution the cache doesn't need to be
83/// populated because the actual EVM database [`State`](revm::database::State) also caches
84/// internally during block execution and the cache is then updated after the block with the entire
85/// [`BundleState`] output of that block which contains all accessed accounts, code, storage. See
86/// also [`ExecutionCache::insert_state`].
87#[derive(Debug)]
88pub struct CachedStateProvider<S, const PREWARM: bool = false> {
89    /// The state provider
90    state_provider: S,
91
92    /// The caches used for the provider
93    caches: ExecutionCache,
94
95    /// Metrics for the cached state provider
96    metrics: CachedStateMetrics,
97}
98
99impl<S> CachedStateProvider<S> {
100    /// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and
101    /// [`CachedStateMetrics`].
102    pub const fn new(
103        state_provider: S,
104        caches: ExecutionCache,
105        metrics: CachedStateMetrics,
106    ) -> Self {
107        Self { state_provider, caches, metrics }
108    }
109}
110
111impl<S> CachedStateProvider<S, true> {
112    /// Creates a new [`CachedStateProvider`] with prewarming enabled.
113    pub const fn new_prewarm(
114        state_provider: S,
115        caches: ExecutionCache,
116        metrics: CachedStateMetrics,
117    ) -> Self {
118        Self { state_provider, caches, metrics }
119    }
120}
121
122/// Metrics for the cached state provider, showing hits / misses / size for each cache.
123///
124/// This struct combines both the provider-level metrics (hits/misses tracked by the provider)
125/// and the fixed-cache internal stats (collisions, size, capacity).
126#[derive(Metrics, Clone)]
127#[metrics(scope = "sync.caching")]
128pub struct CachedStateMetrics {
129    /// Number of times a new execution cache was created
130    execution_cache_created_total: Counter,
131
132    /// Duration of execution cache creation in seconds
133    execution_cache_creation_duration_seconds: Histogram,
134
135    /// Code cache hits
136    code_cache_hits: Gauge,
137
138    /// Code cache misses
139    code_cache_misses: Gauge,
140
141    /// Code cache size (number of entries)
142    code_cache_size: Gauge,
143
144    /// Code cache capacity (maximum entries)
145    code_cache_capacity: Gauge,
146
147    /// Code cache collisions (hash collisions causing eviction)
148    code_cache_collisions: Gauge,
149
150    /// Storage cache hits
151    storage_cache_hits: Gauge,
152
153    /// Storage cache misses
154    storage_cache_misses: Gauge,
155
156    /// Storage cache size (number of entries)
157    storage_cache_size: Gauge,
158
159    /// Storage cache capacity (maximum entries)
160    storage_cache_capacity: Gauge,
161
162    /// Storage cache collisions (hash collisions causing eviction)
163    storage_cache_collisions: Gauge,
164
165    /// Account cache hits
166    account_cache_hits: Gauge,
167
168    /// Account cache misses
169    account_cache_misses: Gauge,
170
171    /// Account cache size (number of entries)
172    account_cache_size: Gauge,
173
174    /// Account cache capacity (maximum entries)
175    account_cache_capacity: Gauge,
176
177    /// Account cache collisions (hash collisions causing eviction)
178    account_cache_collisions: Gauge,
179}
180
181impl CachedStateMetrics {
182    /// Sets all values to zero, indicating that a new block is being executed.
183    pub fn reset(&self) {
184        // code cache
185        self.code_cache_hits.set(0);
186        self.code_cache_misses.set(0);
187        self.code_cache_collisions.set(0);
188
189        // storage cache
190        self.storage_cache_hits.set(0);
191        self.storage_cache_misses.set(0);
192        self.storage_cache_collisions.set(0);
193
194        // account cache
195        self.account_cache_hits.set(0);
196        self.account_cache_misses.set(0);
197        self.account_cache_collisions.set(0);
198    }
199
200    /// Returns a new zeroed-out instance of [`CachedStateMetrics`].
201    pub fn zeroed() -> Self {
202        let zeroed = Self::default();
203        zeroed.reset();
204        zeroed
205    }
206
207    /// Records a new execution cache creation with its duration.
208    pub(crate) fn record_cache_creation(&self, duration: Duration) {
209        self.execution_cache_created_total.increment(1);
210        self.execution_cache_creation_duration_seconds.record(duration.as_secs_f64());
211    }
212}
213
214/// A stats handler for fixed-cache that tracks collisions and size.
215///
216/// Note: Hits and misses are tracked directly by the [`CachedStateProvider`] via
217/// [`CachedStateMetrics`], not here. The stats handler is used for:
218/// - Collision detection (hash collisions causing eviction of a different key)
219/// - Size tracking
220///
221/// ## Size Tracking
222///
223/// Size is tracked via `on_insert` and `on_remove` callbacks:
224/// - `on_insert`: increment size only when inserting into an empty bucket (no eviction)
225/// - `on_remove`: always decrement size
226///
227/// Collisions (evicting a different key) don't change size since they replace an existing entry.
228#[derive(Debug)]
229pub(crate) struct CacheStatsHandler {
230    collisions: AtomicU64,
231    size: AtomicUsize,
232    capacity: usize,
233}
234
235impl CacheStatsHandler {
236    /// Creates a new stats handler with all counters initialized to zero.
237    pub(crate) const fn new(capacity: usize) -> Self {
238        Self { collisions: AtomicU64::new(0), size: AtomicUsize::new(0), capacity }
239    }
240
241    /// Returns the number of cache collisions.
242    pub(crate) fn collisions(&self) -> u64 {
243        self.collisions.load(Ordering::Relaxed)
244    }
245
246    /// Returns the current size (number of entries).
247    pub(crate) fn size(&self) -> usize {
248        self.size.load(Ordering::Relaxed)
249    }
250
251    /// Returns the capacity (maximum number of entries).
252    pub(crate) const fn capacity(&self) -> usize {
253        self.capacity
254    }
255
256    /// Increments the size counter. Called on cache insert.
257    pub(crate) fn increment_size(&self) {
258        let _ = self.size.fetch_add(1, Ordering::Relaxed);
259    }
260
261    /// Decrements the size counter. Called on cache remove.
262    pub(crate) fn decrement_size(&self) {
263        let _ = self.size.fetch_sub(1, Ordering::Relaxed);
264    }
265
266    /// Resets size to zero. Called on cache clear.
267    pub(crate) fn reset_size(&self) {
268        self.size.store(0, Ordering::Relaxed);
269    }
270
271    /// Resets collision counter to zero (but not size).
272    pub(crate) fn reset_stats(&self) {
273        self.collisions.store(0, Ordering::Relaxed);
274    }
275}
276
277impl<K: PartialEq, V> StatsHandler<K, V> for CacheStatsHandler {
278    fn on_hit(&self, _key: &K, _value: &V) {}
279
280    fn on_miss(&self, _key: AnyRef<'_>) {}
281
282    fn on_insert(&self, key: &K, _value: &V, evicted: Option<(&K, &V)>) {
283        match evicted {
284            None => {
285                // Inserting into an empty bucket
286                self.increment_size();
287            }
288            Some((evicted_key, _)) if evicted_key != key => {
289                // Collision: evicting a different key
290                self.collisions.fetch_add(1, Ordering::Relaxed);
291            }
292            Some(_) => {
293                // Updating the same key, size unchanged
294            }
295        }
296    }
297
298    fn on_remove(&self, _key: &K, _value: &V) {
299        self.decrement_size();
300    }
301}
302
303impl<S: AccountReader, const PREWARM: bool> AccountReader for CachedStateProvider<S, PREWARM> {
304    fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
305        if PREWARM {
306            match self.caches.get_or_try_insert_account_with(*address, || {
307                self.state_provider.basic_account(address)
308            })? {
309                CachedStatus::NotCached(value) | CachedStatus::Cached(value) => Ok(value),
310            }
311        } else if let Some(account) = self.caches.0.account_cache.get(address) {
312            self.metrics.account_cache_hits.increment(1);
313            Ok(account)
314        } else {
315            self.metrics.account_cache_misses.increment(1);
316            self.state_provider.basic_account(address)
317        }
318    }
319}
320
321/// Represents the status of a key in the cache.
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub enum CachedStatus<T> {
324    /// The key is not in the cache (or was invalidated). The value was recalculated.
325    NotCached(T),
326    /// The key exists in cache and has a specific value.
327    Cached(T),
328}
329
330impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvider<S, PREWARM> {
331    fn storage(
332        &self,
333        account: Address,
334        storage_key: StorageKey,
335    ) -> ProviderResult<Option<StorageValue>> {
336        if PREWARM {
337            match self.caches.get_or_try_insert_storage_with(account, storage_key, || {
338                self.state_provider.storage(account, storage_key).map(Option::unwrap_or_default)
339            })? {
340                CachedStatus::NotCached(value) | CachedStatus::Cached(value) => {
341                    // The slot that was never written to is indistinguishable from a slot
342                    // explicitly set to zero. We return `None` in both cases.
343                    Ok(Some(value).filter(|v| !v.is_zero()))
344                }
345            }
346        } else if let Some(value) = self.caches.0.storage_cache.get(&(account, storage_key)) {
347            self.metrics.storage_cache_hits.increment(1);
348            Ok(Some(value).filter(|v| !v.is_zero()))
349        } else {
350            self.metrics.storage_cache_misses.increment(1);
351            self.state_provider.storage(account, storage_key)
352        }
353    }
354
355    fn storage_by_hashed_key(
356        &self,
357        address: Address,
358        hashed_storage_key: StorageKey,
359    ) -> ProviderResult<Option<StorageValue>> {
360        self.state_provider.storage_by_hashed_key(address, hashed_storage_key)
361    }
362}
363
364impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvider<S, PREWARM> {
365    fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
366        if PREWARM {
367            match self.caches.get_or_try_insert_code_with(*code_hash, || {
368                self.state_provider.bytecode_by_hash(code_hash)
369            })? {
370                CachedStatus::NotCached(code) | CachedStatus::Cached(code) => Ok(code),
371            }
372        } else if let Some(code) = self.caches.0.code_cache.get(code_hash) {
373            self.metrics.code_cache_hits.increment(1);
374            Ok(code)
375        } else {
376            self.metrics.code_cache_misses.increment(1);
377            self.state_provider.bytecode_by_hash(code_hash)
378        }
379    }
380}
381
382impl<S: StateRootProvider, const PREWARM: bool> StateRootProvider
383    for CachedStateProvider<S, PREWARM>
384{
385    fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult<B256> {
386        self.state_provider.state_root(hashed_state)
387    }
388
389    fn state_root_from_nodes(&self, input: TrieInput) -> ProviderResult<B256> {
390        self.state_provider.state_root_from_nodes(input)
391    }
392
393    fn state_root_with_updates(
394        &self,
395        hashed_state: HashedPostState,
396    ) -> ProviderResult<(B256, TrieUpdates)> {
397        self.state_provider.state_root_with_updates(hashed_state)
398    }
399
400    fn state_root_from_nodes_with_updates(
401        &self,
402        input: TrieInput,
403    ) -> ProviderResult<(B256, TrieUpdates)> {
404        self.state_provider.state_root_from_nodes_with_updates(input)
405    }
406}
407
408impl<S: StateProofProvider, const PREWARM: bool> StateProofProvider
409    for CachedStateProvider<S, PREWARM>
410{
411    fn proof(
412        &self,
413        input: TrieInput,
414        address: Address,
415        slots: &[B256],
416    ) -> ProviderResult<AccountProof> {
417        self.state_provider.proof(input, address, slots)
418    }
419
420    fn multiproof(
421        &self,
422        input: TrieInput,
423        targets: MultiProofTargets,
424    ) -> ProviderResult<MultiProof> {
425        self.state_provider.multiproof(input, targets)
426    }
427
428    fn witness(
429        &self,
430        input: TrieInput,
431        target: HashedPostState,
432    ) -> ProviderResult<Vec<alloy_primitives::Bytes>> {
433        self.state_provider.witness(input, target)
434    }
435}
436
437impl<S: StorageRootProvider, const PREWARM: bool> StorageRootProvider
438    for CachedStateProvider<S, PREWARM>
439{
440    fn storage_root(
441        &self,
442        address: Address,
443        hashed_storage: HashedStorage,
444    ) -> ProviderResult<B256> {
445        self.state_provider.storage_root(address, hashed_storage)
446    }
447
448    fn storage_proof(
449        &self,
450        address: Address,
451        slot: B256,
452        hashed_storage: HashedStorage,
453    ) -> ProviderResult<StorageProof> {
454        self.state_provider.storage_proof(address, slot, hashed_storage)
455    }
456
457    fn storage_multiproof(
458        &self,
459        address: Address,
460        slots: &[B256],
461        hashed_storage: HashedStorage,
462    ) -> ProviderResult<StorageMultiProof> {
463        self.state_provider.storage_multiproof(address, slots, hashed_storage)
464    }
465}
466
467impl<S: BlockHashReader, const PREWARM: bool> BlockHashReader for CachedStateProvider<S, PREWARM> {
468    fn block_hash(&self, number: alloy_primitives::BlockNumber) -> ProviderResult<Option<B256>> {
469        self.state_provider.block_hash(number)
470    }
471
472    fn canonical_hashes_range(
473        &self,
474        start: alloy_primitives::BlockNumber,
475        end: alloy_primitives::BlockNumber,
476    ) -> ProviderResult<Vec<B256>> {
477        self.state_provider.canonical_hashes_range(start, end)
478    }
479}
480
481impl<S: HashedPostStateProvider, const PREWARM: bool> HashedPostStateProvider
482    for CachedStateProvider<S, PREWARM>
483{
484    fn hashed_post_state(&self, bundle_state: &reth_revm::db::BundleState) -> HashedPostState {
485        self.state_provider.hashed_post_state(bundle_state)
486    }
487}
488
489/// Execution cache used during block processing.
490///
491/// Optimizes state access by maintaining in-memory copies of frequently accessed
492/// accounts, storage slots, and bytecode. Works in conjunction with prewarming
493/// to reduce database I/O during block execution.
494///
495/// ## Storage Invalidation
496///
497/// Since EIP-6780, SELFDESTRUCT only works within the same transaction where the
498/// contract was created, so we don't need to handle clearing the storage.
499#[derive(Debug, Clone)]
500pub struct ExecutionCache(Arc<ExecutionCacheInner>);
501
502/// Inner state of the [`ExecutionCache`], wrapped in a single [`Arc`].
503#[derive(Debug)]
504struct ExecutionCacheInner {
505    /// Cache for contract bytecode, keyed by code hash.
506    code_cache: FixedCache<B256, Option<Bytecode>, FbBuildHasher<32>>,
507
508    /// Flat storage cache: maps `(Address, StorageKey)` to storage value.
509    storage_cache: FixedCache<(Address, StorageKey), StorageValue>,
510
511    /// Cache for basic account information (nonce, balance, code hash).
512    account_cache: FixedCache<Address, Option<Account>, FbBuildHasher<20>>,
513
514    /// Stats handler for the code cache (shared with the cache via [`Stats`]).
515    code_stats: Arc<CacheStatsHandler>,
516
517    /// Stats handler for the storage cache (shared with the cache via [`Stats`]).
518    storage_stats: Arc<CacheStatsHandler>,
519
520    /// Stats handler for the account cache (shared with the cache via [`Stats`]).
521    account_stats: Arc<CacheStatsHandler>,
522
523    /// One-time notification when SELFDESTRUCT is encountered
524    selfdestruct_encountered: Once,
525}
526
527impl ExecutionCache {
528    /// Minimum cache size required when epochs are enabled.
529    /// With EPOCHS=true, fixed-cache requires 12 bottom bits to be zero (2 needed + 10 epoch).
530    const MIN_CACHE_SIZE_WITH_EPOCHS: usize = 1 << 12; // 4096
531
532    /// Converts a byte size to number of cache entries, rounding down to a power of two.
533    ///
534    /// Fixed-cache requires power-of-two sizes for efficient indexing.
535    /// With epochs enabled, the minimum size is 4096 entries.
536    pub const fn bytes_to_entries(size_bytes: usize, entry_size: usize) -> usize {
537        let entries = size_bytes / entry_size;
538        // Round down to nearest power of two
539        let rounded = if entries == 0 { 1 } else { (entries + 1).next_power_of_two() >> 1 };
540        // Ensure minimum size for epoch tracking
541        if rounded < Self::MIN_CACHE_SIZE_WITH_EPOCHS {
542            Self::MIN_CACHE_SIZE_WITH_EPOCHS
543        } else {
544            rounded
545        }
546    }
547
548    /// Build an [`ExecutionCache`] struct, so that execution caches can be easily cloned.
549    pub fn new(total_cache_size: usize) -> Self {
550        let code_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
551        let storage_cache_size = (total_cache_size * 8888) / 10000; // 88.88% of total
552        let account_cache_size = (total_cache_size * 556) / 10000; // 5.56% of total
553
554        let code_capacity = Self::bytes_to_entries(code_cache_size, CODE_CACHE_ENTRY_SIZE);
555        let storage_capacity = Self::bytes_to_entries(storage_cache_size, STORAGE_CACHE_ENTRY_SIZE);
556        let account_capacity = Self::bytes_to_entries(account_cache_size, ACCOUNT_CACHE_ENTRY_SIZE);
557
558        let code_stats = Arc::new(CacheStatsHandler::new(code_capacity));
559        let storage_stats = Arc::new(CacheStatsHandler::new(storage_capacity));
560        let account_stats = Arc::new(CacheStatsHandler::new(account_capacity));
561
562        Self(Arc::new(ExecutionCacheInner {
563            code_cache: FixedCache::new(code_capacity, FbBuildHasher::<32>::default())
564                .with_stats(Some(Stats::new(code_stats.clone()))),
565            storage_cache: FixedCache::new(storage_capacity, DefaultHashBuilder::default())
566                .with_stats(Some(Stats::new(storage_stats.clone()))),
567            account_cache: FixedCache::new(account_capacity, FbBuildHasher::<20>::default())
568                .with_stats(Some(Stats::new(account_stats.clone()))),
569            code_stats,
570            storage_stats,
571            account_stats,
572            selfdestruct_encountered: Once::new(),
573        }))
574    }
575
576    /// Gets code from cache, or inserts using the provided function.
577    pub fn get_or_try_insert_code_with<E>(
578        &self,
579        hash: B256,
580        f: impl FnOnce() -> Result<Option<Bytecode>, E>,
581    ) -> Result<CachedStatus<Option<Bytecode>>, E> {
582        let mut miss = false;
583        let result = self.0.code_cache.get_or_try_insert_with(hash, |_| {
584            miss = true;
585            f()
586        })?;
587
588        if miss {
589            Ok(CachedStatus::NotCached(result))
590        } else {
591            Ok(CachedStatus::Cached(result))
592        }
593    }
594
595    /// Gets storage from cache, or inserts using the provided function.
596    pub fn get_or_try_insert_storage_with<E>(
597        &self,
598        address: Address,
599        key: StorageKey,
600        f: impl FnOnce() -> Result<StorageValue, E>,
601    ) -> Result<CachedStatus<StorageValue>, E> {
602        let mut miss = false;
603        let result = self.0.storage_cache.get_or_try_insert_with((address, key), |_| {
604            miss = true;
605            f()
606        })?;
607
608        if miss {
609            Ok(CachedStatus::NotCached(result))
610        } else {
611            Ok(CachedStatus::Cached(result))
612        }
613    }
614
615    /// Gets account from cache, or inserts using the provided function.
616    pub fn get_or_try_insert_account_with<E>(
617        &self,
618        address: Address,
619        f: impl FnOnce() -> Result<Option<Account>, E>,
620    ) -> Result<CachedStatus<Option<Account>>, E> {
621        let mut miss = false;
622        let result = self.0.account_cache.get_or_try_insert_with(address, |_| {
623            miss = true;
624            f()
625        })?;
626
627        if miss {
628            Ok(CachedStatus::NotCached(result))
629        } else {
630            Ok(CachedStatus::Cached(result))
631        }
632    }
633
634    /// Insert storage value into cache.
635    pub fn insert_storage(&self, address: Address, key: StorageKey, value: Option<StorageValue>) {
636        self.0.storage_cache.insert((address, key), value.unwrap_or_default());
637    }
638
639    /// Insert code into cache.
640    fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
641        self.0.code_cache.insert(hash, code);
642    }
643
644    /// Insert account into cache.
645    fn insert_account(&self, address: Address, account: Option<Account>) {
646        self.0.account_cache.insert(address, account);
647    }
648
649    /// Inserts the post-execution state changes into the cache.
650    ///
651    /// This method is called after transaction execution to update the cache with
652    /// the touched and modified state. The insertion order is critical:
653    ///
654    /// 1. Bytecodes: Insert contract code first
655    /// 2. Storage slots: Update storage values for each account
656    /// 3. Accounts: Update account info (nonce, balance, code hash)
657    ///
658    /// ## Why This Order Matters
659    ///
660    /// Account information references bytecode via code hash. If we update accounts
661    /// before bytecode, we might create cache entries pointing to non-existent code.
662    /// The current order ensures cache consistency.
663    ///
664    /// ## Error Handling
665    ///
666    /// Returns an error if the state updates are inconsistent and should be discarded.
667    #[instrument(level = "debug", target = "engine::caching", skip_all)]
668    #[expect(clippy::result_unit_err)]
669    pub fn insert_state(&self, state_updates: &BundleState) -> Result<(), ()> {
670        let _enter =
671            debug_span!(target: "engine::tree", "contracts", len = state_updates.contracts.len())
672                .entered();
673        // Insert bytecodes
674        for (code_hash, bytecode) in &state_updates.contracts {
675            self.insert_code(*code_hash, Some(Bytecode(bytecode.clone())));
676        }
677        drop(_enter);
678
679        let _enter = debug_span!(
680            target: "engine::tree",
681            "accounts",
682            accounts = state_updates.state.len(),
683            storages =
684                state_updates.state.values().map(|account| account.storage.len()).sum::<usize>()
685        )
686        .entered();
687        for (addr, account) in &state_updates.state {
688            // If the account was not modified, as in not changed and not destroyed, then we have
689            // nothing to do w.r.t. this particular account and can move on
690            if account.status.is_not_modified() {
691                continue
692            }
693
694            // If the original account had code (was a contract), we must clear the entire cache
695            // because we can't efficiently invalidate all storage slots for a single address.
696            // This should only happen on pre-Dencun networks.
697            //
698            // If the original account had no code (was an EOA or a not yet deployed contract), we
699            // just remove the account from cache - no storage exists for it.
700            if account.was_destroyed() {
701                let had_code =
702                    account.original_info.as_ref().is_some_and(|info| !info.is_empty_code_hash());
703                if had_code {
704                    self.0.selfdestruct_encountered.call_once(|| {
705                        warn!(
706                            target: "engine::caching",
707                            address = ?addr,
708                            info = ?account.info,
709                            original_info = ?account.original_info,
710                            "Encountered an inter-transaction SELFDESTRUCT that reset the storage cache. Are you running a pre-Dencun network?"
711                        );
712                    });
713                    self.clear();
714                    return Ok(())
715                }
716
717                self.0.account_cache.remove(addr);
718                continue
719            }
720
721            // If we have an account that was modified, but it has a `None` account info, some wild
722            // error has occurred because this state should be unrepresentable. An account with
723            // `None` current info, should be destroyed.
724            let Some(ref account_info) = account.info else {
725                trace!(target: "engine::caching", ?account, "Account with None account info found in state updates");
726                return Err(())
727            };
728
729            // Now we iterate over all storage and make updates to the cached storage values
730            for (key, slot) in &account.storage {
731                self.insert_storage(*addr, (*key).into(), Some(slot.present_value));
732            }
733
734            // Insert will update if present, so we just use the new account info as the new value
735            // for the account cache
736            self.insert_account(*addr, Some(Account::from(account_info)));
737        }
738
739        Ok(())
740    }
741
742    /// Clears storage and account caches, resetting them to empty state.
743    ///
744    /// We do not clear the bytecodes cache, because its mapping can never change, as it's
745    /// `keccak256(bytecode) => bytecode`.
746    pub(crate) fn clear(&self) {
747        self.0.storage_cache.clear();
748        self.0.account_cache.clear();
749
750        self.0.storage_stats.reset_size();
751        self.0.account_stats.reset_size();
752    }
753
754    /// Updates the provided metrics with the current stats from the cache's stats handlers,
755    /// and resets the hit/miss/collision counters.
756    pub(crate) fn update_metrics(&self, metrics: &CachedStateMetrics) {
757        metrics.code_cache_size.set(self.0.code_stats.size() as f64);
758        metrics.code_cache_capacity.set(self.0.code_stats.capacity() as f64);
759        metrics.code_cache_collisions.set(self.0.code_stats.collisions() as f64);
760        self.0.code_stats.reset_stats();
761
762        metrics.storage_cache_size.set(self.0.storage_stats.size() as f64);
763        metrics.storage_cache_capacity.set(self.0.storage_stats.capacity() as f64);
764        metrics.storage_cache_collisions.set(self.0.storage_stats.collisions() as f64);
765        self.0.storage_stats.reset_stats();
766
767        metrics.account_cache_size.set(self.0.account_stats.size() as f64);
768        metrics.account_cache_capacity.set(self.0.account_stats.capacity() as f64);
769        metrics.account_cache_collisions.set(self.0.account_stats.collisions() as f64);
770        self.0.account_stats.reset_stats();
771    }
772}
773
774/// A saved cache that has been used for executing a specific block, which has been updated for its
775/// execution.
776#[derive(Debug, Clone)]
777pub struct SavedCache {
778    /// The hash of the block these caches were used to execute.
779    hash: B256,
780
781    /// The caches used for the provider.
782    caches: ExecutionCache,
783
784    /// Metrics for the cached state provider (includes size/capacity/collisions from fixed-cache)
785    metrics: CachedStateMetrics,
786
787    /// A guard to track in-flight usage of this cache.
788    /// The cache is considered available if the strong count is 1.
789    usage_guard: Arc<()>,
790
791    /// Whether to skip cache metrics recording (can be expensive with large cached state).
792    disable_cache_metrics: bool,
793}
794
795impl SavedCache {
796    /// Creates a new instance with the internals
797    pub fn new(hash: B256, caches: ExecutionCache, metrics: CachedStateMetrics) -> Self {
798        Self { hash, caches, metrics, usage_guard: Arc::new(()), disable_cache_metrics: false }
799    }
800
801    /// Sets whether to disable cache metrics recording.
802    pub const fn with_disable_cache_metrics(mut self, disable: bool) -> Self {
803        self.disable_cache_metrics = disable;
804        self
805    }
806
807    /// Returns the hash for this cache
808    pub const fn executed_block_hash(&self) -> B256 {
809        self.hash
810    }
811
812    /// Splits the cache into its caches, metrics, and `disable_cache_metrics` flag, consuming it.
813    pub fn split(self) -> (ExecutionCache, CachedStateMetrics, bool) {
814        (self.caches, self.metrics, self.disable_cache_metrics)
815    }
816
817    /// Returns true if the cache is available for use (no other tasks are currently using it).
818    pub fn is_available(&self) -> bool {
819        Arc::strong_count(&self.usage_guard) == 1
820    }
821
822    /// Returns the current strong count of the usage guard.
823    pub fn usage_count(&self) -> usize {
824        Arc::strong_count(&self.usage_guard)
825    }
826
827    /// Returns the [`ExecutionCache`] belonging to the tracked hash.
828    pub const fn cache(&self) -> &ExecutionCache {
829        &self.caches
830    }
831
832    /// Returns the metrics associated with this cache.
833    pub const fn metrics(&self) -> &CachedStateMetrics {
834        &self.metrics
835    }
836
837    /// Updates the cache metrics (size/capacity/collisions) from the stats handlers.
838    ///
839    /// Note: This can be expensive with large cached state. Use
840    /// `with_disable_cache_metrics(true)` to skip.
841    pub(crate) fn update_metrics(&self) {
842        if self.disable_cache_metrics {
843            return
844        }
845        self.caches.update_metrics(&self.metrics);
846    }
847
848    /// Clears all caches, resetting them to empty state.
849    pub(crate) fn clear(&self) {
850        self.caches.clear();
851    }
852}
853
854#[cfg(test)]
855impl SavedCache {
856    fn clone_guard_for_test(&self) -> Arc<()> {
857        self.usage_guard.clone()
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864    use alloy_primitives::{map::HashMap, U256};
865    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
866    use reth_revm::db::{AccountStatus, BundleAccount};
867    use revm_state::AccountInfo;
868
869    #[test]
870    fn test_empty_storage_cached_state_provider() {
871        let address = Address::random();
872        let storage_key = StorageKey::random();
873        let account = ExtendedAccount::new(0, U256::ZERO);
874
875        let provider = MockEthProvider::default();
876        provider.extend_accounts(vec![(address, account)]);
877
878        let caches = ExecutionCache::new(1000);
879        let state_provider =
880            CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
881
882        let res = state_provider.storage(address, storage_key);
883        assert!(res.is_ok());
884        assert_eq!(res.unwrap(), None);
885    }
886
887    #[test]
888    fn test_uncached_storage_cached_state_provider() {
889        let address = Address::random();
890        let storage_key = StorageKey::random();
891        let storage_value = U256::from(1);
892        let account =
893            ExtendedAccount::new(0, U256::ZERO).extend_storage(vec![(storage_key, storage_value)]);
894
895        let provider = MockEthProvider::default();
896        provider.extend_accounts(vec![(address, account)]);
897
898        let caches = ExecutionCache::new(1000);
899        let state_provider =
900            CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
901
902        let res = state_provider.storage(address, storage_key);
903        assert!(res.is_ok());
904        assert_eq!(res.unwrap(), Some(storage_value));
905    }
906
907    #[test]
908    fn test_get_storage_populated() {
909        let address = Address::random();
910        let storage_key = StorageKey::random();
911        let storage_value = U256::from(1);
912
913        let caches = ExecutionCache::new(1000);
914        caches.insert_storage(address, storage_key, Some(storage_value));
915
916        let result = caches
917            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
918        assert_eq!(result.unwrap(), CachedStatus::Cached(storage_value));
919    }
920
921    #[test]
922    fn test_get_storage_empty() {
923        let address = Address::random();
924        let storage_key = StorageKey::random();
925
926        let caches = ExecutionCache::new(1000);
927        caches.insert_storage(address, storage_key, None);
928
929        let result = caches
930            .get_or_try_insert_storage_with(address, storage_key, || Ok::<_, ()>(U256::from(999)));
931        assert_eq!(result.unwrap(), CachedStatus::Cached(U256::ZERO));
932    }
933
934    #[test]
935    fn test_saved_cache_is_available() {
936        let execution_cache = ExecutionCache::new(1000);
937        let cache = SavedCache::new(B256::ZERO, execution_cache, CachedStateMetrics::zeroed());
938
939        assert!(cache.is_available(), "Cache should be available initially");
940
941        let _guard = cache.clone_guard_for_test();
942
943        assert!(!cache.is_available(), "Cache should not be available with active guard");
944    }
945
946    #[test]
947    fn test_saved_cache_multiple_references() {
948        let execution_cache = ExecutionCache::new(1000);
949        let cache =
950            SavedCache::new(B256::from([2u8; 32]), execution_cache, CachedStateMetrics::zeroed());
951
952        let guard1 = cache.clone_guard_for_test();
953        let guard2 = cache.clone_guard_for_test();
954        let guard3 = guard1.clone();
955
956        assert!(!cache.is_available());
957
958        drop(guard1);
959        assert!(!cache.is_available());
960
961        drop(guard2);
962        assert!(!cache.is_available());
963
964        drop(guard3);
965        assert!(cache.is_available());
966    }
967
968    #[test]
969    fn test_insert_state_destroyed_account_with_code_clears_cache() {
970        let caches = ExecutionCache::new(1000);
971
972        // Pre-populate caches with some data
973        let addr1 = Address::random();
974        let addr2 = Address::random();
975        let storage_key = StorageKey::random();
976        caches.insert_account(addr1, Some(Account::default()));
977        caches.insert_account(addr2, Some(Account::default()));
978        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
979
980        // Verify caches are populated
981        assert!(caches.0.account_cache.get(&addr1).is_some());
982        assert!(caches.0.account_cache.get(&addr2).is_some());
983        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
984
985        let bundle = BundleState {
986            // BundleState with a destroyed contract (had code)
987            state: HashMap::from_iter([(
988                Address::random(),
989                BundleAccount::new(
990                    Some(AccountInfo {
991                        balance: U256::ZERO,
992                        nonce: 1,
993                        code_hash: B256::random(), // Non-empty code hash
994                        code: None,
995                        account_id: None,
996                    }),
997                    None, // Destroyed, so no current info
998                    Default::default(),
999                    AccountStatus::Destroyed,
1000                ),
1001            )]),
1002            contracts: Default::default(),
1003            reverts: Default::default(),
1004            state_size: 0,
1005            reverts_size: 0,
1006        };
1007
1008        // Insert state should clear all caches because a contract was destroyed
1009        let result = caches.insert_state(&bundle);
1010        assert!(result.is_ok());
1011
1012        // Verify all caches were cleared
1013        assert!(caches.0.account_cache.get(&addr1).is_none());
1014        assert!(caches.0.account_cache.get(&addr2).is_none());
1015        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_none());
1016    }
1017
1018    #[test]
1019    fn test_insert_state_destroyed_account_without_code_removes_only_account() {
1020        let caches = ExecutionCache::new(1000);
1021
1022        // Pre-populate caches with some data
1023        let addr1 = Address::random();
1024        let addr2 = Address::random();
1025        let storage_key = StorageKey::random();
1026        caches.insert_account(addr1, Some(Account::default()));
1027        caches.insert_account(addr2, Some(Account::default()));
1028        caches.insert_storage(addr1, storage_key, Some(U256::from(42)));
1029
1030        let bundle = BundleState {
1031            // BundleState with a destroyed EOA (no code)
1032            state: HashMap::from_iter([(
1033                addr1,
1034                BundleAccount::new(
1035                    Some(AccountInfo {
1036                        balance: U256::from(100),
1037                        nonce: 1,
1038                        code_hash: alloy_primitives::KECCAK256_EMPTY, // Empty code hash = EOA
1039                        code: None,
1040                        account_id: None,
1041                    }),
1042                    None, // Destroyed
1043                    Default::default(),
1044                    AccountStatus::Destroyed,
1045                ),
1046            )]),
1047            contracts: Default::default(),
1048            reverts: Default::default(),
1049            state_size: 0,
1050            reverts_size: 0,
1051        };
1052
1053        // Insert state should only remove the destroyed account
1054        assert!(caches.insert_state(&bundle).is_ok());
1055
1056        // Verify only addr1 was removed, other data is still present
1057        assert!(caches.0.account_cache.get(&addr1).is_none());
1058        assert!(caches.0.account_cache.get(&addr2).is_some());
1059        assert!(caches.0.storage_cache.get(&(addr1, storage_key)).is_some());
1060    }
1061
1062    #[test]
1063    fn test_insert_state_destroyed_account_no_original_info_removes_only_account() {
1064        let caches = ExecutionCache::new(1000);
1065
1066        // Pre-populate caches
1067        let addr1 = Address::random();
1068        let addr2 = Address::random();
1069        caches.insert_account(addr1, Some(Account::default()));
1070        caches.insert_account(addr2, Some(Account::default()));
1071
1072        let bundle = BundleState {
1073            // BundleState with a destroyed account (has no original info)
1074            state: HashMap::from_iter([(
1075                addr1,
1076                BundleAccount::new(
1077                    None, // No original info
1078                    None, // Destroyed
1079                    Default::default(),
1080                    AccountStatus::Destroyed,
1081                ),
1082            )]),
1083            contracts: Default::default(),
1084            reverts: Default::default(),
1085            state_size: 0,
1086            reverts_size: 0,
1087        };
1088
1089        // Insert state should only remove the destroyed account (no code = no full clear)
1090        assert!(caches.insert_state(&bundle).is_ok());
1091
1092        // Verify only addr1 was removed
1093        assert!(caches.0.account_cache.get(&addr1).is_none());
1094        assert!(caches.0.account_cache.get(&addr2).is_some());
1095    }
1096}