1use alloy_primitives::{
4 map::{DefaultHashBuilder, FbBuildHasher},
5 Address, Bytes,
6};
7use moka::policy::EvictionPolicy;
8use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
9use reth_primitives_traits::dashmap::DashMap;
10use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
11use std::{hash::Hash, sync::Arc};
12use tracing::error;
13
14const MAX_CACHE_SIZE: u32 = 1024 * 1024;
16
17const MAX_PRECOMPILE_CACHE_INPUT_SIZE: usize = 2 * 1024;
19
20#[derive(Debug, Clone, Default)]
22pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>, FbBuildHasher<20>>>)
23where
24 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
25
26impl<S> PrecompileCacheMap<S>
27where
28 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
29{
30 pub fn cache_for_address(&self, address: Address) -> PrecompileCache<S> {
32 if let Some(cache) = self.0.get(&address) {
34 return cache.clone();
35 }
36 self.0.entry(address).or_default().clone()
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct PrecompileCache<S>(moka::sync::Cache<Bytes, CacheEntry<S>, DefaultHashBuilder>)
47where
48 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
49
50impl<S> Default for PrecompileCache<S>
51where
52 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
53{
54 fn default() -> Self {
55 Self(
56 moka::sync::CacheBuilder::new(MAX_CACHE_SIZE as u64)
57 .eviction_policy(EvictionPolicy::lru())
58 .weigher(|key: &Bytes, value: &CacheEntry<S>| {
59 (key.len() + value.output.bytes.len()) as u32
60 })
61 .build_with_hasher(Default::default()),
62 )
63 }
64}
65
66impl<S> PrecompileCache<S>
67where
68 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
69{
70 fn get(&self, input: &[u8], spec: S) -> Option<CacheEntry<S>> {
71 self.0.get(input).filter(|e| e.spec == spec)
72 }
73
74 fn insert(&self, input: Bytes, value: CacheEntry<S>) -> usize {
76 self.0.insert(input, value);
77 self.0.entry_count() as usize
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct CacheEntry<S> {
86 output: PrecompileOutput,
87 spec: S,
88}
89
90impl<S> CacheEntry<S> {
91 const fn gas_used(&self) -> u64 {
92 self.output.gas_used
93 }
94
95 fn to_precompile_result(&self, reservoir: u64) -> PrecompileResult {
100 let mut output = self.output.clone();
101 output.reservoir = reservoir;
102 Ok(output)
103 }
104}
105
106#[derive(Debug)]
108pub struct CachedPrecompile<S>
109where
110 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
111{
112 cache: PrecompileCache<S>,
114 precompile: DynPrecompile,
116 metrics: Option<CachedPrecompileMetrics>,
118 spec_id: S,
120}
121
122impl<S> CachedPrecompile<S>
123where
124 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
125{
126 pub const fn new(
128 precompile: DynPrecompile,
129 cache: PrecompileCache<S>,
130 spec_id: S,
131 metrics: Option<CachedPrecompileMetrics>,
132 ) -> Self {
133 Self { precompile, cache, spec_id, metrics }
134 }
135
136 pub fn wrap(
138 precompile: DynPrecompile,
139 cache: PrecompileCache<S>,
140 spec_id: S,
141 metrics: Option<CachedPrecompileMetrics>,
142 ) -> DynPrecompile {
143 let precompile_id = precompile.precompile_id().clone();
144 let wrapped = Self::new(precompile, cache, spec_id, metrics);
145 (precompile_id, move |input: PrecompileInput<'_>| -> PrecompileResult {
146 wrapped.call(input)
147 })
148 .into()
149 }
150
151 fn increment_by_one_precompile_cache_hits(&self) {
152 if let Some(metrics) = &self.metrics {
153 metrics.precompile_cache_hits.increment(1);
154 }
155 }
156
157 fn increment_by_one_precompile_cache_misses(&self) {
158 if let Some(metrics) = &self.metrics {
159 metrics.precompile_cache_misses.increment(1);
160 }
161 }
162
163 fn set_precompile_cache_size_metric(&self, to: f64) {
164 if let Some(metrics) = &self.metrics {
165 metrics.precompile_cache_size.set(to);
166 }
167 }
168
169 fn increment_by_one_precompile_errors(&self) {
170 if let Some(metrics) = &self.metrics {
171 metrics.precompile_errors.increment(1);
172 }
173 }
174}
175
176impl<S> Precompile for CachedPrecompile<S>
177where
178 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
179{
180 fn precompile_id(&self) -> &PrecompileId {
181 self.precompile.precompile_id()
182 }
183
184 fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
185 let cacheable_input = input.data.len() <= MAX_PRECOMPILE_CACHE_INPUT_SIZE;
186 if cacheable_input &&
187 let Some(entry) = &self.cache.get(input.data, self.spec_id.clone()) &&
188 input.gas >= entry.gas_used()
189 {
190 self.increment_by_one_precompile_cache_hits();
191 return entry.to_precompile_result(input.reservoir);
192 }
193
194 let calldata = input.data;
195 let reservoir = input.reservoir;
196 let result = self.precompile.call(input);
197
198 match &result {
199 Ok(output) if cacheable_input && output.is_success() => {
202 if output.reservoir != reservoir {
208 error!(target: "engine::tree", precompile_id = self.precompile.precompile_id().name(), "cacheable precompile decremented reservoir, skipping cache insertion");
209 } else if output.state_gas_used != 0 {
210 error!(target: "engine::tree", precompile_id = self.precompile.precompile_id().name(), "cacheable precompile used state gas, skipping cache insertion");
211 } else {
212 let size = self.cache.insert(
213 Bytes::copy_from_slice(calldata),
214 CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
215 );
216 self.set_precompile_cache_size_metric(size as f64);
217 self.increment_by_one_precompile_cache_misses();
218 }
219 }
220 Ok(output) if output.is_success() => {}
222 _ => {
223 self.increment_by_one_precompile_errors();
224 }
225 }
226 result
227 }
228}
229
230#[derive(reth_metrics::Metrics, Clone)]
232#[metrics(scope = "sync.caching")]
233pub struct CachedPrecompileMetrics {
234 pub precompile_cache_hits: metrics::Counter,
236
237 pub precompile_cache_misses: metrics::Counter,
239
240 pub precompile_cache_size: metrics::Gauge,
242
243 pub precompile_errors: metrics::Counter,
245}
246
247impl CachedPrecompileMetrics {
248 pub fn new_with_address(address: Address) -> Self {
253 Self::new_with_labels(&[("address", format!("0x{address:02x}"))])
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use metrics_util::debugging::{DebugValue, DebuggingRecorder};
261 use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
262 use reth_revm::db::EmptyDB;
263 use revm::{
264 context::TxEnv,
265 precompile::{PrecompileOutput, PrecompileStatus},
266 primitives::hardfork::SpecId,
267 };
268
269 #[test]
270 fn test_precompile_cache_basic() {
271 let dyn_precompile: DynPrecompile = (|_input: PrecompileInput<'_>| -> PrecompileResult {
272 Ok(PrecompileOutput {
273 status: PrecompileStatus::Success,
274 gas_used: 0,
275 state_gas_used: 0,
276 state_gas_spilled: 0,
277 reservoir: 0,
278 gas_refunded: 0,
279 bytes: Bytes::default(),
280 })
281 })
282 .into();
283
284 let cache =
285 CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
286
287 let output = PrecompileOutput {
288 status: PrecompileStatus::Success,
289 gas_used: 50,
290 state_gas_used: 0,
291 state_gas_spilled: 0,
292 reservoir: 0,
293 gas_refunded: 0,
294 bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
295 };
296
297 let input = b"test_input";
298 let expected = CacheEntry { output, spec: SpecId::PRAGUE };
299 cache.cache.insert(input.into(), expected.clone());
300
301 let actual = cache.cache.get(input, SpecId::PRAGUE).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 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 status: PrecompileStatus::Success,
324 gas_used: 5000,
325 state_gas_used: 0,
326 state_gas_spilled: 0,
327 reservoir: 0,
328 gas_refunded: 0,
329 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
330 })
331 }
332 })
333 .into();
334
335 let precompile2: DynPrecompile = (PrecompileId::custom("custom"), {
337 move |input: PrecompileInput<'_>| -> PrecompileResult {
338 assert_eq!(input.data, input_data);
339
340 Ok(PrecompileOutput {
341 status: PrecompileStatus::Success,
342 gas_used: 7000,
343 state_gas_used: 0,
344 state_gas_spilled: 0,
345 reservoir: 0,
346 gas_refunded: 0,
347 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
348 })
349 }
350 })
351 .into();
352
353 let wrapped_precompile1 = CachedPrecompile::wrap(
354 precompile1,
355 cache_map.cache_for_address(address1),
356 SpecId::PRAGUE,
357 None,
358 );
359 let wrapped_precompile2 = CachedPrecompile::wrap(
360 precompile2,
361 cache_map.cache_for_address(address2),
362 SpecId::PRAGUE,
363 None,
364 );
365
366 let precompile1_address = Address::with_last_byte(1);
367 let precompile2_address = Address::with_last_byte(2);
368
369 evm.precompiles_mut().apply_precompile(&precompile1_address, |_| Some(wrapped_precompile1));
370 evm.precompiles_mut().apply_precompile(&precompile2_address, |_| Some(wrapped_precompile2));
371
372 let result1 = evm
374 .transact_raw(TxEnv {
375 caller: Address::ZERO,
376 gas_limit,
377 data: input_data.into(),
378 kind: precompile1_address.into(),
379 ..Default::default()
380 })
381 .unwrap()
382 .result
383 .into_output()
384 .unwrap();
385 assert_eq!(result1.as_ref(), b"output_from_precompile_1");
386
387 let result2 = evm
390 .transact_raw(TxEnv {
391 caller: Address::ZERO,
392 gas_limit,
393 data: input_data.into(),
394 kind: precompile2_address.into(),
395 ..Default::default()
396 })
397 .unwrap()
398 .result
399 .into_output()
400 .unwrap();
401 assert_eq!(result2.as_ref(), b"output_from_precompile_2");
402
403 let result3 = evm
405 .transact_raw(TxEnv {
406 caller: Address::ZERO,
407 gas_limit,
408 data: input_data.into(),
409 kind: precompile1_address.into(),
410 ..Default::default()
411 })
412 .unwrap()
413 .result
414 .into_output()
415 .unwrap();
416 assert_eq!(result3.as_ref(), b"output_from_precompile_1");
417 }
418
419 #[test]
420 fn test_oversized_successful_input_is_not_an_error() {
421 let recorder = DebuggingRecorder::new();
422 let snapshotter = recorder.snapshotter();
423 let cache = PrecompileCache::default();
424 let input_data = Bytes::from(vec![0; MAX_PRECOMPILE_CACHE_INPUT_SIZE + 1]);
425 let address = Address::with_last_byte(1);
426
427 metrics::with_local_recorder(&recorder, || {
428 let precompile: DynPrecompile = (|_input: PrecompileInput<'_>| {
429 Ok(PrecompileOutput {
430 status: PrecompileStatus::Success,
431 gas_used: 0,
432 state_gas_used: 0,
433 state_gas_spilled: 0,
434 reservoir: 0,
435 gas_refunded: 0,
436 bytes: Bytes::default(),
437 })
438 })
439 .into();
440 let wrapped = CachedPrecompile::wrap(
441 precompile,
442 cache.clone(),
443 SpecId::PRAGUE,
444 Some(CachedPrecompileMetrics::new_with_address(address)),
445 );
446 let mut evm =
447 EthEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default());
448 evm.precompiles_mut().apply_precompile(&address, |_| Some(wrapped));
449
450 evm.transact_raw(TxEnv {
451 caller: Address::ZERO,
452 gas_limit: 100_000,
453 data: input_data.clone(),
454 kind: address.into(),
455 ..Default::default()
456 })
457 .unwrap();
458 });
459
460 assert!(cache.get(&input_data, SpecId::PRAGUE).is_none());
461 let error_count = snapshotter.snapshot().into_vec().into_iter().find_map(
462 |(key, _unit, _description, value)| {
463 (key.key().name() == "sync.caching.precompile_errors").then_some(value)
464 },
465 );
466 assert_eq!(error_count, Some(DebugValue::Counter(0)));
467 }
468}