use crate::keys::{blockidx_key, decode_boot, encode_boot, encode_value, slot_key, slot_prefix};
use crate::{
anchor_key, creation_in, settle_created, Archive, ArchiveError, BootState, Provenance, Result,
BLOCKIDX, BOOT, CREATED, META, PENDING, SLOTS, WATCH,
};
use alloy_primitives::{Address, B256};
use bal_source::{BalSource, SourceError, SourcedBlock};
use futures::future::join_all;
use redb::ReadableTable;
use std::collections::BTreeSet;
use tracing::{debug, info};
pub const FETCH_AHEAD: u64 = 8;
#[derive(Debug, Clone, Default)]
pub struct BackfillOpts {
pub to: Option<u64>,
pub max_blocks: Option<u64>,
pub resolve_only: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackfillStop {
Target,
Creation(u64),
Resolved,
Budget,
PreBal(u64),
HistoryUnavailable(u64),
Nothing,
}
#[derive(Debug, Clone)]
pub struct BackfillReport {
pub from: u64,
pub to: u64,
pub blocks_scanned: u64,
pub records_written: usize,
pub slots_resolved: usize,
pub unresolved: usize,
pub created_at: Option<u64>,
pub stopped: BackfillStop,
}
struct Guard<'a>(&'a Archive);
impl Drop for Guard<'_> {
fn drop(&mut self) {
self.0.sync_idle();
}
}
impl Archive {
pub async fn backfill<S: BalSource + ?Sized>(
&self,
source: &S,
addr: Address,
opts: BackfillOpts,
) -> Result<BackfillReport> {
if !self.begin_sync() {
return Err(ArchiveError::SyncInProgress);
}
let _guard = Guard(self);
self.backfill_inner(source, addr, opts).await
}
async fn backfill_inner<S: BalSource + ?Sized>(
&self,
source: &S,
addr: Address,
opts: BackfillOpts,
) -> Result<BackfillReport> {
let start = self.start_of(addr)?.ok_or(ArchiveError::NotWatched(addr))?;
let head = self.head()?.map(|(h, _)| h).unwrap_or(0);
if head < start {
return Err(ArchiveError::HeadBelowStart { head, start });
}
let created = self.created_at(addr)?;
let mut unresolved = self.unknown_pre_values(addr)?;
let mut report = BackfillReport {
from: start,
to: start,
blocks_scanned: 0,
records_written: 0,
slots_resolved: 0,
unresolved: unresolved.len(),
created_at: created,
stopped: BackfillStop::Nothing,
};
let target = opts.to.unwrap_or(1).max(1);
if created.is_some() || target >= start || (opts.resolve_only && unresolved.is_empty()) {
return Ok(report);
}
let above = source.header(start).await?;
if above.number != start {
return Err(wrong_block(start, above.number));
}
let known = match self.header_at(start)? {
Some((h, _)) => Some(h),
None => self.anchor(addr)?,
};
if let Some(h) = known {
if h != above.hash {
return Err(ArchiveError::StartReplaced(start));
}
}
let mut expect = above.parent_hash;
let mut cur = start - 1;
'walk: loop {
if cur < target {
report.stopped = BackfillStop::Target;
break;
}
let mut batch = FETCH_AHEAD.min(cur - target + 1);
if let Some(m) = opts.max_blocks {
batch = batch.min(m.saturating_sub(report.blocks_scanned));
}
if batch == 0 {
report.stopped = BackfillStop::Budget;
break;
}
let numbers: Vec<u64> = (0..batch).map(|i| cur - i).collect();
let fetched = join_all(numbers.iter().map(|&b| source.block(b))).await;
for (b, res) in numbers.into_iter().zip(fetched) {
let blk = match res {
Ok(blk) => blk,
Err(SourceError::BlockNotFound(_)) | Err(SourceError::NoBal(_)) => {
report.stopped = BackfillStop::HistoryUnavailable(b);
break 'walk;
}
Err(e) => return Err(e.into()),
};
if blk.header.number != b {
return Err(wrong_block(b, blk.header.number));
}
if blk.header.hash != expect {
return Err(ArchiveError::InconsistentSource(b + 1));
}
let Some(bal_hash) = blk.header.block_access_list_hash else {
report.stopped = BackfillStop::PreBal(b);
break 'walk;
};
blk.bal
.verify(bal_hash)
.map_err(|err| ArchiveError::Verification { block: b, err })?;
let (written, resolved, is_creation) =
self.backfill_block(addr, b, &blk, &mut unresolved)?;
report.blocks_scanned += 1;
report.records_written += written;
report.slots_resolved += resolved;
report.to = b;
debug!(%addr, block = b, written, "backfilled");
expect = blk.header.parent_hash;
cur = b - 1;
if is_creation {
report.created_at = Some(b);
unresolved.clear();
report.stopped = BackfillStop::Creation(b);
break 'walk;
}
if opts.resolve_only && unresolved.is_empty() {
report.stopped = BackfillStop::Resolved;
break 'walk;
}
}
}
report.unresolved = unresolved.len();
info!(?report, "backfill done");
Ok(report)
}
fn unknown_pre_values(&self, addr: Address) -> Result<BTreeSet<B256>> {
let rtx = self.db.begin_read()?;
let boot = rtx.open_table(BOOT)?;
let mut out = BTreeSet::new();
for k in crate::collect_prefix_keys(&boot, addr.as_slice())? {
let state = boot.get(k.as_slice())?.and_then(|v| decode_boot(v.value()));
if matches!(
state,
Some(BootState::Pending { .. }) | Some(BootState::Lost { .. })
) {
out.insert(B256::from_slice(&k[20..]));
}
}
Ok(out)
}
fn anchor(&self, addr: Address) -> Result<Option<B256>> {
let rtx = self.db.begin_read()?;
let meta = rtx.open_table(META)?;
Ok(meta
.get(anchor_key(addr).as_str())?
.and_then(|v| (v.value().len() == 32).then(|| B256::from_slice(v.value()))))
}
fn backfill_block(
&self,
addr: Address,
block: u64,
blk: &SourcedBlock,
unresolved: &mut BTreeSet<B256>,
) -> Result<(usize, usize, bool)> {
let mut written = 0;
let mut resolved = 0;
let mut is_creation = false;
let txn = self.db.begin_write()?;
{
let mut watch = txn.open_table(WATCH)?;
if watch.get(addr.as_slice())?.map(|v| v.value()) != Some(block + 1) {
return Ok((0, 0, false));
}
if let Some(acc) = blk.bal.account(&addr) {
let mut slots = txn.open_table(SLOTS)?;
let mut idx = txn.open_table(BLOCKIDX)?;
let mut boot = txn.open_table(BOOT)?;
let mut pending = txn.open_table(PENDING)?;
for sc in &acc.storage_changes {
let slot = sc.slot_b256();
if self.config.full_detail {
for ch in &sc.changes {
slots.insert(
slot_key(addr, slot, block, ch.block_access_index).as_slice(),
encode_value(Provenance::Bal, ch.value_b256()).as_slice(),
)?;
written += 1;
}
} else {
let ch = sc.final_change();
slots.insert(
slot_key(addr, slot, block, ch.block_access_index).as_slice(),
encode_value(Provenance::Bal, ch.value_b256()).as_slice(),
)?;
written += 1;
}
idx.insert(blockidx_key(addr, block, slot).as_slice(), ())?;
let key = slot_prefix(addr, slot);
let next = match boot
.get(key.as_slice())?
.and_then(|v| decode_boot(v.value()))
{
Some(BootState::Done) => None,
Some(BootState::Pending { .. }) => {
Some(BootState::Pending { first_seen: block })
}
Some(BootState::Lost { .. }) => Some(BootState::Lost { first_seen: block }),
None => Some(BootState::Pending { first_seen: block }),
};
if let Some(state) = next {
boot.insert(key.as_slice(), encode_boot(state).as_slice())?;
if matches!(state, BootState::Pending { .. }) {
pending.insert(key.as_slice(), block)?;
}
}
if unresolved.remove(&slot) {
resolved += 1;
}
}
if creation_in(acc) {
txn.open_table(CREATED)?.insert(addr.as_slice(), block)?;
settle_created(&mut boot, &mut pending, addr)?;
is_creation = true;
}
}
watch.insert(addr.as_slice(), block)?;
txn.open_table(META)?
.insert(anchor_key(addr).as_str(), blk.header.hash.as_slice())?;
}
txn.commit()?;
Ok((written, resolved, is_creation))
}
}
fn wrong_block(asked: u64, got: u64) -> ArchiveError {
ArchiveError::Source(SourceError::Malformed(format!(
"asked for block {asked}, source answered with block {got}"
)))
}