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 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
15/// Default max cache size for [`PrecompileCache`]
16const MAX_CACHE_SIZE: u32 = 10_000;
17
18/// Stores caches for each precompile.
19#[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/// Cache for precompiles, for each input stores the result.
34///
35/// [`LruMap`] requires a mutable reference on `get` since it updates the LRU order,
36/// so we use a [`Mutex`] instead of an `RwLock`.
37#[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    /// Inserts the given key and value into the cache, returning the new cache size.
60    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/// Cache key, spec id and precompile call input. spec id is included in the key to account for
68/// precompile repricing across fork activations.
69#[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/// Cache key reference, used to avoid cloning the input bytes when looking up using a [`CacheKey`].
79#[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/// Cache entry, precompile successful output.
102#[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/// A cache for precompile inputs / outputs.
116#[derive(Debug)]
117pub(crate) struct CachedPrecompile<S>
118where
119    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
120{
121    /// Cache for precompile results and gas bounds.
122    cache: PrecompileCache<S>,
123    /// The precompile.
124    precompile: DynPrecompile,
125    /// Cache metrics.
126    metrics: Option<CachedPrecompileMetrics>,
127    /// Spec id associated to the EVM from which this cached precompile was created.
128    spec_id: S,
129}
130
131impl<S> CachedPrecompile<S>
132where
133    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
134{
135    /// `CachedPrecompile` constructor.
136    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/// Metrics for the cached precompile.
221#[derive(reth_metrics::Metrics, Clone)]
222#[metrics(scope = "sync.caching")]
223pub(crate) struct CachedPrecompileMetrics {
224    /// Precompile cache hits
225    precompile_cache_hits: metrics::Counter,
226
227    /// Precompile cache misses
228    precompile_cache_misses: metrics::Counter,
229
230    /// Precompile cache size. Uses the LRU cache length as the size metric.
231    precompile_cache_size: metrics::Gauge,
232
233    /// Precompile execution errors.
234    precompile_errors: metrics::Counter,
235}
236
237impl CachedPrecompileMetrics {
238    /// Creates a new instance of [`CachedPrecompileMetrics`] with the given address.
239    ///
240    /// Adds address as an `address` label padded with zeros to at least two hex symbols, prefixed
241    /// by `0x`.
242    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 {
278                gas_used: 0,
279                gas_refunded: 0,
280                bytes: Bytes::default(),
281                reverted: false,
282            })
283        })
284        .into();
285
286        let cache =
287            CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
288
289        let output = PrecompileOutput {
290            gas_used: 50,
291            gas_refunded: 0,
292            bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
293            reverted: false,
294        };
295
296        let key = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
297        let expected = CacheEntry(output);
298        cache.cache.insert(key, expected.clone());
299
300        let key = CacheKeyRef::new(SpecId::PRAGUE, b"test_input");
301        let actual = cache.cache.get(&key).unwrap();
302
303        assert_eq!(actual, expected);
304    }
305
306    #[test]
307    fn test_precompile_cache_map_separate_addresses() {
308        let mut evm = EthEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default());
309        let input_data = b"same_input";
310        let gas_limit = 100_000;
311
312        let address1 = Address::repeat_byte(1);
313        let address2 = Address::repeat_byte(2);
314
315        let mut cache_map = PrecompileCacheMap::default();
316
317        // create the first precompile with a specific output
318        let precompile1: DynPrecompile = (PrecompileId::custom("custom"), {
319            move |input: PrecompileInput<'_>| -> PrecompileResult {
320                assert_eq!(input.data, input_data);
321
322                Ok(PrecompileOutput {
323                    gas_used: 5000,
324                    gas_refunded: 0,
325                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
326                    reverted: false,
327                })
328            }
329        })
330            .into();
331
332        // create the second precompile with a different output
333        let precompile2: DynPrecompile = (PrecompileId::custom("custom"), {
334            move |input: PrecompileInput<'_>| -> PrecompileResult {
335                assert_eq!(input.data, input_data);
336
337                Ok(PrecompileOutput {
338                    gas_used: 7000,
339                    gas_refunded: 0,
340                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
341                    reverted: false,
342                })
343            }
344        })
345            .into();
346
347        let wrapped_precompile1 = CachedPrecompile::wrap(
348            precompile1,
349            cache_map.cache_for_address(address1),
350            SpecId::PRAGUE,
351            None,
352        );
353        let wrapped_precompile2 = CachedPrecompile::wrap(
354            precompile2,
355            cache_map.cache_for_address(address2),
356            SpecId::PRAGUE,
357            None,
358        );
359
360        let precompile1_address = Address::with_last_byte(1);
361        let precompile2_address = Address::with_last_byte(2);
362
363        evm.precompiles_mut().apply_precompile(&precompile1_address, |_| Some(wrapped_precompile1));
364        evm.precompiles_mut().apply_precompile(&precompile2_address, |_| Some(wrapped_precompile2));
365
366        // first invocation of precompile1 (cache miss)
367        let result1 = evm
368            .transact_raw(TxEnv {
369                caller: Address::ZERO,
370                gas_limit,
371                data: input_data.into(),
372                kind: precompile1_address.into(),
373                ..Default::default()
374            })
375            .unwrap()
376            .result
377            .into_output()
378            .unwrap();
379        assert_eq!(result1.as_ref(), b"output_from_precompile_1");
380
381        // first invocation of precompile2 with the same input (should be a cache miss)
382        // if cache was incorrectly shared, we'd get precompile1's result
383        let result2 = evm
384            .transact_raw(TxEnv {
385                caller: Address::ZERO,
386                gas_limit,
387                data: input_data.into(),
388                kind: precompile2_address.into(),
389                ..Default::default()
390            })
391            .unwrap()
392            .result
393            .into_output()
394            .unwrap();
395        assert_eq!(result2.as_ref(), b"output_from_precompile_2");
396
397        // second invocation of precompile1 (should be a cache hit)
398        let result3 = evm
399            .transact_raw(TxEnv {
400                caller: Address::ZERO,
401                gas_limit,
402                data: input_data.into(),
403                kind: precompile1_address.into(),
404                ..Default::default()
405            })
406            .unwrap()
407            .result
408            .into_output()
409            .unwrap();
410        assert_eq!(result3.as_ref(), b"output_from_precompile_1");
411    }
412}