#![doc = include_str!("../README.md")]
mod backfill;
mod keys;
mod sync;
pub use backfill::{BackfillOpts, BackfillReport, BackfillStop};
pub use keys::{BootState, Provenance, SCHEMA_VERSION};
pub use sync::{SyncReport, REORG_HORIZON_FALLBACK};
use alloy_primitives::{Address, B256};
use bal_codec::BlockAccessIndex;
use keys::*;
use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
use std::ops::{Bound, Range, RangeBounds};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Mutex;
pub(crate) const SLOTS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("slots");
pub(crate) const BLOCKIDX: TableDefinition<&[u8], ()> = TableDefinition::new("blockidx");
pub(crate) const META: TableDefinition<&str, &[u8]> = TableDefinition::new("meta");
pub(crate) const WATCH: TableDefinition<&[u8], u64> = TableDefinition::new("watch");
const HASHES: TableDefinition<u64, &[u8]> = TableDefinition::new("blockhashes");
pub(crate) const BOOT: TableDefinition<&[u8], &[u8]> = TableDefinition::new("bootstrap");
pub(crate) const PENDING: TableDefinition<&[u8], u64> = TableDefinition::new("pending");
pub(crate) const CREATED: TableDefinition<&[u8], u64> = TableDefinition::new("created");
const META_SCHEMA: &str = "schema_version";
const META_ANCHOR: &str = "anchor:";
const META_HEAD: &str = "head";
const META_FULL_DETAIL: &str = "full_detail";
#[derive(Debug, thiserror::Error)]
pub enum ArchiveError {
#[error("db: {0}")]
Db(Box<redb::Error>),
#[error("schema version {found} on disk, this build speaks {expected}")]
SchemaMismatch {
found: u32,
expected: u32,
},
#[error("from_block must be >= 1 (got {0})")]
InvalidStart(u64),
#[error("watch from block {from_block} is in the past (head {head}); use backfill for history before the head")]
StartInPast {
from_block: u64,
head: u64,
},
#[error("address {0} is not watched")]
NotWatched(Address),
#[error(
"address {address} is already watched from block {from_block}; unwatch first to change it"
)]
AlreadyWatched {
address: Address,
from_block: u64,
},
#[error("a sync pass is already running on this archive")]
SyncInProgress,
#[error("archive was created with {option} = {on_disk}, opened with {requested}")]
ConfigMismatch {
option: &'static str,
on_disk: String,
requested: String,
},
#[error("archive head {head} is below watch start {start}; nothing to bootstrap yet")]
HeadBelowStart {
head: u64,
start: u64,
},
#[error("source: {0}")]
Source(#[from] bal_source::SourceError),
#[error("block {block} failed BAL verification: {err}")]
Verification {
block: u64,
err: bal_codec::CodecError,
},
#[error(
"block {0} header carries no block_access_list_hash; refusing to apply unverifiable data"
)]
NoBalHash(u64),
#[error("reorg deeper than retained block hashes (fork below block {0})")]
ReorgBeyondHorizon(u64),
#[error(
"source is inconsistent around block {0}: parent hash does not match its own block {0}-1"
)]
InconsistentSource(u64),
#[error("block {0} on the node is not the block the archive holds; run sync first")]
StartReplaced(u64),
#[error("proof: {0}")]
Proof(#[from] bal_source::ProofError),
#[error("corrupt record: {0}")]
Corrupt(&'static str),
}
macro_rules! from_redb {
($($t:ty),*) => {$(
impl From<$t> for ArchiveError {
fn from(e: $t) -> Self { ArchiveError::Db(Box::new(e.into())) }
}
)*};
}
from_redb!(
redb::Error,
redb::DatabaseError,
redb::TransactionError,
redb::TableError,
redb::StorageError,
redb::CommitError
);
pub type Result<T> = std::result::Result<T, ArchiveError>;
pub(crate) type FreshSlots = Vec<(Address, u64, Vec<B256>)>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NotAvailable {
#[error("address {0} is not watched")]
NotWatched(Address),
#[error("block {requested} is before watch start {start}")]
BeforeStart {
requested: u64,
start: u64,
},
#[error("block {requested} is after archive head {head}")]
AfterHead {
requested: u64,
head: u64,
},
#[error("archive has not synced any block yet")]
NotSynced,
#[error("invalid block range {start}..{end}")]
InvalidRange {
start: u64,
end: u64,
},
#[error("no change to this slot is recorded since the watch start; backfill to the contract's creation, or prove it at the head")]
NotBootstrapped,
#[error("no record before block {first_seen} (the slot's earliest recorded change); backfill further back, or prove it while the node still can")]
BootstrapPending {
first_seen: u64,
},
#[error("no record before block {first_seen} (the slot's earliest recorded change); the node can no longer prove it — backfill instead")]
BootstrapLost {
first_seen: u64,
},
#[error("internal: {0}")]
Internal(String),
}
impl From<ArchiveError> for NotAvailable {
fn from(e: ArchiveError) -> Self {
NotAvailable::Internal(e.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StorageValue {
pub value: B256,
pub provenance: Provenance,
pub set_at: u64,
pub index: BlockAccessIndex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HistoryEntry {
pub block: u64,
pub index: BlockAccessIndex,
pub value: B256,
pub provenance: Provenance,
}
#[derive(Debug, Clone)]
pub struct ArchiveStats {
pub head: Option<(u64, B256)>,
pub watches: Vec<(Address, u64)>,
pub created: Vec<(Address, u64)>,
pub slot_records: u64,
pub slots_done: u64,
pub slots_pending: u64,
pub slots_lost: u64,
pub retained_headers: u64,
pub file_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct ArchiveConfig {
pub bootstrap_window: u64,
pub full_detail: bool,
pub allow_unverified: bool,
}
impl Default for ArchiveConfig {
fn default() -> Self {
Self {
bootstrap_window: 120,
full_detail: false,
allow_unverified: false,
}
}
}
pub struct Archive {
db: Database,
path: std::path::PathBuf,
config: ArchiveConfig,
watch_gate: Mutex<()>,
in_flight: AtomicU64,
syncing: AtomicBool,
}
impl Archive {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
Self::open_with(path, ArchiveConfig::default())
}
pub fn open_with(path: impl AsRef<Path>, config: ArchiveConfig) -> Result<Self> {
let path_buf = path.as_ref().to_path_buf();
let db = Database::create(&path_buf)?;
let txn = db.begin_write()?;
{
txn.open_table(SLOTS)?;
txn.open_table(BLOCKIDX)?;
txn.open_table(WATCH)?;
txn.open_table(HASHES)?;
txn.open_table(CREATED)?;
let boot = txn.open_table(BOOT)?;
let mut pending = txn.open_table(PENDING)?;
let mut meta = txn.open_table(META)?;
let found: Option<Vec<u8>> = meta.get(META_SCHEMA)?.map(|v| v.value().to_vec());
match found {
Some(v) => {
let found = u32::from_be_bytes(
v.as_slice()
.try_into()
.map_err(|_| ArchiveError::Corrupt("schema_version"))?,
);
if found != SCHEMA_VERSION {
return Err(ArchiveError::SchemaMismatch {
found,
expected: SCHEMA_VERSION,
});
}
}
None => {
meta.insert(META_SCHEMA, SCHEMA_VERSION.to_be_bytes().as_slice())?;
}
}
let stored_detail = meta.get(META_FULL_DETAIL)?.map(|v| v.value().to_vec());
match stored_detail {
Some(v) => {
let on_disk = v.first().copied().unwrap_or(0) != 0;
if on_disk != config.full_detail {
return Err(ArchiveError::ConfigMismatch {
option: "full_detail",
on_disk: on_disk.to_string(),
requested: config.full_detail.to_string(),
});
}
}
None => {
meta.insert(META_FULL_DETAIL, [config.full_detail as u8].as_slice())?;
}
}
if pending.is_empty()? {
let mut rebuilt = Vec::new();
for item in boot.iter()? {
let (k, v) = item?;
if let Some(BootState::Pending { first_seen }) = decode_boot(v.value()) {
rebuilt.push((k.value().to_vec(), first_seen));
}
}
for (k, f) in rebuilt {
pending.insert(k.as_slice(), f)?;
}
}
}
txn.commit()?;
Ok(Self {
db,
path: path_buf,
config,
watch_gate: Mutex::new(()),
in_flight: AtomicU64::new(0),
syncing: AtomicBool::new(false),
})
}
pub(crate) fn begin_sync(&self) -> bool {
self.syncing
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
}
pub fn config(&self) -> &ArchiveConfig {
&self.config
}
pub fn watch(&self, addr: Address, from_block: u64) -> Result<()> {
if from_block == 0 {
return Err(ArchiveError::InvalidStart(from_block));
}
let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
if let Some(existing) = self.start_of(addr)? {
if existing == from_block {
return Ok(());
}
return Err(ArchiveError::AlreadyWatched {
address: addr,
from_block: existing,
});
}
let head = self.head()?.map(|(h, _)| h).unwrap_or(0);
let floor = head.max(self.in_flight.load(Ordering::SeqCst));
if from_block <= floor {
return Err(ArchiveError::StartInPast {
from_block,
head: floor,
});
}
let txn = self.db.begin_write()?;
txn.open_table(WATCH)?.insert(addr.as_slice(), from_block)?;
txn.commit()?;
Ok(())
}
pub fn unwatch(&self, addr: Address) -> Result<()> {
let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
let txn = self.db.begin_write()?;
{
txn.open_table(WATCH)?.remove(addr.as_slice())?;
let mut slots = txn.open_table(SLOTS)?;
for k in collect_prefix_keys(&slots, addr.as_slice())? {
slots.remove(k.as_slice())?;
}
let mut boot = txn.open_table(BOOT)?;
for k in collect_prefix_keys(&boot, addr.as_slice())? {
boot.remove(k.as_slice())?;
}
let mut pending = txn.open_table(PENDING)?;
for k in collect_prefix_keys(&pending, addr.as_slice())? {
pending.remove(k.as_slice())?;
}
let mut idx = txn.open_table(BLOCKIDX)?;
for k in collect_prefix_keys(&idx, addr.as_slice())? {
idx.remove(k.as_slice())?;
}
txn.open_table(CREATED)?.remove(addr.as_slice())?;
txn.open_table(META)?.remove(anchor_key(addr).as_str())?;
}
txn.commit()?;
Ok(())
}
pub fn created_at(&self, addr: Address) -> Result<Option<u64>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(CREATED)?;
Ok(t.get(addr.as_slice())?.map(|v| v.value()))
}
pub fn watchlist(&self) -> Result<Vec<(Address, u64)>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(WATCH)?;
let mut out = Vec::new();
for item in t.iter()? {
let (k, v) = item?;
out.push((Address::from_slice(k.value()), v.value()));
}
Ok(out)
}
pub(crate) fn watchlist_for(&self, block: u64) -> Result<Vec<(Address, u64)>> {
let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
self.in_flight.store(block, Ordering::SeqCst);
self.watchlist()
}
pub(crate) fn claim_start(&self) -> Result<Option<u64>> {
let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
let earliest = self.watchlist()?.iter().map(|(_, s)| *s).min();
let Some(earliest) = earliest else {
return Ok(None);
};
let next = match self.head()? {
Some((h, _)) => h + 1,
None => earliest,
};
self.in_flight.store(next, Ordering::SeqCst);
Ok(Some(next))
}
pub(crate) fn sync_idle(&self) {
self.in_flight.store(0, Ordering::SeqCst);
self.syncing.store(false, Ordering::SeqCst);
}
pub(crate) fn start_of(&self, addr: Address) -> Result<Option<u64>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(WATCH)?;
Ok(t.get(addr.as_slice())?.map(|v| v.value()))
}
pub fn stats(&self) -> Result<ArchiveStats> {
let rtx = self.db.begin_read()?;
let slot_records = rtx.open_table(SLOTS)?.len()?;
let pending = rtx.open_table(PENDING)?.len()?;
let boot = rtx.open_table(BOOT)?;
let (mut done, mut lost) = (0u64, 0u64);
for item in boot.iter()? {
let (_, v) = item?;
match decode_boot(v.value()) {
Some(BootState::Done) => done += 1,
Some(BootState::Lost { .. }) => lost += 1,
_ => {}
}
}
let retained_headers = rtx.open_table(HASHES)?.len()?;
let file_bytes = std::fs::metadata(&self.path).map(|m| m.len()).unwrap_or(0);
let mut created = Vec::new();
for item in rtx.open_table(CREATED)?.iter()? {
let (k, v) = item?;
created.push((Address::from_slice(k.value()), v.value()));
}
Ok(ArchiveStats {
head: self.head()?,
watches: self.watchlist()?,
created,
slot_records,
slots_done: done,
slots_pending: pending,
slots_lost: lost,
retained_headers,
file_bytes,
})
}
pub fn head(&self) -> Result<Option<(u64, B256)>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(META)?;
match t.get(META_HEAD)? {
None => Ok(None),
Some(v) => {
let b = v.value();
if b.len() != 40 {
return Err(ArchiveError::Corrupt("head"));
}
let num: [u8; 8] = b[..8]
.try_into()
.map_err(|_| ArchiveError::Corrupt("head"))?;
Ok(Some((u64::from_be_bytes(num), B256::from_slice(&b[8..]))))
}
}
}
pub(crate) fn header_at(&self, block: u64) -> Result<Option<(B256, B256)>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(HASHES)?;
match t.get(block)? {
None => Ok(None),
Some(v) => {
let b = v.value();
if b.len() != 64 {
return Err(ArchiveError::Corrupt("blockhashes"));
}
Ok(Some((
B256::from_slice(&b[..32]),
B256::from_slice(&b[32..]),
)))
}
}
}
pub(crate) fn remember_header(&self, block: u64, hash: B256, state_root: B256) -> Result<()> {
let txn = self.db.begin_write()?;
{
let mut hashes = txn.open_table(HASHES)?;
if hashes.get(block)?.is_none() {
hashes.insert(block, header_bytes(hash, state_root).as_slice())?;
}
}
txn.commit()?;
Ok(())
}
fn check_range(&self, addr: Address, block: u64) -> std::result::Result<u64, NotAvailable> {
let start = self.start_of(addr)?.ok_or(NotAvailable::NotWatched(addr))?;
if block < start {
return Err(NotAvailable::BeforeStart {
requested: block,
start,
});
}
let (head, _) = self.head()?.ok_or(NotAvailable::NotSynced)?;
if block > head {
return Err(NotAvailable::AfterHead {
requested: block,
head,
});
}
Ok(start)
}
pub fn storage_at(
&self,
addr: Address,
slot: B256,
block: u64,
) -> std::result::Result<StorageValue, NotAvailable> {
self.check_range(addr, block)?;
let rtx = self.db.begin_read().map_err(ArchiveError::from)?;
let t = rtx.open_table(SLOTS).map_err(ArchiveError::from)?;
let lo = slot_prefix(addr, slot);
let hi = slot_key(addr, slot, block, u32::MAX);
let mut it = t
.range::<&[u8]>(lo.as_slice()..=hi.as_slice())
.map_err(ArchiveError::from)?;
if let Some(item) = it.next_back() {
let (k, v) = item.map_err(ArchiveError::from)?;
let (_, _, set_at, index) =
parse_slot_key(k.value()).ok_or(NotAvailable::Internal("bad slot key".into()))?;
let (provenance, value) =
decode_value(v.value()).ok_or(NotAvailable::Internal("bad slot value".into()))?;
return Ok(StorageValue {
value,
provenance,
set_at,
index,
});
}
drop(it);
let created = rtx.open_table(CREATED).map_err(ArchiveError::from)?;
if let Some(c) = created
.get(addr.as_slice())
.map_err(ArchiveError::from)?
.map(|v| v.value())
{
return Ok(StorageValue {
value: B256::ZERO,
provenance: Provenance::Bal,
set_at: c,
index: u32::MAX,
});
}
let boot = rtx.open_table(BOOT).map_err(ArchiveError::from)?;
match boot.get(lo.as_slice()).map_err(ArchiveError::from)? {
Some(v) => match decode_boot(v.value()) {
Some(BootState::Pending { first_seen }) => {
Err(NotAvailable::BootstrapPending { first_seen })
}
Some(BootState::Lost { first_seen }) => {
Err(NotAvailable::BootstrapLost { first_seen })
}
Some(BootState::Done) => Err(NotAvailable::Internal(
"bootstrap marked done but no record".into(),
)),
None => Err(NotAvailable::Internal("bad bootstrap record".into())),
},
None => Err(NotAvailable::NotBootstrapped),
}
}
pub fn history(
&self,
addr: Address,
slot: B256,
range: Range<u64>,
) -> std::result::Result<Vec<HistoryEntry>, NotAvailable> {
if range.start >= range.end {
return Err(NotAvailable::InvalidRange {
start: range.start,
end: range.end,
});
}
self.check_range(addr, range.start)?;
self.check_range(addr, range.end - 1)?;
let rtx = self.db.begin_read().map_err(ArchiveError::from)?;
let t = rtx.open_table(SLOTS).map_err(ArchiveError::from)?;
let lo = slot_key(addr, slot, range.start, 0);
let hi = slot_key(addr, slot, range.end, 0);
let mut out = Vec::new();
for item in t
.range::<&[u8]>(lo.as_slice()..hi.as_slice())
.map_err(ArchiveError::from)?
{
let (k, v) = item.map_err(ArchiveError::from)?;
let (_, _, block, index) =
parse_slot_key(k.value()).ok_or(NotAvailable::Internal("bad slot key".into()))?;
let (provenance, value) =
decode_value(v.value()).ok_or(NotAvailable::Internal("bad slot value".into()))?;
out.push(HistoryEntry {
block,
index,
value,
provenance,
});
}
Ok(out)
}
pub fn changed_slots(
&self,
addr: Address,
block: u64,
) -> std::result::Result<Vec<B256>, NotAvailable> {
self.check_range(addr, block)?;
let rtx = self.db.begin_read().map_err(ArchiveError::from)?;
let t = rtx.open_table(BLOCKIDX).map_err(ArchiveError::from)?;
let prefix = blockidx_prefix(addr, block);
let mut out = Vec::new();
for k in collect_prefix_keys(&t, &prefix)? {
let (_, _, slot) =
parse_blockidx_key(&k).ok_or(NotAvailable::Internal("bad blockidx key".into()))?;
out.push(slot);
}
Ok(out)
}
pub fn boot_state(&self, addr: Address, slot: B256) -> Result<Option<BootState>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(BOOT)?;
Ok(t.get(slot_prefix(addr, slot).as_slice())?
.and_then(|v| decode_boot(v.value())))
}
pub(crate) fn put_bootstrap(
&self,
addr: Address,
start: u64,
proof_block: u64,
proof_block_hash: B256,
values: &[(B256, B256)],
) -> Result<usize> {
let mut written = 0;
let txn = self.db.begin_write()?;
{
let watch = txn.open_table(WATCH)?;
if watch.get(addr.as_slice())?.map(|v| v.value()) != Some(start) {
return Ok(0);
}
let hashes = txn.open_table(HASHES)?;
let same_block = hashes
.get(proof_block)?
.map(|v| v.value().len() == 64 && v.value()[..32] == proof_block_hash[..])
.unwrap_or(false);
if !same_block {
return Ok(0);
}
let mut slots = txn.open_table(SLOTS)?;
let mut boot = txn.open_table(BOOT)?;
let mut pending = txn.open_table(PENDING)?;
for (slot, value) in values {
let key = slot_prefix(addr, *slot);
let ok = match boot
.get(key.as_slice())?
.and_then(|v| decode_boot(v.value()))
{
None => true,
Some(BootState::Pending { first_seen })
| Some(BootState::Lost { first_seen }) => first_seen > proof_block,
Some(BootState::Done) => false,
};
if !ok {
continue;
}
slots.insert(
slot_key(addr, *slot, start - 1, u32::MAX).as_slice(),
encode_value(Provenance::Proof, *value).as_slice(),
)?;
boot.insert(key.as_slice(), encode_boot(BootState::Done).as_slice())?;
pending.remove(key.as_slice())?;
written += 1;
}
}
txn.commit()?;
Ok(written)
}
pub(crate) fn mark_lost(&self, addr: Address, slots: &[B256], first_seen: u64) -> Result<()> {
let txn = self.db.begin_write()?;
{
if txn.open_table(WATCH)?.get(addr.as_slice())?.is_none() {
return Ok(()); }
let mut boot = txn.open_table(BOOT)?;
let mut pending = txn.open_table(PENDING)?;
for slot in slots {
let key = slot_prefix(addr, *slot);
boot.insert(
key.as_slice(),
encode_boot(BootState::Lost { first_seen }).as_slice(),
)?;
pending.remove(key.as_slice())?;
}
}
txn.commit()?;
Ok(())
}
pub(crate) fn pending_bootstraps(&self) -> Result<Vec<(Address, B256, u64)>> {
let rtx = self.db.begin_read()?;
let t = rtx.open_table(PENDING)?;
let mut out = Vec::new();
for item in t.iter()? {
let (k, v) = item?;
let k = k.value();
if k.len() != SLOT_PREFIX_LEN {
return Err(ArchiveError::Corrupt("pending key"));
}
out.push((
Address::from_slice(&k[..20]),
B256::from_slice(&k[20..]),
v.value(),
));
}
Ok(out)
}
pub fn rollback_to(&self, block: u64) -> Result<()> {
let (hash, _) = self
.header_at(block)?
.ok_or(ArchiveError::ReorgBeyondHorizon(block))?;
let watches = self.watchlist()?;
let txn = self.db.begin_write()?;
{
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 (addr, start) in &watches {
if block + 1 < *start {
for k in collect_prefix_keys(&slots, addr.as_slice())? {
slots.remove(k.as_slice())?;
}
for k in collect_prefix_keys(&idx, addr.as_slice())? {
idx.remove(k.as_slice())?;
}
for k in collect_prefix_keys(&boot, addr.as_slice())? {
boot.remove(k.as_slice())?;
}
for k in collect_prefix_keys(&pending, addr.as_slice())? {
pending.remove(k.as_slice())?;
}
continue;
}
let lo = blockidx_prefix(*addr, block + 1);
let hi = prefix_end(addr.as_slice());
let mut victims = Vec::new();
for item in idx.range::<&[u8]>(bounds(&lo, hi.as_deref()))? {
let (k, _) = item?;
victims.push(k.value().to_vec());
}
for k in victims {
let (_, b, slot) =
parse_blockidx_key(&k).ok_or(ArchiveError::Corrupt("blockidx"))?;
let sl = slot_key(*addr, slot, b, 0);
let sh = slot_key(*addr, slot, b, u32::MAX);
let ks: Vec<Vec<u8>> = slots
.range::<&[u8]>(sl.as_slice()..=sh.as_slice())?
.map(|r| r.map(|(k, _)| k.value().to_vec()))
.collect::<std::result::Result<_, _>>()?;
for sk in ks {
slots.remove(sk.as_slice())?;
}
idx.remove(k.as_slice())?;
let bk = slot_prefix(*addr, slot);
let forget = match boot
.get(bk.as_slice())?
.and_then(|v| decode_boot(v.value()))
{
Some(BootState::Pending { first_seen })
| Some(BootState::Lost { first_seen }) => first_seen > block,
_ => false,
};
if forget {
boot.remove(bk.as_slice())?;
pending.remove(bk.as_slice())?;
}
}
}
let mut hashes = txn.open_table(HASHES)?;
let above: Vec<u64> = hashes
.range(block + 1..)?
.map(|r| r.map(|(k, _)| k.value()))
.collect::<std::result::Result<_, _>>()?;
for b in above {
hashes.remove(b)?;
}
let mut created = txn.open_table(CREATED)?;
let stale: Vec<Vec<u8>> = created
.iter()?
.filter_map(|r| r.ok())
.filter(|(_, v)| v.value() > block)
.map(|(k, _)| k.value().to_vec())
.collect();
for k in stale {
created.remove(k.as_slice())?;
}
let mut meta = txn.open_table(META)?;
meta.insert(META_HEAD, head_bytes(block, hash).as_slice())?;
}
txn.commit()?;
Ok(())
}
pub(crate) fn apply_block(
&self,
header: &bal_source::Header,
bal: &bal_codec::BlockAccessList,
watches: &[(Address, u64)],
verified: bool,
prune_hashes_below: Option<u64>,
) -> Result<(FreshSlots, usize)> {
let n = header.number;
let provenance = if verified {
Provenance::Bal
} else {
Provenance::Unverified
};
let mut fresh = Vec::new();
let mut written = 0usize;
let txn = self.db.begin_write()?;
{
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)?;
let mut created = txn.open_table(CREATED)?;
let watch_now = txn.open_table(WATCH)?;
for (addr, start) in watches {
if n < *start {
continue;
}
if watch_now.get(addr.as_slice())?.is_none() {
continue;
}
let Some(acc) = bal.account(addr) else {
continue;
};
let mut is_created = created.get(addr.as_slice())?.is_some();
if !is_created && verified && creation_in(acc) {
created.insert(addr.as_slice(), n)?;
settle_created(&mut boot, &mut pending, *addr)?;
is_created = true;
}
let mut fresh_here = Vec::new();
for sc in &acc.storage_changes {
let slot = sc.slot_b256();
let prefix = slot_prefix(*addr, slot);
let seen_before = boot.get(prefix.as_slice())?.is_some();
if !seen_before && is_created {
boot.insert(prefix.as_slice(), encode_boot(BootState::Done).as_slice())?;
} else if !seen_before {
boot.insert(
prefix.as_slice(),
encode_boot(BootState::Pending { first_seen: n }).as_slice(),
)?;
pending.insert(prefix.as_slice(), n)?;
fresh_here.push(slot);
}
if self.config.full_detail {
for ch in &sc.changes {
slots.insert(
slot_key(*addr, slot, n, ch.block_access_index).as_slice(),
encode_value(provenance, ch.value_b256()).as_slice(),
)?;
written += 1;
}
} else {
let ch = sc.final_change();
slots.insert(
slot_key(*addr, slot, n, ch.block_access_index).as_slice(),
encode_value(provenance, ch.value_b256()).as_slice(),
)?;
written += 1;
}
idx.insert(blockidx_key(*addr, n, slot).as_slice(), ())?;
}
if !fresh_here.is_empty() {
fresh.push((*addr, *start, fresh_here));
}
}
let mut hashes = txn.open_table(HASHES)?;
hashes.insert(n, header_bytes(header.hash, header.state_root).as_slice())?;
if let Some(below) = prune_hashes_below {
let old: Vec<u64> = hashes
.range(..below)?
.map(|r| r.map(|(k, _)| k.value()))
.collect::<std::result::Result<_, _>>()?;
for b in old {
hashes.remove(b)?;
}
}
let mut meta = txn.open_table(META)?;
meta.insert(META_HEAD, head_bytes(n, header.hash).as_slice())?;
}
txn.commit()?;
Ok((fresh, written))
}
}
pub(crate) fn creation_in(acc: &bal_codec::AccountChanges) -> bool {
acc.code_changes
.iter()
.any(|c| !c.new_code.is_empty() && !c.new_code.starts_with(&[0xef, 0x01, 0x00]))
}
pub(crate) fn settle_created(
boot: &mut redb::Table<'_, &[u8], &[u8]>,
pending: &mut redb::Table<'_, &[u8], u64>,
addr: Address,
) -> Result<()> {
for k in collect_prefix_keys(boot, addr.as_slice())? {
boot.insert(k.as_slice(), encode_boot(BootState::Done).as_slice())?;
}
for k in collect_prefix_keys(pending, addr.as_slice())? {
pending.remove(k.as_slice())?;
}
Ok(())
}
pub(crate) fn anchor_key(addr: Address) -> String {
format!("{META_ANCHOR}{addr}")
}
fn head_bytes(block: u64, hash: B256) -> [u8; 40] {
let mut b = [0u8; 40];
b[..8].copy_from_slice(&block.to_be_bytes());
b[8..].copy_from_slice(hash.as_slice());
b
}
fn header_bytes(hash: B256, state_root: B256) -> [u8; 64] {
let mut b = [0u8; 64];
b[..32].copy_from_slice(hash.as_slice());
b[32..].copy_from_slice(state_root.as_slice());
b
}
fn bounds<'a>(lo: &'a [u8], hi: Option<&'a [u8]>) -> impl RangeBounds<&'a [u8]> {
(
Bound::Included(lo),
match hi {
Some(h) => Bound::Excluded(h),
None => Bound::Unbounded,
},
)
}
pub(crate) fn collect_prefix_keys<V: redb::Value + 'static>(
t: &impl ReadableTable<&'static [u8], V>,
prefix: &[u8],
) -> Result<Vec<Vec<u8>>> {
let hi = prefix_end(prefix);
let mut out = Vec::new();
for item in t.range::<&[u8]>(bounds(prefix, hi.as_deref()))? {
let (k, _) = item?;
out.push(k.value().to_vec());
}
Ok(out)
}