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