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