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