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 anyhow::Result;
14use serde::{Deserialize, Serialize};
15
16use std::collections::HashSet;
17
18use super::versioned;
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 EVM state cache file (bincode format).
83    ///
84    /// This cache stores the complete EVM state (accounts + storage) in
85    /// bincode format for fast serialization/deserialization.
86    pub fn binary_state_cache_path(&self) -> PathBuf {
87        self.chain_dir().join("evm_state.bin")
88    }
89}
90
91/// Cache for immutable on-chain data that doesn't change between blocks.
92///
93/// This includes:
94/// - Token decimals (ERC20 decimals are immutable)
95///
96/// By caching this data, we avoid redundant RPC calls across block changes
97/// and process restarts.
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct ImmutableDataCache {
100    /// Token address -> decimals
101    pub token_decimals: HashMap<Address, u8>,
102}
103
104impl ImmutableDataCache {
105    /// Load immutable data cache from disk (binary format).
106    ///
107    /// Returns `None` if `path` cannot be read, fails the magic/version check, or
108    /// the payload is not valid bincode for this type. Callers should treat
109    /// `None` as "no cache yet" and start fresh.
110    pub fn load(path: &Path) -> Option<Self> {
111        let data = std::fs::read(path).ok()?;
112        versioned::decode(
113            &data,
114            IMMUTABLE_CACHE_MAGIC,
115            IMMUTABLE_CACHE_VERSION,
116            "immutable data cache",
117        )
118    }
119
120    /// Save immutable data cache to disk (binary format).
121    ///
122    /// Creates the parent directory if it does not exist, then writes the
123    /// bincode-serialized cache to `path`.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the parent directory cannot be created, if bincode
128    /// serialization fails, or if writing the file fails.
129    pub fn save(&self, path: &Path) -> Result<()> {
130        if let Some(parent) = path.parent() {
131            std::fs::create_dir_all(parent)?;
132        }
133        let data = versioned::encode(
134            IMMUTABLE_CACHE_MAGIC,
135            IMMUTABLE_CACHE_VERSION,
136            self,
137            "immutable data cache",
138        )?;
139        std::fs::write(path, data)?;
140        Ok(())
141    }
142
143    /// Get cached token decimals.
144    pub fn get_token_decimals(&self, token: Address) -> Option<u8> {
145        self.token_decimals.get(&token).copied()
146    }
147
148    /// Cache token decimals.
149    pub fn set_token_decimals(&mut self, token: Address, decimals: u8) {
150        self.token_decimals.insert(token, decimals);
151    }
152
153    /// Check if the cache is empty.
154    pub fn is_empty(&self) -> bool {
155        self.token_decimals.is_empty()
156    }
157
158    /// Get the total number of cached entries.
159    pub fn len(&self) -> usize {
160        self.token_decimals.len()
161    }
162}