Skip to main content

evm_fork_cache/cache/
code_seeds.rs

1//! Code-seed marks: provenance + trust state for bytecode that did **not**
2//! arrive via the lazy RPC backend.
3//!
4//! Adapters can push runtime code into the cache instead of paying an
5//! `eth_getCode` per address (see `EvmCache::seed_account_code` /
6//! `EvmCache::etch_account_code`). Every such write records a
7//! [`CodeSeedState`] mark; the *absence* of a mark means the code is
8//! RPC-origin (fetched from the provider and trusted as chain state).
9//!
10//! Marks persist across restarts in `code_seeds.bin` so a `Pending` claim can
11//! never masquerade as chain-fetched after a reload. The file is written as a
12//! crate-specific versioned envelope followed by bincode payload, so
13//! incompatible versions are detected as cache misses. Unlike `bytecodes.bin`
14//! (load-merge-save, correct for immutable code), this file is saved as a
15//! **full replace** of the in-memory map: marks are mutable trust state, and
16//! a merge would resurrect marks that were purged this session.
17
18use std::collections::HashMap;
19use std::path::Path;
20
21use alloy_primitives::{Address, B256};
22use serde::{Deserialize, Serialize};
23
24use super::versioned;
25use crate::errors::PersistenceError;
26
27const CODE_SEED_CACHE_MAGIC: &[u8; 8] = b"EFCSEED\0";
28const CODE_SEED_CACHE_VERSION: u32 = 1;
29
30/// Provenance + trust state of an address's cached bytecode, for code that
31/// did **not** arrive via the lazy RPC backend.
32///
33/// Absence of a mark means RPC-origin (fetched from the provider, trusted as
34/// chain state). See the two write primitives:
35/// [`seed_account_code`](crate::cache::EvmCache::seed_account_code) (canonical
36/// claim, verified once) and
37/// [`etch_account_code`](crate::cache::EvmCache::etch_account_code)
38/// (deliberate local divergence).
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub enum CodeSeedState {
41    /// Canonical claim awaiting on-chain code-hash verification
42    /// (`EvmCache::verify_code_seeds`).
43    Pending {
44        /// keccak256 of the seeded runtime code.
45        code_hash: B256,
46    },
47    /// Canonical claim confirmed against the chain. Never re-verified: post
48    /// EIP-6780, deployed code is immutable, so one confirmation is durable.
49    /// On chains without 6780 the escape hatch is
50    /// [`purge_account`](crate::cache::EvmCache::purge_account), which clears
51    /// the mark.
52    Verified {
53        /// keccak256 of the verified runtime code.
54        code_hash: B256,
55        /// Pinned block number at which the on-chain code hash matched.
56        verified_at_block: u64,
57    },
58    /// Deliberate local divergence (an unreleased contract, a test harness).
59    /// Never verified, excluded from all canonical machinery, and reported on
60    /// the health surface via
61    /// [`etched_accounts`](crate::cache::EvmCache::etched_accounts).
62    Etched {
63        /// keccak256 of the etched runtime code.
64        code_hash: B256,
65    },
66}
67
68impl CodeSeedState {
69    /// The keccak256 code hash this mark refers to.
70    pub fn code_hash(&self) -> B256 {
71        match self {
72            Self::Pending { code_hash }
73            | Self::Verified { code_hash, .. }
74            | Self::Etched { code_hash } => *code_hash,
75        }
76    }
77}
78
79/// Serializable code-seed mark store (`code_seeds.bin`).
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub(crate) struct CodeSeedCache {
82    /// Map of address to its code-seed mark.
83    pub(crate) entries: HashMap<Address, CodeSeedState>,
84}
85
86impl CodeSeedCache {
87    /// Load the mark store from disk (binary format).
88    ///
89    /// Returns `None` if `path` cannot be read, fails the magic/version check,
90    /// or fails to decode as bincode for this type — legacy/missing files are
91    /// cache misses, never errors.
92    pub(crate) fn load(path: &Path) -> Option<Self> {
93        let data = std::fs::read(path).ok()?;
94        versioned::decode(
95            &data,
96            CODE_SEED_CACHE_MAGIC,
97            CODE_SEED_CACHE_VERSION,
98            "code seed cache",
99        )
100    }
101
102    /// Save the mark store to disk (binary format), replacing any previous
103    /// file wholesale (see the module docs for why this is not a merge).
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the parent directory cannot be created, if bincode
108    /// serialization fails, or if writing the file fails.
109    pub(crate) fn save(&self, path: &Path) -> Result<(), PersistenceError> {
110        if let Some(parent) = path.parent() {
111            std::fs::create_dir_all(parent)
112                .map_err(|err| PersistenceError::create_dir(parent, err))?;
113        }
114        let data = versioned::encode(
115            CODE_SEED_CACHE_MAGIC,
116            CODE_SEED_CACHE_VERSION,
117            self,
118            "code seed cache",
119        )?;
120        std::fs::write(path, data).map_err(|err| PersistenceError::write(path, err))?;
121        Ok(())
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    fn temp_path(tag: &str) -> std::path::PathBuf {
130        // Keyed by pid so concurrent `cargo test` processes never share (and
131        // never `remove_dir_all`) each other's directory.
132        let dir = std::env::temp_dir().join(format!(
133            "evm_fork_cache_code_seeds_{tag}_{}",
134            std::process::id()
135        ));
136        let _ = std::fs::remove_dir_all(&dir);
137        std::fs::create_dir_all(&dir).expect("create temp dir");
138        dir.join("code_seeds.bin")
139    }
140
141    #[test]
142    fn save_load_round_trip_preserves_all_three_marks() {
143        let path = temp_path("roundtrip");
144        let pending = Address::repeat_byte(0x01);
145        let verified = Address::repeat_byte(0x02);
146        let etched = Address::repeat_byte(0x03);
147
148        let mut cache = CodeSeedCache::default();
149        cache.entries.insert(
150            pending,
151            CodeSeedState::Pending {
152                code_hash: B256::repeat_byte(0xaa),
153            },
154        );
155        cache.entries.insert(
156            verified,
157            CodeSeedState::Verified {
158                code_hash: B256::repeat_byte(0xbb),
159                verified_at_block: 123,
160            },
161        );
162        cache.entries.insert(
163            etched,
164            CodeSeedState::Etched {
165                code_hash: B256::repeat_byte(0xcc),
166            },
167        );
168        cache.save(&path).expect("save code seed cache");
169
170        let bytes = std::fs::read(&path).expect("read saved code seed cache");
171        assert!(
172            bytes.starts_with(b"EFCSEED\0"),
173            "code seed cache must carry a magic header"
174        );
175        assert_eq!(
176            &bytes[8..12],
177            &1u32.to_le_bytes(),
178            "code seed cache must carry an explicit version"
179        );
180
181        let loaded = CodeSeedCache::load(&path).expect("load code seed cache");
182        assert_eq!(loaded.entries.len(), 3);
183        assert_eq!(
184            loaded.entries.get(&pending),
185            Some(&CodeSeedState::Pending {
186                code_hash: B256::repeat_byte(0xaa)
187            }),
188            "Pending survives a reload as Pending — it must never masquerade as RPC-origin"
189        );
190        assert_eq!(
191            loaded.entries.get(&verified),
192            Some(&CodeSeedState::Verified {
193                code_hash: B256::repeat_byte(0xbb),
194                verified_at_block: 123
195            })
196        );
197        assert_eq!(
198            loaded.entries.get(&etched),
199            Some(&CodeSeedState::Etched {
200                code_hash: B256::repeat_byte(0xcc)
201            })
202        );
203
204        let _ = std::fs::remove_dir_all(path.parent().unwrap());
205    }
206
207    #[test]
208    fn load_unversioned_or_missing_is_none() {
209        let path = temp_path("legacy");
210        let mut cache = CodeSeedCache::default();
211        cache.entries.insert(
212            Address::repeat_byte(0x42),
213            CodeSeedState::Etched {
214                code_hash: B256::repeat_byte(0x42),
215            },
216        );
217        std::fs::write(&path, bincode::serialize(&cache).unwrap()).expect("write legacy cache");
218        assert!(
219            CodeSeedCache::load(&path).is_none(),
220            "unversioned legacy bincode must be treated as a cache miss"
221        );
222        assert!(CodeSeedCache::load(std::path::Path::new("/nonexistent/code_seeds.bin")).is_none());
223
224        let _ = std::fs::remove_dir_all(path.parent().unwrap());
225    }
226
227    #[test]
228    fn save_is_full_replace_not_merge() {
229        let path = temp_path("replace");
230        let stale = Address::repeat_byte(0x01);
231        let kept = Address::repeat_byte(0x02);
232
233        let mut first = CodeSeedCache::default();
234        first.entries.insert(
235            stale,
236            CodeSeedState::Pending {
237                code_hash: B256::repeat_byte(0xaa),
238            },
239        );
240        first.save(&path).expect("save first");
241
242        // A second save without the stale entry must not resurrect it: a
243        // purged mark staying purged is the whole point of replace semantics.
244        let mut second = CodeSeedCache::default();
245        second.entries.insert(
246            kept,
247            CodeSeedState::Etched {
248                code_hash: B256::repeat_byte(0xbb),
249            },
250        );
251        second.save(&path).expect("save second");
252
253        let loaded = CodeSeedCache::load(&path).expect("load code seed cache");
254        assert_eq!(loaded.entries.len(), 1, "stale entry must not survive");
255        assert!(loaded.entries.contains_key(&kept));
256        assert!(!loaded.entries.contains_key(&stale));
257
258        let _ = std::fs::remove_dir_all(path.parent().unwrap());
259    }
260}