reth_execution_cache/
lib.rs1#![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#[derive(Clone, Debug, Default)]
48pub struct PayloadExecutionCache {
49 inner: Arc<Mutex<Option<SavedCache>>>,
51 metrics: PayloadExecutionCacheMetrics,
53}
54
55impl PayloadExecutionCache {
56 #[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 let hash_matches = cached_hash == parent_hash;
77 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 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 pub fn wait_for_availability(&self) -> Duration {
118 let start = Instant::now();
119 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 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#[derive(Metrics, Clone)]
161#[metrics(scope = "consensus.engine.beacon")]
162struct PayloadExecutionCacheMetrics {
163 execution_cache_in_use: Counter,
166 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}