evm_fork_cache/cache/
code_seeds.rs1use std::collections::HashMap;
19use std::path::Path;
20
21use alloy_primitives::{Address, B256};
22use serde::{Deserialize, Serialize};
23
24use super::versioned;
25use crate::errors::PersistenceError;
26
27const CODE_SEED_CACHE_MAGIC: &[u8; 8] = b"EFCSEED\0";
28const CODE_SEED_CACHE_VERSION: u32 = 1;
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub enum CodeSeedState {
41 Pending {
44 code_hash: B256,
46 },
47 Verified {
53 code_hash: B256,
55 verified_at_block: u64,
57 },
58 Etched {
63 code_hash: B256,
65 },
66}
67
68impl CodeSeedState {
69 pub fn code_hash(&self) -> B256 {
71 match self {
72 Self::Pending { code_hash }
73 | Self::Verified { code_hash, .. }
74 | Self::Etched { code_hash } => *code_hash,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub(crate) struct CodeSeedCache {
82 pub(crate) entries: HashMap<Address, CodeSeedState>,
84}
85
86impl CodeSeedCache {
87 pub(crate) fn load(path: &Path) -> Option<Self> {
93 let data = std::fs::read(path).ok()?;
94 versioned::decode(
95 &data,
96 CODE_SEED_CACHE_MAGIC,
97 CODE_SEED_CACHE_VERSION,
98 "code seed cache",
99 )
100 }
101
102 pub(crate) fn save(&self, path: &Path) -> Result<(), PersistenceError> {
110 if let Some(parent) = path.parent() {
111 std::fs::create_dir_all(parent)
112 .map_err(|err| PersistenceError::create_dir(parent, err))?;
113 }
114 let data = versioned::encode(
115 CODE_SEED_CACHE_MAGIC,
116 CODE_SEED_CACHE_VERSION,
117 self,
118 "code seed cache",
119 )?;
120 std::fs::write(path, data).map_err(|err| PersistenceError::write(path, err))?;
121 Ok(())
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn temp_path(tag: &str) -> std::path::PathBuf {
130 let dir = std::env::temp_dir().join(format!(
133 "evm_fork_cache_code_seeds_{tag}_{}",
134 std::process::id()
135 ));
136 let _ = std::fs::remove_dir_all(&dir);
137 std::fs::create_dir_all(&dir).expect("create temp dir");
138 dir.join("code_seeds.bin")
139 }
140
141 #[test]
142 fn save_load_round_trip_preserves_all_three_marks() {
143 let path = temp_path("roundtrip");
144 let pending = Address::repeat_byte(0x01);
145 let verified = Address::repeat_byte(0x02);
146 let etched = Address::repeat_byte(0x03);
147
148 let mut cache = CodeSeedCache::default();
149 cache.entries.insert(
150 pending,
151 CodeSeedState::Pending {
152 code_hash: B256::repeat_byte(0xaa),
153 },
154 );
155 cache.entries.insert(
156 verified,
157 CodeSeedState::Verified {
158 code_hash: B256::repeat_byte(0xbb),
159 verified_at_block: 123,
160 },
161 );
162 cache.entries.insert(
163 etched,
164 CodeSeedState::Etched {
165 code_hash: B256::repeat_byte(0xcc),
166 },
167 );
168 cache.save(&path).expect("save code seed cache");
169
170 let bytes = std::fs::read(&path).expect("read saved code seed cache");
171 assert!(
172 bytes.starts_with(b"EFCSEED\0"),
173 "code seed cache must carry a magic header"
174 );
175 assert_eq!(
176 &bytes[8..12],
177 &1u32.to_le_bytes(),
178 "code seed cache must carry an explicit version"
179 );
180
181 let loaded = CodeSeedCache::load(&path).expect("load code seed cache");
182 assert_eq!(loaded.entries.len(), 3);
183 assert_eq!(
184 loaded.entries.get(&pending),
185 Some(&CodeSeedState::Pending {
186 code_hash: B256::repeat_byte(0xaa)
187 }),
188 "Pending survives a reload as Pending — it must never masquerade as RPC-origin"
189 );
190 assert_eq!(
191 loaded.entries.get(&verified),
192 Some(&CodeSeedState::Verified {
193 code_hash: B256::repeat_byte(0xbb),
194 verified_at_block: 123
195 })
196 );
197 assert_eq!(
198 loaded.entries.get(&etched),
199 Some(&CodeSeedState::Etched {
200 code_hash: B256::repeat_byte(0xcc)
201 })
202 );
203
204 let _ = std::fs::remove_dir_all(path.parent().unwrap());
205 }
206
207 #[test]
208 fn load_unversioned_or_missing_is_none() {
209 let path = temp_path("legacy");
210 let mut cache = CodeSeedCache::default();
211 cache.entries.insert(
212 Address::repeat_byte(0x42),
213 CodeSeedState::Etched {
214 code_hash: B256::repeat_byte(0x42),
215 },
216 );
217 std::fs::write(&path, bincode::serialize(&cache).unwrap()).expect("write legacy cache");
218 assert!(
219 CodeSeedCache::load(&path).is_none(),
220 "unversioned legacy bincode must be treated as a cache miss"
221 );
222 assert!(CodeSeedCache::load(std::path::Path::new("/nonexistent/code_seeds.bin")).is_none());
223
224 let _ = std::fs::remove_dir_all(path.parent().unwrap());
225 }
226
227 #[test]
228 fn save_is_full_replace_not_merge() {
229 let path = temp_path("replace");
230 let stale = Address::repeat_byte(0x01);
231 let kept = Address::repeat_byte(0x02);
232
233 let mut first = CodeSeedCache::default();
234 first.entries.insert(
235 stale,
236 CodeSeedState::Pending {
237 code_hash: B256::repeat_byte(0xaa),
238 },
239 );
240 first.save(&path).expect("save first");
241
242 let mut second = CodeSeedCache::default();
245 second.entries.insert(
246 kept,
247 CodeSeedState::Etched {
248 code_hash: B256::repeat_byte(0xbb),
249 },
250 );
251 second.save(&path).expect("save second");
252
253 let loaded = CodeSeedCache::load(&path).expect("load code seed cache");
254 assert_eq!(loaded.entries.len(), 1, "stale entry must not survive");
255 assert!(loaded.entries.contains_key(&kept));
256 assert!(!loaded.entries.contains_key(&stale));
257
258 let _ = std::fs::remove_dir_all(path.parent().unwrap());
259 }
260}