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
17#[derive(Debug, Clone, Default)]
19pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>, FbBuildHasher<20>>>)
20where
21 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
22
23impl<S> PrecompileCacheMap<S>
24where
25 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
26{
27 pub fn cache_for_address(&self, address: Address) -> PrecompileCache<S> {
29 if let Some(cache) = self.0.get(&address) {
31 return cache.clone();
32 }
33 self.0.entry(address).or_default().clone()
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct PrecompileCache<S>(moka::sync::Cache<Bytes, CacheEntry<S>, DefaultHashBuilder>)
44where
45 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
46
47impl<S> Default for PrecompileCache<S>
48where
49 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
50{
51 fn default() -> Self {
52 Self(
53 moka::sync::CacheBuilder::new(MAX_CACHE_SIZE as u64)
54 .eviction_policy(EvictionPolicy::lru())
55 .weigher(|key: &Bytes, value: &CacheEntry<S>| {
56 (key.len() + value.output.bytes.len()) as u32
57 })
58 .build_with_hasher(Default::default()),
59 )
60 }
61}
62
63impl<S> PrecompileCache<S>
64where
65 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
66{
67 fn get(&self, input: &[u8], spec: S) -> Option<CacheEntry<S>> {
68 self.0.get(input).filter(|e| e.spec == spec)
69 }
70
71 fn insert(&self, input: Bytes, value: CacheEntry<S>) -> usize {
73 self.0.insert(input, value);
74 self.0.entry_count() as usize
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct CacheEntry<S> {
83 output: PrecompileOutput,
84 spec: S,
85}
86
87impl<S> CacheEntry<S> {
88 const fn gas_used(&self) -> u64 {
89 self.output.gas_used
90 }
91
92 fn to_precompile_result(&self, reservoir: u64) -> PrecompileResult {
97 let mut output = self.output.clone();
98 output.reservoir = reservoir;
99 Ok(output)
100 }
101}
102
103#[derive(Debug)]
105pub struct CachedPrecompile<S>
106where
107 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
108{
109 cache: PrecompileCache<S>,
111 precompile: DynPrecompile,
113 metrics: Option<CachedPrecompileMetrics>,
115 spec_id: S,
117}
118
119impl<S> CachedPrecompile<S>
120where
121 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
122{
123 pub const fn new(
125 precompile: DynPrecompile,
126 cache: PrecompileCache<S>,
127 spec_id: S,
128 metrics: Option<CachedPrecompileMetrics>,
129 ) -> Self {
130 Self { precompile, cache, spec_id, metrics }
131 }
132
133 pub fn wrap(
135 precompile: DynPrecompile,
136 cache: PrecompileCache<S>,
137 spec_id: S,
138 metrics: Option<CachedPrecompileMetrics>,
139 ) -> DynPrecompile {
140 let precompile_id = precompile.precompile_id().clone();
141 let wrapped = Self::new(precompile, cache, spec_id, metrics);
142 (precompile_id, move |input: PrecompileInput<'_>| -> PrecompileResult {
143 wrapped.call(input)
144 })
145 .into()
146 }
147
148 fn increment_by_one_precompile_cache_hits(&self) {
149 if let Some(metrics) = &self.metrics {
150 metrics.precompile_cache_hits.increment(1);
151 }
152 }
153
154 fn increment_by_one_precompile_cache_misses(&self) {
155 if let Some(metrics) = &self.metrics {
156 metrics.precompile_cache_misses.increment(1);
157 }
158 }
159
160 fn set_precompile_cache_size_metric(&self, to: f64) {
161 if let Some(metrics) = &self.metrics {
162 metrics.precompile_cache_size.set(to);
163 }
164 }
165
166 fn increment_by_one_precompile_errors(&self) {
167 if let Some(metrics) = &self.metrics {
168 metrics.precompile_errors.increment(1);
169 }
170 }
171}
172
173impl<S> Precompile for CachedPrecompile<S>
174where
175 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
176{
177 fn precompile_id(&self) -> &PrecompileId {
178 self.precompile.precompile_id()
179 }
180
181 fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
182 if let Some(entry) = &self.cache.get(input.data, self.spec_id.clone()) &&
183 input.gas >= entry.gas_used()
184 {
185 self.increment_by_one_precompile_cache_hits();
186 return entry.to_precompile_result(input.reservoir);
187 }
188
189 let calldata = input.data;
190 let reservoir = input.reservoir;
191 let result = self.precompile.call(input);
192
193 match &result {
194 Ok(output) if output.is_success() => {
197 if output.reservoir != reservoir {
203 error!(target: "engine::tree", precompile_id = self.precompile.precompile_id().name(), "cacheable precompile decremented reservoir, skipping cache insertion");
204 } else if output.state_gas_used != 0 {
205 error!(target: "engine::tree", precompile_id = self.precompile.precompile_id().name(), "cacheable precompile used state gas, skipping cache insertion");
206 } else {
207 let size = self.cache.insert(
208 Bytes::copy_from_slice(calldata),
209 CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
210 );
211 self.set_precompile_cache_size_metric(size as f64);
212 self.increment_by_one_precompile_cache_misses();
213 }
214 }
215 _ => {
216 self.increment_by_one_precompile_errors();
217 }
218 }
219 result
220 }
221}
222
223#[derive(reth_metrics::Metrics, Clone)]
225#[metrics(scope = "sync.caching")]
226pub struct CachedPrecompileMetrics {
227 pub precompile_cache_hits: metrics::Counter,
229
230 pub precompile_cache_misses: metrics::Counter,
232
233 pub precompile_cache_size: metrics::Gauge,
235
236 pub precompile_errors: metrics::Counter,
238}
239
240impl CachedPrecompileMetrics {
241 pub fn new_with_address(address: Address) -> Self {
246 Self::new_with_labels(&[("address", format!("0x{address:02x}"))])
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
254 use reth_revm::db::EmptyDB;
255 use revm::{
256 context::TxEnv,
257 precompile::{PrecompileOutput, PrecompileStatus},
258 primitives::hardfork::SpecId,
259 };
260
261 #[test]
262 fn test_precompile_cache_basic() {
263 let dyn_precompile: DynPrecompile = (|_input: PrecompileInput<'_>| -> PrecompileResult {
264 Ok(PrecompileOutput {
265 status: PrecompileStatus::Success,
266 gas_used: 0,
267 state_gas_used: 0,
268 reservoir: 0,
269 gas_refunded: 0,
270 bytes: Bytes::default(),
271 })
272 })
273 .into();
274
275 let cache =
276 CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE, None);
277
278 let output = PrecompileOutput {
279 status: PrecompileStatus::Success,
280 gas_used: 50,
281 state_gas_used: 0,
282 reservoir: 0,
283 gas_refunded: 0,
284 bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
285 };
286
287 let input = b"test_input";
288 let expected = CacheEntry { output, spec: SpecId::PRAGUE };
289 cache.cache.insert(input.into(), expected.clone());
290
291 let actual = cache.cache.get(input, SpecId::PRAGUE).unwrap();
292
293 assert_eq!(actual, expected);
294 }
295
296 #[test]
297 fn test_precompile_cache_map_separate_addresses() {
298 let mut evm = EthEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default());
299 let input_data = b"same_input";
300 let gas_limit = 100_000;
301
302 let address1 = Address::repeat_byte(1);
303 let address2 = Address::repeat_byte(2);
304
305 let cache_map = PrecompileCacheMap::default();
306
307 let precompile1: DynPrecompile = (PrecompileId::custom("custom"), {
309 move |input: PrecompileInput<'_>| -> PrecompileResult {
310 assert_eq!(input.data, input_data);
311
312 Ok(PrecompileOutput {
313 status: PrecompileStatus::Success,
314 gas_used: 5000,
315 state_gas_used: 0,
316 reservoir: 0,
317 gas_refunded: 0,
318 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
319 })
320 }
321 })
322 .into();
323
324 let precompile2: DynPrecompile = (PrecompileId::custom("custom"), {
326 move |input: PrecompileInput<'_>| -> PrecompileResult {
327 assert_eq!(input.data, input_data);
328
329 Ok(PrecompileOutput {
330 status: PrecompileStatus::Success,
331 gas_used: 7000,
332 state_gas_used: 0,
333 reservoir: 0,
334 gas_refunded: 0,
335 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
336 })
337 }
338 })
339 .into();
340
341 let wrapped_precompile1 = CachedPrecompile::wrap(
342 precompile1,
343 cache_map.cache_for_address(address1),
344 SpecId::PRAGUE,
345 None,
346 );
347 let wrapped_precompile2 = CachedPrecompile::wrap(
348 precompile2,
349 cache_map.cache_for_address(address2),
350 SpecId::PRAGUE,
351 None,
352 );
353
354 let precompile1_address = Address::with_last_byte(1);
355 let precompile2_address = Address::with_last_byte(2);
356
357 evm.precompiles_mut().apply_precompile(&precompile1_address, |_| Some(wrapped_precompile1));
358 evm.precompiles_mut().apply_precompile(&precompile2_address, |_| Some(wrapped_precompile2));
359
360 let result1 = evm
362 .transact_raw(TxEnv {
363 caller: Address::ZERO,
364 gas_limit,
365 data: input_data.into(),
366 kind: precompile1_address.into(),
367 ..Default::default()
368 })
369 .unwrap()
370 .result
371 .into_output()
372 .unwrap();
373 assert_eq!(result1.as_ref(), b"output_from_precompile_1");
374
375 let result2 = evm
378 .transact_raw(TxEnv {
379 caller: Address::ZERO,
380 gas_limit,
381 data: input_data.into(),
382 kind: precompile2_address.into(),
383 ..Default::default()
384 })
385 .unwrap()
386 .result
387 .into_output()
388 .unwrap();
389 assert_eq!(result2.as_ref(), b"output_from_precompile_2");
390
391 let result3 = evm
393 .transact_raw(TxEnv {
394 caller: Address::ZERO,
395 gas_limit,
396 data: input_data.into(),
397 kind: precompile1_address.into(),
398 ..Default::default()
399 })
400 .unwrap()
401 .result
402 .into_output()
403 .unwrap();
404 assert_eq!(result3.as_ref(), b"output_from_precompile_1");
405 }
406}