reth_execution_cache/
lib.rs1#![doc(
10 html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
11 html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
12 issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
13)]
14#![cfg_attr(docsrs, feature(doc_cfg))]
15#![cfg_attr(not(test), warn(unused_crate_dependencies))]
16
17mod cached_state;
18pub use cached_state::*;
19
20mod txpool;
21pub use txpool::*;
22
23use alloy_primitives::B256;
24use metrics::{Counter, Histogram};
25use parking_lot::Mutex;
26use reth_metrics::Metrics;
27use reth_primitives_traits::FastInstant as Instant;
28use std::{sync::Arc, time::Duration};
29use tracing::{debug, instrument, warn};
30
31#[derive(Clone, Debug, Default)]
45pub struct PayloadExecutionCache {
46 inner: Arc<Mutex<Option<SavedCache>>>,
48 metrics: PayloadExecutionCacheMetrics,
50}
51
52impl PayloadExecutionCache {
53 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
59 pub fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
60 let start = Instant::now();
61 let mut cache = self.inner.lock();
62
63 let elapsed = start.elapsed();
64 self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
65 if elapsed.as_millis() > 5 {
66 warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
67 }
68
69 if let Some(c) = cache.as_mut() {
70 let cached_hash = c.executed_block_hash();
71 let hash_matches = cached_hash == parent_hash;
74 let available = c.is_available();
77 let usage_count = c.usage_count();
78
79 debug!(
80 target: "engine::caching",
81 %cached_hash,
82 %parent_hash,
83 hash_matches,
84 available,
85 usage_count,
86 "Existing cache found"
87 );
88
89 if available {
90 if !hash_matches {
91 c.clear_with_hash(parent_hash);
95 }
96 return Some(c.clone())
97 } else if hash_matches {
98 self.metrics.execution_cache_in_use.increment(1);
99 }
100 } else {
101 debug!(target: "engine::caching", %parent_hash, "No cache found");
102 }
103
104 None
105 }
106
107 pub fn wait_for_availability(&self) -> Duration {
114 let start = Instant::now();
115 let _guard = self.inner.lock();
117 let elapsed = start.elapsed();
118 if elapsed.as_millis() > 5 {
119 debug!(
120 target: "engine::tree::payload_processor",
121 blocked_for=?elapsed,
122 "Waited for execution cache to become available"
123 );
124 }
125 elapsed
126 }
127
128 pub fn update_with_guard<F>(&self, update_fn: F)
142 where
143 F: FnOnce(&mut Option<SavedCache>),
144 {
145 let mut guard = self.inner.lock();
146 update_fn(&mut guard);
147 }
148}
149
150#[derive(Metrics, Clone)]
152#[metrics(scope = "consensus.engine.beacon")]
153struct PayloadExecutionCacheMetrics {
154 execution_cache_in_use: Counter,
157 execution_cache_wait_duration: Histogram,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn single_checkout_blocks_second() {
167 let cache = PayloadExecutionCache::default();
168 let hash = B256::from([1u8; 32]);
169
170 cache.update_with_guard(|slot| {
171 *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
172 });
173
174 let first = cache.get_cache_for(hash);
175 assert!(first.is_some());
176
177 let second = cache.get_cache_for(hash);
178 assert!(second.is_none());
179 }
180
181 #[test]
182 fn checkout_available_after_drop() {
183 let cache = PayloadExecutionCache::default();
184 let hash = B256::from([2u8; 32]);
185
186 cache.update_with_guard(|slot| {
187 *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
188 });
189
190 let checked_out = cache.get_cache_for(hash);
191 assert!(checked_out.is_some());
192 drop(checked_out);
193
194 let second = cache.get_cache_for(hash);
195 assert!(second.is_some());
196 }
197
198 #[test]
199 fn raw_cache_handle_blocks_checkout_until_drop() {
200 let cache = PayloadExecutionCache::default();
201 let hash = B256::from([3u8; 32]);
202
203 cache.update_with_guard(|slot| {
204 *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
205 });
206
207 let checked_out = cache.get_cache_for(hash).expect("checkout should succeed");
208 let cache_handle = checked_out.cache().clone();
209 drop(checked_out);
210
211 let blocked = cache.get_cache_for(hash);
212 assert!(blocked.is_none(), "raw ExecutionCache handle should keep slot in use");
213
214 drop(cache_handle);
215
216 let available = cache.get_cache_for(hash);
217 assert!(available.is_some(), "checkout should succeed after raw handle is dropped");
218 }
219
220 #[test]
221 fn hash_mismatch_clears_and_retags() {
222 let cache = PayloadExecutionCache::default();
223 let hash_a = B256::from([0xAA; 32]);
224 let hash_b = B256::from([0xBB; 32]);
225
226 cache.update_with_guard(|slot| {
227 *slot = Some(SavedCache::new(hash_a, ExecutionCache::new(1_000)))
228 });
229
230 let checked_out = cache.get_cache_for(hash_b);
231 assert!(checked_out.is_some());
232 assert_eq!(checked_out.unwrap().executed_block_hash(), hash_b);
233 }
234
235 #[test]
236 fn empty_cache_returns_none() {
237 let cache = PayloadExecutionCache::default();
238 assert!(cache.get_cache_for(B256::ZERO).is_none());
239 }
240}