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