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