use crate::{Archive, ArchiveError, Result};
use alloy_primitives::{Address, B256};
use bal_source::{check_requested, verify_account_proof, BalSource, SourceError, StateSource};
use std::collections::BTreeMap;
use tracing::{debug, info, warn};
pub const REORG_HORIZON_FALLBACK: u64 = 4096;
const PROOF_CHUNK: usize = 256;
#[derive(Debug, Default, Clone)]
pub struct SyncReport {
pub from: Option<u64>,
pub to: Option<u64>,
pub blocks_applied: u64,
pub reorged_to: Option<u64>,
pub slots_written: usize,
pub bootstrapped: usize,
pub bootstrap_pending: usize,
pub bootstrap_lost: usize,
pub unverified_blocks: u64,
}
struct SyncGuard<'a>(&'a Archive);
impl Drop for SyncGuard<'_> {
fn drop(&mut self) {
self.0.sync_idle();
}
}
impl Archive {
pub async fn sync<S: BalSource + ?Sized>(
&self,
source: &S,
state: Option<&dyn StateSource>,
) -> Result<SyncReport> {
if !self.begin_sync() {
return Err(ArchiveError::SyncInProgress);
}
let _guard = SyncGuard(self);
self.sync_inner(source, state).await
}
async fn sync_inner<S: BalSource + ?Sized>(
&self,
source: &S,
state: Option<&dyn StateSource>,
) -> Result<SyncReport> {
let mut report = SyncReport::default();
let Some(mut next) = self.claim_start()? else {
return Ok(report);
};
let src_head = source.head().await?;
let horizon_floor = src_head.saturating_sub(REORG_HORIZON_FALLBACK);
let finalized = match source.finalized().await {
Ok(f) => f.min(src_head).max(horizon_floor),
Err(e) => {
debug!(%e, "no finalized tag; using fixed reorg horizon");
horizon_floor
}
};
if let Some((h, hash)) = self.head()? {
let cur = match source.header(h).await {
Ok(hdr) => hdr,
Err(SourceError::BlockNotFound(_)) => {
debug!(head = h, "head not served by upstream yet; skipping pass");
return Ok(report);
}
Err(e) => return Err(e.into()),
};
if cur.hash != hash {
let fork = self.find_fork(source, h).await?;
warn!(head = h, fork, "reorg detected at start of sync");
self.rollback_to(fork)?;
report.reorged_to = Some(fork);
next = fork + 1;
}
}
report.from = Some(next);
let mut last_fork: Option<u64> = None;
while next <= src_head {
let blk = match source.block(next).await {
Ok(b) => b,
Err(SourceError::BlockNotFound(n)) if n >= src_head.saturating_sub(2) => {
debug!(block = n, "not yet available upstream; stopping this pass");
break;
}
Err(e) => return Err(e.into()),
};
let header = blk.header.clone();
if header.number != next {
return Err(ArchiveError::Source(SourceError::Malformed(format!(
"asked for block {next}, source answered with block {}",
header.number
))));
}
if let Some((stored_hash, _)) = self.header_at(next - 1)? {
if stored_hash != header.parent_hash {
let fork = self.find_fork(source, next - 1).await?;
if last_fork == Some(fork) {
return Err(ArchiveError::InconsistentSource(next));
}
last_fork = Some(fork);
warn!(block = next, fork, "reorg detected mid-sync");
self.rollback_to(fork)?;
report.reorged_to = Some(fork);
next = fork + 1;
continue;
}
}
let verified = match header.block_access_list_hash {
Some(expected) => {
blk.bal
.verify(expected)
.map_err(|err| ArchiveError::Verification { block: next, err })?;
true
}
None if self.config.allow_unverified => {
report.unverified_blocks += 1;
warn!(
block = next,
"applying block without BAL hash (allow_unverified)"
);
false
}
None => return Err(ArchiveError::NoBalHash(next)),
};
let watches = self.watchlist_for(next)?;
let prune_below =
Some(finalized.min(src_head.saturating_sub(self.config.bootstrap_window + 1)));
let (fresh, written) =
self.apply_block(&header, &blk.bal, &watches, verified, prune_below)?;
report.slots_written += written;
report.blocks_applied += 1;
report.to = Some(next);
debug!(block = next, written, fresh = fresh.len(), "applied");
if !fresh.is_empty() {
let prev = match state {
None => None,
Some(_) => self.header_of(source, next - 1).await,
};
for (addr, start, slots) in &fresh {
match (state, prev) {
(Some(st), Some((hash, root))) => {
match self
.bootstrap_at(st, root, hash, *addr, *start, slots, next - 1)
.await
{
Ok(n) => {
report.bootstrapped += n;
report.bootstrap_pending += slots.len() - n;
}
Err(e) => {
warn!(%addr, block = next, %e, "early bootstrap failed; left pending");
report.bootstrap_pending += slots.len();
}
}
}
_ => report.bootstrap_pending += slots.len(),
}
}
}
next += 1;
}
if let Some(st) = state {
let (ok, pending, lost) = self.retry_pending(source, st, src_head).await?;
report.bootstrapped += ok;
report.bootstrap_pending = pending;
report.bootstrap_lost = lost;
}
info!(?report, "sync done");
Ok(report)
}
async fn header_of<S: BalSource + ?Sized>(
&self,
source: &S,
block: u64,
) -> Option<(B256, B256)> {
match self.header_at(block) {
Ok(Some(h)) => return Some(h),
Ok(None) => {}
Err(e) => {
warn!(block, %e, "cannot read stored header");
return None;
}
}
match source.header(block).await {
Ok(h) => {
if let Err(e) = self.remember_header(block, h.hash, h.state_root) {
warn!(block, %e, "cannot remember header");
}
Some((h.hash, h.state_root))
}
Err(e) => {
warn!(block, %e, "cannot fetch header for proof root");
None
}
}
}
async fn find_fork<S: BalSource + ?Sized>(&self, source: &S, from: u64) -> Result<u64> {
let floor = from.saturating_sub(REORG_HORIZON_FALLBACK);
let mut b = from;
loop {
let Some((stored, _)) = self.header_at(b)? else {
return Err(ArchiveError::ReorgBeyondHorizon(b));
};
let live = source.header(b).await?.hash;
if live == stored {
return Ok(b);
}
if b == 0 || b <= floor {
return Err(ArchiveError::ReorgBeyondHorizon(b));
}
b -= 1;
}
}
#[allow(clippy::too_many_arguments)]
async fn bootstrap_at(
&self,
state: &dyn StateSource,
state_root: B256,
block_hash: B256,
addr: Address,
start: u64,
slots: &[B256],
block: u64,
) -> Result<usize> {
let mut stored = 0;
for chunk in slots.chunks(PROOF_CHUNK) {
let proof = state.proof(addr, chunk, block).await?;
check_requested(chunk, &proof)?;
let values = verify_account_proof(state_root, &proof)?;
let values: Vec<(B256, B256)> = values
.into_iter()
.map(|(k, v)| (k, B256::from(v.to_be_bytes::<32>())))
.collect();
stored += self.put_bootstrap(addr, start, block, block_hash, &values)?;
}
Ok(stored)
}
async fn retry_pending<S: BalSource + ?Sized>(
&self,
source: &S,
state: &dyn StateSource,
src_head: u64,
) -> Result<(usize, usize, usize)> {
let pending = self.pending_bootstraps()?;
if pending.is_empty() {
return Ok((0, 0, 0));
}
let watches = self.watchlist()?;
let start_of = |a: Address| watches.iter().find(|(x, _)| *x == a).map(|(_, s)| *s);
let (mut ok, mut still, mut lost) = (0, 0, 0);
let mut groups: BTreeMap<(Address, u64), Vec<B256>> = BTreeMap::new();
for (addr, slot, first_seen) in pending {
groups.entry((addr, first_seen)).or_default().push(slot);
}
for ((addr, first_seen), slots) in groups {
let Some(start) = start_of(addr) else {
continue;
};
let Some(at) = first_seen.checked_sub(1) else {
return Err(ArchiveError::Corrupt("pending first_seen"));
};
if src_head.saturating_sub(at) > self.config.bootstrap_window {
self.mark_lost(addr, &slots, first_seen)?;
lost += slots.len();
continue;
}
let Some((hash, root)) = self.header_of(source, at).await else {
still += slots.len();
continue;
};
match self
.bootstrap_at(state, root, hash, addr, start, &slots, at)
.await
{
Ok(n) => {
ok += n;
still += slots.len() - n;
}
Err(e) => {
debug!(%addr, first_seen, %e, "pending bootstrap retry failed");
still += slots.len();
}
}
}
Ok((ok, still, lost))
}
pub async fn bootstrap_slot(
&self,
state: &dyn StateSource,
addr: Address,
slot: B256,
) -> Result<()> {
let start = self.start_of(addr)?.ok_or(ArchiveError::NotWatched(addr))?;
let (head, _) = self
.head()?
.ok_or(ArchiveError::HeadBelowStart { head: 0, start })?;
if head < start {
return Err(ArchiveError::HeadBelowStart { head, start });
}
if self.boot_state(addr, slot)?.is_some() {
return Ok(()); }
let (hash, root) = self
.header_at(head)?
.ok_or(ArchiveError::ReorgBeyondHorizon(head))?;
self.bootstrap_at(state, root, hash, addr, start, &[slot], head)
.await?;
Ok(())
}
}