use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use alloy_primitives::{Address, B256, U256};
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use crate::cache::versioned;
use crate::cold_start::plan::ColdStartPlan;
use crate::cold_start::planner::{ColdStartPlanner, ColdStartStep};
use crate::cold_start::results::ColdStartResults;
use crate::errors::PersistenceError;
use crate::events::StateView;
const ROOT_BASELINE_MAGIC: &[u8; 8] = b"EFCROOT\0";
const ROOT_BASELINE_VERSION: u32 = 1;
#[derive(Serialize, Deserialize)]
struct RootBaselineFile {
roots: Vec<(Address, B256)>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RootBaseline {
roots: BTreeMap<Address, B256>,
}
impl RootBaseline {
pub fn insert(&mut self, address: Address, root: B256) -> Option<B256> {
self.roots.insert(address, root)
}
pub fn get(&self, address: &Address) -> Option<B256> {
self.roots.get(address).copied()
}
pub fn len(&self) -> usize {
self.roots.len()
}
pub fn is_empty(&self) -> bool {
self.roots.is_empty()
}
pub fn save(&self, path: &Path) -> Result<(), PersistenceError> {
let file = RootBaselineFile {
roots: self.roots.iter().map(|(a, r)| (*a, *r)).collect(),
};
let data = versioned::encode(
ROOT_BASELINE_MAGIC,
ROOT_BASELINE_VERSION,
&file,
"root baseline",
)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|err| PersistenceError::create_dir(parent, err))?;
}
std::fs::write(path, &data).map_err(|err| PersistenceError::write(path, err))?;
debug!(
entries = self.roots.len(),
bytes = data.len(),
"Saved root baseline"
);
Ok(())
}
pub fn load(path: &Path) -> Option<Self> {
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
debug!("No root baseline file found, starting fresh");
return None;
}
Err(e) => {
warn!(error = %e, "Failed to read root baseline, starting fresh");
return None;
}
};
let file = versioned::decode::<RootBaselineFile>(
&data,
ROOT_BASELINE_MAGIC,
ROOT_BASELINE_VERSION,
"root baseline",
)?;
let baseline = Self {
roots: file.roots.into_iter().collect(),
};
debug!(
entries = baseline.roots.len(),
bytes = data.len(),
"Loaded root baseline"
);
Some(baseline)
}
}
#[derive(Clone, Debug)]
pub struct RootProbeOutcome {
pub address: Address,
pub root: Option<B256>,
}
#[derive(Clone, Copy, Debug)]
enum RootBaselinePhase {
Probe,
Verify,
}
pub struct RootBaselinePlanner {
tracked: Vec<(Address, Vec<U256>)>,
baseline: RootBaseline,
updated: RootBaseline,
phase: RootBaselinePhase,
}
impl RootBaselinePlanner {
pub fn new(tracked: Vec<(Address, Vec<U256>)>, baseline: RootBaseline) -> Self {
let updated = baseline.clone();
Self {
tracked,
baseline,
updated,
phase: RootBaselinePhase::Probe,
}
}
pub fn updated_baseline(&self) -> RootBaseline {
self.updated.clone()
}
}
impl ColdStartPlanner for RootBaselinePlanner {
fn initial_plan(&mut self, _state: &dyn StateView) -> ColdStartPlan {
ColdStartPlan {
probe_roots: self.tracked.iter().map(|&(address, _)| address).collect(),
..Default::default()
}
}
fn on_results(&mut self, results: &ColdStartResults, _state: &dyn StateView) -> ColdStartStep {
match self.phase {
RootBaselinePhase::Probe => {
let observed: HashMap<Address, Option<B256>> = results
.probed_roots
.iter()
.map(|o| (o.address, o.root))
.collect();
let mut verify: Vec<(Address, U256)> = Vec::new();
for (address, slots) in &self.tracked {
match observed.get(address).copied().flatten() {
Some(root) => {
let current = self.baseline.get(address) == Some(root);
self.updated.insert(*address, root);
if !current {
verify.extend(slots.iter().map(|&slot| (*address, slot)));
}
}
None => verify.extend(slots.iter().map(|&slot| (*address, slot))),
}
}
if verify.is_empty() {
return ColdStartStep::Done;
}
self.phase = RootBaselinePhase::Verify;
ColdStartStep::Continue(ColdStartPlan {
verify,
..Default::default()
})
}
RootBaselinePhase::Verify => ColdStartStep::Done,
}
}
}