Skip to main content

reth_engine_tree/tree/
precompile_cache.rs

1//! Contains a precompile cache backed by `schnellru::LruMap` (LRU by length).
2
3use alloy_primitives::{
4    map::{DefaultHashBuilder, FbBuildHasher},
5    Bytes,
6};
7use moka::policy::EvictionPolicy;
8use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
9use reth_primitives_traits::dashmap::DashMap;
10use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
11use revm_primitives::Address;
12use std::{hash::Hash, sync::Arc};
13
14/// Default max cache size for [`PrecompileCache`]
15const MAX_CACHE_SIZE: u32 = 10_000;
16
17/// Stores caches for each precompile.
18#[derive(Debug, Clone, Default)]
19pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>, FbBuildHasher<20>>>)
20where
21    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
22
23impl<S> PrecompileCacheMap<S>
24where
25    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
26{
27    /// Get the precompile cache for the given address.
28    pub fn cache_for_address(&self, address: Address) -> PrecompileCache<S> {
29        // Try just using `.get` first to avoid acquiring a write lock.
30        if let Some(cache) = self.0.get(&address) {
31            return cache.clone();
32        }
33        // Otherwise, fallback to `.entry` and initialize the cache.
34        //
35        // This should be very rare as caches for all precompiles will be initialized as soon as
36        // first EVM is created.
37        self.0.entry(address).or_default().clone()
38    }
39}
40
41/// Cache for precompiles, for each input stores the result.
42#[derive(Debug, Clone)]
43pub struct PrecompileCache<S>(moka::sync::Cache<Bytes, CacheEntry<S>, DefaultHashBuilder>)
44where
45    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
46
47impl<S> Default for PrecompileCache<S>
48where
49    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
50{
51    fn default() -> Self {
52        Self(
53            moka::sync::CacheBuilder::new(MAX_CACHE_SIZE as u64)
54                .initial_capacity(MAX_CACHE_SIZE as usize)
55                .eviction_policy(EvictionPolicy::lru())
56                .build_with_hasher(Default::default()),
57        )
58    }
59}
60
61impl<S> PrecompileCache<S>
62where
63    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
64{
65    fn get(&self, input: &[u8], spec: S) -> Option<CacheEntry<S>> {
66        self.0.get(input).filter(|e| e.spec == spec)
67    }
68
69    /// Inserts the given key and value into the cache, returning the new cache size.
70    fn insert(&self, input: Bytes, value: CacheEntry<S>) -> usize {
71        self.0.insert(input, value);
72        self.0.entry_count() as usize
73    }
74}
75
76/// Cache entry, precompile successful output.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct CacheEntry<S> {
79    output: PrecompileOutput,
80    spec: S,
81}
82
83impl<S> CacheEntry<S> {
84    const fn gas_used(&self) -> u64 {
85        self.output.gas_used
86    }
87
88    fn to_precompile_result(&self) -> PrecompileResult {
89        Ok(self.output.clone())
90    }
91}
92
93/// A cache for precompile inputs / outputs.
94#[derive(Debug)]
95pub struct CachedPrecompile<S>
96where
97    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
98{
99    /// Cache for precompile results and gas bounds.
100    cache: PrecompileCache<S>,
101    /// The precompile.
102    precompile: DynPrecompile,
103    /// Cache metrics.
104    metrics: Option<CachedPrecompileMetrics>,
105    /// Spec id associated to the EVM from which this cached precompile was created.
106    spec_id: S,
107}
108
109impl<S> CachedPrecompile<S>
110where
111    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
112{
113    /// `CachedPrecompile` constructor.
114    pub const fn new(
115        precompile: DynPrecompile,
116        cache: PrecompileCache<S>,
117        spec_id: S,
118        metrics: Option<CachedPrecompileMetrics>,
119    ) -> Self {
120        Self { precompile, cache, spec_id, metrics }
121    }
122
123    /// Wrap the given precompile in a cached precompile.
124    pub fn wrap(
125        precompile: DynPrecompile,
126        cache: PrecompileCache<S>,
127        spec_id: S,
128        metrics: Option<CachedPrecompileMetrics>,
129    ) -> DynPrecompile {
130        let precompile_id = precompile.precompile_id().clone();
131        let wrapped = Self::new(precompile, cache, spec_id, metrics);
132        (precompile_id, move |input: PrecompileInput<'_>| -> PrecompileResult {
133            wrapped.call(input)
134        })
135            .into()
136    }
137
138    fn increment_by_one_precompile_cache_hits(&self) {
139        if let Some(metrics) = &self.metrics {
140            metrics.precompile_cache_hits.increment(1);
141        }
142    }
143
144    fn increment_by_one_precompile_cache_misses(&self) {
145        if let Some(metrics) = &self.metrics {
146            metrics.precompile_cache_misses.increment(1);
147        }
148    }
149
150    fn set_precompile_cache_size_metric(&self, to: f64) {
151        if let Some(metrics) = &self.metrics {
152            metrics.precompile_cache_size.set(to);
153        }
154    }
155
156    fn increment_by_one_precompile_errors(&self) {
157        if let Some(metrics) = &self.metrics {
158            metrics.precompile_errors.increment(1);
159        }
160    }
161}
162
163impl<S> Precompile for CachedPrecompile<S>
164where
165    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
166{
167    fn precompile_id(&self) -> &PrecompileId {
168        self.precompile.precompile_id()
169    }
170
171    fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
172        if let Some(entry) = &self.cache.get(input.data, self.spec_id.clone()) {
173            self.increment_by_one_precompile_cache_hits();
174            if input.gas >= entry.gas_used() {
175                return entry.to_precompile_result()
176            }
177        }
178
179        let calldata = input.data;
180        let result = self.precompile.call(input);
181
182        match &result {
183            Ok(output) => {
184                let size = self.cache.insert(
185                    Bytes::copy_from_slice(calldata),
186                    CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
187                );
188                self.set_precompile_cache_size_metric(size as f64);
189                self.increment_by_one_precompile_cache_misses();
190            }
191            _ => {
192                self.increment_by_one_precompile_errors();
193            }
194        }
195        result
196    }
197}
198
199/// Metrics for the cached precompile.
200#[derive(reth_metrics::Metrics, Clone)]
201#[metrics(scope = "sync.caching")]
202pub struct CachedPrecompileMetrics {
203    /// Precompile cache hits
204    pub precompile_cache_hits: metrics::Counter,
205
206    /// Precompile cache misses
207    pub precompile_cache_misses: metrics::Counter,
208
209    /// Precompile cache size. Uses the LRU cache length as the size metric.
210    pub precompile_cache_size: metrics::Gauge,
211
212    /// Precompile execution errors.
213    pub precompile_errors: metrics::Counter,
214}
215
216impl CachedPrecompileMetrics {
217    /// Creates a new instance of [`CachedPrecompileMetrics`] with the given address.
218    ///
219    /// Adds address as an `address` label padded with zeros to at least two hex symbols, prefixed
220    /// by `0x`.
221    pub fn new_with_address(address: Address) -> Self {
222        Self::new_with_labels(&[("address", format!("0x{address:02x}"))])
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
230    use reth_revm::db::EmptyDB;
231    use revm::{context::TxEnv, precompile::PrecompileOutput};
232    use revm_primitives::hardfork::SpecId;
233
234    #[test]
235    fn test_precompile_cache_basic() {
236        let dyn_precompile: DynPrecompile = (|_input: PrecompileInput<'_>| -> PrecompileResult {
237            Ok(PrecompileOutput {
238                gas_used: 0,
239                gas_refunded: 0,
240                bytes: Bytes::default(),
241                reverted: false,
242            })
243        })
244        .into();
245
246        let cache =
247            CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
248
249        let output = PrecompileOutput {
250            gas_used: 50,
251            gas_refunded: 0,
252            bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
253            reverted: false,
254        };
255
256        let input = b"test_input";
257        let expected = CacheEntry { output, spec: SpecId::PRAGUE };
258        cache.cache.insert(input.into(), expected.clone());
259
260        let actual = cache.cache.get(input, SpecId::PRAGUE).unwrap();
261
262        assert_eq!(actual, expected);
263    }
264
265    #[test]
266    fn test_precompile_cache_map_separate_addresses() {
267        let mut evm = EthEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default());
268        let input_data = b"same_input";
269        let gas_limit = 100_000;
270
271        let address1 = Address::repeat_byte(1);
272        let address2 = Address::repeat_byte(2);
273
274        let cache_map = PrecompileCacheMap::default();
275
276        // create the first precompile with a specific output
277        let precompile1: DynPrecompile = (PrecompileId::custom("custom"), {
278            move |input: PrecompileInput<'_>| -> PrecompileResult {
279                assert_eq!(input.data, input_data);
280
281                Ok(PrecompileOutput {
282                    gas_used: 5000,
283                    gas_refunded: 0,
284                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
285                    reverted: false,
286                })
287            }
288        })
289            .into();
290
291        // create the second precompile with a different output
292        let precompile2: DynPrecompile = (PrecompileId::custom("custom"), {
293            move |input: PrecompileInput<'_>| -> PrecompileResult {
294                assert_eq!(input.data, input_data);
295
296                Ok(PrecompileOutput {
297                    gas_used: 7000,
298                    gas_refunded: 0,
299                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
300                    reverted: false,
301                })
302            }
303        })
304            .into();
305
306        let wrapped_precompile1 = CachedPrecompile::wrap(
307            precompile1,
308            cache_map.cache_for_address(address1),
309            SpecId::PRAGUE,
310            None,
311        );
312        let wrapped_precompile2 = CachedPrecompile::wrap(
313            precompile2,
314            cache_map.cache_for_address(address2),
315            SpecId::PRAGUE,
316            None,
317        );
318
319        let precompile1_address = Address::with_last_byte(1);
320        let precompile2_address = Address::with_last_byte(2);
321
322        evm.precompiles_mut().apply_precompile(&precompile1_address, |_| Some(wrapped_precompile1));
323        evm.precompiles_mut().apply_precompile(&precompile2_address, |_| Some(wrapped_precompile2));
324
325        // first invocation of precompile1 (cache miss)
326        let result1 = evm
327            .transact_raw(TxEnv {
328                caller: Address::ZERO,
329                gas_limit,
330                data: input_data.into(),
331                kind: precompile1_address.into(),
332                ..Default::default()
333            })
334            .unwrap()
335            .result
336            .into_output()
337            .unwrap();
338        assert_eq!(result1.as_ref(), b"output_from_precompile_1");
339
340        // first invocation of precompile2 with the same input (should be a cache miss)
341        // if cache was incorrectly shared, we'd get precompile1's result
342        let result2 = evm
343            .transact_raw(TxEnv {
344                caller: Address::ZERO,
345                gas_limit,
346                data: input_data.into(),
347                kind: precompile2_address.into(),
348                ..Default::default()
349            })
350            .unwrap()
351            .result
352            .into_output()
353            .unwrap();
354        assert_eq!(result2.as_ref(), b"output_from_precompile_2");
355
356        // second invocation of precompile1 (should be a cache hit)
357        let result3 = evm
358            .transact_raw(TxEnv {
359                caller: Address::ZERO,
360                gas_limit,
361                data: input_data.into(),
362                kind: precompile1_address.into(),
363                ..Default::default()
364            })
365            .unwrap()
366            .result
367            .into_output()
368            .unwrap();
369        assert_eq!(result3.as_ref(), b"output_from_precompile_1");
370    }
371}