1#![doc = include_str!("../README.md")]
2mod backfill;
16mod keys;
17mod reads;
18mod sync;
19mod writes;
20
21pub use backfill::{BackfillOpts, BackfillReport, BackfillStop};
22pub use keys::{BootState, Provenance, OLDEST_UPGRADABLE, SCHEMA_VERSION};
23pub use sync::{SyncReport, REORG_HORIZON_FALLBACK, TOUCHED_CAP};
24
25use alloy_primitives::{Address, B256};
26use bal_codec::BlockAccessIndex;
27use keys::*;
28use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
29use std::ops::{Bound, Range, RangeBounds};
30use std::path::Path;
31use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
32use std::sync::Mutex;
33
34pub(crate) const SLOTS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("slots");
35pub(crate) const BLOCKIDX: TableDefinition<&[u8], &[u8]> = TableDefinition::new("blockslots");
38const LEGACY_BLOCKIDX: TableDefinition<&[u8], ()> = TableDefinition::new("blockidx");
40pub(crate) const META: TableDefinition<&str, &[u8]> = TableDefinition::new("meta");
41pub(crate) const WATCH: TableDefinition<&[u8], u64> = TableDefinition::new("watch");
42const HASHES: TableDefinition<u64, &[u8]> = TableDefinition::new("blockhashes");
44pub(crate) const BOOT: TableDefinition<&[u8], &[u8]> = TableDefinition::new("bootstrap");
45pub(crate) const PENDING: TableDefinition<&[u8], u64> = TableDefinition::new("pending");
48pub(crate) const CREATED: TableDefinition<&[u8], u64> = TableDefinition::new("created");
53
54const META_SCHEMA: &str = "schema_version";
55const META_ANCHOR: &str = "anchor:";
58const META_HEAD: &str = "head";
59const META_FULL_DETAIL: &str = "full_detail";
60
61#[derive(Debug, thiserror::Error)]
64pub enum ArchiveError {
65 #[error("db: {0}")]
67 Db(Box<redb::Error>),
68 #[error("schema version {found} on disk, this build speaks {expected}")]
70 SchemaMismatch {
71 found: u32,
73 expected: u32,
75 },
76 #[error("from_block must be >= 1 (got {0})")]
78 InvalidStart(u64),
79 #[error("watch from block {from_block} is in the past (head {head}); use backfill for history before the head")]
82 StartInPast {
83 from_block: u64,
85 head: u64,
87 },
88 #[error("address {0} is not watched")]
90 NotWatched(Address),
91 #[error(
93 "address {address} is already watched from block {from_block}; unwatch first to change it"
94 )]
95 AlreadyWatched {
96 address: Address,
98 from_block: u64,
100 },
101 #[error("a sync pass is already running on this archive")]
103 SyncInProgress,
104 #[error("archive was created with {option} = {on_disk}, opened with {requested}")]
106 ConfigMismatch {
107 option: &'static str,
109 on_disk: String,
111 requested: String,
113 },
114 #[error("archive head {head} is below watch start {start}; nothing to bootstrap yet")]
116 HeadBelowStart {
117 head: u64,
119 start: u64,
121 },
122 #[error("source: {0}")]
124 Source(#[from] bal_source::SourceError),
125 #[error("block {block} failed BAL verification: {err}")]
127 Verification {
128 block: u64,
130 err: bal_codec::CodecError,
132 },
133 #[error(
135 "block {0} header carries no block_access_list_hash; refusing to apply unverifiable data"
136 )]
137 NoBalHash(u64),
138 #[error("reorg deeper than retained block hashes (fork below block {0})")]
141 ReorgBeyondHorizon(u64),
142 #[error(
145 "source is inconsistent around block {0}: parent hash does not match its own block {0}-1"
146 )]
147 InconsistentSource(u64),
148 #[error("block {0} on the node is not the block the archive holds; run sync first")]
152 StartReplaced(u64),
153 #[error("proof: {0}")]
155 Proof(#[from] bal_source::ProofError),
156 #[error("corrupt record: {0}")]
158 Corrupt(&'static str),
159}
160
161macro_rules! from_redb {
162 ($($t:ty),*) => {$(
163 impl From<$t> for ArchiveError {
164 fn from(e: $t) -> Self { ArchiveError::Db(Box::new(e.into())) }
165 }
166 )*};
167}
168from_redb!(
169 redb::Error,
170 redb::DatabaseError,
171 redb::TransactionError,
172 redb::TableError,
173 redb::StorageError,
174 redb::CommitError,
175 redb::CompactionError
176);
177
178pub type Result<T> = std::result::Result<T, ArchiveError>;
180
181pub(crate) type FreshSlots = Vec<(Address, u64, Vec<B256>)>;
183
184#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
187pub enum NotAvailable {
188 #[error("address {0} is not watched")]
190 NotWatched(Address),
191 #[error("block {requested} is before watch start {start}")]
193 BeforeStart {
194 requested: u64,
196 start: u64,
198 },
199 #[error("block {requested} is after archive head {head}")]
201 AfterHead {
202 requested: u64,
204 head: u64,
206 },
207 #[error("archive has not synced any block yet")]
209 NotSynced,
210 #[error("invalid block range {start}..{end}")]
212 InvalidRange {
213 start: u64,
215 end: u64,
217 },
218 #[error("no change to this slot is recorded since the watch start; backfill to the contract's creation, or prove it at the head")]
223 NeverRecorded,
224 #[error("no record before block {first_seen} (the slot's earliest recorded change); backfill further back")]
228 UnknownBefore {
229 first_seen: u64,
231 },
232 #[error("internal: {0}")]
234 Internal(String),
235}
236
237impl From<ArchiveError> for NotAvailable {
238 fn from(e: ArchiveError) -> Self {
239 NotAvailable::Internal(e.to_string())
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct StorageValue {
246 pub value: B256,
248 pub provenance: Provenance,
250 pub set_at: u64,
253 pub index: BlockAccessIndex,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct HistoryEntry {
260 pub block: u64,
262 pub index: BlockAccessIndex,
264 pub value: B256,
266 pub provenance: Provenance,
268}
269
270#[derive(Debug, Clone)]
272pub struct ArchiveStats {
273 pub head: Option<(u64, B256)>,
275 pub watches: Vec<(Address, u64)>,
277 pub created: Vec<(Address, u64)>,
280 pub slot_records: u64,
282 pub slots_done: u64,
284 pub slots_pending: u64,
286 pub slots_lost: u64,
288 pub retained_headers: u64,
290 pub file_bytes: u64,
292}
293
294#[derive(Debug, Clone)]
296pub struct ArchiveConfig {
297 pub bootstrap_window: u64,
300 pub full_detail: bool,
303 pub allow_unverified: bool,
306}
307
308impl Default for ArchiveConfig {
309 fn default() -> Self {
310 Self {
311 bootstrap_window: 120,
312 full_detail: false,
313 allow_unverified: false,
314 }
315 }
316}
317
318pub struct Archive {
321 db: Database,
322 path: std::path::PathBuf,
323 config: ArchiveConfig,
324 watch_gate: Mutex<()>,
327 in_flight: AtomicU64,
331 syncing: AtomicBool,
333}
334
335impl Archive {
336 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
338 Self::open_with(path, ArchiveConfig::default())
339 }
340
341 pub fn open_with(path: impl AsRef<Path>, config: ArchiveConfig) -> Result<Self> {
344 let path_buf = path.as_ref().to_path_buf();
345 let db = Database::create(&path_buf)?;
346 let txn = db.begin_write()?;
347 {
348 txn.open_table(SLOTS)?;
349 txn.open_table(BLOCKIDX)?;
350 txn.open_table(WATCH)?;
351 txn.open_table(HASHES)?;
352 txn.open_table(CREATED)?;
353 let boot = txn.open_table(BOOT)?;
354 let mut pending = txn.open_table(PENDING)?;
355 let mut meta = txn.open_table(META)?;
356 let found: Option<Vec<u8>> = meta.get(META_SCHEMA)?.map(|v| v.value().to_vec());
357 match found {
358 Some(v) => {
359 let found = u32::from_be_bytes(
360 v.as_slice()
361 .try_into()
362 .map_err(|_| ArchiveError::Corrupt("schema_version"))?,
363 );
364 if (OLDEST_UPGRADABLE..SCHEMA_VERSION).contains(&found) {
365 if found < 3 {
366 migrate_block_index(&txn)?;
367 }
368 meta.insert(META_SCHEMA, SCHEMA_VERSION.to_be_bytes().as_slice())?;
371 } else if found != SCHEMA_VERSION {
372 return Err(ArchiveError::SchemaMismatch {
373 found,
374 expected: SCHEMA_VERSION,
375 });
376 }
377 }
378 None => {
379 meta.insert(META_SCHEMA, SCHEMA_VERSION.to_be_bytes().as_slice())?;
380 }
381 }
382 let stored_detail = meta.get(META_FULL_DETAIL)?.map(|v| v.value().to_vec());
384 match stored_detail {
385 Some(v) => {
386 let on_disk = v.first().copied().unwrap_or(0) != 0;
387 if on_disk != config.full_detail {
388 return Err(ArchiveError::ConfigMismatch {
389 option: "full_detail",
390 on_disk: on_disk.to_string(),
391 requested: config.full_detail.to_string(),
392 });
393 }
394 }
395 None => {
396 meta.insert(META_FULL_DETAIL, [config.full_detail as u8].as_slice())?;
397 }
398 }
399 if pending.is_empty()? {
401 let mut rebuilt = Vec::new();
402 for item in boot.iter()? {
403 let (k, v) = item?;
404 if let Some(BootState::Pending { first_seen }) = decode_boot(v.value()) {
405 rebuilt.push((k.value().to_vec(), first_seen));
406 }
407 }
408 for (k, f) in rebuilt {
409 pending.insert(k.as_slice(), f)?;
410 }
411 }
412 }
413 txn.commit()?;
414 Ok(Self {
415 db,
416 path: path_buf,
417 config,
418 watch_gate: Mutex::new(()),
419 in_flight: AtomicU64::new(0),
420 syncing: AtomicBool::new(false),
421 })
422 }
423
424 pub(crate) fn begin_sync(&self) -> bool {
426 self.syncing
427 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
428 .is_ok()
429 }
430
431 pub fn config(&self) -> &ArchiveConfig {
433 &self.config
434 }
435
436 pub fn watch(&self, addr: Address, from_block: u64) -> Result<()> {
444 if from_block == 0 {
445 return Err(ArchiveError::InvalidStart(from_block));
446 }
447 let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
448 if let Some(existing) = self.start_of(addr)? {
449 if existing == from_block {
450 return Ok(());
451 }
452 return Err(ArchiveError::AlreadyWatched {
453 address: addr,
454 from_block: existing,
455 });
456 }
457 let head = self.head()?.map(|(h, _)| h).unwrap_or(0);
458 let floor = head.max(self.in_flight.load(Ordering::SeqCst));
459 if from_block <= floor {
460 return Err(ArchiveError::StartInPast {
461 from_block,
462 head: floor,
463 });
464 }
465 let txn = self.db.begin_write()?;
466 txn.open_table(WATCH)?.insert(addr.as_slice(), from_block)?;
467 txn.commit()?;
468 Ok(())
469 }
470
471 pub fn unwatch(&self, addr: Address) -> Result<()> {
476 let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
477 let txn = self.db.begin_write()?;
478 {
479 txn.open_table(WATCH)?.remove(addr.as_slice())?;
480 let mut slots = txn.open_table(SLOTS)?;
481 for k in collect_prefix_keys(&slots, addr.as_slice())? {
482 slots.remove(k.as_slice())?;
483 }
484 let mut boot = txn.open_table(BOOT)?;
485 for k in collect_prefix_keys(&boot, addr.as_slice())? {
486 boot.remove(k.as_slice())?;
487 }
488 let mut pending = txn.open_table(PENDING)?;
489 for k in collect_prefix_keys(&pending, addr.as_slice())? {
490 pending.remove(k.as_slice())?;
491 }
492 let mut idx = txn.open_table(BLOCKIDX)?;
493 for k in collect_prefix_keys(&idx, addr.as_slice())? {
494 idx.remove(k.as_slice())?;
495 }
496 txn.open_table(CREATED)?.remove(addr.as_slice())?;
497 txn.open_table(META)?.remove(anchor_key(addr).as_str())?;
498 }
499 txn.commit()?;
500 Ok(())
501 }
502
503 pub fn compact_file(path: impl AsRef<Path>) -> Result<bool> {
509 let mut db = Database::open(path.as_ref())?;
510 Ok(db.compact()?)
511 }
512
513 pub fn created_at(&self, addr: Address) -> Result<Option<u64>> {
515 let rtx = self.db.begin_read()?;
516 let t = rtx.open_table(CREATED)?;
517 Ok(t.get(addr.as_slice())?.map(|v| v.value()))
518 }
519
520 pub fn watchlist(&self) -> Result<Vec<(Address, u64)>> {
522 let rtx = self.db.begin_read()?;
523 let t = rtx.open_table(WATCH)?;
524 let mut out = Vec::new();
525 for item in t.iter()? {
526 let (k, v) = item?;
527 out.push((Address::from_slice(k.value()), v.value()));
528 }
529 Ok(out)
530 }
531
532 pub(crate) fn watchlist_for(&self, block: u64) -> Result<Vec<(Address, u64)>> {
535 let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
536 self.in_flight.store(block, Ordering::SeqCst);
537 self.watchlist()
538 }
539
540 pub(crate) fn claim_start(&self) -> Result<Option<u64>> {
546 let _gate = self.watch_gate.lock().unwrap_or_else(|p| p.into_inner());
547 let earliest = self.watchlist()?.iter().map(|(_, s)| *s).min();
548 let Some(earliest) = earliest else {
549 return Ok(None);
550 };
551 let next = match self.head()? {
552 Some((h, _)) => h + 1,
553 None => earliest,
554 };
555 self.in_flight.store(next, Ordering::SeqCst);
556 Ok(Some(next))
557 }
558
559 pub(crate) fn sync_idle(&self) {
562 self.in_flight.store(0, Ordering::SeqCst);
563 self.syncing.store(false, Ordering::SeqCst);
564 }
565
566 pub(crate) fn start_of(&self, addr: Address) -> Result<Option<u64>> {
567 let rtx = self.db.begin_read()?;
568 let t = rtx.open_table(WATCH)?;
569 Ok(t.get(addr.as_slice())?.map(|v| v.value()))
570 }
571
572 pub fn stats(&self) -> Result<ArchiveStats> {
578 let rtx = self.db.begin_read()?;
579 let slot_records = rtx.open_table(SLOTS)?.len()?;
580 let pending = rtx.open_table(PENDING)?.len()?;
581 let boot = rtx.open_table(BOOT)?;
582 let (mut done, mut lost) = (0u64, 0u64);
583 for item in boot.iter()? {
584 let (_, v) = item?;
585 match decode_boot(v.value()) {
586 Some(BootState::Done) => done += 1,
587 Some(BootState::Lost { .. }) => lost += 1,
588 _ => {}
589 }
590 }
591 let retained_headers = rtx.open_table(HASHES)?.len()?;
592 let file_bytes = std::fs::metadata(&self.path).map(|m| m.len()).unwrap_or(0);
593 let mut created = Vec::new();
594 for item in rtx.open_table(CREATED)?.iter()? {
595 let (k, v) = item?;
596 created.push((Address::from_slice(k.value()), v.value()));
597 }
598 Ok(ArchiveStats {
599 head: self.head()?,
600 watches: self.watchlist()?,
601 created,
602 slot_records,
603 slots_done: done,
604 slots_pending: pending,
605 slots_lost: lost,
606 retained_headers,
607 file_bytes,
608 })
609 }
610
611 pub fn head(&self) -> Result<Option<(u64, B256)>> {
613 let rtx = self.db.begin_read()?;
614 let t = rtx.open_table(META)?;
615 match t.get(META_HEAD)? {
616 None => Ok(None),
617 Some(v) => {
618 let b = v.value();
619 if b.len() != 40 {
620 return Err(ArchiveError::Corrupt("head"));
621 }
622 let num: [u8; 8] = b[..8]
623 .try_into()
624 .map_err(|_| ArchiveError::Corrupt("head"))?;
625 Ok(Some((u64::from_be_bytes(num), B256::from_slice(&b[8..]))))
626 }
627 }
628 }
629
630 pub(crate) fn header_at(&self, block: u64) -> Result<Option<(B256, B256)>> {
632 let rtx = self.db.begin_read()?;
633 let t = rtx.open_table(HASHES)?;
634 match t.get(block)? {
635 None => Ok(None),
636 Some(v) => {
637 let b = v.value();
638 if b.len() != 64 {
639 return Err(ArchiveError::Corrupt("blockhashes"));
640 }
641 Ok(Some((
642 B256::from_slice(&b[..32]),
643 B256::from_slice(&b[32..]),
644 )))
645 }
646 }
647 }
648
649 pub(crate) fn remember_header(&self, block: u64, hash: B256, state_root: B256) -> Result<()> {
652 let txn = self.db.begin_write()?;
653 {
654 let mut hashes = txn.open_table(HASHES)?;
655 if hashes.get(block)?.is_none() {
656 hashes.insert(block, header_bytes(hash, state_root).as_slice())?;
657 }
658 }
659 txn.commit()?;
660 Ok(())
661 }
662}
663
664pub(crate) fn creation_in(acc: &bal_codec::AccountChanges) -> bool {
670 acc.code_changes
671 .iter()
672 .any(|c| !c.new_code.is_empty() && !c.new_code.starts_with(&[0xef, 0x01, 0x00]))
673}
674
675pub(crate) fn settle_created(
678 boot: &mut redb::Table<'_, &[u8], &[u8]>,
679 pending: &mut redb::Table<'_, &[u8], u64>,
680 addr: Address,
681) -> Result<()> {
682 for k in collect_prefix_keys(boot, addr.as_slice())? {
683 boot.insert(k.as_slice(), encode_boot(BootState::Done).as_slice())?;
684 }
685 for k in collect_prefix_keys(pending, addr.as_slice())? {
686 pending.remove(k.as_slice())?;
687 }
688 Ok(())
689}
690
691fn migrate_block_index(txn: &redb::WriteTransaction) -> Result<()> {
695 let mut grouped: std::collections::BTreeMap<(Address, u64), Vec<B256>> =
696 std::collections::BTreeMap::new();
697 {
698 let old = txn.open_table(LEGACY_BLOCKIDX)?;
699 for item in old.iter()? {
700 let (k, _) = item?;
701 let (a, b, s) = parse_legacy_blockidx_key(k.value())
702 .ok_or(ArchiveError::Corrupt("legacy blockidx"))?;
703 grouped.entry((a, b)).or_default().push(s);
704 }
705 }
706 {
707 let mut new = txn.open_table(BLOCKIDX)?;
708 for ((a, b), slots) in &grouped {
709 new.insert(
710 blockidx_key(*a, *b).as_slice(),
711 encode_slots(slots).as_slice(),
712 )?;
713 }
714 }
715 txn.delete_table(LEGACY_BLOCKIDX)?;
716 Ok(())
717}
718
719pub(crate) fn anchor_key(addr: Address) -> String {
720 format!("{META_ANCHOR}{addr}")
721}
722
723fn head_bytes(block: u64, hash: B256) -> [u8; 40] {
724 let mut b = [0u8; 40];
725 b[..8].copy_from_slice(&block.to_be_bytes());
726 b[8..].copy_from_slice(hash.as_slice());
727 b
728}
729
730fn header_bytes(hash: B256, state_root: B256) -> [u8; 64] {
731 let mut b = [0u8; 64];
732 b[..32].copy_from_slice(hash.as_slice());
733 b[32..].copy_from_slice(state_root.as_slice());
734 b
735}
736
737fn bounds<'a>(lo: &'a [u8], hi: Option<&'a [u8]>) -> impl RangeBounds<&'a [u8]> {
739 (
740 Bound::Included(lo),
741 match hi {
742 Some(h) => Bound::Excluded(h),
743 None => Bound::Unbounded,
744 },
745 )
746}
747
748pub(crate) fn collect_prefix_keys<V: redb::Value + 'static>(
750 t: &impl ReadableTable<&'static [u8], V>,
751 prefix: &[u8],
752) -> Result<Vec<Vec<u8>>> {
753 let hi = prefix_end(prefix);
754 let mut out = Vec::new();
755 for item in t.range::<&[u8]>(bounds(prefix, hi.as_deref()))? {
756 let (k, _) = item?;
757 out.push(k.value().to_vec());
758 }
759 Ok(out)
760}