reth_engine_tree/tree/
precompile_cache.rs

1//! Contains a precompile cache that is backed by a moka cache.
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 { gas_used: 0, bytes: Bytes::default(), reverted: false })
278        }
279        .into();
280
281        let cache =
282            CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
283
284        let output = PrecompileOutput {
285            gas_used: 50,
286            bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
287            reverted: false,
288        };
289
290        let key = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
291        let expected = CacheEntry(output);
292        cache.cache.insert(key, expected.clone());
293
294        let key = CacheKeyRef::new(SpecId::PRAGUE, b"test_input");
295        let actual = cache.cache.get(&key).unwrap();
296
297        assert_eq!(actual, expected);
298    }
299
300    #[test]
301    fn test_precompile_cache_map_separate_addresses() {
302        let mut evm = EthEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default());
303        let input_data = b"same_input";
304        let gas_limit = 100_000;
305
306        let address1 = Address::repeat_byte(1);
307        let address2 = Address::repeat_byte(2);
308
309        let mut cache_map = PrecompileCacheMap::default();
310
311        // create the first precompile with a specific output
312        let precompile1: DynPrecompile = (PrecompileId::custom("custom"), {
313            move |input: PrecompileInput<'_>| -> PrecompileResult {
314                assert_eq!(input.data, input_data);
315
316                Ok(PrecompileOutput {
317                    gas_used: 5000,
318                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
319                    reverted: false,
320                })
321            }
322        })
323            .into();
324
325        // create the second precompile with a different output
326        let precompile2: DynPrecompile = (PrecompileId::custom("custom"), {
327            move |input: PrecompileInput<'_>| -> PrecompileResult {
328                assert_eq!(input.data, input_data);
329
330                Ok(PrecompileOutput {
331                    gas_used: 7000,
332                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
333                    reverted: false,
334                })
335            }
336        })
337            .into();
338
339        let wrapped_precompile1 = CachedPrecompile::wrap(
340            precompile1,
341            cache_map.cache_for_address(address1),
342            SpecId::PRAGUE,
343            None,
344        );
345        let wrapped_precompile2 = CachedPrecompile::wrap(
346            precompile2,
347            cache_map.cache_for_address(address2),
348            SpecId::PRAGUE,
349            None,
350        );
351
352        let precompile1_address = Address::with_last_byte(1);
353        let precompile2_address = Address::with_last_byte(2);
354
355        evm.precompiles_mut().apply_precompile(&precompile1_address, |_| Some(wrapped_precompile1));
356        evm.precompiles_mut().apply_precompile(&precompile2_address, |_| Some(wrapped_precompile2));
357
358        // first invocation of precompile1 (cache miss)
359        let result1 = evm
360            .transact_raw(TxEnv {
361                caller: Address::ZERO,
362                gas_limit,
363                data: input_data.into(),
364                kind: precompile1_address.into(),
365                ..Default::default()
366            })
367            .unwrap()
368            .result
369            .into_output()
370            .unwrap();
371        assert_eq!(result1.as_ref(), b"output_from_precompile_1");
372
373        // first invocation of precompile2 with the same input (should be a cache miss)
374        // if cache was incorrectly shared, we'd get precompile1's result
375        let result2 = evm
376            .transact_raw(TxEnv {
377                caller: Address::ZERO,
378                gas_limit,
379                data: input_data.into(),
380                kind: precompile2_address.into(),
381                ..Default::default()
382            })
383            .unwrap()
384            .result
385            .into_output()
386            .unwrap();
387        assert_eq!(result2.as_ref(), b"output_from_precompile_2");
388
389        // second invocation of precompile1 (should be a cache hit)
390        let result3 = evm
391            .transact_raw(TxEnv {
392                caller: Address::ZERO,
393                gas_limit,
394                data: input_data.into(),
395                kind: precompile1_address.into(),
396                ..Default::default()
397            })
398            .unwrap()
399            .result
400            .into_output()
401            .unwrap();
402        assert_eq!(result3.as_ref(), b"output_from_precompile_1");
403    }
404}