1use std::path::Path;
12use std::time::Instant;
13
14use alloy_primitives::map::HashMap;
15use alloy_primitives::{Address, B256, U256};
16use anyhow::{Context as _, Result};
17use foundry_fork_db::BlockchainDb;
18use revm::state::AccountInfo;
19use serde::{Deserialize, Serialize};
20use tracing::{debug, warn};
21
22use super::versioned;
23
24const BINARY_STATE_MAGIC: &[u8; 8] = b"EFCSTAT\0";
25const BINARY_STATE_VERSION: u32 = 1;
26
27#[derive(Serialize, Deserialize)]
30struct BinaryEvmState {
31 accounts: Vec<(Address, BinaryAccountInfo)>,
32 storage: Vec<(Address, Vec<(U256, U256)>)>,
33}
34
35#[derive(Serialize, Deserialize)]
37struct BinaryAccountInfo {
38 balance: U256,
39 nonce: u64,
40 code_hash: B256,
41}
42
43pub fn save_binary_state(blockchain_db: &BlockchainDb, path: &Path) -> Result<()> {
58 let start = Instant::now();
59
60 let accounts: Vec<(Address, BinaryAccountInfo)> = blockchain_db
61 .accounts()
62 .read()
63 .iter()
64 .map(|(addr, info)| {
65 (
66 *addr,
67 BinaryAccountInfo {
68 balance: info.balance,
69 nonce: info.nonce,
70 code_hash: info.code_hash,
71 },
72 )
73 })
74 .collect();
75
76 let storage: Vec<(Address, Vec<(U256, U256)>)> = blockchain_db
77 .storage()
78 .read()
79 .iter()
80 .map(|(addr, slots)| (*addr, slots.iter().map(|(k, v)| (*k, *v)).collect()))
81 .collect();
82
83 let state = BinaryEvmState { accounts, storage };
84
85 let data = versioned::encode(
86 BINARY_STATE_MAGIC,
87 BINARY_STATE_VERSION,
88 &state,
89 "binary EVM state",
90 )?;
91 if let Some(parent) = path.parent() {
92 std::fs::create_dir_all(parent)
93 .with_context(|| format!("failed to create binary EVM state directory {parent:?}"))?;
94 }
95 std::fs::write(path, &data)
96 .with_context(|| format!("failed to write binary EVM state to {path:?}"))?;
97
98 let ms = start.elapsed().as_millis();
99 debug!(
100 accounts = state.accounts.len(),
101 storage_contracts = state.storage.len(),
102 bytes = data.len(),
103 save_ms = ms,
104 "Saved binary EVM state"
105 );
106 Ok(())
107}
108
109pub fn load_binary_state(blockchain_db: &BlockchainDb, path: &Path) -> bool {
121 let start = Instant::now();
122
123 let data = match std::fs::read(path) {
124 Ok(d) => d,
125 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
126 debug!("No binary EVM state file found, starting fresh");
127 return false;
128 }
129 Err(e) => {
130 warn!(error = %e, "Failed to read binary EVM state, starting fresh");
131 return false;
132 }
133 };
134
135 let Some(state) = versioned::decode::<BinaryEvmState>(
136 &data,
137 BINARY_STATE_MAGIC,
138 BINARY_STATE_VERSION,
139 "binary EVM state",
140 ) else {
141 warn!("Failed to decode binary EVM state, starting fresh");
142 return false;
143 };
144
145 let account_count = state.accounts.len();
146 let storage_contract_count = state.storage.len();
147 let mut total_slots = 0usize;
148
149 {
151 let mut accounts = blockchain_db.accounts().write();
152 for (addr, info) in state.accounts {
153 accounts.insert(
154 addr,
155 AccountInfo {
156 balance: info.balance,
157 nonce: info.nonce,
158 code_hash: info.code_hash,
159 code: None,
160 account_id: None,
161 },
162 );
163 }
164 }
165
166 {
168 let mut storage = blockchain_db.storage().write();
169 for (addr, slots) in state.storage {
170 total_slots += slots.len();
171 let map: HashMap<U256, U256> = slots.into_iter().collect();
172 storage.insert(addr, map);
173 }
174 }
175
176 let ms = start.elapsed().as_millis();
177 debug!(
178 accounts = account_count,
179 storage_contracts = storage_contract_count,
180 total_slots,
181 bytes = data.len(),
182 load_ms = ms,
183 "Loaded binary EVM state"
184 );
185
186 true
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use foundry_fork_db::cache::BlockchainDbMeta;
193
194 #[test]
195 fn test_save_load_round_trip() {
196 let dir = std::env::temp_dir().join("evm_fork_cache_test_binary_state");
197 let path = dir.join("test_state.bin");
198 let _ = std::fs::remove_file(&path);
199
200 let meta = BlockchainDbMeta::default();
202 let db = BlockchainDb::new(meta, None);
203
204 let addr1 = Address::repeat_byte(0x01);
205 let addr2 = Address::repeat_byte(0x02);
206
207 {
209 let mut accounts = db.accounts().write();
210 accounts.insert(
211 addr1,
212 AccountInfo {
213 balance: U256::from(1000),
214 nonce: 5,
215 code_hash: B256::repeat_byte(0xAA),
216 code: None,
217 account_id: None,
218 },
219 );
220 accounts.insert(
221 addr2,
222 AccountInfo {
223 balance: U256::from(2000),
224 nonce: 10,
225 code_hash: B256::repeat_byte(0xBB),
226 code: None,
227 account_id: None,
228 },
229 );
230 }
231
232 {
234 let mut storage = db.storage().write();
235 let mut slots1 = HashMap::default();
236 slots1.insert(U256::from(0), U256::from(42));
237 slots1.insert(U256::from(1), U256::from(99));
238 storage.insert(addr1, slots1);
239
240 let mut slots2 = HashMap::default();
241 slots2.insert(U256::from(4), U256::from(777));
242 storage.insert(addr2, slots2);
243 }
244
245 save_binary_state(&db, &path).expect("save binary state");
247 assert!(path.exists());
248 let bytes = std::fs::read(&path).expect("read saved state");
249 assert!(
250 bytes.starts_with(b"EFCSTAT\0"),
251 "binary state cache must carry a magic header"
252 );
253 assert_eq!(
254 &bytes[8..12],
255 &1u32.to_le_bytes(),
256 "binary state cache must carry an explicit version"
257 );
258
259 let meta2 = BlockchainDbMeta::default();
261 let db2 = BlockchainDb::new(meta2, None);
262 assert!(load_binary_state(&db2, &path));
263
264 {
266 let accounts = db2.accounts().read();
267 assert_eq!(accounts.len(), 2);
268 let info1 = accounts.get(&addr1).unwrap();
269 assert_eq!(info1.balance, U256::from(1000));
270 assert_eq!(info1.nonce, 5);
271 assert!(info1.code.is_none()); }
273
274 {
276 let storage = db2.storage().read();
277 assert_eq!(storage.len(), 2);
278 assert_eq!(
279 *storage.get(&addr1).unwrap().get(&U256::from(0)).unwrap(),
280 U256::from(42)
281 );
282 assert_eq!(
283 *storage.get(&addr2).unwrap().get(&U256::from(4)).unwrap(),
284 U256::from(777)
285 );
286 }
287
288 let _ = std::fs::remove_file(&path);
289 let _ = std::fs::remove_dir(&dir);
290 }
291
292 #[test]
293 fn save_binary_state_reports_write_failures() {
294 let dir = std::env::temp_dir().join("evm_fork_cache_test_binary_state_write_error");
295 let _ = std::fs::remove_dir_all(&dir);
296 let _ = std::fs::remove_file(&dir);
297 std::fs::write(&dir, b"not a directory").expect("create file path conflict");
298
299 let db = BlockchainDb::new(BlockchainDbMeta::default(), None);
300 let path = dir.join("state.bin");
301 let err = save_binary_state(&db, &path).expect_err("save must report write failure");
302 assert!(
303 err.to_string().contains("directory") || err.to_string().contains("Not a directory"),
304 "unexpected error: {err:#}"
305 );
306
307 let _ = std::fs::remove_file(&dir);
308 }
309
310 #[test]
311 fn test_load_missing_file_returns_false() {
312 let meta = BlockchainDbMeta::default();
313 let db = BlockchainDb::new(meta, None);
314 assert!(!load_binary_state(
315 &db,
316 std::path::Path::new("/tmp/nonexistent_binary_state.bin")
317 ));
318 }
319
320 #[test]
321 fn test_load_corrupt_file_returns_false() {
322 let dir = std::env::temp_dir().join("evm_fork_cache_test_binary_state_corrupt");
323 let path = dir.join("corrupt.bin");
324 let _ = std::fs::create_dir_all(&dir);
325 std::fs::write(&path, b"not valid bincode").unwrap();
326
327 let meta = BlockchainDbMeta::default();
328 let db = BlockchainDb::new(meta, None);
329 assert!(!load_binary_state(&db, &path));
330
331 let _ = std::fs::remove_file(&path);
332 let _ = std::fs::remove_dir(&dir);
333 }
334
335 #[test]
336 fn load_legacy_raw_bincode_returns_false() {
337 let dir = std::env::temp_dir().join("evm_fork_cache_test_binary_state_legacy");
338 let path = dir.join("legacy.bin");
339 let _ = std::fs::create_dir_all(&dir);
340 let legacy = BinaryEvmState {
341 accounts: Vec::new(),
342 storage: Vec::new(),
343 };
344 std::fs::write(&path, bincode::serialize(&legacy).unwrap()).unwrap();
345
346 let db = BlockchainDb::new(BlockchainDbMeta::default(), None);
347 assert!(
348 !load_binary_state(&db, &path),
349 "unversioned legacy bincode must be treated as a cache miss"
350 );
351
352 let _ = std::fs::remove_file(&path);
353 let _ = std::fs::remove_dir(&dir);
354 }
355}