Skip to main content

evm_fork_cache/cache/
metadata.rs

1//! Disk-cache configuration and immutable side-data persistence.
2//!
3//! Alongside the raw EVM state, the cache tracks values that rarely or never
4//! change for a given fork, currently ERC-20 token decimals. This module defines
5//! the on-disk cache layout
6//! ([`CacheConfig`]) and the serializable containers used to persist and reload
7//! that side data so subsequent runs avoid re-fetching it over RPC.
8
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12use alloy_primitives::{Address, U256};
13use serde::{Deserialize, Serialize};
14
15use std::collections::HashSet;
16
17use super::versioned;
18use crate::errors::PersistenceError;
19
20const IMMUTABLE_CACHE_MAGIC: &[u8; 8] = b"EFCMETA\0";
21const IMMUTABLE_CACHE_VERSION: u32 = 2;
22
23/// Configuration for disk-based caching of EVM state.
24///
25/// Enables on-disk persistence of fetched fork state. Cache files are laid out
26/// per chain under `cache_dir` (see [`CacheConfig::binary_state_cache_path`] and
27/// the other path helpers), so multiple chains can share one base directory
28/// without colliding.
29///
30/// The `maintain_*` fields drive selective retention when state is reloaded:
31/// `maintain_addresses` whitelists accounts whose storage is kept in full, while
32/// `maintain_slots` whitelists individual slots for accounts whose remaining
33/// storage should be purged. Together they let a cache load keep only the
34/// long-lived state worth reusing and drop the rest.
35#[derive(Debug, Clone)]
36pub struct CacheConfig {
37    /// Base directory for cache files.
38    pub cache_dir: PathBuf,
39    /// Chain ID for namespace isolation.
40    pub chain_id: u64,
41    /// Addresses whose entire storage is preserved on cache load.
42    pub maintain_addresses: HashSet<Address>,
43    /// Addresses with specific slots to preserve (all other slots purged).
44    pub maintain_slots: HashMap<Address, HashSet<U256>>,
45}
46
47impl CacheConfig {
48    /// Create a new cache configuration.
49    ///
50    /// `cache_dir` is the base directory for all per-chain cache files,
51    /// `chain_id` namespaces them, and `maintain_addresses` / `maintain_slots`
52    /// select which state survives a reload (see the type-level docs).
53    pub fn new(
54        cache_dir: impl Into<PathBuf>,
55        chain_id: u64,
56        maintain_addresses: HashSet<Address>,
57        maintain_slots: HashMap<Address, HashSet<U256>>,
58    ) -> Self {
59        Self {
60            cache_dir: cache_dir.into(),
61            chain_id,
62            maintain_addresses,
63            maintain_slots,
64        }
65    }
66
67    /// Get the directory for this chain's cache files.
68    pub(crate) fn chain_dir(&self) -> PathBuf {
69        self.cache_dir.join(format!("chain_{}", self.chain_id))
70    }
71
72    /// Get the path for the bytecode cache file (binary format, persists across blocks).
73    pub(crate) fn bytecode_cache_path(&self) -> PathBuf {
74        self.chain_dir().join("bytecodes.bin")
75    }
76
77    /// Get the path for the immutable data cache file (binary format).
78    pub(crate) fn immutable_cache_path(&self) -> PathBuf {
79        self.chain_dir().join("immutable_data.bin")
80    }
81
82    /// Get the path for the code-seed mark cache file (binary format).
83    ///
84    /// Saved by `flush()` strictly before `bytecodes.bin` so persisted code
85    /// can never outrun the trust marks describing it.
86    pub(crate) fn code_seeds_cache_path(&self) -> PathBuf {
87        self.chain_dir().join("code_seeds.bin")
88    }
89
90    /// Get the path for the EVM state cache file (bincode format).
91    ///
92    /// This cache stores the complete EVM state (accounts + storage) in
93    /// bincode format for fast serialization/deserialization.
94    pub fn binary_state_cache_path(&self) -> PathBuf {
95        self.chain_dir().join("evm_state.bin")
96    }
97}
98
99/// Cache for immutable on-chain data that doesn't change between blocks.
100///
101/// This includes:
102/// - Token decimals (ERC20 decimals are immutable)
103///
104/// By caching this data, we avoid redundant RPC calls across block changes
105/// and process restarts.
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct ImmutableDataCache {
108    /// Token address -> decimals
109    pub token_decimals: HashMap<Address, u8>,
110}
111
112impl ImmutableDataCache {
113    /// Load immutable data cache from disk (binary format).
114    ///
115    /// Returns `None` if `path` cannot be read, fails the magic/version check, or
116    /// the payload is not valid bincode for this type. Callers should treat
117    /// `None` as "no cache yet" and start fresh.
118    pub fn load(path: &Path) -> Option<Self> {
119        let data = std::fs::read(path).ok()?;
120        versioned::decode(
121            &data,
122            IMMUTABLE_CACHE_MAGIC,
123            IMMUTABLE_CACHE_VERSION,
124            "immutable data cache",
125        )
126    }
127
128    /// Save immutable data cache to disk (binary format).
129    ///
130    /// Creates the parent directory if it does not exist, then writes the
131    /// bincode-serialized cache to `path`.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if the parent directory cannot be created, if bincode
136    /// serialization fails, or if writing the file fails.
137    pub fn save(&self, path: &Path) -> Result<(), PersistenceError> {
138        if let Some(parent) = path.parent() {
139            std::fs::create_dir_all(parent)
140                .map_err(|err| PersistenceError::create_dir(parent, err))?;
141        }
142        let data = versioned::encode(
143            IMMUTABLE_CACHE_MAGIC,
144            IMMUTABLE_CACHE_VERSION,
145            self,
146            "immutable data cache",
147        )?;
148        std::fs::write(path, data).map_err(|err| PersistenceError::write(path, err))?;
149        Ok(())
150    }
151
152    /// Get cached token decimals.
153    pub fn get_token_decimals(&self, token: Address) -> Option<u8> {
154        self.token_decimals.get(&token).copied()
155    }
156
157    /// Cache token decimals.
158    pub fn set_token_decimals(&mut self, token: Address, decimals: u8) {
159        self.token_decimals.insert(token, decimals);
160    }
161
162    /// Check if the cache is empty.
163    pub fn is_empty(&self) -> bool {
164        self.token_decimals.is_empty()
165    }
166
167    /// Get the total number of cached entries.
168    pub fn len(&self) -> usize {
169        self.token_decimals.len()
170    }
171}