Skip to main content

reth_execution_cache/
lib.rs

1//! Cross-block execution cache for payload processing.
2//!
3//! This crate provides the core caching infrastructure used during block execution:
4//! - [`ExecutionCache`]: Fixed-size concurrent caches for accounts, storage, and bytecode
5//! - [`SavedCache`]: An execution cache snapshot associated with a specific block hash
6//! - [`PayloadExecutionCache`]: Thread-safe wrapper for sharing cached state across payload
7//!   processing tasks
8//! - [`precompile_cache`]: Cross-block cache of precompile results
9
10#![doc(
11    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
12    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
13    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
14)]
15#![cfg_attr(docsrs, feature(doc_cfg))]
16#![cfg_attr(not(test), warn(unused_crate_dependencies))]
17
18mod cached_state;
19pub use cached_state::*;
20
21mod txpool;
22pub use txpool::*;
23
24pub mod precompile_cache;
25
26use alloy_primitives::B256;
27use metrics::{Counter, Histogram};
28use parking_lot::Mutex;
29use reth_metrics::Metrics;
30use reth_primitives_traits::FastInstant as Instant;
31use std::{sync::Arc, time::Duration};
32use tracing::{debug, instrument, warn};
33
34/// A guarded, thread-safe cache of execution state that tracks the most recent block's caches.
35///
36/// This is the cross-block cache used to accelerate sequential payload processing.
37/// When a new block arrives, its parent's cached state can be reused to avoid
38/// redundant database lookups.
39///
40/// This process assumes that payloads are received sequentially.
41///
42/// ## Cache Safety
43///
44/// **CRITICAL**: Cache update operations require exclusive access. All concurrent cache users
45/// (such as prewarming tasks) must be terminated before calling
46/// [`PayloadExecutionCache::update_with_guard`], otherwise the cache may be corrupted or cleared.
47#[derive(Clone, Debug, Default)]
48pub struct PayloadExecutionCache {
49    /// Guarded cloneable cache identified by a block hash.
50    inner: Arc<Mutex<Option<SavedCache>>>,
51    /// Metrics for cache operations.
52    metrics: PayloadExecutionCacheMetrics,
53}
54
55impl PayloadExecutionCache {
56    /// Returns the cache for `parent_hash` if it's available for use.
57    ///
58    /// A cache is considered available when:
59    /// - It exists and matches the requested parent hash
60    /// - No other tasks are currently using it (checked via Arc reference count)
61    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
62    pub fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
63        let start = Instant::now();
64        let mut cache = self.inner.lock();
65
66        let elapsed = start.elapsed();
67        self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
68        if elapsed.as_millis() > 5 {
69            warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
70        }
71
72        if let Some(c) = cache.as_mut() {
73            let cached_hash = c.executed_block_hash();
74            // Check that the cache hash matches the parent hash of the current block. It won't
75            // match in case it's a fork block.
76            let hash_matches = cached_hash == parent_hash;
77            // Check `is_available()` to ensure no other tasks (e.g., prewarming) currently hold
78            // a reference to this cache. We can only reuse it when we have exclusive access.
79            let available = c.is_available();
80            let usage_count = c.usage_count();
81
82            debug!(
83                target: "engine::caching",
84                %cached_hash,
85                %parent_hash,
86                hash_matches,
87                available,
88                usage_count,
89                "Existing cache found"
90            );
91
92            if available {
93                if !hash_matches {
94                    // Fork block: clear and update the hash on the ORIGINAL before cloning.
95                    // This prevents the canonical chain from matching on the stale hash
96                    // and picking up polluted data if the fork block fails.
97                    c.clear_with_hash(parent_hash);
98                }
99                return Some(c.clone())
100            } else if hash_matches {
101                self.metrics.execution_cache_in_use.increment(1);
102            }
103        } else {
104            debug!(target: "engine::caching", %parent_hash, "No cache found");
105        }
106
107        None
108    }
109
110    /// Waits for the mutex protecting the stored `Option<SavedCache>` to be released.
111    ///
112    /// This does not wait for other users to drop their [`ExecutionCache`] clones or for removed
113    /// caches to finish dropping after unlocking. A subsequent [`Self::get_cache_for`] can still
114    /// return `None`, causing its caller to allocate a new cache while those drops run.
115    ///
116    /// Returns only the time spent waiting for the mutex, excluding post-unlock cleanup.
117    pub fn wait_for_availability(&self) -> Duration {
118        let start = Instant::now();
119        // Acquire lock to wait for any current holders to finish
120        let _guard = self.inner.lock();
121        let elapsed = start.elapsed();
122        if elapsed.as_millis() > 5 {
123            debug!(
124                target: "engine::tree::payload_processor",
125                blocked_for=?elapsed,
126                "Waited for execution cache to become available"
127            );
128        }
129        elapsed
130    }
131
132    /// Runs `update_fn` with mutable access to the stored `Option<SavedCache>` under the mutex.
133    /// Returns the closure's result after releasing the mutex, allowing removed caches to be
134    /// dropped outside the lock.
135    ///
136    /// Drop extra [`SavedCache`] or [`ExecutionCache`] clones of the stored cache before the
137    /// closure returns: [`Self::get_cache_for`] requires that cache's Arc strong reference
138    /// count to be one.
139    ///
140    /// ## CRITICAL SAFETY REQUIREMENT
141    ///
142    /// **Before calling this method, you MUST ensure there are no other active cache users.**
143    /// This includes:
144    /// - No running prewarming task instances that could write to the cache
145    /// - No concurrent transactions that might access the cached state
146    /// - All prewarming operations must be completed or cancelled
147    ///
148    /// Violating this requirement can result in cache corruption, incorrect state data,
149    /// and potential consensus failures.
150    pub fn update_with_guard<F, R>(&self, update_fn: F) -> R
151    where
152        F: FnOnce(&mut Option<SavedCache>) -> R,
153    {
154        let mut guard = self.inner.lock();
155        update_fn(&mut guard)
156    }
157}
158
159/// Metrics for [`PayloadExecutionCache`] operations.
160#[derive(Metrics, Clone)]
161#[metrics(scope = "consensus.engine.beacon")]
162struct PayloadExecutionCacheMetrics {
163    /// Counter for when the execution cache was unavailable because other threads
164    /// (e.g., prewarming) are still using it.
165    execution_cache_in_use: Counter,
166    /// Time spent waiting for execution cache mutex to become available.
167    execution_cache_wait_duration: Histogram,
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn single_checkout_blocks_second() {
176        let cache = PayloadExecutionCache::default();
177        let hash = B256::from([1u8; 32]);
178
179        cache.update_with_guard(|slot| {
180            *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
181        });
182
183        let first = cache.get_cache_for(hash);
184        assert!(first.is_some());
185
186        let second = cache.get_cache_for(hash);
187        assert!(second.is_none());
188    }
189
190    #[test]
191    fn checkout_available_after_drop() {
192        let cache = PayloadExecutionCache::default();
193        let hash = B256::from([2u8; 32]);
194
195        cache.update_with_guard(|slot| {
196            *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
197        });
198
199        let checked_out = cache.get_cache_for(hash);
200        assert!(checked_out.is_some());
201        drop(checked_out);
202
203        let second = cache.get_cache_for(hash);
204        assert!(second.is_some());
205    }
206
207    #[test]
208    fn raw_cache_handle_blocks_checkout_until_drop() {
209        let cache = PayloadExecutionCache::default();
210        let hash = B256::from([3u8; 32]);
211
212        cache.update_with_guard(|slot| {
213            *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
214        });
215
216        let checked_out = cache.get_cache_for(hash).expect("checkout should succeed");
217        let cache_handle = checked_out.cache().clone();
218        drop(checked_out);
219
220        let blocked = cache.get_cache_for(hash);
221        assert!(blocked.is_none(), "raw ExecutionCache handle should keep slot in use");
222
223        drop(cache_handle);
224
225        let available = cache.get_cache_for(hash);
226        assert!(available.is_some(), "checkout should succeed after raw handle is dropped");
227    }
228
229    #[test]
230    fn hash_mismatch_clears_and_retags() {
231        let cache = PayloadExecutionCache::default();
232        let hash_a = B256::from([0xAA; 32]);
233        let hash_b = B256::from([0xBB; 32]);
234
235        cache.update_with_guard(|slot| {
236            *slot = Some(SavedCache::new(hash_a, ExecutionCache::new(1_000)))
237        });
238
239        let checked_out = cache.get_cache_for(hash_b);
240        assert!(checked_out.is_some());
241        assert_eq!(checked_out.unwrap().executed_block_hash(), hash_b);
242    }
243
244    #[test]
245    fn empty_cache_returns_none() {
246        let cache = PayloadExecutionCache::default();
247        assert!(cache.get_cache_for(B256::ZERO).is_none());
248    }
249}