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
20use alloy_primitives::B256;
21use metrics::{Counter, Histogram};
22use parking_lot::Mutex;
23use reth_metrics::Metrics;
24use reth_primitives_traits::FastInstant as Instant;
25use std::{sync::Arc, time::Duration};
26use tracing::{debug, instrument, warn};
27
28#[derive(Clone, Debug, Default)]
42pub struct PayloadExecutionCache {
43 inner: Arc<Mutex<Option<SavedCache>>>,
45 metrics: PayloadExecutionCacheMetrics,
47}
48
49impl PayloadExecutionCache {
50 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
56 pub fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
57 let start = Instant::now();
58 let mut cache = self.inner.lock();
59
60 let elapsed = start.elapsed();
61 self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
62 if elapsed.as_millis() > 5 {
63 warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
64 }
65
66 if let Some(c) = cache.as_mut() {
67 let cached_hash = c.executed_block_hash();
68 let hash_matches = cached_hash == parent_hash;
71 let available = c.is_available();
74 let usage_count = c.usage_count();
75
76 debug!(
77 target: "engine::caching",
78 %cached_hash,
79 %parent_hash,
80 hash_matches,
81 available,
82 usage_count,
83 "Existing cache found"
84 );
85
86 if available {
87 if !hash_matches {
88 c.clear_with_hash(parent_hash);
92 }
93 return Some(c.clone())
94 } else if hash_matches {
95 self.metrics.execution_cache_in_use.increment(1);
96 }
97 } else {
98 debug!(target: "engine::caching", %parent_hash, "No cache found");
99 }
100
101 None
102 }
103
104 pub fn wait_for_availability(&self) -> Duration {
111 let start = Instant::now();
112 let _guard = self.inner.lock();
114 let elapsed = start.elapsed();
115 if elapsed.as_millis() > 5 {
116 debug!(
117 target: "engine::tree::payload_processor",
118 blocked_for=?elapsed,
119 "Waited for execution cache to become available"
120 );
121 }
122 elapsed
123 }
124
125 pub fn update_with_guard<F>(&self, update_fn: F)
139 where
140 F: FnOnce(&mut Option<SavedCache>),
141 {
142 let mut guard = self.inner.lock();
143 update_fn(&mut guard);
144 }
145}
146
147#[derive(Metrics, Clone)]
149#[metrics(scope = "consensus.engine.beacon")]
150struct PayloadExecutionCacheMetrics {
151 execution_cache_in_use: Counter,
154 execution_cache_wait_duration: Histogram,
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn single_checkout_blocks_second() {
164 let cache = PayloadExecutionCache::default();
165 let hash = B256::from([1u8; 32]);
166
167 cache.update_with_guard(|slot| {
168 *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
169 });
170
171 let first = cache.get_cache_for(hash);
172 assert!(first.is_some());
173
174 let second = cache.get_cache_for(hash);
175 assert!(second.is_none());
176 }
177
178 #[test]
179 fn checkout_available_after_drop() {
180 let cache = PayloadExecutionCache::default();
181 let hash = B256::from([2u8; 32]);
182
183 cache.update_with_guard(|slot| {
184 *slot = Some(SavedCache::new(hash, ExecutionCache::new(1_000)))
185 });
186
187 let checked_out = cache.get_cache_for(hash);
188 assert!(checked_out.is_some());
189 drop(checked_out);
190
191 let second = cache.get_cache_for(hash);
192 assert!(second.is_some());
193 }
194
195 #[test]
196 fn hash_mismatch_clears_and_retags() {
197 let cache = PayloadExecutionCache::default();
198 let hash_a = B256::from([0xAA; 32]);
199 let hash_b = B256::from([0xBB; 32]);
200
201 cache.update_with_guard(|slot| {
202 *slot = Some(SavedCache::new(hash_a, ExecutionCache::new(1_000)))
203 });
204
205 let checked_out = cache.get_cache_for(hash_b);
206 assert!(checked_out.is_some());
207 assert_eq!(checked_out.unwrap().executed_block_hash(), hash_b);
208 }
209
210 #[test]
211 fn empty_cache_returns_none() {
212 let cache = PayloadExecutionCache::default();
213 assert!(cache.get_cache_for(B256::ZERO).is_none());
214 }
215}