Skip to main content

reth_evm/
precompile_cache.rs

1//! Precompile cache for payload prewarming.
2
3use 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
19/// Default max cache size for [`PrecompileCache`].
20const MAX_CACHE_SIZE: u32 = 1024 * 1024;
21
22/// Maximum input retained by a precompile cache.
23const MAX_PRECOMPILE_CACHE_INPUT_SIZE: usize = 2 * 1024;
24
25/// Stores caches for each precompile.
26pub 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    /// Get the precompile cache for the given address.
51    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
60/// Cache for one precompile's inputs and outputs.
61struct 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/// Cache entry for a successful precompile output.
106#[derive(Debug, Clone, PartialEq, Eq)]
107struct CacheEntry<S> {
108    output: PrecompileOutput,
109    regular_gas_used: u64,
110    spec: S,
111}
112
113/// A caching EVM precompile provider.
114pub 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 already resolved from `cache_map`, so repeated calls skip the shared map lookup.
122    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    /// Creates a new cached precompile provider.
145    ///
146    /// Metrics are labelled by address and shared with the cache. Disable them for prewarming
147    /// so speculative work does not count towards execution cache statistics.
148    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            // The shared cache still belongs to the unmodified table used by other EVMs.
198            // Relocated entries need a private cache because address alone no longer identifies
199            // the same precompile implementation.
200            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/// Precompiles whose output and gas usage depend only on input and specification.
310///
311/// Eligible precompiles must not read execution context or state, mutate state, or emit logs.
312/// Custom factories are uncached unless they explicitly select a policy.
313#[derive(Debug, Clone, Default)]
314pub enum PrecompileCachePolicy {
315    /// No precompiles are eligible for caching.
316    #[default]
317    Disabled,
318    /// Every installed precompile is pure and eligible for caching.
319    All,
320    /// Only these precompile implementations are eligible for caching.
321    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/// Metrics for the cached precompile.
354#[cfg(feature = "metrics")]
355#[derive(Metrics, Clone)]
356#[metrics(scope = "sync.caching")]
357pub struct CachedPrecompileMetrics {
358    /// Precompile cache hits.
359    pub precompile_cache_hits: metrics::Counter,
360
361    /// Precompile cache misses.
362    pub precompile_cache_misses: metrics::Counter,
363
364    /// Precompile cache size. Uses the LRU cache length as the size metric.
365    pub precompile_cache_size: metrics::Gauge,
366
367    /// Precompile execution errors.
368    pub precompile_errors: metrics::Counter,
369}
370
371#[cfg(feature = "metrics")]
372impl CachedPrecompileMetrics {
373    /// Registers cache metrics labelled by the precompile address.
374    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            // Move the stateful implementation onto a formerly cacheable address.
622            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}