Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8596149
Repair corpus test type boundaries
metaphorics Oct 27, 2026
07001f1
Support indexed corpus custody in comparator
metaphorics Oct 27, 2026
6eeaaa8
Add validated offline block importer
metaphorics Oct 27, 2026
904fe1b
Accept produced corpus manifests in offline import
metaphorics Oct 27, 2026
bb4ef14
Cover corpus archive boundary rejection
metaphorics Oct 27, 2026
1921a81
Type and lint corpus boundary tests
metaphorics Oct 27, 2026
fa81e61
Bind offline imports to validated archive history
metaphorics Oct 27, 2026
a681e44
Build verified indexes from Core block files
metaphorics Oct 27, 2026
8e1f0ec
Verify indexed corpus custody and frame boundaries
metaphorics Oct 27, 2026
797a0d6
Name typed reads in embedding errors
metaphorics Oct 27, 2026
2785fd8
Format offline import bindings
metaphorics Oct 27, 2026
4aa6694
Bound the corpus index read before buffering
gormes-agent[bot] Oct 28, 2026
acda6b5
Refuse non-hex manifest magic and bound the manifest read
gormes-agent[bot] Oct 28, 2026
fae5a35
Stream corpus index pages and separate spool scratch
metaphorics Oct 28, 2026
46c7f42
Merge main into stack/08-corpus-offline
metaphorics Oct 28, 2026
deb16cd
Merge remote-tracking branch 'origin/main' into stack/08-corpus-offline
metaphorics Oct 28, 2026
0b75b00
Adapt merged sync tests to branch ownership API
metaphorics Oct 28, 2026
21fc9fe
corpus: treat zero-padded Core tail as clean EOF, fail closed on obfu…
metaphorics Oct 28, 2026
e51bd9e
corpus: harden indexed-source custody against symlink swap and drift
metaphorics Oct 28, 2026
b490881
corpus: pack archives from the verified index with digest binding
metaphorics Oct 28, 2026
33c9c86
corpus: focused tests for offline scan hardening
metaphorics Oct 28, 2026
985b478
Merge remote-tracking branch 'origin/main' into stack/08-corpus-offline
metaphorics Oct 28, 2026
1c8c5d8
portable: drop duplicated export paging helpers from merge
metaphorics Oct 28, 2026
0e4a10d
offline import: relieve journal retention pressure inline with one retry
metaphorics Oct 28, 2026
520fbfc
offline import: gate resume skip on full-validation provenance
metaphorics Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions bin/bitcoin-rs/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ macro_rules! emit_cli {
pub(crate) config: Option<PathBuf>,
#[arg(long = "bitcoin-conf")]
pub(crate) bitcoin_conf: Option<PathBuf>,
/// Single regular Core-framed archive file for foreground offline import.
///
/// Meta flag: never reaches `UserConfig`. All three import flags
/// must be present together to run the offline import; otherwise
/// the daemon runs. The file must be the custody archive pin
/// whose bytes equal the indexed logical stream by parity.
#[arg(long)]
pub(crate) corpus: Option<PathBuf>,
/// Archive manifest binding the corpus identity and stop tip.
///
/// Meta flag: never reaches `UserConfig`. Required with
/// `--corpus` and `--state` for the offline import.
#[arg(long)]
pub(crate) manifest: Option<PathBuf>,
/// Output certified-state JSON for the offline import.
///
/// Meta flag: never reaches `UserConfig`. Required with
/// `--corpus` and `--manifest` for the offline import.
#[arg(long)]
pub(crate) state: Option<PathBuf>,
$(
$(#[$fdoc])*
$($fcli)*
Expand Down
18 changes: 16 additions & 2 deletions bin/bitcoin-rs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,22 @@ fn main() -> ExitCode {
Ok(cli) => cli,
Err(error) => error.exit(),
};
let result = config::resolve(cli, std::env::vars_os())
.and_then(|config| bitcoin_rs_node::run(config, bitcoin_rs_node::RuntimeInputs::default()));
// Foreground offline import shares the timed `{corpus_path}` contract:
// one regular file, never the indexed `sources` directory. All three
// flags must appear together; a partial set is a caller error, not a
// daemon run.
let import = (cli.corpus.clone(), cli.manifest.clone(), cli.state.clone());
let result = config::resolve(cli, std::env::vars_os()).and_then(|config| match import {
(Some(corpus), Some(manifest), Some(state)) => {
bitcoin_rs_node::offline_import::run_offline_import(config, &corpus, &manifest, &state)
}
(None, None, None) => {
bitcoin_rs_node::run(config, bitcoin_rs_node::RuntimeInputs::default())
}
_ => Err(anyhow::anyhow!(
"--corpus, --manifest, and --state must be given together"
)),
});
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
Expand Down
5 changes: 2 additions & 3 deletions crates/index/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ pub use filters::BasicFilter;
pub use prepared::{PreparedBatch, PreparedBatchLimits, PreparedBlock};
pub use reader::Indexer;
pub use rows::IndexRowCounts;
#[cfg(test)]
pub(crate) use snapshot::RowVisitor;
pub use snapshot::{
IndexReader, ScriptLiveScan, TxIndexFamily, TxIndexScan, TxIndexScanRow, TxIndexSnapshot,
IndexReader, ScriptLiveScan, TxIndexFamily, TxIndexPage, TxIndexScan, TxIndexScanRow,
TxIndexSnapshot,
};
pub use state::{ConsumerCursorUpdate, IndexWriteFence};
pub use write::IndexWriter;
Expand Down
77 changes: 52 additions & 25 deletions crates/index/src/index/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@ use crate::{
};
use bitcoin_rs_primitives::{OutPoint, Txid};
use bitcoin_rs_storage::{ColumnFamily, KvSnapshot, KvStore, PrefixScanLimit};
use std::ops::ControlFlow;

/// Per-row visitor of [`TxIndexSnapshot::for_each_row`]: the parsed key, the
/// raw value, and [`ControlFlow::Break`] to stop the stream early.
pub(crate) type RowVisitor<'a> =
dyn FnMut(HashPrefixRow, &[u8]) -> Result<ControlFlow<()>, IndexError> + 'a;

/// One persisted `TxIndex` row family that [`TxIndexSnapshot::for_each_row`] streams.
/// One persisted `TxIndex` row family that [`TxIndexSnapshot::scan_family_page`]
/// pages.
///
/// `ScriptHistory` occurrences are intentionally absent: that family has no
/// qualified portable layout yet, and a variant is added only with its codec.
Expand Down Expand Up @@ -56,6 +51,17 @@ pub struct TxIndexScan {
pub complete: bool,
}

/// One bounded page of a `TxIndex` family in ascending stored-key order.
#[derive(Debug)]
pub struct TxIndexPage {
/// Parsed rows of this page in ascending stored-key order.
pub rows: Vec<TxIndexScanRow>,
/// Whether the family ends with this page. With a positive `max_rows`
/// limit, a page that is not complete is always non-empty, so callers
/// advance with the last returned key until `complete`.
pub complete: bool,
}

/// Result of one bounded `ScriptLive` prefix scan.
#[derive(Debug)]
pub struct ScriptLiveScan {
Expand All @@ -78,18 +84,19 @@ pub trait TxIndexSnapshot: Send + Sync {
&self,
capability: IndexCapability,
) -> Result<Option<IndexWatermark>, IndexError>;
/// Streams every row of `family` in ascending stored-key order from this
/// snapshot, one row at a time.
/// Scans one bounded page of `family` in ascending stored-key order.
///
/// There is no seek, paging cursor, or family-sized vector: `visit`
/// receives each parsed key and its raw value and answers
/// [`ControlFlow::Break`] to stop early. A malformed key or a storage
/// failure ends the stream with an error.
fn for_each_row(
/// `start_after = None` starts at the family head; otherwise the page
/// begins at the first key strictly above `start_after`. Backends seek
/// the snapshot to the cursor, so no family-sized copy is made on any
/// backend. A malformed key or a storage failure ends the page with an
/// error.
fn scan_family_page(
&self,
family: TxIndexFamily,
visit: &mut RowVisitor<'_>,
) -> Result<(), IndexError>;
start_after: Option<&[u8]>,
limit: PrefixScanLimit,
) -> Result<TxIndexPage, IndexError>;
/// Loads one capability's coverage floor: the first height its committed
/// rows cover. `0` means complete coverage from genesis.
fn capability_floor(&self, capability: IndexCapability) -> Result<u32, IndexError> {
Expand Down Expand Up @@ -169,20 +176,40 @@ impl TxIndexSnapshot for StoreTxIndexSnapshot<'_> {
crate::index::capability::read_coverage_floor(self.snapshot.as_ref(), capability)
}

fn for_each_row(
fn scan_family_page(
&self,
family: TxIndexFamily,
visit: &mut RowVisitor<'_>,
) -> Result<(), IndexError> {
for item in self.snapshot.iter_prefix(family.column_family(), &[])? {
let (key, value) = item?;
start_after: Option<&[u8]>,
limit: PrefixScanLimit,
) -> Result<TxIndexPage, IndexError> {
// Resume after `cursor` with `cursor || 0x00`: stored keys are
// fixed-width, so the suffixed key is the least key strictly above
// `cursor`. No `start_after` begins at the family head.
let start: Vec<u8> = match start_after {
Some(cursor) => {
let mut start = Vec::with_capacity(cursor.len().saturating_add(1));
start.extend_from_slice(cursor);
start.push(0);
start
}
None => Vec::new(),
};
let scan =
self.snapshot
.scan_prefix_from_bounded(family.column_family(), &[], &start, limit)?;
let mut rows = Vec::with_capacity(scan.rows.len());
for (key, value) in scan.rows {
if key.len() != crate::HASH_PREFIX_ROW_SIZE {
return Err(IndexError::InvalidPrefixRowLength { len: key.len() });
}
let row = zerocopy::FromBytes::read_from_bytes(&key)
.map_err(|_| IndexError::InvalidPrefixRowLength { len: key.len() })?;
if visit(row, &value)?.is_break() {
break;
}
rows.push(TxIndexScanRow { row, value });
}
Ok(())
Ok(TxIndexPage {
rows,
complete: scan.complete,
})
}

fn transaction_rows(
Expand Down
2 changes: 1 addition & 1 deletion crates/index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub use index::{
BasicFilter, ConsumerCursorUpdate, IndexCapabilities, IndexCapability, IndexError, IndexReader,
IndexRowCounts, IndexWatermark, IndexWatermarks, IndexWriteFence, IndexWriter, Indexer,
MAX_LIVE_SCRIPT_SIZE, PreparedBatch, PreparedBatchLimits, PreparedBlock, ScriptLiveScan,
SpentCoinScripts, TxIndexFamily, TxIndexScan, TxIndexScanRow, TxIndexSnapshot,
SpentCoinScripts, TxIndexFamily, TxIndexPage, TxIndexScan, TxIndexScanRow, TxIndexSnapshot,
};
pub use query_api::{
CompactFilterQuery, DerivedIndexInfo, DerivedIndexQuery, RollbackWarningSource,
Expand Down
2 changes: 1 addition & 1 deletion crates/index/src/portable/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ impl<'a> BodyReader<'a> {
}
}

fn decode(bytes: &[u8], expected_len: u32) -> Located {
pub(super) fn decode(bytes: &[u8], expected_len: u32) -> Located {
if usize::try_from(expected_len) != Ok(bytes.len()) {
return Located::Mismatch;
}
Expand Down
Loading
Loading