Skip to main content

evm_fork_cache/cold_start/
roots.rs

1//! Cold-start root baseline (`roots.bin`): persist observed storage roots so a
2//! restarting process can cheaply detect which tracked accounts changed while
3//! it was down.
4//!
5//! An account's storage-trie root (`storageHash`) is a collision-resistant
6//! commitment over *all* of that account's storage, so `root_unchanged ⟹
7//! nothing under the account changed`. The baseline compares the on-chain root
8//! **across time** — never a locally-reconstructed root against the chain: on
9//! restart, probe each tracked account's root now and, where it equals the
10//! persisted baseline, the cached tracked slots are provably current and are
11//! **not** re-read. Where it diverges (or no baseline exists, or the probe
12//! fails), the tracked slots are re-read and the new root adopted.
13//!
14//! This is a **currency** gate, not a **completeness** gate (spec §6): an
15//! unchanged root proves the tracked subset did not change, but it cannot tell
16//! you that a slot you *should* have been tracking was missing all along.
17
18use std::collections::{BTreeMap, HashMap};
19use std::path::Path;
20
21use alloy_primitives::{Address, B256, U256};
22use serde::{Deserialize, Serialize};
23use tracing::{debug, warn};
24
25use crate::cache::versioned;
26use crate::cold_start::plan::ColdStartPlan;
27use crate::cold_start::planner::{ColdStartPlanner, ColdStartStep};
28use crate::cold_start::results::ColdStartResults;
29use crate::errors::PersistenceError;
30use crate::events::StateView;
31
32const ROOT_BASELINE_MAGIC: &[u8; 8] = b"EFCROOT\0";
33const ROOT_BASELINE_VERSION: u32 = 1;
34
35/// Serialized payload of a [`RootBaseline`]: sorted `(address, root)` pairs.
36///
37/// A `Vec` of pairs sorted by address (the [`BTreeMap`] iteration order), so the
38/// on-disk bytes are deterministic for a given set of entries.
39#[derive(Serialize, Deserialize)]
40struct RootBaselineFile {
41    roots: Vec<(Address, B256)>,
42}
43
44/// A persisted map of `Address -> B256` storage roots — each tracked account's
45/// last **observed** on-chain storage root (`storageHash` from `eth_getProof`).
46///
47/// Persisted via [`save`](Self::save) / [`load`](Self::load) using the same
48/// versioned envelope as the binary state cache (magic bytes + version + bincode
49/// payload); an unknown magic, unknown version, or corrupt payload is a cache
50/// miss (`None`), never an error. The persistence location is the caller's
51/// choice — conventionally `roots.bin` alongside the binary state file
52/// (`evm_state.bin`).
53///
54/// # Currency, not completeness
55///
56/// The baseline is compared **across time** (the root observed now vs. the root
57/// observed at the last run), never local-vs-chain. `root_unchanged` proves the
58/// tracked subset did not change since the baseline block — it cannot detect
59/// that a slot you should have tracked was missing (spec §6).
60#[derive(Clone, Debug, Default, PartialEq, Eq)]
61pub struct RootBaseline {
62    roots: BTreeMap<Address, B256>,
63}
64
65impl RootBaseline {
66    /// Record `root` as the observed storage root of `address`, returning the
67    /// previously recorded root (if any).
68    pub fn insert(&mut self, address: Address, root: B256) -> Option<B256> {
69        self.roots.insert(address, root)
70    }
71
72    /// The recorded storage root of `address`, if one was observed.
73    pub fn get(&self, address: &Address) -> Option<B256> {
74        self.roots.get(address).copied()
75    }
76
77    /// Number of recorded `(address, root)` entries.
78    pub fn len(&self) -> usize {
79        self.roots.len()
80    }
81
82    /// `true` when no roots are recorded.
83    pub fn is_empty(&self) -> bool {
84        self.roots.is_empty()
85    }
86
87    /// Persist the baseline to `path` (conventionally `roots.bin` next to
88    /// `evm_state.bin`).
89    ///
90    /// The on-disk format carries magic bytes and a version number before the
91    /// bincode payload, matching the binary state cache envelope. Entries are
92    /// written in address order, so the bytes are deterministic. Returns an
93    /// error if serialization, parent-directory creation, or writing fails.
94    pub fn save(&self, path: &Path) -> Result<(), PersistenceError> {
95        let file = RootBaselineFile {
96            roots: self.roots.iter().map(|(a, r)| (*a, *r)).collect(),
97        };
98        let data = versioned::encode(
99            ROOT_BASELINE_MAGIC,
100            ROOT_BASELINE_VERSION,
101            &file,
102            "root baseline",
103        )?;
104        if let Some(parent) = path.parent() {
105            std::fs::create_dir_all(parent)
106                .map_err(|err| PersistenceError::create_dir(parent, err))?;
107        }
108        std::fs::write(path, &data).map_err(|err| PersistenceError::write(path, err))?;
109        debug!(
110            entries = self.roots.len(),
111            bytes = data.len(),
112            "Saved root baseline"
113        );
114        Ok(())
115    }
116
117    /// Load a baseline from `path`.
118    ///
119    /// Returns `None` (rather than erroring) for a missing file, an unreadable
120    /// file, an unknown magic header, an unknown version, or a corrupt payload —
121    /// all are cache misses, matching the binary state cache's handling. A
122    /// missing file (the normal first-run case) is logged at `debug`; a read
123    /// error and any magic/version/decode failure are logged at `warn`.
124    pub fn load(path: &Path) -> Option<Self> {
125        let data = match std::fs::read(path) {
126            Ok(d) => d,
127            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
128                debug!("No root baseline file found, starting fresh");
129                return None;
130            }
131            Err(e) => {
132                warn!(error = %e, "Failed to read root baseline, starting fresh");
133                return None;
134            }
135        };
136
137        let file = versioned::decode::<RootBaselineFile>(
138            &data,
139            ROOT_BASELINE_MAGIC,
140            ROOT_BASELINE_VERSION,
141            "root baseline",
142        )?;
143
144        let baseline = Self {
145            roots: file.roots.into_iter().collect(),
146        };
147        debug!(
148            entries = baseline.roots.len(),
149            bytes = data.len(),
150            "Loaded root baseline"
151        );
152        Some(baseline)
153    }
154}
155
156/// The outcome of one declared probe-roots address: the storage root the
157/// account-proof fetcher observed at the pinned block, or `None` when it
158/// could not be observed.
159#[derive(Clone, Debug)]
160pub struct RootProbeOutcome {
161    /// The probed account.
162    pub address: Address,
163    /// The observed storage root; `None` when the probe failed or the fetcher
164    /// omitted the address (treat as unknown -> conservative re-read).
165    pub root: Option<B256>,
166}
167
168/// Which round the [`RootBaselinePlanner`] is in.
169#[derive(Clone, Copy, Debug)]
170enum RootBaselinePhase {
171    /// Round 1: probe every tracked account's root.
172    Probe,
173    /// Round 2 (if needed): re-read the tracked slots of diverged/unknown accounts.
174    Verify,
175}
176
177/// A restart planner that root-gates the tracked working set (Phase-8 §5.5).
178///
179/// Round 1 probes every tracked account's storage root via the account-proof
180/// fetcher (no reads are injected). For each tracked account:
181///
182/// - observed root **equal** to the baseline ⇒ the cached tracked slots are
183///   provably current — the root is retained in the updated baseline and the
184///   slots are **not** re-read;
185/// - observed root **diverged** (or no baseline entry) ⇒ the new root is
186///   adopted into the updated baseline and the account's tracked slots are
187///   re-read in a second verify round;
188/// - probe **failed** (`root: None`) ⇒ conservative: the tracked slots are
189///   re-read, and no root is adopted — an unobserved root never clobbers a
190///   previously persisted one.
191///
192/// When nothing needs re-reading the run finishes after the probe round.
193/// [`updated_baseline`](Self::updated_baseline) returns the baseline with this
194/// run's adoptions applied — persist it as the next `roots.bin`.
195///
196/// Like [`RootBaseline`], this is a **currency** gate, not a completeness gate
197/// (spec §6): it detects change in the tracked subset, not slots that were
198/// never tracked.
199pub struct RootBaselinePlanner {
200    /// Tracked accounts and the storage slots kept live for each.
201    tracked: Vec<(Address, Vec<U256>)>,
202    /// The persisted baseline this run compares against (never mutated).
203    baseline: RootBaseline,
204    /// Baseline-with-adoptions: starts as a copy of `baseline`, updated with
205    /// every root observed by this run.
206    updated: RootBaseline,
207    /// Which round the planner is in.
208    phase: RootBaselinePhase,
209}
210
211impl RootBaselinePlanner {
212    /// Build a planner over `tracked` (`account -> its tracked slots`) and
213    /// `baseline` loaded from `roots.bin` (empty when nothing was persisted).
214    pub fn new(tracked: Vec<(Address, Vec<U256>)>, baseline: RootBaseline) -> Self {
215        let updated = baseline.clone();
216        Self {
217            tracked,
218            baseline,
219            updated,
220            phase: RootBaselinePhase::Probe,
221        }
222    }
223
224    /// The observed roots adopted by this run, layered over the loaded baseline
225    /// (persist as the next `roots.bin`).
226    ///
227    /// Accounts whose probe failed keep their previous baseline entry (if any);
228    /// accounts whose root was observed carry the observed root.
229    pub fn updated_baseline(&self) -> RootBaseline {
230        self.updated.clone()
231    }
232}
233
234impl ColdStartPlanner for RootBaselinePlanner {
235    fn initial_plan(&mut self, _state: &dyn StateView) -> ColdStartPlan {
236        ColdStartPlan {
237            probe_roots: self.tracked.iter().map(|&(address, _)| address).collect(),
238            ..Default::default()
239        }
240    }
241
242    fn on_results(&mut self, results: &ColdStartResults, _state: &dyn StateView) -> ColdStartStep {
243        match self.phase {
244            RootBaselinePhase::Probe => {
245                // Index the probe outcomes; an address absent from the results
246                // entirely is treated the same as a failed probe (unknown).
247                let observed: HashMap<Address, Option<B256>> = results
248                    .probed_roots
249                    .iter()
250                    .map(|o| (o.address, o.root))
251                    .collect();
252
253                let mut verify: Vec<(Address, U256)> = Vec::new();
254                for (address, slots) in &self.tracked {
255                    match observed.get(address).copied().flatten() {
256                        Some(root) => {
257                            let current = self.baseline.get(address) == Some(root);
258                            // Adopt the observed root either way; skip the
259                            // re-read only when it matches the baseline.
260                            self.updated.insert(*address, root);
261                            if !current {
262                                verify.extend(slots.iter().map(|&slot| (*address, slot)));
263                            }
264                        }
265                        // Probe failed / omitted: conservative re-read, and do
266                        // NOT adopt a root — `updated` keeps the old baseline
267                        // entry (if any) rather than clobbering it.
268                        None => verify.extend(slots.iter().map(|&slot| (*address, slot))),
269                    }
270                }
271
272                if verify.is_empty() {
273                    return ColdStartStep::Done;
274                }
275                self.phase = RootBaselinePhase::Verify;
276                ColdStartStep::Continue(ColdStartPlan {
277                    verify,
278                    ..Default::default()
279                })
280            }
281            RootBaselinePhase::Verify => ColdStartStep::Done,
282        }
283    }
284}