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 serde::{Deserialize, Serialize};
14
15use std::collections::HashSet;
16
17use super::versioned;
18use crate::errors::PersistenceError;
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 code-seed mark cache file (binary format).
83 ///
84 /// Saved by `flush()` strictly before `bytecodes.bin` so persisted code
85 /// can never outrun the trust marks describing it.
86 pub(crate) fn code_seeds_cache_path(&self) -> PathBuf {
87 self.chain_dir().join("code_seeds.bin")
88 }
89
90 /// Get the path for the EVM state cache file (bincode format).
91 ///
92 /// This cache stores the complete EVM state (accounts + storage) in
93 /// bincode format for fast serialization/deserialization.
94 pub fn binary_state_cache_path(&self) -> PathBuf {
95 self.chain_dir().join("evm_state.bin")
96 }
97}
98
99/// Cache for immutable on-chain data that doesn't change between blocks.
100///
101/// This includes:
102/// - Token decimals (ERC20 decimals are immutable)
103///
104/// By caching this data, we avoid redundant RPC calls across block changes
105/// and process restarts.
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct ImmutableDataCache {
108 /// Token address -> decimals
109 pub token_decimals: HashMap<Address, u8>,
110}
111
112impl ImmutableDataCache {
113 /// Load immutable data cache from disk (binary format).
114 ///
115 /// Returns `None` if `path` cannot be read, fails the magic/version check, or
116 /// the payload is not valid bincode for this type. Callers should treat
117 /// `None` as "no cache yet" and start fresh.
118 pub fn load(path: &Path) -> Option<Self> {
119 let data = std::fs::read(path).ok()?;
120 versioned::decode(
121 &data,
122 IMMUTABLE_CACHE_MAGIC,
123 IMMUTABLE_CACHE_VERSION,
124 "immutable data cache",
125 )
126 }
127
128 /// Save immutable data cache to disk (binary format).
129 ///
130 /// Creates the parent directory if it does not exist, then writes the
131 /// bincode-serialized cache to `path`.
132 ///
133 /// # Errors
134 ///
135 /// Returns an error if the parent directory cannot be created, if bincode
136 /// serialization fails, or if writing the file fails.
137 pub fn save(&self, path: &Path) -> Result<(), PersistenceError> {
138 if let Some(parent) = path.parent() {
139 std::fs::create_dir_all(parent)
140 .map_err(|err| PersistenceError::create_dir(parent, err))?;
141 }
142 let data = versioned::encode(
143 IMMUTABLE_CACHE_MAGIC,
144 IMMUTABLE_CACHE_VERSION,
145 self,
146 "immutable data cache",
147 )?;
148 std::fs::write(path, data).map_err(|err| PersistenceError::write(path, err))?;
149 Ok(())
150 }
151
152 /// Get cached token decimals.
153 pub fn get_token_decimals(&self, token: Address) -> Option<u8> {
154 self.token_decimals.get(&token).copied()
155 }
156
157 /// Cache token decimals.
158 pub fn set_token_decimals(&mut self, token: Address, decimals: u8) {
159 self.token_decimals.insert(token, decimals);
160 }
161
162 /// Check if the cache is empty.
163 pub fn is_empty(&self) -> bool {
164 self.token_decimals.is_empty()
165 }
166
167 /// Get the total number of cached entries.
168 pub fn len(&self) -> usize {
169 self.token_decimals.len()
170 }
171}