evm-fork-cache 0.4.0-alpha.3

Forked EVM state cache, snapshots, overlays, and simulation utilities for EVM search
Documentation
use bincode::Options;
use serde::{Serialize, de::DeserializeOwned};
use tracing::warn;

use crate::errors::PersistenceError;

const VERSION_BYTES: usize = 4;

pub(crate) fn encode<T: Serialize>(
    magic: &[u8; 8],
    version: u32,
    value: &T,
    label: &'static str,
) -> Result<Vec<u8>, PersistenceError> {
    let payload =
        bincode::serialize(value).map_err(|err| PersistenceError::serialize(label, err))?;
    let mut data = Vec::with_capacity(magic.len() + VERSION_BYTES + payload.len());
    data.extend_from_slice(magic);
    data.extend_from_slice(&version.to_le_bytes());
    data.extend_from_slice(&payload);
    Ok(data)
}

pub(crate) fn decode<T: DeserializeOwned>(
    data: &[u8],
    magic: &[u8; 8],
    version: u32,
    label: &'static str,
) -> Option<T> {
    let header_len = magic.len() + VERSION_BYTES;
    if data.len() < header_len {
        warn!(
            cache = label,
            bytes = data.len(),
            "Cache file is missing version header; treating as cache miss"
        );
        return None;
    }

    if &data[..magic.len()] != magic {
        warn!(
            cache = label,
            "Cache file has unrecognized magic header; treating as cache miss"
        );
        return None;
    }

    let version_start = magic.len();
    let found_version = u32::from_le_bytes(
        data[version_start..header_len]
            .try_into()
            .expect("version slice length is fixed"),
    );
    if found_version != version {
        warn!(
            cache = label,
            expected = version,
            found = found_version,
            "Cache file version mismatch; treating as cache miss"
        );
        return None;
    }

    let payload = &data[header_len..];
    let mut cursor = std::io::Cursor::new(payload);
    let value = bincode::DefaultOptions::new()
        .with_fixint_encoding()
        .with_limit(payload.len() as u64)
        .deserialize_from(&mut cursor)
        .inspect_err(|e| warn!(cache = label, error = %e, "Failed to parse cache payload"))
        .ok()?;
    if cursor.position() != payload.len() as u64 {
        warn!(
            cache = label,
            consumed = cursor.position(),
            bytes = payload.len(),
            "Cache payload has trailing bytes; treating as cache miss"
        );
        return None;
    }
    Some(value)
}

#[cfg(test)]
mod tests {
    use super::*;

    const MAGIC: &[u8; 8] = b"VERSION\0";

    #[test]
    fn exact_payload_round_trips() {
        let encoded = encode(MAGIC, 1, &vec![1_u64, 2, 3], "test").expect("encode");
        assert_eq!(
            decode::<Vec<u64>>(&encoded, MAGIC, 1, "test"),
            Some(vec![1, 2, 3])
        );
    }

    #[test]
    fn trailing_payload_bytes_are_rejected() {
        let mut encoded = encode(MAGIC, 1, &42_u64, "test").expect("encode");
        encoded.push(0xff);
        assert_eq!(decode::<u64>(&encoded, MAGIC, 1, "test"), None);
    }
}