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