evm_fork_cache/cache/metadata.rs
1//! Disk-cache configuration and immutable side-data persistence.
2//!
3//! Alongside the raw EVM state, the cache tracks values that rarely or never
4//! change for a given fork, currently ERC-20 token decimals. This module defines
5//! the on-disk cache layout
6//! ([`CacheConfig`]) and the serializable containers used to persist and reload
7//! that side data so subsequent runs avoid re-fetching it over RPC.
8
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12use alloy_primitives::{Address, U256};
13use anyhow::Result;
14use serde::{Deserialize, Serialize};
15
16use std::collections::HashSet;
17
18use super::versioned;
19
20const IMMUTABLE_CACHE_MAGIC: &[u8; 8] = b"EFCMETA\0";
21const IMMUTABLE_CACHE_VERSION: u32 = 2;
22
23/// Configuration for disk-based caching of EVM state.
24///
25/// Enables on-disk persistence of fetched fork state. Cache files are laid out
26/// per chain under `cache_dir` (see [`CacheConfig::binary_state_cache_path`] and
27/// the other path helpers), so multiple chains can share one base directory
28/// without colliding.
29///
30/// The `maintain_*` fields drive selective retention when state is reloaded:
31/// `maintain_addresses` whitelists accounts whose storage is kept in full, while
32/// `maintain_slots` whitelists individual slots for accounts whose remaining
33/// storage should be purged. Together they let a cache load keep only the
34/// long-lived state worth reusing and drop the rest.
35#[derive(Debug, Clone)]
36pub struct CacheConfig {
37 /// Base directory for cache files.
38 pub cache_dir: PathBuf,
39 /// Chain ID for namespace isolation.
40 pub chain_id: u64,
41 /// Addresses whose entire storage is preserved on cache load.
42 pub maintain_addresses: HashSet<Address>,
43 /// Addresses with specific slots to preserve (all other slots purged).
44 pub maintain_slots: HashMap<Address, HashSet<U256>>,
45}
46
47impl CacheConfig {
48 /// Create a new cache configuration.
49 ///
50 /// `cache_dir` is the base directory for all per-chain cache files,
51 /// `chain_id` namespaces them, and `maintain_addresses` / `maintain_slots`
52 /// select which state survives a reload (see the type-level docs).
53 pub fn new(
54 cache_dir: impl Into<PathBuf>,
55 chain_id: u64,
56 maintain_addresses: HashSet<Address>,
57 maintain_slots: HashMap<Address, HashSet<U256>>,
58 ) -> Self {
59 Self {
60 cache_dir: cache_dir.into(),
61 chain_id,
62 maintain_addresses,
63 maintain_slots,
64 }
65 }
66
67 /// Get the directory for this chain's cache files.
68 pub(crate) fn chain_dir(&self) -> PathBuf {
69 self.cache_dir.join(format!("chain_{}", self.chain_id))
70 }
71
72 /// Get the path for the bytecode cache file (binary format, persists across blocks).
73 pub(crate) fn bytecode_cache_path(&self) -> PathBuf {
74 self.chain_dir().join("bytecodes.bin")
75 }
76
77 /// Get the path for the immutable data cache file (binary format).
78 pub(crate) fn immutable_cache_path(&self) -> PathBuf {
79 self.chain_dir().join("immutable_data.bin")
80 }
81
82 /// Get the path for the EVM state cache file (bincode format).
83 ///
84 /// This cache stores the complete EVM state (accounts + storage) in
85 /// bincode format for fast serialization/deserialization.
86 pub fn binary_state_cache_path(&self) -> PathBuf {
87 self.chain_dir().join("evm_state.bin")
88 }
89}
90
91/// Cache for immutable on-chain data that doesn't change between blocks.
92///
93/// This includes:
94/// - Token decimals (ERC20 decimals are immutable)
95///
96/// By caching this data, we avoid redundant RPC calls across block changes
97/// and process restarts.
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct ImmutableDataCache {
100 /// Token address -> decimals
101 pub token_decimals: HashMap<Address, u8>,
102}
103
104impl ImmutableDataCache {
105 /// Load immutable data cache from disk (binary format).
106 ///
107 /// Returns `None` if `path` cannot be read, fails the magic/version check, or
108 /// the payload is not valid bincode for this type. Callers should treat
109 /// `None` as "no cache yet" and start fresh.
110 pub fn load(path: &Path) -> Option<Self> {
111 let data = std::fs::read(path).ok()?;
112 versioned::decode(
113 &data,
114 IMMUTABLE_CACHE_MAGIC,
115 IMMUTABLE_CACHE_VERSION,
116 "immutable data cache",
117 )
118 }
119
120 /// Save immutable data cache to disk (binary format).
121 ///
122 /// Creates the parent directory if it does not exist, then writes the
123 /// bincode-serialized cache to `path`.
124 ///
125 /// # Errors
126 ///
127 /// Returns an error if the parent directory cannot be created, if bincode
128 /// serialization fails, or if writing the file fails.
129 pub fn save(&self, path: &Path) -> Result<()> {
130 if let Some(parent) = path.parent() {
131 std::fs::create_dir_all(parent)?;
132 }
133 let data = versioned::encode(
134 IMMUTABLE_CACHE_MAGIC,
135 IMMUTABLE_CACHE_VERSION,
136 self,
137 "immutable data cache",
138 )?;
139 std::fs::write(path, data)?;
140 Ok(())
141 }
142
143 /// Get cached token decimals.
144 pub fn get_token_decimals(&self, token: Address) -> Option<u8> {
145 self.token_decimals.get(&token).copied()
146 }
147
148 /// Cache token decimals.
149 pub fn set_token_decimals(&mut self, token: Address, decimals: u8) {
150 self.token_decimals.insert(token, decimals);
151 }
152
153 /// Check if the cache is empty.
154 pub fn is_empty(&self) -> bool {
155 self.token_decimals.is_empty()
156 }
157
158 /// Get the total number of cached entries.
159 pub fn len(&self) -> usize {
160 self.token_decimals.len()
161 }
162}