1use alloy_primitives::Bytes;
4use reth_evm::precompiles::{DynPrecompile, Precompile};
5use revm::precompile::{PrecompileOutput, PrecompileResult};
6use revm_primitives::{Address, HashMap};
7use std::{hash::Hash, sync::Arc};
8
9#[derive(Debug, Clone, Default)]
11pub struct PrecompileCacheMap<S>(HashMap<Address, PrecompileCache<S>>)
12where
13 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
14
15impl<S> PrecompileCacheMap<S>
16where
17 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
18{
19 pub(crate) fn cache_for_address(&mut self, address: Address) -> PrecompileCache<S> {
20 self.0.entry(address).or_default().clone()
21 }
22}
23
24#[derive(Debug, Clone)]
26pub struct PrecompileCache<S>(
27 Arc<mini_moka::sync::Cache<CacheKey<S>, CacheEntry, alloy_primitives::map::DefaultHashBuilder>>,
28)
29where
30 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
31
32impl<S> Default for PrecompileCache<S>
33where
34 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
35{
36 fn default() -> Self {
37 Self(Arc::new(
38 mini_moka::sync::CacheBuilder::new(100_000)
39 .build_with_hasher(alloy_primitives::map::DefaultHashBuilder::default()),
40 ))
41 }
42}
43
44impl<S> PrecompileCache<S>
45where
46 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
47{
48 fn get(&self, key: &CacheKey<S>) -> Option<CacheEntry> {
49 self.0.get(key)
50 }
51
52 fn insert(&self, key: CacheKey<S>, value: CacheEntry) {
53 self.0.insert(key, value);
54 }
55
56 fn weighted_size(&self) -> u64 {
57 self.0.weighted_size()
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct CacheKey<S>((S, Bytes));
65
66impl<S> CacheKey<S> {
67 const fn new(spec_id: S, input: Bytes) -> Self {
68 Self((spec_id, input))
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CacheEntry(PrecompileOutput);
75
76impl CacheEntry {
77 const fn gas_used(&self) -> u64 {
78 self.0.gas_used
79 }
80
81 fn to_precompile_result(&self) -> PrecompileResult {
82 Ok(self.0.clone())
83 }
84}
85
86#[derive(Debug)]
88pub(crate) struct CachedPrecompile<S>
89where
90 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
91{
92 cache: PrecompileCache<S>,
94 precompile: DynPrecompile,
96 metrics: CachedPrecompileMetrics,
98 spec_id: S,
100}
101
102impl<S> CachedPrecompile<S>
103where
104 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
105{
106 pub(crate) fn new(precompile: DynPrecompile, cache: PrecompileCache<S>, spec_id: S) -> Self {
108 Self { precompile, cache, spec_id, metrics: Default::default() }
109 }
110
111 pub(crate) fn wrap(
112 precompile: DynPrecompile,
113 cache: PrecompileCache<S>,
114 spec_id: S,
115 ) -> DynPrecompile {
116 let wrapped = Self::new(precompile, cache, spec_id);
117 move |data: &[u8], gas_limit: u64| -> PrecompileResult { wrapped.call(data, gas_limit) }
118 .into()
119 }
120
121 fn increment_by_one_precompile_cache_hits(&self) {
122 self.metrics.precompile_cache_hits.increment(1);
123 }
124
125 fn increment_by_one_precompile_cache_misses(&self) {
126 self.metrics.precompile_cache_misses.increment(1);
127 }
128
129 fn increment_by_one_precompile_errors(&self) {
130 self.metrics.precompile_errors.increment(1);
131 }
132
133 fn update_precompile_cache_size(&self) {
134 let new_size = self.cache.weighted_size();
135 self.metrics.precompile_cache_size.set(new_size as f64);
136 }
137}
138
139impl<S> Precompile for CachedPrecompile<S>
140where
141 S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
142{
143 fn call(&self, data: &[u8], gas_limit: u64) -> PrecompileResult {
144 let key = CacheKey::new(self.spec_id.clone(), Bytes::copy_from_slice(data));
145
146 if let Some(entry) = &self.cache.get(&key) {
147 self.increment_by_one_precompile_cache_hits();
148 if gas_limit >= entry.gas_used() {
149 return entry.to_precompile_result()
150 }
151 }
152
153 let result = self.precompile.call(data, gas_limit);
154
155 match &result {
156 Ok(output) => {
157 self.increment_by_one_precompile_cache_misses();
158 self.cache.insert(key, CacheEntry(output.clone()));
159 }
160 _ => {
161 self.increment_by_one_precompile_errors();
162 }
163 }
164
165 self.update_precompile_cache_size();
166 result
167 }
168}
169
170#[derive(reth_metrics::Metrics, Clone)]
172#[metrics(scope = "sync.caching")]
173pub(crate) struct CachedPrecompileMetrics {
174 precompile_cache_hits: metrics::Counter,
176
177 precompile_cache_misses: metrics::Counter,
179
180 precompile_cache_size: metrics::Gauge,
184
185 precompile_errors: metrics::Counter,
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use revm::precompile::PrecompileOutput;
193 use revm_primitives::hardfork::SpecId;
194
195 #[test]
196 fn test_precompile_cache_basic() {
197 let dyn_precompile: DynPrecompile = |_input: &[u8], _gas: u64| -> PrecompileResult {
198 Ok(PrecompileOutput { gas_used: 0, bytes: Bytes::default() })
199 }
200 .into();
201
202 let cache =
203 CachedPrecompile::new(dyn_precompile, PrecompileCache::default(), SpecId::PRAGUE);
204
205 let key = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
206
207 let output = PrecompileOutput {
208 gas_used: 50,
209 bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
210 };
211
212 let expected = CacheEntry(output);
213 cache.cache.insert(key.clone(), expected.clone());
214
215 let actual = cache.cache.get(&key).unwrap();
216
217 assert_eq!(actual, expected);
218 }
219
220 #[test]
221 fn test_precompile_cache_map_separate_addresses() {
222 let input_data = b"same_input";
223 let gas_limit = 100_000;
224
225 let address1 = Address::repeat_byte(1);
226 let address2 = Address::repeat_byte(2);
227
228 let mut cache_map = PrecompileCacheMap::default();
229
230 let precompile1: DynPrecompile = {
232 move |data: &[u8], _gas: u64| -> PrecompileResult {
233 assert_eq!(data, input_data);
234
235 Ok(PrecompileOutput {
236 gas_used: 5000,
237 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
238 })
239 }
240 }
241 .into();
242
243 let precompile2: DynPrecompile = {
245 move |data: &[u8], _gas: u64| -> PrecompileResult {
246 assert_eq!(data, input_data);
247
248 Ok(PrecompileOutput {
249 gas_used: 7000,
250 bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
251 })
252 }
253 }
254 .into();
255
256 let wrapped_precompile1 = CachedPrecompile::wrap(
257 precompile1,
258 cache_map.cache_for_address(address1),
259 SpecId::PRAGUE,
260 );
261 let wrapped_precompile2 = CachedPrecompile::wrap(
262 precompile2,
263 cache_map.cache_for_address(address2),
264 SpecId::PRAGUE,
265 );
266
267 let result1 = wrapped_precompile1.call(input_data, gas_limit).unwrap();
269 assert_eq!(result1.bytes.as_ref(), b"output_from_precompile_1");
270
271 let result2 = wrapped_precompile2.call(input_data, gas_limit).unwrap();
274 assert_eq!(result2.bytes.as_ref(), b"output_from_precompile_2");
275
276 let result3 = wrapped_precompile1.call(input_data, gas_limit).unwrap();
278 assert_eq!(result3.bytes.as_ref(), b"output_from_precompile_1");
279 }
280}