Skip to main content

evm_fork_cache/cache/
binary_state.rs

1//! Binary EVM state persistence for fast startup.
2//!
3//! Provides a bincode-serialized alternative to foundry-fork-db's JSON format.
4//! On save, we extract accounts (without bytecode) and storage from BlockchainDb
5//! and write a compact binary file. On load, we populate BlockchainDb directly,
6//! then seed bytecodes from the separate bytecodes.bin cache.
7//!
8//! The file format is a tiny crate-specific envelope (magic bytes + version)
9//! followed by bincode payload. Unknown magic/version values are cache misses.
10
11use 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/// Binary-serializable EVM state. Stores accounts without bytecode (bytecodes
28/// are loaded separately from bytecodes.bin) and all storage slots.
29#[derive(Serialize, Deserialize)]
30struct BinaryEvmState {
31    accounts: Vec<(Address, BinaryAccountInfo)>,
32    storage: Vec<(Address, Vec<(U256, U256)>)>,
33}
34
35/// Compact account info without bytecode.
36#[derive(Serialize, Deserialize)]
37struct BinaryAccountInfo {
38    balance: U256,
39    nonce: u64,
40    code_hash: B256,
41}
42
43/// Save the current BlockchainDb state to a binary file.
44///
45/// This extracts accounts (without code) and storage from the MemDb
46/// and serializes them with bincode for fast restoration. Bytecode is excluded
47/// and persisted separately to `bytecodes.bin`; the saved account info keeps
48/// only the `code_hash`.
49///
50/// Returns an error if serialization, parent-directory creation, or writing
51/// fails, so explicit flush callers can distinguish a successful save from a
52/// stale or missing on-disk cache.
53///
54/// The on-disk format carries magic bytes and a version number before the
55/// bincode payload. Unknown versions are treated as a cache miss rather than
56/// being migrated.
57pub 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
109/// Load binary EVM state and populate the BlockchainDb.
110///
111/// Returns `true` if the binary state was loaded successfully, `false` otherwise.
112/// When successful, accounts (without code) and storage are populated in the MemDb.
113/// Bytecodes should be seeded separately from bytecodes.bin.
114///
115/// Returns `false` (rather than erroring) when `path` cannot be read or its
116/// contents fail the magic/version check or fail to decode as the expected
117/// bincode layout. A missing file (the normal cold-start case) is logged at
118/// `debug`; an actual read error (e.g. permission denied) and any
119/// magic/version/decode failure are logged at `warn`.
120pub 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    // Populate accounts (without code — bytecodes loaded separately)
150    {
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    // Populate storage
167    {
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        // Create a BlockchainDb with some data
201        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        // Add accounts
208        {
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        // Add storage
233        {
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
246        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        // Load into a fresh db
260        let meta2 = BlockchainDbMeta::default();
261        let db2 = BlockchainDb::new(meta2, None);
262        assert!(load_binary_state(&db2, &path));
263
264        // Verify accounts
265        {
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()); // code not stored in binary
272        }
273
274        // Verify storage
275        {
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}