1use std::collections::{HashMap, HashSet};
16use std::path::Path;
17
18use alloy_primitives::{Address, U256};
19use serde::{Deserialize, Serialize};
20use tracing::{debug, info, warn};
21
22use crate::StorageAccessList;
23use crate::cache::{EvmCache, versioned};
24use crate::errors::PersistenceError;
25
26const PREFETCH_REGISTRY_MAGIC: &[u8; 8] = b"EFC-PFRG";
27const PREFETCH_REGISTRY_VERSION: u32 = 1;
28
29#[derive(Debug, Default, Clone, Serialize, Deserialize)]
35pub struct PrefetchRegistry {
36 phases: HashMap<String, StorageAccessList>,
38 keyed_phases: HashMap<String, HashMap<Address, StorageAccessList>>,
41}
42
43impl PrefetchRegistry {
44 pub fn load(path: &Path) -> Self {
52 match std::fs::read(path) {
53 Ok(data) => {
54 if let Some(registry) = versioned::decode::<PrefetchRegistry>(
55 &data,
56 PREFETCH_REGISTRY_MAGIC,
57 PREFETCH_REGISTRY_VERSION,
58 "prefetch registry",
59 ) {
60 let phase_count = registry.phases.len();
61 let keyed_phase_count = registry.keyed_phases.len();
62 let total_slots: usize = registry
63 .phases
64 .values()
65 .map(|al| al.slots.len())
66 .sum::<usize>()
67 + registry
68 .keyed_phases
69 .values()
70 .flat_map(|m| m.values())
71 .map(|al| al.slots.len())
72 .sum::<usize>();
73 info!(
74 phases = phase_count,
75 keyed_phases = keyed_phase_count,
76 total_slots,
77 "Loaded prefetch registry"
78 );
79 registry
80 } else {
81 warn!("Prefetch registry cache miss, starting fresh");
82 Self::default()
83 }
84 }
85 Err(_) => {
86 debug!("No prefetch registry file found, starting fresh");
87 Self::default()
88 }
89 }
90 }
91
92 pub fn save(&self, path: &Path) -> Result<(), PersistenceError> {
98 if let Some(parent) = path.parent() {
99 std::fs::create_dir_all(parent)
100 .map_err(|err| PersistenceError::create_dir(parent, err))?;
101 }
102 let data = versioned::encode(
103 PREFETCH_REGISTRY_MAGIC,
104 PREFETCH_REGISTRY_VERSION,
105 self,
106 "prefetch registry",
107 )?;
108 std::fs::write(path, data).map_err(|err| PersistenceError::write(path, err))?;
109
110 let total_slots: usize = self.phases.values().map(|al| al.slots.len()).sum::<usize>()
111 + self
112 .keyed_phases
113 .values()
114 .flat_map(|m| m.values())
115 .map(|al| al.slots.len())
116 .sum::<usize>();
117 debug!(total_slots, "Saved prefetch registry");
118 Ok(())
119 }
120
121 pub fn record(&mut self, phase: &str, access_list: StorageAccessList) {
150 self.phases.insert(phase.to_string(), access_list);
151 }
152
153 pub fn record_keyed(&mut self, phase: &str, key: Address, access_list: StorageAccessList) {
160 self.keyed_phases
161 .entry(phase.to_string())
162 .or_default()
163 .insert(key, access_list);
164 }
165
166 pub fn prefetch_phase(&self, phase: &str, cache: &mut EvmCache) -> (usize, usize) {
170 let Some(access_list) = self.phases.get(phase) else {
171 debug!(phase, "No prefetch data for phase");
172 return (0, 0);
173 };
174
175 if access_list.slots.is_empty() {
176 return (0, 0);
177 }
178
179 batch_prefetch(cache, access_list.slots.iter().copied(), phase)
180 }
181
182 pub fn prefetch_keyed(
189 &self,
190 phase: &str,
191 keys: &[Address],
192 cache: &mut EvmCache,
193 exclude: &HashSet<(Address, U256)>,
194 ) -> (usize, usize) {
195 let Some(keyed_map) = self.keyed_phases.get(phase) else {
196 debug!(phase, "No keyed prefetch data for phase");
197 return (0, 0);
198 };
199
200 let slots: HashSet<(Address, U256)> = keys
201 .iter()
202 .filter_map(|addr| keyed_map.get(addr))
203 .flat_map(|al| al.slots.iter().copied())
204 .filter(|slot| !exclude.contains(slot))
205 .collect();
206
207 if slots.is_empty() {
208 debug!(
209 phase,
210 keys = keys.len(),
211 excluded = exclude.len(),
212 "All keyed slots excluded or empty"
213 );
214 return (0, 0);
215 }
216
217 batch_prefetch(cache, slots.into_iter(), phase)
218 }
219
220 pub fn phase_slots(&self, phase: &str) -> HashSet<(Address, U256)> {
227 self.phases
228 .get(phase)
229 .map(|al| al.slots.clone())
230 .unwrap_or_default()
231 }
232}
233
234fn batch_prefetch(
244 cache: &mut EvmCache,
245 slots: impl Iterator<Item = (Address, U256)>,
246 phase: &str,
247) -> (usize, usize) {
248 let requests: Vec<(Address, U256)> = slots.collect();
249 if requests.is_empty() {
250 return (0, 0);
251 }
252
253 let fetcher = match cache.storage_batch_fetcher().cloned() {
254 Some(f) => f,
255 None => {
256 debug!(
257 "No batch fetcher available, skipping prefetch for {}",
258 phase
259 );
260 return (0, 0);
261 }
262 };
263
264 let start = std::time::Instant::now();
265 let total_requested = requests.len();
266 let results = fetcher(requests, cache.block());
268
269 let mut successes: Vec<(Address, U256, U256)> = Vec::with_capacity(results.len());
270 let mut errors = 0usize;
271 for (addr, slot, result) in results {
272 match result {
273 Ok(value) => successes.push((addr, slot, value)),
274 Err(_) => errors += 1,
275 }
276 }
277
278 let fetched = successes.len();
279 cache.inject_storage_batch(&successes);
280
281 let ms = start.elapsed().as_millis() as u64;
282 info!(
283 phase,
284 slots_requested = total_requested,
285 slots_fetched = fetched,
286 errors,
287 prefetch_ms = ms,
288 "Prefetch complete"
289 );
290
291 (fetched, errors)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_registry_record_and_phase_slots() {
300 let mut registry = PrefetchRegistry::default();
301 let mut al = StorageAccessList::default();
302 let addr = Address::repeat_byte(0x01);
303 al.slots.insert((addr, U256::from(1)));
304 al.slots.insert((addr, U256::from(2)));
305
306 registry.record("pool_refresh", al);
307
308 let slots = registry.phase_slots("pool_refresh");
309 assert_eq!(slots.len(), 2);
310 assert!(slots.contains(&(addr, U256::from(1))));
311 assert!(slots.contains(&(addr, U256::from(2))));
312
313 assert!(registry.phase_slots("nonexistent").is_empty());
315 }
316
317 #[test]
318 fn test_registry_record_keyed() {
319 let mut registry = PrefetchRegistry::default();
320 let key_a = Address::repeat_byte(0x01);
321 let key_b = Address::repeat_byte(0x02);
322
323 let mut al1 = StorageAccessList::default();
324 al1.slots.insert((key_a, U256::from(10)));
325
326 let mut al2 = StorageAccessList::default();
327 al2.slots.insert((key_b, U256::from(20)));
328
329 registry.record_keyed("per_target", key_a, al1);
330 registry.record_keyed("per_target", key_b, al2);
331
332 let map = registry.keyed_phases.get("per_target").unwrap();
334 assert_eq!(map.len(), 2);
335 assert!(
336 map.get(&key_a)
337 .unwrap()
338 .slots
339 .contains(&(key_a, U256::from(10)))
340 );
341 }
342
343 fn temp_path(tag: &str) -> std::path::PathBuf {
348 let dir = std::env::temp_dir().join(format!(
349 "evm_fork_cache_prefetch_registry_{tag}_{}",
350 std::process::id()
351 ));
352 let _ = std::fs::remove_dir_all(&dir);
353 std::fs::create_dir_all(&dir).expect("create temp dir");
354 dir.join("registry.bin")
355 }
356
357 #[test]
358 fn test_save_load_round_trip() {
359 let path = temp_path("round_trip");
360
361 let mut registry = PrefetchRegistry::default();
362 let addr = Address::repeat_byte(0xAA);
363 let mut al = StorageAccessList::default();
364 al.slots.insert((addr, U256::from(42)));
365 al.accounts.insert(addr);
366 registry.record("test_phase", al);
367
368 let key = Address::repeat_byte(0xBB);
369 let mut sal = StorageAccessList::default();
370 sal.slots.insert((key, U256::from(99)));
371 registry.record_keyed("per_target", key, sal);
372
373 registry.save(&path).expect("save registry");
374 let data = std::fs::read(&path).expect("read saved registry");
375 assert!(
376 data.starts_with(b"EFC-PFRG"),
377 "prefetch registry files must carry a magic/version header"
378 );
379
380 let loaded = PrefetchRegistry::load(&path);
381 assert_eq!(loaded.phases.len(), 1);
382 assert!(
383 loaded.phases["test_phase"]
384 .slots
385 .contains(&(addr, U256::from(42)))
386 );
387 assert_eq!(loaded.keyed_phases.len(), 1);
388 assert!(
389 loaded.keyed_phases["per_target"]
390 .get(&key)
391 .unwrap()
392 .slots
393 .contains(&(key, U256::from(99)))
394 );
395 }
396
397 #[test]
398 fn save_reports_write_failures() {
399 let dir = std::env::temp_dir().join(format!(
402 "evm_fork_cache_prefetch_registry_write_error_{}",
403 std::process::id()
404 ));
405 let _ = std::fs::remove_dir_all(&dir);
406 let _ = std::fs::remove_file(&dir);
407 std::fs::write(&dir, b"not a directory").expect("create file path conflict");
408
409 let registry = PrefetchRegistry::default();
410 let path = dir.join("registry.bin");
411 let err = registry
412 .save(&path)
413 .expect_err("save must report write failure");
414 assert!(
415 err.to_string().contains("directory") || err.to_string().contains("Not a directory"),
416 "unexpected error: {err:#}"
417 );
418
419 let _ = std::fs::remove_file(&dir);
420 }
421
422 #[test]
423 fn legacy_raw_bincode_loads_as_default() {
424 let path = temp_path("legacy");
425
426 let mut registry = PrefetchRegistry::default();
427 let addr = Address::repeat_byte(0xAA);
428 let mut al = StorageAccessList::default();
429 al.slots.insert((addr, U256::from(42)));
430 registry.record("legacy_phase", al);
431 let legacy = bincode::serialize(®istry).expect("serialize legacy registry");
432 std::fs::write(&path, legacy).expect("write legacy registry");
433
434 let loaded = PrefetchRegistry::load(&path);
435 assert!(
436 loaded.phases.is_empty() && loaded.keyed_phases.is_empty(),
437 "legacy raw bincode must be treated as a cache miss"
438 );
439 }
440
441 #[test]
442 fn test_load_missing_file_returns_default() {
443 let path = std::path::Path::new("/tmp/nonexistent_prefetch_registry.bin");
444 let registry = PrefetchRegistry::load(path);
445 assert!(registry.phases.is_empty());
446 assert!(registry.keyed_phases.is_empty());
447 }
448
449 #[test]
450 fn test_record_replaces_existing() {
451 let mut registry = PrefetchRegistry::default();
452 let addr = Address::repeat_byte(0x01);
453
454 let mut al1 = StorageAccessList::default();
455 al1.slots.insert((addr, U256::from(1)));
456 registry.record("phase", al1);
457
458 let mut al2 = StorageAccessList::default();
459 al2.slots.insert((addr, U256::from(2)));
460 registry.record("phase", al2);
461
462 let slots = registry.phase_slots("phase");
463 assert_eq!(slots.len(), 1);
464 assert!(slots.contains(&(addr, U256::from(2))));
465 }
466}