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::RwLock;
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<RwLock<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.write();
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.write();
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.write();
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(
169 hash,
170 ExecutionCache::new(1_000),
171 CachedStateMetrics::zeroed(),
172 ))
173 });
174
175 let first = cache.get_cache_for(hash);
176 assert!(first.is_some());
177
178 let second = cache.get_cache_for(hash);
179 assert!(second.is_none());
180 }
181
182 #[test]
183 fn checkout_available_after_drop() {
184 let cache = PayloadExecutionCache::default();
185 let hash = B256::from([2u8; 32]);
186
187 cache.update_with_guard(|slot| {
188 *slot = Some(SavedCache::new(
189 hash,
190 ExecutionCache::new(1_000),
191 CachedStateMetrics::zeroed(),
192 ))
193 });
194
195 let checked_out = cache.get_cache_for(hash);
196 assert!(checked_out.is_some());
197 drop(checked_out);
198
199 let second = cache.get_cache_for(hash);
200 assert!(second.is_some());
201 }
202
203 #[test]
204 fn hash_mismatch_clears_and_retags() {
205 let cache = PayloadExecutionCache::default();
206 let hash_a = B256::from([0xAA; 32]);
207 let hash_b = B256::from([0xBB; 32]);
208
209 cache.update_with_guard(|slot| {
210 *slot = Some(SavedCache::new(
211 hash_a,
212 ExecutionCache::new(1_000),
213 CachedStateMetrics::zeroed(),
214 ))
215 });
216
217 let checked_out = cache.get_cache_for(hash_b);
218 assert!(checked_out.is_some());
219 assert_eq!(checked_out.unwrap().executed_block_hash(), hash_b);
220 }
221
222 #[test]
223 fn empty_cache_returns_none() {
224 let cache = PayloadExecutionCache::default();
225 assert!(cache.get_cache_for(B256::ZERO).is_none());
226 }
227}