use std::path::Path;
use std::time::Instant;
use alloy_primitives::map::HashMap;
use alloy_primitives::{Address, B256, U256};
use foundry_fork_db::BlockchainDb;
use revm::state::AccountInfo;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use super::versioned;
use crate::errors::PersistenceError;
const BINARY_STATE_MAGIC: &[u8; 8] = b"EFCSTAT\0";
const BINARY_STATE_VERSION: u32 = 1;
#[derive(Serialize, Deserialize)]
struct BinaryEvmState {
accounts: Vec<(Address, BinaryAccountInfo)>,
storage: Vec<(Address, Vec<(U256, U256)>)>,
}
#[derive(Serialize, Deserialize)]
struct BinaryAccountInfo {
balance: U256,
nonce: u64,
code_hash: B256,
}
pub fn save_binary_state(
blockchain_db: &BlockchainDb,
path: &Path,
) -> Result<(), PersistenceError> {
let start = Instant::now();
let accounts: Vec<(Address, BinaryAccountInfo)> = blockchain_db
.accounts()
.read()
.iter()
.map(|(addr, info)| {
(
*addr,
BinaryAccountInfo {
balance: info.balance,
nonce: info.nonce,
code_hash: info.code_hash,
},
)
})
.collect();
let storage: Vec<(Address, Vec<(U256, U256)>)> = blockchain_db
.storage()
.read()
.iter()
.map(|(addr, slots)| (*addr, slots.iter().map(|(k, v)| (*k, *v)).collect()))
.collect();
let state = BinaryEvmState { accounts, storage };
let data = versioned::encode(
BINARY_STATE_MAGIC,
BINARY_STATE_VERSION,
&state,
"binary EVM state",
)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|err| PersistenceError::create_dir(parent, err))?;
}
std::fs::write(path, &data).map_err(|err| PersistenceError::write(path, err))?;
let ms = start.elapsed().as_millis();
debug!(
accounts = state.accounts.len(),
storage_contracts = state.storage.len(),
bytes = data.len(),
save_ms = ms,
"Saved binary EVM state"
);
Ok(())
}
pub fn load_binary_state(blockchain_db: &BlockchainDb, path: &Path) -> bool {
let start = Instant::now();
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
debug!("No binary EVM state file found, starting fresh");
return false;
}
Err(e) => {
warn!(error = %e, "Failed to read binary EVM state, starting fresh");
return false;
}
};
let Some(state) = versioned::decode::<BinaryEvmState>(
&data,
BINARY_STATE_MAGIC,
BINARY_STATE_VERSION,
"binary EVM state",
) else {
warn!("Failed to decode binary EVM state, starting fresh");
return false;
};
let account_count = state.accounts.len();
let storage_contract_count = state.storage.len();
let mut total_slots = 0usize;
{
let mut accounts = blockchain_db.accounts().write();
for (addr, info) in state.accounts {
accounts.insert(
addr,
AccountInfo {
balance: info.balance,
nonce: info.nonce,
code_hash: info.code_hash,
code: None,
account_id: None,
},
);
}
}
{
let mut storage = blockchain_db.storage().write();
for (addr, slots) in state.storage {
total_slots += slots.len();
let map: HashMap<U256, U256> = slots.into_iter().collect();
storage.insert(addr, map);
}
}
let ms = start.elapsed().as_millis();
debug!(
accounts = account_count,
storage_contracts = storage_contract_count,
total_slots,
bytes = data.len(),
load_ms = ms,
"Loaded binary EVM state"
);
true
}
#[cfg(test)]
mod tests {
use super::*;
use foundry_fork_db::cache::BlockchainDbMeta;
fn temp_path(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"evm_fork_cache_binary_state_{tag}_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
dir.join("state.bin")
}
#[test]
fn test_save_load_round_trip() {
let path = temp_path("round_trip");
let meta = BlockchainDbMeta::default();
let db = BlockchainDb::new(meta, None);
let addr1 = Address::repeat_byte(0x01);
let addr2 = Address::repeat_byte(0x02);
{
let mut accounts = db.accounts().write();
accounts.insert(
addr1,
AccountInfo {
balance: U256::from(1000),
nonce: 5,
code_hash: B256::repeat_byte(0xAA),
code: None,
account_id: None,
},
);
accounts.insert(
addr2,
AccountInfo {
balance: U256::from(2000),
nonce: 10,
code_hash: B256::repeat_byte(0xBB),
code: None,
account_id: None,
},
);
}
{
let mut storage = db.storage().write();
let mut slots1 = HashMap::default();
slots1.insert(U256::from(0), U256::from(42));
slots1.insert(U256::from(1), U256::from(99));
storage.insert(addr1, slots1);
let mut slots2 = HashMap::default();
slots2.insert(U256::from(4), U256::from(777));
storage.insert(addr2, slots2);
}
save_binary_state(&db, &path).expect("save binary state");
assert!(path.exists());
let bytes = std::fs::read(&path).expect("read saved state");
assert!(
bytes.starts_with(b"EFCSTAT\0"),
"binary state cache must carry a magic header"
);
assert_eq!(
&bytes[8..12],
&1u32.to_le_bytes(),
"binary state cache must carry an explicit version"
);
let meta2 = BlockchainDbMeta::default();
let db2 = BlockchainDb::new(meta2, None);
assert!(load_binary_state(&db2, &path));
{
let accounts = db2.accounts().read();
assert_eq!(accounts.len(), 2);
let info1 = accounts.get(&addr1).unwrap();
assert_eq!(info1.balance, U256::from(1000));
assert_eq!(info1.nonce, 5);
assert!(info1.code.is_none()); }
{
let storage = db2.storage().read();
assert_eq!(storage.len(), 2);
assert_eq!(
*storage.get(&addr1).unwrap().get(&U256::from(0)).unwrap(),
U256::from(42)
);
assert_eq!(
*storage.get(&addr2).unwrap().get(&U256::from(4)).unwrap(),
U256::from(777)
);
}
}
#[test]
fn save_binary_state_reports_write_failures() {
let dir = std::env::temp_dir().join(format!(
"evm_fork_cache_binary_state_write_error_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&dir);
std::fs::write(&dir, b"not a directory").expect("create file path conflict");
let db = BlockchainDb::new(BlockchainDbMeta::default(), None);
let path = dir.join("state.bin");
let err = save_binary_state(&db, &path).expect_err("save must report write failure");
assert!(
err.to_string().contains("directory") || err.to_string().contains("Not a directory"),
"unexpected error: {err:#}"
);
let _ = std::fs::remove_file(&dir);
}
#[test]
fn test_load_missing_file_returns_false() {
let meta = BlockchainDbMeta::default();
let db = BlockchainDb::new(meta, None);
assert!(!load_binary_state(
&db,
std::path::Path::new("/tmp/nonexistent_binary_state.bin")
));
}
#[test]
fn test_load_corrupt_file_returns_false() {
let path = temp_path("corrupt");
std::fs::write(&path, b"not valid bincode").unwrap();
let meta = BlockchainDbMeta::default();
let db = BlockchainDb::new(meta, None);
assert!(!load_binary_state(&db, &path));
}
#[test]
fn load_legacy_raw_bincode_returns_false() {
let path = temp_path("legacy");
let legacy = BinaryEvmState {
accounts: Vec::new(),
storage: Vec::new(),
};
std::fs::write(&path, bincode::serialize(&legacy).unwrap()).unwrap();
let db = BlockchainDb::new(BlockchainDbMeta::default(), None);
assert!(
!load_binary_state(&db, &path),
"unversioned legacy bincode must be treated as a cache miss"
);
}
}