1use 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
15const MAX_CACHE_SIZE: u32 = 10_000;
17
18#[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#[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 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#[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#[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#[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#[derive(Debug)]
117pub(crate) struct CachedPrecompile<S>
118where
119 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
120{
121 cache: PrecompileCache<S>,
123 precompile: DynPrecompile,
125 metrics: Option<CachedPrecompileMetrics>,
127 spec_id: S,
129}
130
131impl<S> CachedPrecompile<S>
132where
133 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
134{
135 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#[derive(reth_metrics::Metrics, Clone)]
222#[metrics(scope = "sync.caching")]
223pub(crate) struct CachedPrecompileMetrics {
224 precompile_cache_hits: metrics::Counter,
226
227 precompile_cache_misses: metrics::Counter,
229
230 precompile_cache_size: metrics::Gauge,
232
233 precompile_errors: metrics::Counter,
235}
236
237impl CachedPrecompileMetrics {
238 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 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 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 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 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 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}