1use alloy_primitives::{
4 map::{AddressMap, AddressSet, DefaultHashBuilder, FbBuildHasher},
5 Address, Bytes,
6};
7use evm2::{
8 evm::precompile::{PrecompileOutput, PrecompileProvider},
9 interpreter::{GasTracker, Message},
10 Evm, EvmTypesHost, PrecompileError,
11};
12use moka::policy::EvictionPolicy;
13#[cfg(feature = "metrics")]
14use reth_metrics::Metrics;
15use reth_primitives_traits::dashmap::DashMap;
16use std::{fmt, hash::Hash, sync::Arc};
17use tracing::error;
18
19const MAX_CACHE_SIZE: u32 = 1024 * 1024;
21
22const MAX_PRECOMPILE_CACHE_INPUT_SIZE: usize = 2 * 1024;
24
25pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, Arc<PrecompileCache<S>>, FbBuildHasher<20>>>);
27
28impl<S> fmt::Debug for PrecompileCacheMap<S> {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.debug_struct("PrecompileCacheMap").finish_non_exhaustive()
31 }
32}
33
34impl<S> Clone for PrecompileCacheMap<S> {
35 fn clone(&self) -> Self {
36 Self(Arc::clone(&self.0))
37 }
38}
39
40impl<S> Default for PrecompileCacheMap<S> {
41 fn default() -> Self {
42 Self(Arc::new(DashMap::with_hasher(Default::default())))
43 }
44}
45
46impl<S> PrecompileCacheMap<S>
47where
48 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
49{
50 fn cache_for_address(&self, address: Address) -> Arc<PrecompileCache<S>> {
52 if let Some(cache) = self.0.get(&address) {
53 return cache.clone()
54 }
55
56 self.0.entry(address).or_default().clone()
57 }
58}
59
60struct PrecompileCache<S> {
62 cache: moka::sync::Cache<Bytes, CacheEntry<S>, DefaultHashBuilder>,
63 #[cfg(feature = "metrics")]
64 metrics: std::sync::OnceLock<CachedPrecompileMetrics>,
65}
66
67impl<S> fmt::Debug for PrecompileCache<S> {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.debug_struct("PrecompileCache").finish_non_exhaustive()
70 }
71}
72
73impl<S> Default for PrecompileCache<S>
74where
75 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
76{
77 fn default() -> Self {
78 Self {
79 cache: moka::sync::CacheBuilder::new(MAX_CACHE_SIZE as u64)
80 .eviction_policy(EvictionPolicy::lru())
81 .weigher(|key: &Bytes, value: &CacheEntry<S>| {
82 (key.len() + value.output.bytes().len()) as u32
83 })
84 .build_with_hasher(Default::default()),
85 #[cfg(feature = "metrics")]
86 metrics: Default::default(),
87 }
88 }
89}
90
91impl<S> PrecompileCache<S>
92where
93 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
94{
95 fn get(&self, input: &[u8], spec: S) -> Option<CacheEntry<S>> {
96 self.cache.get(input).filter(|entry| entry.spec == spec)
97 }
98
99 fn insert(&self, input: Bytes, value: CacheEntry<S>) -> usize {
100 self.cache.insert(input, value);
101 self.cache.entry_count() as usize
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107struct CacheEntry<S> {
108 output: PrecompileOutput,
109 regular_gas_used: u64,
110 spec: S,
111}
112
113pub struct CachedPrecompileProvider<T, S>
115where
116 T: EvmTypesHost,
117 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
118{
119 inner: evm2::Precompiles<T>,
120 cache_map: PrecompileCacheMap<S>,
121 caches: AddressMap<Arc<PrecompileCache<S>>>,
123 spec_id: S,
124 policy: PrecompileCachePolicy,
125 #[cfg_attr(not(feature = "metrics"), allow(dead_code))]
126 metrics_enabled: bool,
127}
128
129impl<T, S> fmt::Debug for CachedPrecompileProvider<T, S>
130where
131 T: EvmTypesHost,
132 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
133{
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.debug_struct("CachedPrecompileProvider").finish_non_exhaustive()
136 }
137}
138
139impl<T, S> CachedPrecompileProvider<T, S>
140where
141 T: EvmTypesHost,
142 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
143{
144 pub fn new(
149 inner: evm2::Precompiles<T>,
150 cache_map: PrecompileCacheMap<S>,
151 spec_id: S,
152 policy: PrecompileCachePolicy,
153 metrics_enabled: bool,
154 ) -> Self {
155 Self { inner, cache_map, caches: Default::default(), spec_id, policy, metrics_enabled }
156 }
157}
158
159impl<T, S> PrecompileProvider<T> for CachedPrecompileProvider<T, S>
160where
161 T: EvmTypesHost,
162 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
163{
164 fn addresses(&self) -> Vec<Address> {
165 self.inner.addresses()
166 }
167
168 fn precompile_ids(&self) -> Vec<(Address, evm2::precompiles::PrecompileId)> {
169 self.inner.precompile_ids()
170 }
171
172 fn move_precompiles(
173 &mut self,
174 moves: &[(Address, Address)],
175 ) -> Result<(), evm2::precompiles::MovePrecompileError> {
176 self.inner.move_precompiles(moves)?;
177 if moves.iter().any(|(source, dest)| source != dest) {
178 if let PrecompileCachePolicy::Addresses(addresses) = &mut self.policy {
179 let mut sources = AddressSet::default();
180 let moved = moves
181 .iter()
182 .filter(|(source, dest)| source != dest && sources.insert(*source))
183 .map(|(source, dest)| (*source, *dest, addresses.contains(source)))
184 .collect::<Vec<_>>();
185 let addresses = Arc::make_mut(addresses);
186 for (source, _, _) in &moved {
187 addresses.remove(source);
188 }
189 for (_, dest, cacheable) in moved {
190 if cacheable {
191 addresses.insert(dest);
192 } else {
193 addresses.remove(&dest);
194 }
195 }
196 }
197 self.cache_map = PrecompileCacheMap::default();
201 self.caches.clear();
202 }
203 Ok(())
204 }
205
206 fn contains(&self, address: &Address) -> bool {
207 self.inner.contains(address)
208 }
209
210 fn execute(
211 &mut self,
212 evm: &mut Evm<'_, T>,
213 message: &Message<T>,
214 gas: &mut GasTracker,
215 ) -> Option<Result<PrecompileOutput, PrecompileError>> {
216 let address = message.call_target;
217 if !self.policy.allows(&address) {
218 return self.inner.execute(evm, message, gas);
219 }
220 let cache =
221 self.caches.entry(address).or_insert_with(|| self.cache_map.cache_for_address(address));
222 #[cfg(feature = "metrics")]
223 let metrics = self.metrics_enabled.then(|| {
224 cache.metrics.get_or_init(|| CachedPrecompileMetrics::new_with_address(address))
225 });
226
227 let cacheable_input = message.input.len() <= MAX_PRECOMPILE_CACHE_INPUT_SIZE;
228 if cacheable_input &&
229 let Some(entry) = cache.get(message.input.as_ref(), self.spec_id.clone())
230 {
231 return Some(match gas.spend(entry.regular_gas_used).map_err(PrecompileError::from) {
232 Ok(()) => {
233 #[cfg(feature = "metrics")]
234 if let Some(metrics) = metrics {
235 metrics.precompile_cache_hits.increment(1);
236 }
237 Ok(entry.output)
238 }
239 Err(err) => {
240 #[cfg(feature = "metrics")]
241 if let Some(metrics) = metrics {
242 metrics.precompile_errors.increment(1);
243 }
244 Err(err)
245 }
246 })
247 }
248
249 let before = GasSnapshot::new(gas);
250 let result = self.inner.execute(evm, message, gas)?;
251 let after = GasSnapshot::new(gas);
252
253 match &result {
254 Ok(output) if cacheable_input => {
255 if before.reservoir != after.reservoir {
256 error!(
257 target: "evm::precompile_cache",
258 %address,
259 "cacheable precompile decremented reservoir, skipping cache insertion"
260 );
261 } else if before.state_gas_spent != after.state_gas_spent {
262 error!(
263 target: "evm::precompile_cache",
264 %address,
265 "cacheable precompile used state gas, skipping cache insertion"
266 );
267 } else if before.refunded != after.refunded {
268 error!(
269 target: "evm::precompile_cache",
270 %address,
271 "cacheable precompile changed refund gas, skipping cache insertion"
272 );
273 } else if let Some(regular_gas_used) = after.spent.checked_sub(before.spent) {
274 let _size = cache.insert(
275 Bytes::copy_from_slice(message.input.as_ref()),
276 CacheEntry {
277 output: output.clone(),
278 regular_gas_used,
279 spec: self.spec_id.clone(),
280 },
281 );
282 #[cfg(feature = "metrics")]
283 if let Some(metrics) = metrics {
284 metrics.precompile_cache_size.set(_size as f64);
285 metrics.precompile_cache_misses.increment(1);
286 }
287 } else {
288 error!(
289 target: "evm::precompile_cache",
290 %address,
291 "cacheable precompile returned regular gas, skipping cache insertion"
292 );
293 }
294 }
295 Ok(_) => {}
296 Err(_) =>
297 {
298 #[cfg(feature = "metrics")]
299 if let Some(metrics) = metrics {
300 metrics.precompile_errors.increment(1);
301 }
302 }
303 }
304
305 Some(result)
306 }
307}
308
309#[derive(Debug, Clone, Default)]
314pub enum PrecompileCachePolicy {
315 #[default]
317 Disabled,
318 All,
320 Addresses(Arc<AddressSet>),
322}
323
324impl PrecompileCachePolicy {
325 fn allows(&self, address: &Address) -> bool {
326 match self {
327 Self::Disabled => false,
328 Self::All => true,
329 Self::Addresses(addresses) => addresses.contains(address),
330 }
331 }
332}
333
334#[derive(Debug)]
335struct GasSnapshot {
336 spent: u64,
337 reservoir: u64,
338 state_gas_spent: i64,
339 refunded: i64,
340}
341
342impl GasSnapshot {
343 const fn new(gas: &GasTracker) -> Self {
344 Self {
345 spent: gas.spent(),
346 reservoir: gas.reservoir(),
347 state_gas_spent: gas.state_gas_spent(),
348 refunded: gas.refunded(),
349 }
350 }
351}
352
353#[cfg(feature = "metrics")]
355#[derive(Metrics, Clone)]
356#[metrics(scope = "sync.caching")]
357pub struct CachedPrecompileMetrics {
358 pub precompile_cache_hits: metrics::Counter,
360
361 pub precompile_cache_misses: metrics::Counter,
363
364 pub precompile_cache_size: metrics::Gauge,
366
367 pub precompile_errors: metrics::Counter,
369}
370
371#[cfg(feature = "metrics")]
372impl CachedPrecompileMetrics {
373 pub fn new_with_address(address: Address) -> Self {
375 Self::new_with_labels(&[("address", format!("0x{address:02x}"))])
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use alloy_primitives::{Address, B256, U256};
383 use evm2::{
384 env::BlockEnv,
385 evm::{precompile::NoPrecompiles, InMemoryDB},
386 interpreter::{Message, MessageKind},
387 registry::TxRegistry,
388 BaseEvmTypes, SpecId,
389 };
390
391 #[test]
392 fn cached_precompile_metadata_matches_inner() {
393 let precompiles = evm2::Precompiles::<BaseEvmTypes>::base(SpecId::OSAKA);
394 let expected = precompiles.precompile_ids();
395 assert!(!expected.is_empty());
396 let cached = CachedPrecompileProvider::new(
397 precompiles,
398 PrecompileCacheMap::default(),
399 SpecId::OSAKA,
400 PrecompileCachePolicy::All,
401 false,
402 );
403 assert_eq!(cached.precompile_ids(), expected);
404 }
405
406 #[test]
407 fn moves_preserve_custom_precompiles_with_borrowed_database() {
408 let spec = SpecId::OSAKA;
409 let identity = Address::with_last_byte(4);
410 let custom = Address::with_last_byte(0x40);
411 let moved = Address::with_last_byte(0x41);
412 let mut precompiles = evm2::Precompiles::<BaseEvmTypes>::base(spec);
413 let custom_entry = precompiles.as_map_mut().remove(identity).unwrap().with_address(custom);
414 precompiles.as_map_mut().insert(custom_entry);
415 let provider = CachedPrecompileProvider::new(
416 precompiles,
417 PrecompileCacheMap::default(),
418 spec,
419 PrecompileCachePolicy::All,
420 false,
421 );
422 let mut database = InMemoryDB::default();
423 let mut evm = Evm::<BaseEvmTypes>::new(
424 spec,
425 BlockEnv::<BaseEvmTypes>::default(),
426 TxRegistry::new(),
427 &mut database,
428 provider,
429 );
430 let standard = Address::with_last_byte(2);
431 crate::Evm::move_precompiles(&mut evm, [(standard, moved)]).unwrap();
432 assert!(evm.precompiles().contains(&custom));
433 assert!(!evm.precompiles().contains(&identity));
434 assert!(!evm.precompiles().contains(&standard));
435 crate::Evm::move_precompiles(&mut evm, [(custom, identity)]).unwrap();
436 assert!(!evm.precompiles().contains(&custom));
437 assert!(evm.precompiles().contains(&identity));
438 assert!(evm.precompiles().contains(&moved));
439 let before = evm.precompiles().precompile_ids();
440 assert!(crate::Evm::move_precompiles(&mut evm, [(custom, standard)]).is_err());
441 assert_eq!(evm.precompiles().precompile_ids(), before);
442 }
443
444 #[test]
445 fn moves_isolate_cached_outputs_without_disabling_caching() {
446 let spec = SpecId::OSAKA;
447 let identity = Address::with_last_byte(4);
448 let destination = Address::with_last_byte(2);
449 let input = Bytes::from_static(b"input");
450 let shared = PrecompileCacheMap::default();
451 shared.cache_for_address(destination).insert(
452 input.clone(),
453 CacheEntry {
454 output: PrecompileOutput::new(Bytes::from_static(b"old implementation")),
455 regular_gas_used: 60,
456 spec,
457 },
458 );
459 let mut provider = CachedPrecompileProvider::new(
460 evm2::Precompiles::<BaseEvmTypes>::base(spec),
461 shared.clone(),
462 spec,
463 PrecompileCachePolicy::Addresses(Arc::new(AddressSet::from_iter([identity]))),
464 false,
465 );
466 provider.move_precompiles(&[(identity, destination)]).unwrap();
467 let mut evm = Evm::<BaseEvmTypes>::new(
468 spec,
469 BlockEnv::<BaseEvmTypes>::default(),
470 TxRegistry::new(),
471 InMemoryDB::default(),
472 NoPrecompiles::default(),
473 );
474 let message = Message::<BaseEvmTypes> {
475 kind: MessageKind::Call,
476 gas_limit: 30_000,
477 destination,
478 code_address: destination,
479 call_target: destination,
480 input: input.clone(),
481 ..Default::default()
482 };
483 for _ in 0..2 {
484 let output = provider
485 .execute(&mut evm, &message, &mut GasTracker::new(30_000))
486 .unwrap()
487 .unwrap();
488 assert_eq!(output.bytes(), input.as_ref());
489 }
490 assert_eq!(
491 provider
492 .cache_map
493 .cache_for_address(destination)
494 .get(&input, spec)
495 .unwrap()
496 .output
497 .bytes(),
498 input.as_ref()
499 );
500 assert_eq!(
501 shared.cache_for_address(destination).get(&input, spec).unwrap().output.bytes(),
502 b"old implementation"
503 );
504 }
505
506 #[test]
507 fn caches_successful_precompile_output() {
508 let cache_map = PrecompileCacheMap::default();
509 let mut provider = CachedPrecompileProvider::new(
510 evm2::Precompiles::base(SpecId::OSAKA),
511 cache_map.clone(),
512 SpecId::OSAKA,
513 PrecompileCachePolicy::All,
514 false,
515 );
516 let mut evm = Evm::<BaseEvmTypes>::new(
517 SpecId::OSAKA,
518 BlockEnv::<BaseEvmTypes>::default(),
519 TxRegistry::new(),
520 InMemoryDB::default(),
521 NoPrecompiles::default(),
522 );
523 let address = Address::with_last_byte(4);
524 let message = Message::<BaseEvmTypes> {
525 kind: MessageKind::Call,
526 gas_limit: 30_000,
527 destination: address,
528 caller: Address::ZERO,
529 input: Bytes::copy_from_slice(b"cached-input"),
530 value: U256::ZERO,
531 code_address: address,
532 call_target: address,
533 salt: B256::ZERO,
534 ..Default::default()
535 };
536
537 let mut gas = GasTracker::new(30_000);
538 let output = provider
539 .execute(&mut evm, &message, &mut gas)
540 .expect("identity precompile exists")
541 .expect("identity precompile succeeds");
542 assert_eq!(output.bytes(), b"cached-input");
543
544 let cache = cache_map.cache_for_address(address);
545 let entry = cache.get(message.input.as_ref(), SpecId::OSAKA).expect("cache entry exists");
546 assert_eq!(entry.output.bytes(), b"cached-input");
547 assert_eq!(entry.regular_gas_used, 18);
548
549 let mut hit_gas = GasTracker::new(30_000);
550 let output = provider
551 .execute(&mut evm, &message, &mut hit_gas)
552 .expect("identity precompile exists")
553 .expect("cached identity precompile succeeds");
554 assert_eq!(output.bytes(), b"cached-input");
555 assert_eq!(hit_gas.spent(), 18);
556 }
557 #[test]
558 fn identity_cache_obeys_input_size_boundary() {
559 let cache = PrecompileCacheMap::default();
560 let mut provider = CachedPrecompileProvider::new(
561 evm2::Precompiles::base(SpecId::OSAKA),
562 cache.clone(),
563 SpecId::OSAKA,
564 PrecompileCachePolicy::All,
565 false,
566 );
567 let mut evm = Evm::<BaseEvmTypes>::new(
568 SpecId::OSAKA,
569 BlockEnv::<BaseEvmTypes>::default(),
570 TxRegistry::new(),
571 InMemoryDB::default(),
572 NoPrecompiles::default(),
573 );
574 let address = Address::with_last_byte(4);
575 for len in [2048, 2049] {
576 let message = Message::<BaseEvmTypes> {
577 kind: MessageKind::Call,
578 gas_limit: 30_000,
579 destination: address,
580 code_address: address,
581 call_target: address,
582 input: Bytes::from(vec![1; len]),
583 ..Default::default()
584 };
585 let output = provider
586 .execute(&mut evm, &message, &mut GasTracker::new(30_000))
587 .unwrap()
588 .unwrap();
589 assert_eq!(output.bytes(), message.input.as_ref());
590 assert_eq!(
591 cache
592 .cache_for_address(address)
593 .get(message.input.as_ref(), SpecId::OSAKA)
594 .is_some(),
595 len == 2048
596 );
597 }
598 }
599
600 #[test]
601 fn caller_dependent_precompiles_are_not_cached() {
602 let spec = SpecId::OSAKA;
603 let custom = Address::with_last_byte(0x40);
604 let identity = Address::with_last_byte(4);
605 for policy in [
606 PrecompileCachePolicy::Disabled,
607 PrecompileCachePolicy::Addresses(Arc::new(AddressSet::from_iter([identity]))),
608 ] {
609 let cache = PrecompileCacheMap::default();
610 let mut precompiles = evm2::Precompiles::<BaseEvmTypes>::base(spec);
611 precompiles.as_map_mut().insert(evm2::precompiles::Precompile::new(
612 custom,
613 evm2::precompiles::PrecompileId::custom("caller"),
614 |_, message, gas| {
615 gas.spend(10)?;
616 Ok(PrecompileOutput::new(Bytes::copy_from_slice(message.caller.as_slice())))
617 },
618 ));
619 let mut provider =
620 CachedPrecompileProvider::new(precompiles, cache.clone(), spec, policy, false);
621 provider.move_precompiles(&[(identity, custom), (custom, identity)]).unwrap();
623 let mut evm = Evm::<BaseEvmTypes>::new(
624 spec,
625 BlockEnv::<BaseEvmTypes>::default(),
626 TxRegistry::new(),
627 InMemoryDB::default(),
628 NoPrecompiles::default(),
629 );
630 for caller in [Address::repeat_byte(0xaa), Address::repeat_byte(0xbb)] {
631 let message = Message::<BaseEvmTypes> {
632 caller,
633 destination: identity,
634 code_address: identity,
635 call_target: identity,
636 gas_limit: 30_000,
637 ..Default::default()
638 };
639 let output = provider
640 .execute(&mut evm, &message, &mut GasTracker::new(30_000))
641 .unwrap()
642 .unwrap();
643 assert_eq!(output.bytes(), caller.as_slice());
644 }
645 assert!(!cache.0.contains_key(&identity));
646 assert!(!provider.cache_map.0.contains_key(&identity));
647 }
648 }
649}