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 reth_evm::precompiles::{DynPrecompile, Precompile};
5use revm::precompile::{PrecompileOutput, PrecompileResult};
6use revm_primitives::{Address, HashMap};
7use std::{hash::Hash, sync::Arc};
8
9/// Stores caches for each precompile.
10#[derive(Debug, Clone, Default)]
11pub struct PrecompileCacheMap<S>(HashMap<Address, PrecompileCache<S>>)
12where
13    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
14
15impl<S> PrecompileCacheMap<S>
16where
17    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
18{
19    pub(crate) fn cache_for_address(&mut self, address: Address) -> PrecompileCache<S> {
20        self.0.entry(address).or_default().clone()
21    }
22}
23
24/// Cache for precompiles, for each input stores the result.
25#[derive(Debug, Clone)]
26pub struct PrecompileCache<S>(
27    Arc<mini_moka::sync::Cache<CacheKey<S>, CacheEntry, alloy_primitives::map::DefaultHashBuilder>>,
28)
29where
30    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
31
32impl<S> Default for PrecompileCache<S>
33where
34    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
35{
36    fn default() -> Self {
37        Self(Arc::new(
38            mini_moka::sync::CacheBuilder::new(100_000)
39                .build_with_hasher(alloy_primitives::map::DefaultHashBuilder::default()),
40        ))
41    }
42}
43
44impl<S> PrecompileCache<S>
45where
46    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
47{
48    fn get(&self, key: &CacheKey<S>) -> Option<CacheEntry> {
49        self.0.get(key)
50    }
51
52    fn insert(&self, key: CacheKey<S>, value: CacheEntry) {
53        self.0.insert(key, value);
54    }
55
56    fn weighted_size(&self) -> u64 {
57        self.0.weighted_size()
58    }
59}
60
61/// Cache key, spec id and precompile call input. spec id is included in the key to account for
62/// precompile repricing across fork activations.
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct CacheKey<S>((S, Bytes));
65
66impl<S> CacheKey<S> {
67    const fn new(spec_id: S, input: Bytes) -> Self {
68        Self((spec_id, input))
69    }
70}
71
72/// Cache entry, precompile successful output.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CacheEntry(PrecompileOutput);
75
76impl CacheEntry {
77    const fn gas_used(&self) -> u64 {
78        self.0.gas_used
79    }
80
81    fn to_precompile_result(&self) -> PrecompileResult {
82        Ok(self.0.clone())
83    }
84}
85
86/// A cache for precompile inputs / outputs.
87#[derive(Debug)]
88pub(crate) struct CachedPrecompile<S>
89where
90    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
91{
92    /// Cache for precompile results and gas bounds.
93    cache: PrecompileCache<S>,
94    /// The precompile.
95    precompile: DynPrecompile,
96    /// Cache metrics.
97    metrics: CachedPrecompileMetrics,
98    /// Spec id associated to the EVM from which this cached precompile was created.
99    spec_id: S,
100}
101
102impl<S> CachedPrecompile<S>
103where
104    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
105{
106    /// `CachedPrecompile` constructor.
107    pub(crate) fn new(precompile: DynPrecompile, cache: PrecompileCache<S>, spec_id: S) -> Self {
108        Self { precompile, cache, spec_id, metrics: Default::default() }
109    }
110
111    pub(crate) fn wrap(
112        precompile: DynPrecompile,
113        cache: PrecompileCache<S>,
114        spec_id: S,
115    ) -> DynPrecompile {
116        let wrapped = Self::new(precompile, cache, spec_id);
117        move |data: &[u8], gas_limit: u64| -> PrecompileResult { wrapped.call(data, gas_limit) }
118            .into()
119    }
120
121    fn increment_by_one_precompile_cache_hits(&self) {
122        self.metrics.precompile_cache_hits.increment(1);
123    }
124
125    fn increment_by_one_precompile_cache_misses(&self) {
126        self.metrics.precompile_cache_misses.increment(1);
127    }
128
129    fn increment_by_one_precompile_errors(&self) {
130        self.metrics.precompile_errors.increment(1);
131    }
132
133    fn update_precompile_cache_size(&self) {
134        let new_size = self.cache.weighted_size();
135        self.metrics.precompile_cache_size.set(new_size as f64);
136    }
137}
138
139impl<S> Precompile for CachedPrecompile<S>
140where
141    S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
142{
143    fn call(&self, data: &[u8], gas_limit: u64) -> PrecompileResult {
144        let key = CacheKey::new(self.spec_id.clone(), Bytes::copy_from_slice(data));
145
146        if let Some(entry) = &self.cache.get(&key) {
147            self.increment_by_one_precompile_cache_hits();
148            if gas_limit >= entry.gas_used() {
149                return entry.to_precompile_result()
150            }
151        }
152
153        let result = self.precompile.call(data, gas_limit);
154
155        match &result {
156            Ok(output) => {
157                self.increment_by_one_precompile_cache_misses();
158                self.cache.insert(key, CacheEntry(output.clone()));
159            }
160            _ => {
161                self.increment_by_one_precompile_errors();
162            }
163        }
164
165        self.update_precompile_cache_size();
166        result
167    }
168}
169
170/// Metrics for the cached precompile.
171#[derive(reth_metrics::Metrics, Clone)]
172#[metrics(scope = "sync.caching")]
173pub(crate) struct CachedPrecompileMetrics {
174    /// Precompile cache hits
175    precompile_cache_hits: metrics::Counter,
176
177    /// Precompile cache misses
178    precompile_cache_misses: metrics::Counter,
179
180    /// Precompile cache size
181    ///
182    /// NOTE: this uses the moka caches`weighted_size` method to calculate size.
183    precompile_cache_size: metrics::Gauge,
184
185    /// Precompile execution errors.
186    precompile_errors: metrics::Counter,
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use revm::precompile::PrecompileOutput;
193    use revm_primitives::hardfork::SpecId;
194
195    #[test]
196    fn test_precompile_cache_basic() {
197        let dyn_precompile: DynPrecompile = |_input: &[u8], _gas: u64| -> PrecompileResult {
198            Ok(PrecompileOutput { gas_used: 0, bytes: Bytes::default() })
199        }
200        .into();
201
202        let cache =
203            CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE);
204
205        let key = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
206
207        let output = PrecompileOutput {
208            gas_used: 50,
209            bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
210        };
211
212        let expected = CacheEntry(output);
213        cache.cache.insert(key.clone(), expected.clone());
214
215        let actual = cache.cache.get(&key).unwrap();
216
217        assert_eq!(actual, expected);
218    }
219
220    #[test]
221    fn test_precompile_cache_map_separate_addresses() {
222        let input_data = b"same_input";
223        let gas_limit = 100_000;
224
225        let address1 = Address::repeat_byte(1);
226        let address2 = Address::repeat_byte(2);
227
228        let mut cache_map = PrecompileCacheMap::default();
229
230        // create the first precompile with a specific output
231        let precompile1: DynPrecompile = {
232            move |data: &[u8], _gas: u64| -> PrecompileResult {
233                assert_eq!(data, input_data);
234
235                Ok(PrecompileOutput {
236                    gas_used: 5000,
237                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
238                })
239            }
240        }
241        .into();
242
243        // create the second precompile with a different output
244        let precompile2: DynPrecompile = {
245            move |data: &[u8], _gas: u64| -> PrecompileResult {
246                assert_eq!(data, input_data);
247
248                Ok(PrecompileOutput {
249                    gas_used: 7000,
250                    bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
251                })
252            }
253        }
254        .into();
255
256        let wrapped_precompile1 = CachedPrecompile::wrap(
257            precompile1,
258            cache_map.cache_for_address(address1),
259            SpecId::PRAGUE,
260        );
261        let wrapped_precompile2 = CachedPrecompile::wrap(
262            precompile2,
263            cache_map.cache_for_address(address2),
264            SpecId::PRAGUE,
265        );
266
267        // first invocation of precompile1 (cache miss)
268        let result1 = wrapped_precompile1.call(input_data, gas_limit).unwrap();
269        assert_eq!(result1.bytes.as_ref(), b"output_from_precompile_1");
270
271        // first invocation of precompile2 with the same input (should be a cache miss)
272        // if cache was incorrectly shared, we'd get precompile1's result
273        let result2 = wrapped_precompile2.call(input_data, gas_limit).unwrap();
274        assert_eq!(result2.bytes.as_ref(), b"output_from_precompile_2");
275
276        // second invocation of precompile1 (should be a cache hit)
277        let result3 = wrapped_precompile1.call(input_data, gas_limit).unwrap();
278        assert_eq!(result3.bytes.as_ref(), b"output_from_precompile_1");
279    }
280}