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