1use alloy_primitives::Bytes;
4use parking_lot::Mutex;
5use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
6use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
7use revm_primitives::Address;
8use schnellru::LruMap;
9use std::{
10 collections::HashMap,
11 hash::{Hash, Hasher},
12 sync::Arc,
13};
14
15const MAX_CACHE_SIZE: u32 = 10_000;
17
18#[derive(Debug, Clone, Default)]
20pub struct PrecompileCacheMap<S>(HashMap<Address, PrecompileCache<S>>)
21where
22 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
23
24impl<S> PrecompileCacheMap<S>
25where
26 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
27{
28 pub(crate) fn cache_for_address(&mut self, address: Address) -> PrecompileCache<S> {
29 self.0.entry(address).or_default().clone()
30 }
31}
32
33#[derive(Debug, Clone)]
38pub struct PrecompileCache<S>(Arc<Mutex<LruMap<CacheKey<S>, CacheEntry>>>)
39where
40 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
41
42impl<S> Default for PrecompileCache<S>
43where
44 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
45{
46 fn default() -> Self {
47 Self(Arc::new(Mutex::new(LruMap::new(schnellru::ByLength::new(MAX_CACHE_SIZE)))))
48 }
49}
50
51impl<S> PrecompileCache<S>
52where
53 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
54{
55 fn get(&self, key: &CacheKeyRef<'_, S>) -> Option<CacheEntry> {
56 self.0.lock().get(key).cloned()
57 }
58
59 fn insert(&self, key: CacheKey<S>, value: CacheEntry) -> usize {
61 let mut cache = self.0.lock();
62 cache.insert(key, value);
63 cache.len()
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
70pub struct CacheKey<S>((S, Bytes));
71
72impl<S> CacheKey<S> {
73 const fn new(spec_id: S, input: Bytes) -> Self {
74 Self((spec_id, input))
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct CacheKeyRef<'a, S>((S, &'a [u8]));
81
82impl<'a, S> CacheKeyRef<'a, S> {
83 const fn new(spec_id: S, input: &'a [u8]) -> Self {
84 Self((spec_id, input))
85 }
86}
87
88impl<S: PartialEq> PartialEq<CacheKey<S>> for CacheKeyRef<'_, S> {
89 fn eq(&self, other: &CacheKey<S>) -> bool {
90 self.0 .0 == other.0 .0 && self.0 .1 == other.0 .1.as_ref()
91 }
92}
93
94impl<'a, S: Hash> Hash for CacheKeyRef<'a, S> {
95 fn hash<H: Hasher>(&self, state: &mut H) {
96 self.0 .0.hash(state);
97 self.0 .1.hash(state);
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct CacheEntry(PrecompileOutput);
104
105impl CacheEntry {
106 const fn gas_used(&self) -> u64 {
107 self.0.gas_used
108 }
109
110 fn to_precompile_result(&self) -> PrecompileResult {
111 Ok(self.0.clone())
112 }
113}
114
115#[derive(Debug)]
117pub(crate) struct CachedPrecompile<S>
118where
119 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
120{
121 cache: PrecompileCache<S>,
123 precompile: DynPrecompile,
125 metrics: Option<CachedPrecompileMetrics>,
127 spec_id: S,
129}
130
131impl<S> CachedPrecompile<S>
132where
133 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
134{
135 pub(crate) const fn new(
137 precompile: DynPrecompile,
138 cache: PrecompileCache<S>,
139 spec_id: S,
140 metrics: Option<CachedPrecompileMetrics>,
141 ) -> Self {
142 Self { precompile, cache, spec_id, metrics }
143 }
144
145 pub(crate) fn wrap(
146 precompile: DynPrecompile,
147 cache: PrecompileCache<S>,
148 spec_id: S,
149 metrics: Option<CachedPrecompileMetrics>,
150 ) -> DynPrecompile {
151 let precompile_id = precompile.precompile_id().clone();
152 let wrapped = Self::new(precompile, cache, spec_id, metrics);
153 (precompile_id, move |input: PrecompileInput<'_>| -> PrecompileResult {
154 wrapped.call(input)
155 })
156 .into()
157 }
158
159 fn increment_by_one_precompile_cache_hits(&self) {
160 if let Some(metrics) = &self.metrics {
161 metrics.precompile_cache_hits.increment(1);
162 }
163 }
164
165 fn increment_by_one_precompile_cache_misses(&self) {
166 if let Some(metrics) = &self.metrics {
167 metrics.precompile_cache_misses.increment(1);
168 }
169 }
170
171 fn set_precompile_cache_size_metric(&self, to: f64) {
172 if let Some(metrics) = &self.metrics {
173 metrics.precompile_cache_size.set(to);
174 }
175 }
176
177 fn increment_by_one_precompile_errors(&self) {
178 if let Some(metrics) = &self.metrics {
179 metrics.precompile_errors.increment(1);
180 }
181 }
182}
183
184impl<S> Precompile for CachedPrecompile<S>
185where
186 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
187{
188 fn precompile_id(&self) -> &PrecompileId {
189 self.precompile.precompile_id()
190 }
191
192 fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
193 let key = CacheKeyRef::new(self.spec_id.clone(), input.data);
194
195 if let Some(entry) = &self.cache.get(&key) {
196 self.increment_by_one_precompile_cache_hits();
197 if input.gas >= entry.gas_used() {
198 return entry.to_precompile_result()
199 }
200 }
201
202 let calldata = input.data;
203 let result = self.precompile.call(input);
204
205 match &result {
206 Ok(output) => {
207 let key = CacheKey::new(self.spec_id.clone(), Bytes::copy_from_slice(calldata));
208 let size = self.cache.insert(key, CacheEntry(output.clone()));
209 self.set_precompile_cache_size_metric(size as f64);
210 self.increment_by_one_precompile_cache_misses();
211 }
212 _ => {
213 self.increment_by_one_precompile_errors();
214 }
215 }
216 result
217 }
218}
219
220#[derive(reth_metrics::Metrics, Clone)]
222#[metrics(scope = "sync.caching")]
223pub(crate) struct CachedPrecompileMetrics {
224 precompile_cache_hits: metrics::Counter,
226
227 precompile_cache_misses: metrics::Counter,
229
230 precompile_cache_size: metrics::Gauge,
232
233 precompile_errors: metrics::Counter,
235}
236
237impl CachedPrecompileMetrics {
238 pub(crate) fn new_with_address(address: Address) -> Self {
243 Self::new_with_labels(&[("address", format!("0x{address:02x}"))])
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use std::hash::DefaultHasher;
250
251 use super::*;
252 use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
253 use reth_revm::db::EmptyDB;
254 use revm::{context::TxEnv, precompile::PrecompileOutput};
255 use revm_primitives::hardfork::SpecId;
256
257 #[test]
258 fn test_cache_key_ref_hash() {
259 let key1 = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
260 let key2 = CacheKeyRef::new(SpecId::PRAGUE, b"test_input");
261 assert!(PartialEq::eq(&key2, &key1));
262
263 let mut hasher = DefaultHasher::new();
264 key1.hash(&mut hasher);
265 let hash1 = hasher.finish();
266
267 let mut hasher = DefaultHasher::new();
268 key2.hash(&mut hasher);
269 let hash2 = hasher.finish();
270
271 assert_eq!(hash1, hash2);
272 }
273
274 #[test]
275 fn test_precompile_cache_basic() {
276 let dyn_precompile: DynPrecompile = |_input: PrecompileInput<'_>| -> PrecompileResult {
277 Ok(PrecompileOutput { gas_used: 0, bytes: Bytes::default(), reverted: false })
278 }
279 .into();
280
281 let cache =
282 CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
283
284 let output = PrecompileOutput {
285 gas_used: 50,
286 bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
287 reverted: false,
288 };
289
290 let key = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
291 let expected = CacheEntry(output);
292 cache.cache.insert(key, expected.clone());
293
294 let key = CacheKeyRef::new(SpecId::PRAGUE, b"test_input");
295 let actual = cache.cache.get(&key).unwrap();
296
297 assert_eq!(actual, expected);
298 }
299
300 #[test]
301 fn test_precompile_cache_map_separate_addresses() {
302 let mut evm = EthEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default());
303 let input_data = b"same_input";
304 let gas_limit = 100_000;
305
306 let address1 = Address::repeat_byte(1);
307 let address2 = Address::repeat_byte(2);
308
309 let mut cache_map = PrecompileCacheMap::default();
310
311 let precompile1: DynPrecompile = (PrecompileId::custom("custom"), {
313 move |input: PrecompileInput<'_>| -> PrecompileResult {
314 assert_eq!(input.data, input_data);
315
316 Ok(PrecompileOutput {
317 gas_used: 5000,
318 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
319 reverted: false,
320 })
321 }
322 })
323 .into();
324
325 let precompile2: DynPrecompile = (PrecompileId::custom("custom"), {
327 move |input: PrecompileInput<'_>| -> PrecompileResult {
328 assert_eq!(input.data, input_data);
329
330 Ok(PrecompileOutput {
331 gas_used: 7000,
332 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
333 reverted: false,
334 })
335 }
336 })
337 .into();
338
339 let wrapped_precompile1 = CachedPrecompile::wrap(
340 precompile1,
341 cache_map.cache_for_address(address1),
342 SpecId::PRAGUE,
343 None,
344 );
345 let wrapped_precompile2 = CachedPrecompile::wrap(
346 precompile2,
347 cache_map.cache_for_address(address2),
348 SpecId::PRAGUE,
349 None,
350 );
351
352 let precompile1_address = Address::with_last_byte(1);
353 let precompile2_address = Address::with_last_byte(2);
354
355 evm.precompiles_mut().apply_precompile(&precompile1_address, |_| Some(wrapped_precompile1));
356 evm.precompiles_mut().apply_precompile(&precompile2_address, |_| Some(wrapped_precompile2));
357
358 let result1 = evm
360 .transact_raw(TxEnv {
361 caller: Address::ZERO,
362 gas_limit,
363 data: input_data.into(),
364 kind: precompile1_address.into(),
365 ..Default::default()
366 })
367 .unwrap()
368 .result
369 .into_output()
370 .unwrap();
371 assert_eq!(result1.as_ref(), b"output_from_precompile_1");
372
373 let result2 = evm
376 .transact_raw(TxEnv {
377 caller: Address::ZERO,
378 gas_limit,
379 data: input_data.into(),
380 kind: precompile2_address.into(),
381 ..Default::default()
382 })
383 .unwrap()
384 .result
385 .into_output()
386 .unwrap();
387 assert_eq!(result2.as_ref(), b"output_from_precompile_2");
388
389 let result3 = evm
391 .transact_raw(TxEnv {
392 caller: Address::ZERO,
393 gas_limit,
394 data: input_data.into(),
395 kind: precompile1_address.into(),
396 ..Default::default()
397 })
398 .unwrap()
399 .result
400 .into_output()
401 .unwrap();
402 assert_eq!(result3.as_ref(), b"output_from_precompile_1");
403 }
404}