diff --git a/Cargo.lock b/Cargo.lock index b26a2d6fb0..c2f0819795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -909,9 +909,7 @@ dependencies = [ "bitcoin", "bitcoin_hashes 0.14.101", "mutants", - "rbitcoin-log", "rbitcoin-primitives", - "rbitcoin-query", "rbitcoin-store", "ruzstd", "serde_json", @@ -1019,6 +1017,9 @@ dependencies = [ [[package]] name = "rbitcoin-primitives" version = "0.8.99" +dependencies = [ + "bitcoin_hashes 0.14.101", +] [[package]] name = "rbitcoin-query" @@ -1027,6 +1028,7 @@ dependencies = [ "arc-swap", "bitcoin", "mutants", + "rbitcoin-consensus", "rbitcoin-log", "rbitcoin-primitives", "rbitcoin-store", diff --git a/changelog.d/consensus-off-store.md b/changelog.d/consensus-off-store.md new file mode 100644 index 0000000000..fbfa6207c4 --- /dev/null +++ b/changelog.d/consensus-off-store.md @@ -0,0 +1,4 @@ +Changed + +- **Consensus rules no longer open the store.** `rbitcoin-consensus` decides header, block, and transaction accepts and rejects from bytes and caller facts. `rbitcoin-query` loads rows, writes Class A, Class C, and spend annotations, and calls those functions. A disk, io_uring, or cancel fault stays a pipeline error and is not cached as an invalid block. +- **Electrum opens after the follow header round.** A block mined while the scripthash index materializes is connected and indexed before the wallet port opens. diff --git a/crates/rbitcoin-consensus/AGENTS.md b/crates/rbitcoin-consensus/AGENTS.md index 7e6da2c2ad..0d8c18ec4b 100644 --- a/crates/rbitcoin-consensus/AGENTS.md +++ b/crates/rbitcoin-consensus/AGENTS.md @@ -1,7 +1,8 @@ # rbitcoin-consensus -Header, block, and script validation. Depends on primitives, query, and store. -Peer and RPC script checks use the detached worker. The thread rule is +Header, block, and script rules from bytes and caller facts. Depends on +primitives. It does not open a store or write Class A. Peer and RPC script +checks use the detached worker. The thread rule is [`docs/concurrency.md`](../../docs/concurrency.md). ## Read first @@ -17,9 +18,9 @@ Open the row that matches the change. Leave the other owners closed. ## Where - Scripts: `src/script/` (`interpreter.rs` is the opcode match) -- Confirm: `src/confirm_run/` -- Headers and blocks: `src/header.rs`, `src/block/` +- Headers and blocks: `src/header.rs`, `src/block/` (`facts.rs` is the spend and assemble decisions) - Worker pool: `src/script_pool.rs` +- Confirm pipeline (lookup, load, pin, write, index): `rbitcoin-query` `src/confirm_run/` and `src/block_pipeline.rs` ## Verify diff --git a/crates/rbitcoin-consensus/Cargo.toml b/crates/rbitcoin-consensus/Cargo.toml index f29d058684..523600d270 100644 --- a/crates/rbitcoin-consensus/Cargo.toml +++ b/crates/rbitcoin-consensus/Cargo.toml @@ -9,9 +9,6 @@ description = "Block and transaction validation / confirmability" [dependencies] rbitcoin-primitives = { workspace = true } -rbitcoin-query = { workspace = true } -rbitcoin-store = { workspace = true } -rbitcoin-log = { workspace = true } arc-swap = { workspace = true } bitcoin = { workspace = true } bitcoin_hashes = { workspace = true } diff --git a/crates/rbitcoin-consensus/src/block/facts.rs b/crates/rbitcoin-consensus/src/block/facts.rs new file mode 100644 index 0000000000..c2b45fac85 --- /dev/null +++ b/crates/rbitcoin-consensus/src/block/facts.rs @@ -0,0 +1,318 @@ +//! Rule decisions the confirm pipeline used to construct inline. +//! +//! Query loads rows and then calls these. A missing store row stays a store +//! error in the caller. The `if` that chooses the reject lives here. + +use super::{exceeds_sigops_limit, is_final_tx, sequence_locks_satisfied}; +use crate::ConsensusError; +use bitcoin::block::Header; +use bitcoin::Transaction; + +pub fn reject_non_final( + tx: &Transaction, + block_height: u32, + lock_time_cutoff: u32, +) -> Result<(), ConsensusError> { + if is_final_tx(tx, block_height, lock_time_cutoff) { + Ok(()) + } else { + Err(ConsensusError::BadTx("bad-txns-nonfinal")) + } +} + +pub fn reject_tx_without_outputs(tx: &Transaction) -> Result<(), ConsensusError> { + if tx.output.is_empty() { + Err(ConsensusError::BadTx("no outputs")) + } else { + Ok(()) + } +} + +pub fn reject_tx_without_inputs(tx: &Transaction) -> Result<(), ConsensusError> { + if tx.input.is_empty() { + Err(ConsensusError::BadTx("no inputs")) + } else { + Ok(()) + } +} + +pub fn reject_assemble_block_shape( + n_tx: usize, + n_txid: usize, + n_archived_fk: Option, + first_is_coinbase: bool, +) -> Result<(), ConsensusError> { + if let Some(n_fk) = n_archived_fk { + if n_fk != n_tx { + return Err(ConsensusError::BadBlock("archived tx fk count mismatch")); + } + } + if n_txid != n_tx { + return Err(ConsensusError::BadBlock( + "invariant: create_txids length must match block.txdata (no assemble re-hash)", + )); + } + if n_tx == 0 { + return Err(ConsensusError::BadBlock("empty block")); + } + if !first_is_coinbase { + return Err(ConsensusError::BadBlock("first tx not coinbase")); + } + Ok(()) +} + +pub fn add_sigops(cost: u64, legacy: u64, input: u64) -> Result { + let next = cost + .checked_add(legacy) + .and_then(|c| c.checked_add(input)) + .ok_or(ConsensusError::BadBlock("bad-blk-sigops"))?; + if exceeds_sigops_limit(next) { + return Err(ConsensusError::BadBlock("bad-blk-sigops")); + } + Ok(next) +} + +pub fn fee_from_values(value_in: i64, value_out: i64) -> Result { + if value_out > value_in { + return Err(ConsensusError::BadTx("in < out")); + } + value_in + .checked_sub(value_out) + .ok_or(ConsensusError::BadTx("fee overflow")) +} + +pub fn fee_sat(fee: i64) -> Result { + u64::try_from(fee).map_err(|_| ConsensusError::BadTx("fee overflow")) +} + +pub fn add_fee(fees: i64, fee: i64) -> Result { + fees.checked_add(fee) + .ok_or(ConsensusError::BadTx("fee overflow")) +} + +pub fn add_value_in(value_in: i64, sat: u64) -> Result { + value_in + .checked_add(sat as i64) + .ok_or(ConsensusError::BadTx("value in overflow")) +} + +pub fn reject_genesis_coinbase_spend(is_genesis: bool) -> Result<(), ConsensusError> { + if is_genesis { + Err(ConsensusError::MissingPrevout) + } else { + Ok(()) + } +} + +pub fn reject_duplicate_input(inserted: bool) -> Result<(), ConsensusError> { + if inserted { + Ok(()) + } else { + Err(ConsensusError::BadTx("double spend in block")) + } +} + +/// Same-block prevout. A later create is not yet a coin. The coinbase of this +/// block is immature. +pub fn reject_same_block_create( + prev_index: usize, + spend_index: usize, +) -> Result<(), ConsensusError> { + if prev_index >= spend_index { + return Err(ConsensusError::MissingPrevout); + } + if prev_index == 0 { + return Err(ConsensusError::BadTx("coinbase immature")); + } + Ok(()) +} + +pub fn reject_unresolved_spend(missing: bool) -> Result<(), ConsensusError> { + if missing { + Err(ConsensusError::MissingPrevout) + } else { + Ok(()) + } +} + +pub fn reject_unspendable_prevout(unspendable: bool) -> Result<(), ConsensusError> { + if unspendable { + Err(ConsensusError::MissingPrevout) + } else { + Ok(()) + } +} + +pub fn reject_prevout_spent(spent: bool) -> Result<(), ConsensusError> { + if spent { + Err(ConsensusError::PrevoutSpent) + } else { + Ok(()) + } +} + +pub fn reject_spend_cursor(in_range: bool) -> Result<(), ConsensusError> { + if in_range { + Ok(()) + } else { + Err(ConsensusError::BadBlock( + "structural spends/tx input mismatch", + )) + } +} + +pub fn reject_csv_create_height(height: Option) -> Result { + height.ok_or(ConsensusError::BadTx("bad-txns-nonfinal")) +} + +pub fn reject_sequence_locks( + tx: &Transaction, + prev_heights: &[u32], + coin_mtps: &[u32], + block_height: u32, + prev_mtp: u32, +) -> Result<(), ConsensusError> { + if sequence_locks_satisfied(tx, prev_heights, coin_mtps, block_height, prev_mtp) { + Ok(()) + } else { + Err(ConsensusError::BadTx("bad-txns-nonfinal")) + } +} + +pub fn reject_witness_before_segwit( + has_witness: bool, + segwit_active: bool, +) -> Result<(), ConsensusError> { + if has_witness && !segwit_active { + Err(ConsensusError::BadBlock("unexpected witness before segwit")) + } else { + Ok(()) + } +} + +pub fn reject_empty_confirm_batch(empty: bool) -> Result<(), ConsensusError> { + if empty { + Err(ConsensusError::BadBlock("empty confirm batch")) + } else { + Ok(()) + } +} + +pub fn require_retarget_first( + on_boundary: bool, + no_retarget: bool, + first: Option, +) -> Result, ConsensusError> { + if on_boundary && !no_retarget && first.is_none() { + Err(ConsensusError::BadHeader("missing retarget first header")) + } else { + Ok(first) + } +} + +pub fn reject_confirm_batch(heights: &[u32]) -> Result<(), ConsensusError> { + if heights.is_empty() { + return Err(ConsensusError::BadBlock("empty confirm batch")); + } + for w in heights.windows(2) { + if w[1] != w[0].saturating_add(1) { + return Err(ConsensusError::BadBlock("confirm run not contiguous")); + } + } + Ok(()) +} + +pub fn reject_txid_count(n_tx: usize, n_txid: usize) -> Result<(), ConsensusError> { + if n_tx == n_txid { + Ok(()) + } else { + Err(ConsensusError::BadBlock("txid count mismatch")) + } +} + +pub fn require_linked_parent(row: Option) -> Result { + row.ok_or(ConsensusError::BadPrev) +} + +pub fn reject_prev_link(prev: &[u8; 32], expect: &[u8; 32]) -> Result<(), ConsensusError> { + if prev == expect { + Ok(()) + } else { + Err(ConsensusError::BadPrev) + } +} + +pub fn reject_carried_header_hash( + hash: &[u8; 32], + expect: &[u8; 32], +) -> Result<(), ConsensusError> { + if hash == expect { + Ok(()) + } else { + Err(ConsensusError::BadBlock("carried header hash mismatch")) + } +} + +pub fn reject_batch_starts_at(height: u32, path_lo: u32) -> Result<(), ConsensusError> { + if height == path_lo { + Ok(()) + } else { + Err(ConsensusError::BadPrev) + } +} + +/// A header window with no timestamps is not a median. +pub fn median_or_bad_header(times: &[u32]) -> Result { + crate::header::median_time_past_times(times) + .map_err(|_| ConsensusError::BadHeader("empty median time")) +} + +/// `None` from the work walk is a missing ancestor, which is `BadPrev`. +pub fn require_next_work( + bits: Option, +) -> Result { + bits.ok_or(ConsensusError::BadPrev) +} + +pub fn reject_vout_past_n_out(max_vout: u32, n_out: Option) -> Result<(), ConsensusError> { + if n_out.is_some_and(|n| u64::from(max_vout) >= n) { + Err(ConsensusError::MissingPrevout) + } else { + Ok(()) + } +} + +pub fn require_same_block_output( + tx: Option<&Transaction>, + vout: usize, +) -> Result<&bitcoin::TxOut, ConsensusError> { + let tx = tx.ok_or(ConsensusError::MissingPrevout)?; + tx.output.get(vout).ok_or(ConsensusError::MissingPrevout) +} + +/// Gap at or below the confirmed tip is a bad prev. Above the tip the caller +/// keeps the retryable load error. +pub fn header_gap_on_chain(height: u32, tip: u32) -> Result<(), ConsensusError> { + if height > tip { + Ok(()) + } else { + Err(ConsensusError::BadPrev) + } +} + +pub fn reject_bits( + header: &Header, + expected: bitcoin::CompactTarget, +) -> Result<(), ConsensusError> { + if header.bits == expected { + Ok(()) + } else { + Err(ConsensusError::BadHeader("incorrect proof of work bits")) + } +} + +/// Parent create_fk that the store could not resolve is a missing prevout, +/// not a corrupt row. +pub fn unresolved_parent_create() -> ConsensusError { + ConsensusError::MissingPrevout +} diff --git a/crates/rbitcoin-consensus/src/block/finality_tests.rs b/crates/rbitcoin-consensus/src/block/finality_tests.rs index 62f705ce7c..e0621e3dbd 100644 --- a/crates/rbitcoin-consensus/src/block/finality_tests.rs +++ b/crates/rbitcoin-consensus/src/block/finality_tests.rs @@ -141,79 +141,6 @@ fn bip68_height_zero_time_lock_uses_the_median() { 1, mtp.saturating_sub(1) )); - - // The median comes from the confirmed genesis header through - // `structural_bip68`, not from a caller-supplied MTP slice. - let (_dir, query) = rbitcoin_query::testutil::tiny_query_labeled("bip68-h0"); - let header_fk = query - .store() - .put_header(&rbitcoin_store::HeaderRecord { - timestamp: mtp, - hash: [0x11; 32], - ..Default::default() - }) - .unwrap(); - query - .store() - .confirmed - .set(rbitcoin_primitives::Height(0), header_fk) - .unwrap(); - query.store().rebuild_height_fence().unwrap(); - let params = Box::leak(Box::new(crate::params::ChainParams::regtest())); - let ctx = super::ValidationContext::at( - params, - rbitcoin_primitives::Height(1), - crate::milestone::Milestone::NONE, - ); - let create = rbitcoin_primitives::Fk(1); - let mut create_heights = rbitcoin_query::FkMap::default(); - create_heights.insert(create, 0); - let spends = [([0u8; 32], 0u32, rbitcoin_primitives::Fk::NULL, create, 0u32)]; - let block = height_zero_timelock_block(type_flag); - super::structural_bip68( - &query, - &block, - &ctx, - &spends, - &create_heights, - &mut rbitcoin_query::U32Map::default(), - ) - .expect("height 0 time lock uses the genesis median"); - let too_far = height_zero_timelock_block(type_flag | 1); - let err = super::structural_bip68( - &query, - &too_far, - &ctx, - &spends, - &create_heights, - &mut rbitcoin_query::U32Map::default(), - ) - .expect_err("512s relative to the genesis median is not final at height 1"); - assert!( - format!("{err}").contains("nonfinal"), - "structural path must apply the median, got {err}" - ); -} - -fn height_zero_timelock_block(sequence: u32) -> bitcoin::Block { - use bitcoin::block::{Header, Version}; - use bitcoin::hashes::Hash; - use bitcoin::{Block, BlockHash, CompactTarget, TxMerkleNode}; - let spend = bare_tx(2, LockTime::ZERO, Sequence::from_consensus(sequence)); - let coinbase = bare_tx(1, LockTime::ZERO, Sequence::MAX); - let mut block = Block { - header: Header { - version: Version::from_consensus(1), - prev_blockhash: BlockHash::from_byte_array([0; 32]), - merkle_root: TxMerkleNode::from_byte_array([0; 32]), - time: 1_231_006_505, - bits: CompactTarget::from_consensus(0x1d00_ffff), - nonce: 0, - }, - txdata: vec![coinbase, spend], - }; - block.header.merkle_root = block.compute_merkle_root().unwrap(); - block } /// Exactly 500_000_000 is a timestamp. One below the threshold is a height. @@ -237,168 +164,12 @@ fn bip68_height_type_ignores_zero_mtp() { assert!(!sequence_locks_satisfied(&tx, &[100], &[0], 109, 0)); } -fn seq_tx(seqs: &[u32]) -> Transaction { - Transaction { - version: bitcoin::transaction::Version(2), - lock_time: LockTime::ZERO, - input: seqs - .iter() - .map(|seq| TxIn { - previous_output: OutPoint::null(), - script_sig: ScriptBuf::new(), - sequence: Sequence::from_consensus(*seq), - witness: Witness::new(), - }) - .collect(), - output: vec![TxOut { - value: Amount::from_sat(1), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }], - } -} - -fn csv_block(txs: Vec) -> bitcoin::Block { - use bitcoin::block::{Header, Version}; - use bitcoin::hashes::Hash; - use bitcoin::{Block, BlockHash, CompactTarget, TxMerkleNode}; - let mut block = Block { - header: Header { - version: Version::from_consensus(1), - prev_blockhash: BlockHash::from_byte_array([0; 32]), - merkle_root: TxMerkleNode::from_byte_array([0; 32]), - time: 1_231_006_505, - bits: CompactTarget::from_consensus(0x1d00_ffff), - nonce: 0, - }, - txdata: txs, - }; - block.header.merkle_root = block.compute_merkle_root().unwrap(); - block -} - -/// Three two-input txs must connect. `si * n_in` false-rejects the third. -#[test] -fn structural_bip68_multi_input_block_connects() { - let params = Box::leak(Box::new(crate::params::ChainParams::regtest())); - let ctx = super::ValidationContext::at( - params, - rbitcoin_primitives::Height(1), - crate::milestone::Milestone::NONE, - ); - let (_dir, query) = rbitcoin_query::testutil::tiny_query_labeled("bip68-multi"); - let coinbase = bare_tx(1, LockTime::ZERO, Sequence::MAX); - let spend = |seq: u32| seq_tx(&[seq, seq]); - let block = csv_block(vec![ - coinbase, - spend(u32::MAX), - spend(u32::MAX), - spend(u32::MAX), - ]); - let spends = vec![ - ( - [0u8; 32], - 0u32, - rbitcoin_primitives::Fk::NULL, - rbitcoin_primitives::Fk::NULL, - 0u32 - ); - 6 - ]; - let mut cache = rbitcoin_query::U32Map::default(); - cache.insert(0, 1_000); - super::structural_bip68( - &query, - &block, - &ctx, - &spends, - &rbitcoin_query::FkMap::default(), - &mut cache, - ) - .expect("a well-formed multi-input block passes the spend cursor"); -} - -/// Height-type CSV does not prefetch a median. A missing parent header must -/// still pass when the block's own previous median is already cached. +/// The coinbase relative lock in the connect pin fails when that tx is checked as a spend. #[test] -fn structural_bip68_height_lock_skips_missing_coin_mtp() { - let params = Box::leak(Box::new(crate::params::ChainParams::regtest())); - let ctx = super::ValidationContext::at( - params, - rbitcoin_primitives::Height(6), - crate::milestone::Milestone::NONE, - ); - let (_dir, query) = rbitcoin_query::testutil::tiny_query_labeled("bip68-height-mtp"); - let create = rbitcoin_primitives::Fk(3); - let mut create_heights = rbitcoin_query::FkMap::default(); - create_heights.insert(create, 5); - let block = csv_block(vec![ - bare_tx(1, LockTime::ZERO, Sequence::MAX), - seq_tx(&[0]), - ]); - let spends = [([0u8; 32], 0u32, rbitcoin_primitives::Fk::NULL, create, 0u32)]; - let mut cache = rbitcoin_query::U32Map::default(); - cache.insert(5, 1_500_000); - super::structural_bip68(&query, &block, &ctx, &spends, &create_heights, &mut cache) - .expect("height-type lock must not read the coin median"); -} - -/// BIP68 walks `txdata[1..]`. A coinbase whose relative lock would fail still connects. -#[test] -fn bip68_skips_a_coinbase_whose_relative_lock_would_fail() { - use crate::{accept_and_connect_block, prepare_regtest_candidate, ConsensusError}; - +fn coinbase_relative_lock_fails_as_a_normal_tx() { let coinbase = bare_tx(2, LockTime::ZERO, Sequence::from_consensus(200)); assert!( !sequence_locks_satisfied(&coinbase, &[], &[], 1, 0), "the same lock fails closed when the coinbase is treated as a normal tx" ); - - let (_dir, q) = rbitcoin_query::testutil::tiny_query_labeled("bip68-coinbase"); - let params = crate::params::ChainParams::regtest(); - let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - accept_and_connect_block( - &q, - ¶ms, - rbitcoin_primitives::Height::GENESIS, - &genesis, - crate::Milestone::NONE, - ) - .unwrap(); - let mut block = crate::mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); - block.txdata[0].version = bitcoin::transaction::Version::TWO; - block.txdata[0].input[0].sequence = Sequence::from_consensus(200); - prepare_regtest_candidate(&mut block, genesis.block_hash(), genesis.header.time + 600); - accept_and_connect_block( - &q, - ¶ms, - rbitcoin_primitives::Height(1), - &block, - crate::Milestone::NONE, - ) - .expect("coinbase sequence is not a BIP68 input"); - assert_eq!(q.tip_height(), Some(rbitcoin_primitives::Height(1))); - - let params = Box::leak(Box::new(crate::params::ChainParams::regtest())); - let ctx = super::ValidationContext::at( - params, - rbitcoin_primitives::Height(6), - crate::milestone::Milestone::NONE, - ); - let (_dir, query) = rbitcoin_query::testutil::tiny_query_labeled("bip68-noncb"); - let create = rbitcoin_primitives::Fk(3); - let mut create_heights = rbitcoin_query::FkMap::default(); - create_heights.insert(create, 5); - let block = csv_block(vec![ - bare_tx(1, LockTime::ZERO, Sequence::MAX), - bare_tx(2, LockTime::ZERO, Sequence::from_consensus(200)), - ]); - let spends = [([0u8; 32], 0u32, rbitcoin_primitives::Fk::NULL, create, 0u32)]; - let mut cache = rbitcoin_query::U32Map::default(); - cache.insert(5, 1_500_000); - let err = super::structural_bip68(&query, &block, &ctx, &spends, &create_heights, &mut cache) - .expect_err("the same lock rejects a non-coinbase"); - assert!( - matches!(err, ConsensusError::BadTx(s) if s.contains("nonfinal")), - "{err:?}" - ); } diff --git a/crates/rbitcoin-consensus/src/block/mod.rs b/crates/rbitcoin-consensus/src/block/mod.rs index 6efc7d57c1..7143cd0e9f 100644 --- a/crates/rbitcoin-consensus/src/block/mod.rs +++ b/crates/rbitcoin-consensus/src/block/mod.rs @@ -1,14 +1,12 @@ -#[cfg(test)] -use crate::confirm_phase_stats; use crate::error::ConsensusError; use crate::milestone::Milestone; use crate::params::ChainParams; +use crate::TxidHasher; use bitcoin::block::Block; use bitcoin::hashes::{sha256d, Hash}; use bitcoin::script::{Script, ScriptBuf}; -use bitcoin::{Amount, OutPoint, Transaction, TxOut, Witness}; +use bitcoin::{Amount, Transaction, TxOut, Witness}; use rbitcoin_primitives::Height; -use rbitcoin_query::{FkMap, Query, TxidHasher, U32Map, U64Map}; use std::borrow::Borrow; use std::hash::BuildHasherDefault; use std::ops::{Deref, DerefMut}; @@ -55,7 +53,7 @@ impl<'a> ValidationContext<'a> { } } -const MAX_BLOCK_STRIPPED_SIZE: usize = 1_000_000; +pub const MAX_BLOCK_STRIPPED_SIZE: usize = 1_000_000; /// Consensus block weight limit. Compact-block tx count is this divided by /// [`MIN_TX_WEIGHT`]. pub const MAX_BLOCK_WEIGHT: u64 = 4_000_000; @@ -64,7 +62,7 @@ pub const MIN_TX_WEIGHT: u64 = 10 * 4; /// Most transactions a valid block can contain. pub const MAX_BLOCK_TX_COUNT: usize = (MAX_BLOCK_WEIGHT / MIN_TX_WEIGHT) as usize; -fn check_tx_local(tx: &Transaction, base_size: usize) -> Result<(), ConsensusError> { +pub fn check_tx_local(tx: &Transaction, base_size: usize) -> Result<(), ConsensusError> { if tx.input.is_empty() { return Err(ConsensusError::BadTx("no inputs")); } @@ -74,8 +72,8 @@ fn check_tx_local(tx: &Transaction, base_size: usize) -> Result<(), ConsensusErr if base_size > MAX_BLOCK_STRIPPED_SIZE { return Err(ConsensusError::BadTx("bad-txns-oversize")); } - let mut seen: rbitcoin_query::OutPointSet = - rbitcoin_query::OutPointSet::with_capacity_and_hasher(tx.input.len(), Default::default()); + let mut seen: crate::OutPointSet = + crate::OutPointSet::with_capacity_and_hasher(tx.input.len(), Default::default()); for inp in &tx.input { if !tx.is_coinbase() && inp.previous_output.is_null() { return Err(ConsensusError::BadTx("bad-txns-prevout-null")); @@ -167,7 +165,7 @@ pub fn validate_block_structure_with_pres( block: &Block, ctx: &ValidationContext<'_>, pres: Option>, - stats: Option<&rbitcoin_query::ConfirmStats>, + mut note: Option<&mut dyn FnMut(u64, u64, u64)>, ) -> Result, ConsensusError> { let n = block.txdata.len(); let (pres, txid_ns) = if let Some(stashed) = pres { @@ -185,7 +183,7 @@ pub fn validate_block_structure_with_pres( // Core CheckMerkleRoot runs before any body rule. A body the header does // not commit to is the peer's fault, not the block hash's. let txids: Vec<[u8; 32]> = pres.iter().map(|p| p.txid).collect(); - let (merkle, mutated) = rbitcoin_store::merkle_root_mutated(&txids); + let (merkle, mutated) = rbitcoin_primitives::merkle_root_mutated(&txids); if merkle != block.header.merkle_root.to_byte_array() { return Err(ConsensusError::BadBlock("merkle root mismatch")); } @@ -201,8 +199,7 @@ pub fn validate_block_structure_with_pres( return Err(ConsensusError::BadBlock("bad-cb-length")); } - let mut seen: rbitcoin_query::TxidSet = - rbitcoin_query::TxidSet::with_capacity_and_hasher(n, Default::default()); + let mut seen: crate::TxidSet = crate::TxidSet::with_capacity_and_hasher(n, Default::default()); for p in pres.iter() { if !seen.insert(p.txid) { return Err(ConsensusError::BadBlock("duplicate txid")); @@ -264,8 +261,8 @@ pub fn validate_block_structure_with_pres( } let walk_ns = t_walk.elapsed().as_nanos() as u64; - if let Some(stats) = stats { - stats.note_struct_parts(txid_ns, 0, walk_ns); + if let Some(note) = note.as_mut() { + note(txid_ns, 0, walk_ns); } // BIP325 signet solution is not checked here — tip confirm only. @@ -385,13 +382,13 @@ pub fn legacy_sigop_count(tx: &Transaction) -> u64 { n } -pub(crate) use rbitcoin_primitives::script_sigop_count; +pub use rbitcoin_primitives::script_sigop_count; /// Last push in a script for P2SH/BIP141 sigops. /// /// Opcode `> OP_16` or a truncated push → no redeem (0 sigops). OP_N / OP_1NEGATE /// count as an empty push. -fn last_script_push(script: &[u8]) -> Option<&[u8]> { +pub fn last_script_push(script: &[u8]) -> Option<&[u8]> { let mut i = 0usize; let mut last: &[u8] = &[]; while i < script.len() { @@ -447,15 +444,15 @@ fn last_script_push(script: &[u8]) -> Option<&[u8]> { Some(last) } -fn is_p2sh_script(spk: &[u8]) -> bool { +pub fn is_p2sh_script(spk: &[u8]) -> bool { spk.len() == 23 && spk[0] == 0xa9 && spk[1] == 0x14 && spk[22] == 0x87 } -fn is_p2wpkh_program(prog: &[u8]) -> bool { +pub fn is_p2wpkh_program(prog: &[u8]) -> bool { prog.len() == 22 && prog[0] == 0x00 && prog[1] == 0x14 } -fn is_p2wsh_program(prog: &[u8]) -> bool { +pub fn is_p2wsh_program(prog: &[u8]) -> bool { prog.len() == 34 && prog[0] == 0x00 && prog[1] == 0x20 } @@ -515,7 +512,7 @@ fn witness_sigop_count(tx: &Transaction, prev_spks: &[&[u8]]) -> u64 { n } -fn prevout_spk_sigops(inp: &bitcoin::TxIn, spk: &[u8], bip16: bool, witness: bool) -> u64 { +pub fn prevout_spk_sigops(inp: &bitcoin::TxIn, spk: &[u8], bip16: bool, witness: bool) -> u64 { const WITNESS_SCALE: u64 = 4; let mut n = 0u64; if bip16 { @@ -582,16 +579,13 @@ fn check_witness_commitment_with_wtxids( } /// Merkle root over 32-byte leaves (txid or wtxid tree). Public for tests. -pub(crate) fn merkle_root_bytes(leaves: &[[u8; 32]]) -> [u8; 32] { - rbitcoin_store::merkle_root_from_txids(leaves) +pub fn merkle_root_bytes(leaves: &[[u8; 32]]) -> [u8; 32] { + rbitcoin_primitives::merkle_root_from_txids(leaves) } /// BIP34: coinbase scriptSig must start with the block height as a script integer /// push: 0 → `OP_0`; 1..=16 → `OP_1`..=`OP_16`; else minimal sign-aware little-endian. -pub(crate) fn check_bip34_coinbase( - coinbase: &Transaction, - height: u32, -) -> Result<(), ConsensusError> { +pub fn check_bip34_coinbase(coinbase: &Transaction, height: u32) -> Result<(), ConsensusError> { let script = &coinbase.input[0].script_sig; let bytes = script.as_bytes(); if bytes.is_empty() { @@ -661,7 +655,7 @@ const BIP16_EXCEPTION_TESTNET3: [u8; 32] = [ /// Callers must pass the same prev-block MTP used for BIP113 / header MTP checks /// and the once-computed block hash (plan `meta.hash` / structure). #[inline] -pub(crate) fn bip16_active_from_prev_mtp( +pub fn bip16_active_from_prev_mtp( params: &ChainParams, height: u32, block_hash: &[u8; 32], @@ -691,7 +685,7 @@ pub(crate) fn bip16_active_from_prev_mtp( /// /// Deref to [`Transaction`] so script paths keep `job.tx.input` / `&job.tx` ergonomics. #[derive(Clone)] -pub(crate) struct JobTx { +pub struct JobTx { inner: JobTxInner, } @@ -703,14 +697,14 @@ enum JobTxInner { impl JobTx { #[inline] - pub(crate) fn owned(tx: Transaction) -> Self { + pub fn owned(tx: Transaction) -> Self { Self { inner: JobTxInner::Owned(tx), } } #[inline] - pub(crate) fn shared(block: Arc, index: usize) -> Self { + pub fn shared(block: Arc, index: usize) -> Self { debug_assert!(index < block.txdata.len()); Self { inner: JobTxInner::Shared { block, index }, @@ -878,28 +872,48 @@ impl ScriptVerifyFlags { } } +/// Bytes of one pinned prevout. The `Arc` is the pin allocation; `parts` +/// borrows the script inside it (no second copy). +pub trait PinParts: Send + Sync { + fn parts(&self, vout: u32) -> Option<(u64, &[u8])>; +} + +/// Prevout a confirm job reads without copying script bytes. +/// +/// `Wire` is `(tx_index, vout)` in the job's wire block. `Pinned` is one vout +/// of the pin outs `Arc`. +#[derive(Clone)] +pub enum SharedPrevout { + Wire { tx_index: u32, vout: u32 }, + Pinned { outs: Arc, vout: u32 }, +} + /// Prevout scripts for one script job. /// /// Owned jobs (tests, non-wire connect) store [`TxOut`]. Confirm jobs store -/// [`rbitcoin_query::SharedPrevoutScript`]: same-block bytes stay in the wire -/// block, historical bytes stay in the pin outs `Arc`. -#[derive(Clone, Debug)] -pub(crate) enum JobPrevouts { +/// [`SharedPrevout`]: same-block bytes stay in the wire block, historical +/// bytes stay in the pin outs `Arc`. +#[derive(Clone)] +pub enum JobPrevouts { Owned(Vec), - Shared(Vec), + Shared(Vec), } impl JobPrevouts { - pub(crate) fn owned(v: Vec) -> Self { + pub fn owned(v: Vec) -> Self { Self::Owned(v) } - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { match self { Self::Owned(v) => v.len(), Self::Shared(v) => v.len(), } } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } /// Script-verify job for one non-coinbase create. @@ -909,10 +923,10 @@ impl JobPrevouts { /// re-hashing. pub struct ScriptCheckJob { /// Wire txid (assemble / [`Self::new`]); used for mempool preverified skip. - pub(crate) txid: [u8; 32], - pub(crate) prevouts: JobPrevouts, + pub txid: [u8; 32], + pub prevouts: JobPrevouts, /// Owned (tests) or shared wire block + index (confirm path). - pub(crate) tx: JobTx, + pub tx: JobTx, pub(crate) flags: ScriptVerifyFlags, /// Lookup/structure `TxPrecompute`. Set on the confirm path; tests lazy-`from_tx`. pub(crate) pre: std::sync::OnceLock, @@ -933,9 +947,9 @@ impl DerefMut for ScriptCheckJob { /// Confirm jobs borrow the lookup/structure slice; tests own an `Arc`. pub(crate) enum JobPre { - Owned(std::sync::Arc), + Owned(std::sync::Arc), Slice { - slice: std::sync::Arc<[rbitcoin_query::TxPrecompute]>, + slice: std::sync::Arc<[crate::TxPrecompute]>, idx: usize, }, } @@ -951,7 +965,7 @@ impl ScriptCheckJob { /// Owned-tx path (tests / benches / unit connect): reuse precomputed txid. #[inline] - pub(crate) fn with_txid( + pub fn with_txid( txid: [u8; 32], prevouts: Vec, tx: Transaction, @@ -962,7 +976,7 @@ impl ScriptCheckJob { /// Single construction site for activation + production standardness defaults. #[inline] - pub(crate) fn from_parts( + pub fn from_parts( txid: [u8; 32], prevouts: JobPrevouts, tx: JobTx, @@ -979,20 +993,14 @@ impl ScriptCheckJob { /// Confirm assemble: `slice[idx]` by refcount only (no `TxPrecompute` clone). #[inline] - pub(crate) fn with_pre_slice( - self, - slice: std::sync::Arc<[rbitcoin_query::TxPrecompute]>, - idx: usize, - ) -> Self { + pub fn with_pre_slice(self, slice: std::sync::Arc<[crate::TxPrecompute]>, idx: usize) -> Self { let _ = self.pre.set(JobPre::Slice { slice, idx }); self } fn job_pre(&self) -> &JobPre { self.pre.get_or_init(|| { - JobPre::Owned(std::sync::Arc::new(rbitcoin_query::TxPrecompute::from_tx( - &self.tx, - ))) + JobPre::Owned(std::sync::Arc::new(crate::TxPrecompute::from_tx(&self.tx))) }) } @@ -1000,32 +1008,26 @@ impl ScriptCheckJob { /// /// Shared wire prevouts read the job's block. Pinned prevouts read the outs /// `Arc`. A missing vout is `Corrupt`, not an empty script. - pub(crate) fn prevout_parts(&self, i: usize) -> Result<(u64, &[u8]), ConsensusError> { + pub fn prevout_parts(&self, i: usize) -> Result<(u64, &[u8]), ConsensusError> { match &self.prevouts { JobPrevouts::Owned(v) => { - let o = - v.get(i) - .ok_or(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: prevout index", - )))?; + let o = v + .get(i) + .ok_or(ConsensusError::Internal("invariant: prevout index"))?; Ok((o.value.to_sat(), o.script_pubkey.as_bytes())) } JobPrevouts::Shared(v) => { - let slot = - v.get(i) - .ok_or(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: prevout index", - )))?; + let slot = v + .get(i) + .ok_or(ConsensusError::Internal("invariant: prevout index"))?; match slot { - rbitcoin_query::SharedPrevoutScript::Wire { tx_index, vout } => { + SharedPrevout::Wire { tx_index, vout } => { let o = self.wire_output(*tx_index, *vout)?; Ok((o.value.to_sat(), o.script_pubkey.as_bytes())) } - rbitcoin_query::SharedPrevoutScript::Pinned(_) => { - slot.parts().ok_or(ConsensusError::Store( - rbitcoin_store::StoreError::Corrupt("invariant: pinned prevout vout"), - )) - } + SharedPrevout::Pinned { outs, vout } => outs + .parts(*vout) + .ok_or(ConsensusError::Internal("invariant: pinned prevout vout")), } } } @@ -1034,13 +1036,11 @@ impl ScriptCheckJob { fn wire_output(&self, tx_index: u32, vout: u32) -> Result<&TxOut, ConsensusError> { self.tx .output_at(tx_index, vout) - .ok_or(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: wire prevout", - ))) + .ok_or(ConsensusError::Internal("invariant: wire prevout")) } #[inline] - pub(crate) fn prevout_script(&self, i: usize) -> Result<&[u8], ConsensusError> { + pub fn prevout_script(&self, i: usize) -> Result<&[u8], ConsensusError> { self.prevout_parts(i).map(|(_, script)| script) } @@ -1051,7 +1051,7 @@ impl ScriptCheckJob { } #[inline] - pub(crate) fn pre(&self) -> &rbitcoin_query::TxPrecompute { + pub fn pre(&self) -> &crate::TxPrecompute { match self.job_pre() { JobPre::Owned(a) => a.as_ref(), JobPre::Slice { slice, idx } => &slice[*idx], @@ -1060,231 +1060,21 @@ impl ScriptCheckJob { } /// Identity-hashed `txid → V` (assemble index / pack creates). -pub(crate) type TxidMap = std::collections::HashMap<[u8; 32], V, BuildHasherDefault>; +pub type TxidMap = std::collections::HashMap<[u8; 32], V, BuildHasherDefault>; /// Pack-local create fk by parent txid (not per-vout — fk is per tx). -pub(crate) type PendingCreates = TxidMap; - -/// Block-local prevout path counts; flush to [`Query::confirm_stats`] once. -#[derive(Default)] -struct AsmPrevoutAcc { - in_n: u64, - same_n: u64, - batch_n: u64, - cold_n: u64, - cold_null_fk_n: u64, - cold_not_pin_n: u64, - cold_txid_mismatch_n: u64, - cold_vout_miss_n: u64, -} - -impl AsmPrevoutAcc { - fn flush(&self, stats: &rbitcoin_query::ConfirmStats) { - rbitcoin_query::note_confirm(&stats.asm_in_n, self.in_n); - rbitcoin_query::note_confirm(&stats.asm_prev_same_n, self.same_n); - rbitcoin_query::note_confirm(&stats.asm_prev_batch_n, self.batch_n); - rbitcoin_query::note_confirm(&stats.asm_prev_cold_n, self.cold_n); - rbitcoin_query::note_confirm(&stats.asm_prev_cold_null_fk_n, self.cold_null_fk_n); - rbitcoin_query::note_confirm(&stats.asm_prev_cold_not_pin_n, self.cold_not_pin_n); - rbitcoin_query::note_confirm( - &stats.asm_prev_cold_txid_mismatch_n, - self.cold_txid_mismatch_n, - ); - rbitcoin_query::note_confirm(&stats.asm_prev_cold_vout_miss_n, self.cold_vout_miss_n); - } -} - -#[allow(clippy::too_many_arguments)] // call-site args stay unbundled -#[allow(clippy::type_complexity)] // packed row / pin / script-hash tuple is the on-disk shape -/// Sequential assemble: resolve prevout **content**, build script jobs, collect spends. -/// -/// Confirm IBD path: no durable spentness / maturity / BIP68 create-height -/// resolution — those run in [`structural_validate_spends`] after scripts (load -/// must not walk create height per parent). Absolute nLockTime finality -/// (BIP113 MTP of prev block) still runs here — it only needs header MTP. -/// -/// `pending_spent`: pack-local double-spend (early reject before scripts). -/// `pending_creates`: pack-local `txid → create_fk` (not per-vout). -/// Same-block outs use `txid_index` (this block, `pj < ti`); meters flush -/// once per block (no per-input Instant / atomics). -/// -/// Prevouts resolve from per-batch [`rbitcoin_query::BatchParents`] + -/// [`rbitcoin_query::SpendEdges`]. Optimistic miss is `invariant:` (no head recover). -/// -/// Returns `(script_jobs, spends, fees, tx_fees)`. `fees` is the block sum -/// for the coinbase subsidy check. `tx_fees` is one satoshi fee per tx -/// (coinbase is 0) for the `txstat` stamp. -/// -/// `prev_mtp` / `block_hash` / `bip16_active` must be computed **once** by the -/// caller (assemble_run header window) — no re-walk of headers and no rehash. -/// -/// `wire`: when `Some`, script jobs share that Arc (no `Transaction` clone). -/// When `None` (unit-test connect), jobs own a deep clone of each non-cb tx. -pub(crate) fn assemble_block_prevouts( - query: &Query, - block: &Block, - ctx: &ValidationContext<'_>, - archived_tx_fks: Option<&[rbitcoin_primitives::Fk]>, - pending_spent: &mut rbitcoin_query::OutPointSet, - pending_creates: &mut PendingCreates, - batch_parents: &rbitcoin_query::BatchParents, - spend_edges: &rbitcoin_query::SpendEdges, - create_txids: &[[u8; 32]], - prev_mtp: u32, - block_hash: &[u8; 32], - bip16_active: bool, - wire: Option<&Arc>, - pres: Option<&Arc<[rbitcoin_query::TxPrecompute]>>, -) -> Result< - ( - Vec, - Vec<( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )>, - i64, - Vec, - ), - ConsensusError, -> { - assemble_prevout_guards(block, archived_tx_fks, create_txids)?; - // Caller-supplied BIP16 must match hash+prev_mtp (no silent re-resolve). - debug_assert_eq!( - bip16_active, - bip16_active_from_prev_mtp(ctx.params, ctx.height.0, block_hash, prev_mtp) - ); - let flags = ScriptVerifyFlags::consensus_at(ctx, block_hash, bip16_active); - - let n_tx = block.txdata.len(); - let mut txid_index: TxidMap = - TxidMap::with_capacity_and_hasher(n_tx, Default::default()); - for (i, id) in create_txids.iter().enumerate() { - txid_index.insert(*id, i); - } - let mut acc = AsmPrevoutAcc::default(); - let mut clk_job = 0u64; - let mut fees = 0i64; - let mut tx_fees = vec![0u64; n_tx]; - let build_script_jobs = - !crate::milestone::skips_on_query(ctx.milestone, query, ctx.height.0, block_hash); - let mut script_jobs: Vec = if build_script_jobs { - Vec::with_capacity(n_tx.saturating_sub(1)) - } else { - Vec::new() - }; - let mut block_sigops_cost = match pres.and_then(|p| p.first()) { - Some(p) => p.sigops.saturating_mul(4), - None => legacy_sigop_count(&block.txdata[0]).saturating_mul(4), - }; - let mut spends: Vec<( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )> = Vec::with_capacity(n_tx.saturating_mul(2)); - - let t_loop = Instant::now(); - - let lock_time_cutoff = assemble_lock_time_cutoff(ctx, block, prev_mtp); - - for (ti, tx) in block.txdata.iter().enumerate() { - let spend_fk = archived_tx_fks.map(|fks| fks[ti]); - let txid = create_txids[ti]; - - if !is_final_tx(tx, ctx.height.0, lock_time_cutoff) { - return Err(ConsensusError::BadTx("bad-txns-nonfinal")); - } - if tx.output.is_empty() { - return Err(ConsensusError::BadTx("no outputs")); - } - if ti > 0 { - tx_fees[ti] = assemble_non_cb_tx( - ctx.params, - block, - tx, - ti, - txid, - spend_fk, - flags, - bip16_active, - wire, - pres, - &txid_index, - pending_spent, - pending_creates, - batch_parents, - spend_edges, - &mut acc, - &mut script_jobs, - &mut spends, - &mut fees, - &mut block_sigops_cost, - build_script_jobs, - &mut clk_job, - )?; - } - - let create_fk = spend_fk.unwrap_or(rbitcoin_primitives::Fk::NULL); - if !create_fk.is_null() { - pending_creates.insert(txid, create_fk); - } - } - - let clk_prev = t_loop.elapsed().as_nanos() as u64; - acc.flush(query.confirm_stats()); - rbitcoin_query::note_confirm(&query.confirm_stats().asm_prevout_ns, clk_prev); - rbitcoin_query::note_confirm(&query.confirm_stats().asm_job_ns, clk_job); - Ok((script_jobs, spends, fees, tx_fees)) -} - -fn assemble_prevout_guards( - block: &Block, - archived_tx_fks: Option<&[rbitcoin_primitives::Fk]>, - create_txids: &[[u8; 32]], -) -> Result<(), ConsensusError> { - if let Some(fks) = archived_tx_fks { - if fks.len() != block.txdata.len() { - return Err(ConsensusError::BadBlock("archived tx fk count mismatch")); - } - } - if create_txids.len() != block.txdata.len() { - return Err(ConsensusError::BadBlock( - "invariant: create_txids length must match block.txdata (no assemble re-hash)", - )); - } - if block.txdata.is_empty() { - return Err(ConsensusError::BadBlock("empty block")); - } - if !block.txdata[0].is_coinbase() { - return Err(ConsensusError::BadBlock("first tx not coinbase")); - } - Ok(()) -} - -fn assemble_lock_time_cutoff(ctx: &ValidationContext<'_>, block: &Block, prev_mtp: u32) -> u32 { - // Height 0 has no MTP history, so the cutoff is the block time even - // when CSV is active. - if ctx.height.0 == 0 || !ctx.params.csv_active_at(ctx.height.0) { - block.header.time - } else { - prev_mtp - } -} +pub type PendingCreates = TxidMap; /// `MAX_BLOCK_SIGOPS_COST` (20_000 legacy sigops × witness scale 4). #[inline] -fn exceeds_sigops_limit(cost: u64) -> bool { +pub fn exceeds_sigops_limit(cost: u64) -> bool { const MAX_BLOCK_SIGOPS_COST: u64 = 80_000; cost > MAX_BLOCK_SIGOPS_COST } /// Attach the precompute slice only when `ti` is inside it. #[inline] -fn should_use_pres(ti: usize, len: usize) -> bool { +pub fn should_use_pres(ti: usize, len: usize) -> bool { ti < len } @@ -1296,13 +1086,13 @@ fn should_use_pres(ti: usize, len: usize) -> bool { /// cannot become negative when `money_range_out_sum` or /// `assemble_tx_value_out` casts it. #[inline] -fn exceeds_max_money(sats: u64) -> bool { +pub fn exceeds_max_money(sats: u64) -> bool { sats > Amount::MAX_MONEY.to_sat() } /// Signed satoshis for an `out_sum` that already passed [`exceeds_max_money`]. #[inline] -fn money_range_out_sum(out_sum: u64) -> i64 { +pub fn money_range_out_sum(out_sum: u64) -> i64 { debug_assert!( !exceeds_max_money(out_sum), "out_sum above MAX_MONEY must be rejected in validate_block_structure_with_pres" @@ -1310,111 +1100,10 @@ fn money_range_out_sum(out_sum: u64) -> i64 { out_sum as i64 } -#[allow(clippy::too_many_arguments)] // one non-coinbase assemble; call site stays unbundled -fn assemble_non_cb_tx( - params: &ChainParams, - block: &Block, - tx: &Transaction, - ti: usize, - txid: [u8; 32], - spend_fk: Option, - flags: ScriptVerifyFlags, - bip16_active: bool, - wire: Option<&Arc>, - pres: Option<&Arc<[rbitcoin_query::TxPrecompute]>>, - txid_index: &TxidMap, - pending_spent: &mut rbitcoin_query::OutPointSet, - pending_creates: &PendingCreates, - batch_parents: &rbitcoin_query::BatchParents, - spend_edges: &rbitcoin_query::SpendEdges, - acc: &mut AsmPrevoutAcc, - script_jobs: &mut Vec, - spends: &mut Vec<( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )>, - fees: &mut i64, - block_sigops_cost: &mut u64, - build_script_jobs: bool, - clk_job: &mut u64, -) -> Result { - if tx.input.is_empty() { - return Err(ConsensusError::BadTx("no inputs")); - } - let (value_in, prevouts, tx_in_sigops) = assemble_non_cb_inputs( - params, - block, - tx, - ti, - spend_fk, - flags, - bip16_active, - txid_index, - pending_spent, - pending_creates, - batch_parents, - spend_edges, - wire, - acc, - spends, - build_script_jobs, - )?; - let tx_legacy_sigops = match pres.and_then(|p| p.get(ti)) { - Some(p) => p.sigops.saturating_mul(4), - None => legacy_sigop_count(tx).saturating_mul(4), - }; - *block_sigops_cost = block_sigops_cost - .checked_add(tx_legacy_sigops) - .and_then(|c| c.checked_add(tx_in_sigops)) - .ok_or(ConsensusError::BadBlock("bad-blk-sigops"))?; - if exceeds_sigops_limit(*block_sigops_cost) { - return Err(ConsensusError::BadBlock("bad-blk-sigops")); - } - let value_out = assemble_tx_value_out(tx, ti, pres)?; - if value_out > value_in { - return Err(ConsensusError::BadTx("in < out")); - } - let fee = value_in - .checked_sub(value_out) - .ok_or(ConsensusError::BadTx("fee overflow"))?; - let fee_sat = u64::try_from(fee).map_err(|_| ConsensusError::BadTx("fee overflow"))?; - *fees = fees - .checked_add(fee) - .ok_or(ConsensusError::BadTx("fee overflow"))?; - if build_script_jobs { - let t_job = Instant::now(); - let mut job = match (wire, prevouts) { - (Some(w), JobPrevouts::Shared(v)) => ScriptCheckJob::from_parts( - txid, - JobPrevouts::Shared(v), - JobTx::shared(Arc::clone(w), ti), - flags, - ), - (None, JobPrevouts::Owned(v)) => ScriptCheckJob::with_txid(txid, v, tx.clone(), flags), - _ => { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: script job prevouts", - ))); - } - }; - if let Some(ps) = pres { - if should_use_pres(ti, ps.len()) { - job = job.with_pre_slice(Arc::clone(ps), ti); - } - } - script_jobs.push(job); - *clk_job = clk_job.saturating_add(t_job.elapsed().as_nanos() as u64); - } - Ok(fee_sat) -} - -fn assemble_tx_value_out( +pub fn assemble_tx_value_out( tx: &Transaction, ti: usize, - pres: Option<&Arc<[rbitcoin_query::TxPrecompute]>>, + pres: Option<&Arc<[crate::TxPrecompute]>>, ) -> Result { match pres.and_then(|p| p.get(ti)) { Some(p) => Ok(money_range_out_sum(p.out_sum)), @@ -1434,111 +1123,7 @@ fn assemble_tx_value_out( } } -#[allow(clippy::too_many_arguments)] // one non-coinbase input walk; call site stays unbundled -fn assemble_non_cb_inputs( - params: &ChainParams, - block: &Block, - tx: &Transaction, - ti: usize, - spend_fk: Option, - flags: ScriptVerifyFlags, - bip16_active: bool, - txid_index: &TxidMap, - pending_spent: &mut rbitcoin_query::OutPointSet, - pending_creates: &PendingCreates, - batch_parents: &rbitcoin_query::BatchParents, - spend_edges: &rbitcoin_query::SpendEdges, - wire: Option<&Arc>, - acc: &mut AsmPrevoutAcc, - spends: &mut Vec<( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )>, - build_script_jobs: bool, -) -> Result<(i64, JobPrevouts, u64), ConsensusError> { - let mut value_in = 0i64; - let mut prevouts = if build_script_jobs && wire.is_some() { - JobPrevouts::Shared(Vec::with_capacity(tx.input.len())) - } else { - JobPrevouts::owned(Vec::with_capacity(if build_script_jobs { - tx.input.len() - } else { - 0 - })) - }; - let edges = spend_fk.and_then(|fk| fk.get().and_then(|id| spend_edges.get(&id))); - let mut tx_in_sigops = 0u64; - for (ii, input) in tx.input.iter().enumerate() { - let op = input.previous_output; - let key = (op.txid.to_byte_array(), op.vout); - // The genesis coinbase stays indexed for RPC and Electrum, but it is - // not a coin (Core `ConnectBlock` genesis early return). - if params.is_genesis_coinbase(&key.0) { - return Err(ConsensusError::MissingPrevout); - } - if !pending_spent.insert(key) { - return Err(ConsensusError::BadTx("double spend in block")); - } - if let Some(&pj) = txid_index.get(&key.0) { - if pj >= ti { - return Err(ConsensusError::MissingPrevout); - } - if pj == 0 { - return Err(ConsensusError::BadTx("coinbase immature")); - } - } - let prev_fk = edges - .as_ref() - .and_then(|t| t.get(ii)) - .and_then(|e| e.create_fk.get().map(|_| e.create_fk)) - .or_else(|| pending_creates.get(&key.0).copied()); - let prev_out = resolve_prevout( - block, - op, - input, - prev_fk, - txid_index, - ti, - batch_parents, - bip16_active, - flags.witness_active, - build_script_jobs, - wire, - acc, - )?; - let create_fk = prev_out.create_fk; - tx_in_sigops = tx_in_sigops.saturating_add(prev_out.input_sigops); - spends.push(( - key.0, - key.1, - spend_fk.unwrap_or(rbitcoin_primitives::Fk::NULL), - create_fk, - ii as u32, - )); - value_in = value_in - .checked_add(prev_out.txout.value.to_sat() as i64) - .ok_or(ConsensusError::BadTx("value in overflow"))?; - if build_script_jobs { - match &mut prevouts { - JobPrevouts::Shared(v) => { - let shared = prev_out.shared.ok_or(ConsensusError::Store( - rbitcoin_store::StoreError::Corrupt( - "invariant: wire confirm prevout shares script bytes", - ), - ))?; - v.push(shared); - } - JobPrevouts::Owned(v) => v.push(prev_out.txout), - } - } - } - Ok((value_in, prevouts, tx_in_sigops)) -} - -fn check_coinbase_subsidy( +pub fn check_coinbase_subsidy( block: &Block, ctx: &ValidationContext<'_>, fees: i64, @@ -1559,792 +1144,40 @@ fn check_coinbase_subsidy( Ok(()) } -/// Local wall times for one block's structural pass (write path diagnostics). -/// -/// Measured with `Instant` — **not** deltas of window atomics (those race with -/// `sample_and_reset` mid-batch and produced false `spent=0` on slow writes). -#[derive(Debug, Clone, Copy, Default)] -pub(crate) struct StructuralPhaseNs { - pub spent_ns: u64, - /// Pin abs collect + bulk on-disk 8-byte spender meta pread. - pub spent_abs_ns: u64, - /// `is_confirmed_strong_at` on non-null fields (still durable authority). - pub spent_strong_ns: u64, - /// Cold unspent_create_vouts / null-create probes. - pub spent_cold_ns: u64, - /// pending_spent order gate (CPU). - pub spent_pending_ns: u64, - pub create_h_ns: u64, - pub bip68_ns: u64, -} - -/// Post-script structural checks: durable spentness, maturity, BIP68, coinbase subsidy. -/// -/// Runs in height order on the write path (after scripts). `pending_spent` is -/// write-local across a multi-height run. -/// -/// **BIP68** create-height lives here (not optimistic load assemble) so confirm -/// load does not walk create height for every parent. Heights: bulk fence. -/// Coin MTP only for time-type relative locks on version ≥2 txs (v1 skipped). -/// -/// Write-path spend annotate slots: abs edge plus the meta structural already read. -/// -/// Filled during spentness and passed to annotate. No second edge copy. -#[derive(Clone, Debug, Default)] -pub(crate) struct AnnotateSlots { - pub abs_edges: Vec<( - u64, - rbitcoin_primitives::Fk, - u32, - rbitcoin_primitives::Fk, - u32, - )>, - pub known: Vec<(rbitcoin_primitives::Fk, u8, u32)>, -} - -impl AnnotateSlots { - pub(crate) fn push( - &mut self, - edge: ( - u64, - rbitcoin_primitives::Fk, - u32, - rbitcoin_primitives::Fk, - u32, - ), - known: (rbitcoin_primitives::Fk, u8, u32), - ) { - self.abs_edges.push(edge); - self.known.push(known); - } -} - -/// Per-block spentness scratch. Capacity stays across blocks in one write batch. -/// [`AnnotateSlots`] accumulate for the whole batch. -pub(crate) struct StructuralScratch { - abs_jobs: Vec, - unique_fks: Vec, - height_by_id: U64Map, - skip_n: std::collections::HashMap< - (u64, u32), - u32, - BuildHasherDefault, - >, - skip: OverlayMetaSkip, - disk_jobs: Vec, - abs_offs: Vec, - field_fks: Vec, - field_seen: rbitcoin_query::U64Set, - field_h_by_id: U64Map, - durable_spent: DurableSpentSet, - height_list: Vec, - create_height_by_fk: FkMap, - pub slots: AnnotateSlots, -} - -impl Default for StructuralScratch { - fn default() -> Self { - Self { - abs_jobs: Vec::new(), - unique_fks: Vec::new(), - height_by_id: U64Map::default(), - skip_n: std::collections::HashMap::with_hasher(Default::default()), - skip: OverlayMetaSkip::with_hasher(Default::default()), - disk_jobs: Vec::new(), - abs_offs: Vec::new(), - field_fks: Vec::new(), - field_seen: rbitcoin_query::U64Set::default(), - field_h_by_id: U64Map::default(), - durable_spent: DurableSpentSet::with_hasher(Default::default()), - height_list: Vec::new(), - create_height_by_fk: FkMap::default(), - slots: AnnotateSlots::default(), - } - } -} - -impl StructuralScratch { - /// Drop annotate slots from the previous write batch. Per-block buffers - /// stay for [`Self::begin_block`]; slots accumulate inside one batch. - pub(crate) fn begin_batch(&mut self) { - self.slots.abs_edges.clear(); - self.slots.known.clear(); - } - - fn begin_block(&mut self) { - self.abs_jobs.clear(); - self.unique_fks.clear(); - self.height_by_id.clear(); - self.skip_n.clear(); - self.skip.clear(); - self.disk_jobs.clear(); - self.abs_offs.clear(); - self.field_fks.clear(); - self.field_seen.clear(); - self.field_h_by_id.clear(); - self.durable_spent.clear(); - self.height_list.clear(); - self.create_height_by_fk.clear(); - } -} - -#[allow(clippy::too_many_arguments)] // call-site args stay unbundled -/// **Spentness:** pin denserels → abs + bulk 8-byte meta. Sparse durable-**spent** -/// set (not unspent). Missing abs / short meta is hard `Err`. **Multi-list** after -/// reorg annotate is a protocol cold walk (`has_confirmed_strong_spender_create`) -/// — not a hard fail (tip-follow reorgs leave multi flags by design). Emits -/// Annotate slots on `scratch` for pure-write annotate. -pub(crate) fn structural_validate_spends( - query: &Query, - block: &Block, - ctx: &ValidationContext<'_>, - archived_tx_fks: Option<&[rbitcoin_primitives::Fk]>, - spends: &[( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )], - fees: i64, - pending_spent: &mut rbitcoin_query::OutPointSet, - batch_parents: &rbitcoin_query::BatchParents, - mtp_cache: &mut U32Map, - run_create_height: &RunCreateHeight, - class_a_wave: &ClassAWave, - scratch: &mut StructuralScratch, - abs_jobs: &[StructuralAbsJob], -) -> Result { - use std::time::Instant; - - scratch.begin_block(); - let maturity = ctx.params.coinbase_maturity(); - // BIP30's txid batch is inside `spent_ns` (signet runs it on every block). - let t_spent = Instant::now(); - reject_bip30_unspent_overwrite(query, block, ctx)?; - let t_abs = Instant::now(); - structural_abs_heights(query, run_create_height, scratch, abs_jobs)?; - let tip = query.tip_height().map(|h| h.0); - let mut spent_strong_ns = 0u64; - let mut multi_list_ns = 0u64; - if !scratch.abs_jobs.is_empty() { - fill_overlay_skip(spends, class_a_wave, scratch); - let loaded = structural_load_durable_spent(query, tip, scratch)?; - multi_list_ns = loaded.0; - spent_strong_ns = loaded.1; - } - let spent_abs_ns = (t_abs.elapsed().as_nanos() as u64).saturating_sub(spent_strong_ns); - let spent_cold_ns = multi_list_ns; - let t_pending = Instant::now(); - structural_mark_pending(spends, pending_spent, &scratch.durable_spent)?; - let spent_pending_ns = t_pending.elapsed().as_nanos() as u64; - let spent_ns = t_spent.elapsed().as_nanos() as u64; - let t_create = Instant::now(); - structural_create_heights( - query, - batch_parents, - run_create_height, - ctx.height.0, - maturity, - scratch, - )?; - let create_h_ns = t_create.elapsed().as_nanos() as u64; - let t_bip68 = Instant::now(); - structural_bip68( - query, - block, - ctx, - spends, - &scratch.create_height_by_fk, - mtp_cache, - )?; - let bip68_ns = t_bip68.elapsed().as_nanos() as u64; - let _ = archived_tx_fks; - check_coinbase_subsidy(block, ctx, fees)?; - Ok(StructuralPhaseNs { - spent_ns, - spent_abs_ns, - spent_strong_ns, - spent_cold_ns, - spent_pending_ns, - create_h_ns, - bip68_ns, - }) -} - -pub(crate) type StructuralAbsJob = (u64, u32, u64, rbitcoin_primitives::Fk, u32); - -/// Creates this write batch's Class A append committed. -/// -/// Only these spent slots carry the same-batch sole-spender overlay. A run -/// create archived by an earlier wave has whatever that wave and later -/// annotates left on disk, so structural reads and annotates it. -#[derive(Default)] -pub(crate) struct ClassAWave(Vec); - -impl ClassAWave { - pub(crate) fn new(mut fks: Vec) -> Self { - fks.sort_unstable_by_key(|f| f.0); - Self(fks) - } - - fn contains(&self, fk: rbitcoin_primitives::Fk) -> bool { - self.0.binary_search_by_key(&fk.0, |f| f.0).is_ok() - } -} - -/// Create heights for one write batch. -/// -/// Contiguous per-block fk spans are the IBD shape (`first` + count). A gap -/// or overlap uses the map so a fk between spans does not inherit a height. -/// Each block's first fk is its coinbase: a span's `first`, or the map's flag. -pub(crate) enum RunCreateHeight { - Spans(Vec<(u64, u64, u32)>), - Map(FkMap<(u32, bool)>), -} - -impl RunCreateHeight { - pub(crate) fn from_blocks<'a>( - blocks: impl IntoIterator, - ) -> Self { - let blocks: Vec<(u32, &'a [rbitcoin_primitives::Fk])> = blocks.into_iter().collect(); - if let Some(spans) = contiguous_create_spans(&blocks) { - Self::Spans(spans) - } else { - let mut map = FkMap::default(); - for (height, fks) in blocks { - for (i, fk) in fks.iter().enumerate() { - map.insert(*fk, (height, i == 0)); - } - } - Self::Map(map) - } - } - - pub(crate) fn get(&self, fk: rbitcoin_primitives::Fk) -> Option { - self.create(fk).map(|(height, _)| height) - } - - /// Height of a create in this batch and whether it is its block's coinbase. - pub(crate) fn create(&self, fk: rbitcoin_primitives::Fk) -> Option<(u32, bool)> { - match self { - Self::Map(map) => map.get(&fk).copied(), - Self::Spans(spans) => { - let id = fk.get()?; - let i = spans.partition_point(|span| span.0 <= id); - let (first, end, height) = spans.get(i.checked_sub(1)?)?; - (*end > id).then_some((*height, *first == id)) - } - } - } -} - -/// `Some` when every non-empty block is `[first, first+n)` and spans do not overlap. -/// -/// A null fk, a gap, or an id that would wrap is `None` so the caller uses the map. -/// A wrapped end must not cover fks that are not in the block. -fn contiguous_create_spans( - blocks: &[(u32, &[rbitcoin_primitives::Fk])], -) -> Option> { - let mut spans = Vec::with_capacity(blocks.len()); - for &(height, fks) in blocks { - if fks.is_empty() { - continue; - } - let mut ids = fks.iter().map(|fk| fk.get()); - let first = ids.next().flatten()?; - let mut prev = first; - for id in ids { - let id = id?; - let next = prev.checked_add(1)?; - if id != next { - return None; - } - prev = id; - } - let end = prev.checked_add(1)?; - spans.push((first, end, height)); - } - spans.sort_unstable_by_key(|span| span.0); - for pair in spans.windows(2) { - if pair[0].1 > pair[1].0 { - return None; - } - } - Some(spans) -} -type DurableSpentSet = - std::collections::HashSet<(u64, u32), BuildHasherDefault>; -type OverlayMetaSkip = std::collections::HashMap< - (u64, u32), - (rbitcoin_primitives::Fk, u32), - BuildHasherDefault, ->; - -fn fill_overlay_skip( - spends: &[( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )], - class_a_wave: &ClassAWave, - scratch: &mut StructuralScratch, -) { - for &(_, vout, sfk, cfk, vin) in spends { - if !class_a_wave.contains(cfk) { - continue; - } - let Some(id) = cfk.get() else { - continue; - }; - let key = (id, vout); - *scratch.skip_n.entry(key).or_insert(0) += 1; - scratch.skip.entry(key).or_insert((sfk, vin)); - } - let skip_n = &scratch.skip_n; - scratch - .skip - .retain(|k, _| skip_n.get(k).copied() == Some(1)); -} - -fn overlay_meta_is_skip( - id: u64, - vout: u32, - sfk: rbitcoin_primitives::Fk, - vin: u32, - skip: &OverlayMetaSkip, -) -> bool { - skip.get(&(id, vout)) == Some(&(sfk, vin)) -} - -fn structural_abs_heights( - query: &Query, - run_create_height: &RunCreateHeight, - scratch: &mut StructuralScratch, - abs_jobs: &[StructuralAbsJob], -) -> Result<(), ConsensusError> { - scratch.abs_jobs.extend_from_slice(abs_jobs); - scratch.unique_fks.extend( - scratch - .abs_jobs - .iter() - .map(|(id, _, _, _, _)| rbitcoin_primitives::Fk(*id)), - ); - scratch.unique_fks.sort_unstable_by_key(|f| f.0); - scratch.unique_fks.dedup(); - let durable_heights = query - .store() - .tx_height_get_batch(&scratch.unique_fks) - .map_err(ConsensusError::from)?; - for (fk, h) in scratch.unique_fks.iter().zip(durable_heights) { - let Some(id) = fk.get() else { - continue; - }; - let Some(h) = h.or_else(|| run_create_height.get(*fk)) else { - continue; - }; - scratch.height_by_id.insert(id, h); - } - Ok(()) -} - -fn structural_load_durable_spent( - query: &Query, - tip: Option, - scratch: &mut StructuralScratch, -) -> Result<(u64, u64), ConsensusError> { - use std::time::Instant; - let mut ovl_n = 0u64; - for &job in &scratch.abs_jobs { - let (id, vout, abs, sfk, vin) = job; - if overlay_meta_is_skip(id, vout, sfk, vin, &scratch.skip) { - ovl_n = ovl_n.saturating_add(1); - continue; - } - scratch.disk_jobs.push(job); - scratch.abs_offs.push(abs); - } - rbitcoin_query::note_confirm(&query.confirm_stats().spend_overlay_skip_n, ovl_n); - if scratch.abs_offs.is_empty() { - return Ok((0, 0)); - } - let meta_backend = rbitcoin_store::spend_meta_backend(); - let t_meta = Instant::now(); - let metas = query - .store() - .get_spender_meta_at_abs_batch_backend(&scratch.abs_offs, meta_backend) - .map_err(ConsensusError::from)?; - let meta_ns = t_meta.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().spend_meta_ns, meta_ns); - rbitcoin_query::note_confirm( - &query.confirm_stats().spend_meta_n, - scratch.abs_offs.len() as u64, - ); - let _ = meta_backend; - if metas.len() != scratch.disk_jobs.len() { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: structural meta batch length", - ))); - } - let t_strong = Instant::now(); - for row in &metas { - let Some((field, _, _)) = row else { - continue; - }; - if field.is_null() { - continue; - } - if let Some(fid) = field.get() { - if scratch.field_seen.insert(fid) { - scratch.field_fks.push(*field); - } - } - } - let field_heights = query - .store() - .tx_height_get_batch(&scratch.field_fks) - .map_err(ConsensusError::from)?; - for (fk, h) in scratch.field_fks.iter().zip(field_heights) { - if let Some((id, h)) = fk.get().zip(h) { - scratch.field_h_by_id.insert(id, h); - } - } - let jobs = std::mem::take(&mut scratch.disk_jobs); - let applied = jobs - .iter() - .copied() - .zip(metas) - .try_fold(0u64, |acc, (job, meta)| { - let (id, vout, abs, sfk, vin) = job; - let ns = structural_apply_one_meta(query, meta, id, vout, abs, sfk, vin, tip, scratch)?; - Ok::(acc.saturating_add(ns)) - }); - scratch.disk_jobs = jobs; - let multi_list_ns = applied?; - let spent_strong_ns = t_strong - .elapsed() - .as_nanos() - .saturating_sub(multi_list_ns as u128) as u64; - Ok((multi_list_ns, spent_strong_ns)) -} - -#[allow(clippy::too_many_arguments)] // one spent-slot apply; call site stays unbundled -fn structural_apply_one_meta( - query: &Query, - meta: Option<(rbitcoin_primitives::Fk, u8, u32)>, - id: u64, - vout: u32, - abs: u64, - sfk: rbitcoin_primitives::Fk, - vin: u32, - tip: Option, - scratch: &mut StructuralScratch, -) -> Result { - use std::time::Instant; - let Some((field, flags, field_vin)) = meta else { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: structural spender meta short/OOB (cold forbidden)", - ))); - }; - scratch.slots.push( - (abs, rbitcoin_primitives::Fk(id), vout, sfk, vin), - (field, flags, field_vin), - ); - let multi = flags & rbitcoin_store::output_flags::MULTI_SPENDER != 0; - if multi { - let t_m = Instant::now(); - let spent = query - .store() - .has_confirmed_strong_spender_create(rbitcoin_primitives::Fk(id), vout, None) - .map_err(ConsensusError::from)?; - let ns = t_m.elapsed().as_nanos() as u64; - if spent { - scratch.durable_spent.insert((id, vout)); - } - return Ok(ns); - } - if field.is_null() { - return Ok(0); - } - let strong = query - .store() - .is_confirmed_strong_at(field, tip) - .map_err(ConsensusError::from)?; - if !strong { - return Ok(0); - } - let create_h = scratch.height_by_id.get(&id).copied(); - let spend_h = field - .get() - .and_then(|fid| scratch.field_h_by_id.get(&fid).copied()); - if let (Some(ch), Some(sh)) = (create_h, spend_h) { - if sh < ch { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: confirmed spender below its create height", - ))); - } - } - scratch.durable_spent.insert((id, vout)); - Ok(0) -} - -fn structural_mark_pending( - spends: &[( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )], - pending_spent: &mut rbitcoin_query::OutPointSet, - durable_spent: &DurableSpentSet, +/// BIP30: an earlier connected instance with an unspent spendable output may +/// not be overwritten. The same height is this block. +pub fn reject_bip30_unspent_prior( + spend_height: u32, + prior_height: u32, + unspent_spendable: bool, ) -> Result<(), ConsensusError> { - for &(prev_txid, vout, _spend_fk, create_fk, _vin) in spends { - let key = (prev_txid, vout); - let spent = if create_fk.is_null() { - false - } else if let Some(id) = create_fk.get() { - durable_spent.contains(&(id, vout)) - } else { - false - }; - if spent || !pending_spent.insert(key) { - return Err(ConsensusError::PrevoutSpent); - } + if prior_height != spend_height && unspent_spendable { + return Err(ConsensusError::BadTx("bad-txns-BIP30")); } Ok(()) } -fn structural_create_heights( - query: &Query, - batch_parents: &rbitcoin_query::BatchParents, - run_create_height: &RunCreateHeight, +/// Create-height facts for one spent output. `None` is not a coin. A later +/// height in this batch is not yet a coin. A coinbase inside maturity is not +/// spendable. +pub fn reject_one_create( spend_height: u32, maturity: u32, - scratch: &mut StructuralScratch, + create_height: Option, + is_coinbase: bool, ) -> Result<(), ConsensusError> { - // `confirmed[h]` for a height in this batch is written at Class C, after - // structural; the batch index owns coinbase identity there. - scratch.height_list.extend( - scratch - .height_by_id - .iter() - .filter(|&(&id, &h)| run_create_height.get(rbitcoin_primitives::Fk(id)) != Some(h)) - .map(|(_, &h)| h), - ); - scratch.height_list.sort_unstable(); - scratch.height_list.dedup(); - let coinbase_fk_by_height = query - .store() - .coinbase_fk_at_heights(&scratch.height_list) - .map_err(ConsensusError::from)?; - let n_unique = scratch.unique_fks.len(); - for i in 0..n_unique { - let create_fk = scratch.unique_fks[i]; - let Some(id) = create_fk.get() else { - continue; - }; - let Some(&durable_h) = scratch.height_by_id.get(&id) else { - return Err(ConsensusError::BadTx("bad-txns-inputs-missingorspent")); - }; - // Core connects a run one block at a time: a create from a later - // block of this batch is not yet a coin when this block spends it. - if durable_h > spend_height { - return Err(ConsensusError::MissingPrevout); - } - let pin_cb = batch_parents.get_parent_coinbase(create_fk); - let is_cb = match (pin_cb, run_create_height.create(create_fk)) { - (Some(cb), _) => cb, - (None, Some((h, cb))) if h == durable_h => cb, - (None, _) => { - let cb = coinbase_fk_by_height - .get(&durable_h) - .ok_or(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: no coinbase fk at confirmed create height", - )))?; - *cb == create_fk - } - }; - if is_cb && spend_height < durable_h.saturating_add(maturity) { - return Err(ConsensusError::BadTx("coinbase immature")); - } - scratch.create_height_by_fk.insert(create_fk, durable_h); - } - Ok(()) -} - -fn structural_bip68( - query: &Query, - block: &Block, - ctx: &ValidationContext<'_>, - spends: &[( - [u8; 32], - u32, - rbitcoin_primitives::Fk, - rbitcoin_primitives::Fk, - u32, - )], - create_height_by_fk: &FkMap, - mtp_cache: &mut U32Map, -) -> Result<(), ConsensusError> { - if !ctx.params.csv_active_at(ctx.height.0) { - return Ok(()); - } - const DISABLE: u32 = 1 << 31; - const TYPE_FLAG: u32 = 1 << 22; - let prev_mtp = if ctx.height.0 == 0 { - 0 - } else { - mtp_at(query, Height(ctx.height.0 - 1), mtp_cache)? + let Some(create_height) = create_height else { + return Err(ConsensusError::BadTx("bad-txns-inputs-missingorspent")); }; - let mut si = 0usize; - let mut prev_heights: Vec = Vec::new(); - let mut coin_mtps: Vec = Vec::new(); - for tx in block.txdata.iter().skip(1) { - let n_in = tx.input.len(); - if si + n_in > spends.len() { - return Err(ConsensusError::BadBlock( - "structural spends/tx input mismatch", - )); - } - let tx_spends = &spends[si..si + n_in]; - si += n_in; - if !bip68_active_for_tx(tx) { - continue; - } - prev_heights.clear(); - coin_mtps.clear(); - prev_heights.reserve(n_in); - coin_mtps.reserve(n_in); - for (inp, &(_ptid, _vout, _sfk, create_fk, _vin)) in tx.input.iter().zip(tx_spends.iter()) { - let ch = if create_fk.is_null() { - ctx.height.0 - } else { - match create_height_by_fk.get(&create_fk) { - Some(&h) => h, - None => return Err(ConsensusError::BadTx("bad-txns-nonfinal")), - } - }; - prev_heights.push(ch); - let seq = inp.sequence.to_consensus_u32(); - let need_mtp = seq & DISABLE == 0 && seq & TYPE_FLAG != 0; - // Height 0 is the genesis coin: one median lookup of block 0. - // A missing create height is not height 0. - let mtp = if !need_mtp { - 0 - } else { - mtp_at(query, Height(ch.saturating_sub(1)), mtp_cache)? - }; - coin_mtps.push(mtp); - } - if !sequence_locks_satisfied(tx, &prev_heights, &coin_mtps, ctx.height.0, prev_mtp) { - return Err(ConsensusError::BadTx("bad-txns-nonfinal")); - } + if create_height > spend_height { + return Err(ConsensusError::MissingPrevout); } - if si != spends.len() { - return Err(ConsensusError::BadBlock( - "structural spends/tx input mismatch", - )); + if is_coinbase && spend_height < create_height.saturating_add(maturity) { + return Err(ConsensusError::BadTx("coinbase immature")); } Ok(()) } -/// MTP for write structural. Prefers assemble-carried `prev_mtp` (seeded into -/// `cache`). Misses go to durable headers only — never `get_header_plan`. -/// BIP30: a connected instance with any unspent spendable output may not be -/// overwritten. -/// Skipped for the two mainnet repeats, and when the header at BIP34 height -/// is this network's BIP34 hash and the block is below -/// [`crate::params::BIP34_IMPLIES_BIP30_LIMIT`]. Signet and regtest have no -/// BIP34 hash, so every block is checked. Just-archived self is unconnected. -fn reject_bip30_unspent_overwrite( - query: &Query, - block: &Block, - ctx: &ValidationContext<'_>, -) -> Result<(), ConsensusError> { - if ctx.params.is_bip30_repeat(ctx.height.0, block.block_hash()) - || bip34_ancestry_skips_bip30(query, ctx) - { - return Ok(()); - } - let create_txids: Vec<[u8; 32]> = block - .txdata - .iter() - .map(|tx| tx.compute_txid().to_byte_array()) - .collect(); - let hits = query - .store() - .get_fk_by_txid_batch(&create_txids) - .map_err(ConsensusError::from)?; - for (_txid, row) in hits { - let Some((old_fk, _)) = row else { - continue; - }; - // Connected instance at *this* height is ourselves (re-validate / - // already-confirmed fixture). BIP30 is an earlier unspent sibling. - if query - .store() - .tx_height_get(old_fk) - .map_err(ConsensusError::from)? - == Some(ctx.height.0) - { - continue; - } - let (_, outs) = query - .store() - .get_tx_meta_and_outputs(old_fk) - .map_err(ConsensusError::from)?; - let mut unspent = false; - for (v, out) in (0u32..).zip(&outs) { - // Core's AddCoins never stores an unspendable output, so it is - // not a coin that the overwrite could clobber. - if crate::policy::is_unspendable(&out.script) { - continue; - } - let spent = query - .store() - .has_confirmed_strong_spender_create(old_fk, v, None) - .map_err(ConsensusError::from)?; - if !spent { - unspent = true; - break; - } - } - if unspent { - return Err(ConsensusError::BadTx("bad-txns-BIP30")); - } - } - Ok(()) -} - -fn bip34_ancestry_skips_bip30(query: &Query, ctx: &ValidationContext<'_>) -> bool { - let height = ctx.height.0; - if ctx.params.bip34_hash.is_none() { - return false; - } - if height <= ctx.params.btc.bip34_height { - return false; - } - let ancestor = query - .header_at_height(Height(ctx.params.btc.bip34_height)) - .ok() - .flatten() - .map(|(_, rec)| bitcoin::BlockHash::from_byte_array(rec.hash)); - ctx.params - .bip30_skipped_for_bip34_ancestry(height, ancestor) -} - -fn mtp_at(query: &Query, height: Height, cache: &mut U32Map) -> Result { - if let Some(&t) = cache.get(&height.0) { - return Ok(t); - } - let t = crate::header::median_time_past_store(query, height)?; - cache.insert(height.0, t); - Ok(t) -} - /// Whether this job can skip `verify_job_all_inputs`. /// /// OP_TRUE scriptPubKey alone is **not** sufficient: scriptSig still runs @@ -2371,7 +1204,7 @@ fn job_needs_script_check(job: &ScriptCheckJob) -> bool { } #[inline] -pub(crate) fn verify_one_script_job(job: &ScriptCheckJob) -> Result<(), ConsensusError> { +pub fn verify_one_script_job(job: &ScriptCheckJob) -> Result<(), ConsensusError> { if job_needs_script_check(job) { crate::script::verify_job_all_inputs(job) } else { @@ -2389,17 +1222,6 @@ pub fn block_subsidy(height: u32, params: &ChainParams) -> i64 { 50_0000_0000i64 >> halvings } -struct ResolvedPrevout { - txout: TxOut, - /// Set when the script bytes stay in a block or pin `Arc`. - shared: Option, - /// P2SH+witness sigop cost for this input's prevout script (not legacy). - input_sigops: u64, - /// Class A create fk for this prevout (or `NULL` for same-block). Load pin - /// denserels must carry identity matching wire `prev_txid` for this fk. - create_fk: rbitcoin_primitives::Fk, -} - /// BIP65/113 nLockTime threshold: values below are block heights, above are unix times. pub const LOCKTIME_THRESHOLD: u32 = 500_000_000; @@ -2488,222 +1310,57 @@ pub fn sequence_locks_satisfied( true } -#[allow(clippy::too_many_arguments)] // call-site args stay unbundled -fn resolve_prevout( - block: &Block, - op: OutPoint, - inp: &bitcoin::TxIn, - prev_fk_hint: Option, - txid_index: &TxidMap, - spend_ti: usize, - batch_parents: &rbitcoin_query::BatchParents, - bip16: bool, - witness: bool, - need_script_buf: bool, - wire: Option<&Arc>, - acc: &mut AsmPrevoutAcc, -) -> Result { - let prev_txid = op.txid.to_byte_array(); - - if let Some(&pj) = txid_index.get(&prev_txid) { - if pj < spend_ti { - let tx = block.txdata.get(pj).ok_or(ConsensusError::MissingPrevout)?; - let v = op.vout as usize; - let o = tx.output.get(v).ok_or(ConsensusError::MissingPrevout)?; - // Core never puts this script in the coin view. The returned - // `txout` is often empty; the wire output holds the real bytes. - if crate::policy::is_unspendable(o.script_pubkey.as_bytes()) { - return Err(ConsensusError::MissingPrevout); - } - acc.in_n = acc.in_n.saturating_add(1); - acc.same_n = acc.same_n.saturating_add(1); - let shared = if need_script_buf { - wire.map(|_| rbitcoin_query::SharedPrevoutScript::Wire { - tx_index: pj as u32, - vout: op.vout, - }) - } else { - None - }; - return Ok(ResolvedPrevout { - txout: if shared.is_some() || !need_script_buf { - TxOut { - value: o.value, - script_pubkey: ScriptBuf::new(), - } - } else { - o.clone() - }, - shared, - input_sigops: prevout_spk_sigops(inp, o.script_pubkey.as_bytes(), bip16, witness), - create_fk: rbitcoin_primitives::Fk::NULL, - }); - } - } - - // Batch pin first (no TxRecord clone — A3). Pin identity/vout misses are - // hard invariants (load must fill schema-13 identity + denserels). - if let Some(prev_fk) = prev_fk_hint { - if let Some((pinned, parent_txid)) = batch_parents.share_parent_prevout(prev_fk, op.vout) { - if parent_txid != prev_txid { - acc.cold_txid_mismatch_n = acc.cold_txid_mismatch_n.saturating_add(1); - #[cfg(test)] - confirm_phase_stats::tl_note_cold_why_txid_mismatch(); - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: pin parent create identity mismatch wire prev_txid", - ))); - } - let (value, script) = pinned.parts().ok_or(ConsensusError::Store( - rbitcoin_store::StoreError::Corrupt( - "invariant: pin incomplete outs for spent parent vout", - ), - ))?; - // Same rule as the same-block arm. `txout.script_pubkey` below is - // empty; `script` is the archived bytes, including when scripts - // are not executed. - if crate::policy::is_unspendable(script) { - return Err(ConsensusError::MissingPrevout); - } - let input_sigops = prevout_spk_sigops(inp, script, bip16, witness); - acc.in_n = acc.in_n.saturating_add(1); - acc.batch_n = acc.batch_n.saturating_add(1); - #[cfg(test)] - confirm_phase_stats::tl_note_batch_hit(); - return Ok(ResolvedPrevout { - txout: TxOut { - value: Amount::from_sat(value), - script_pubkey: ScriptBuf::new(), - }, - shared: need_script_buf.then_some(pinned), - input_sigops, - create_fk: prev_fk, - }); - } else if batch_parents.contains(prev_fk) { - acc.cold_vout_miss_n = acc.cold_vout_miss_n.saturating_add(1); - #[cfg(test)] - confirm_phase_stats::tl_note_cold_why_vout_miss(); - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: pin incomplete outs for spent parent vout", - ))); - } - } - - Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: lookup stage miss (assemble parent create_fk)", - ))) -} - fn is_anyone_can_spend(script: &Script) -> bool { crate::script::is_anyone_can_spend(script) } -pub use rbitcoin_query::TxPrecompute; +mod facts; +pub use facts::{ + add_fee, add_sigops, add_value_in, fee_from_values, fee_sat, header_gap_on_chain, + median_or_bad_header, reject_assemble_block_shape, reject_batch_starts_at, reject_bits, + reject_carried_header_hash, reject_confirm_batch, reject_csv_create_height, + reject_duplicate_input, reject_empty_confirm_batch, reject_genesis_coinbase_spend, + reject_non_final, reject_prev_link, reject_prevout_spent, reject_same_block_create, + reject_sequence_locks, reject_spend_cursor, reject_tx_without_inputs, + reject_tx_without_outputs, reject_txid_count, reject_unresolved_spend, + reject_unspendable_prevout, reject_vout_past_n_out, reject_witness_before_segwit, + require_linked_parent, require_next_work, require_retarget_first, require_same_block_output, + unresolved_parent_create, +}; -#[cfg(test)] -mod overlay_meta_skip_tests { - use super::*; - use rbitcoin_primitives::Fk; - - fn spends(rows: &[(u32, Fk, Fk, u32)]) -> Vec<([u8; 32], u32, Fk, Fk, u32)> { - rows.iter() - .map(|&(vout, sfk, cfk, vin)| ([0u8; 32], vout, sfk, cfk, vin)) - .collect() - } - - #[test] - fn overlay_meta_skip_omits_matching_abs() { - let wave = ClassAWave::new(vec![Fk(10)]); - let mut map = FkMap::default(); - map.insert(Fk(10), (5, false)); - let run = RunCreateHeight::Map(map); - assert_eq!(run.create(Fk(10)), Some((5, false))); - let spends = spends(&[(0, Fk(11), Fk(10), 0)]); - let mut scratch = StructuralScratch::default(); - fill_overlay_skip(&spends, &wave, &mut scratch); - assert!(overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip)); - assert!(!overlay_meta_is_skip(10, 0, Fk(11), 1, &scratch.skip)); - assert!(!overlay_meta_is_skip(10, 1, Fk(11), 0, &scratch.skip)); - assert!(!overlay_meta_is_skip(99, 0, Fk(11), 0, &scratch.skip)); - } - - #[test] - fn overlay_meta_skip_keeps_conflicting_spender_on_disk_list() { - let wave = ClassAWave::new(vec![Fk(10)]); - let mut map = FkMap::default(); - map.insert(Fk(10), (5, false)); - let run = RunCreateHeight::Map(map); - assert_eq!(run.create(Fk(10)), Some((5, false))); - let spends = spends(&[(0, Fk(11), Fk(10), 0), (0, Fk(12), Fk(10), 0)]); - let mut scratch = StructuralScratch::default(); - fill_overlay_skip(&spends, &wave, &mut scratch); - assert!(!overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip)); - assert!(!overlay_meta_is_skip(10, 0, Fk(12), 0, &scratch.skip)); - } - - #[test] - fn overlay_meta_skip_historical_create_stays_on_disk() { - let wave = ClassAWave::default(); - let spends = spends(&[(0, Fk(11), Fk(10), 0)]); - let mut scratch = StructuralScratch::default(); - fill_overlay_skip(&spends, &wave, &mut scratch); - assert!(!overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip)); - } -} +pub use crate::TxPrecompute; #[cfg(test)] -mod run_create_height_tests { - use super::RunCreateHeight; - use rbitcoin_primitives::Fk; - - fn fk(id: u64) -> Fk { - Fk(id) - } +mod fact_reject_tests { + use super::{reject_bip30_unspent_prior, reject_one_create}; + use crate::ConsensusError; #[test] - fn run_create_height_span_misses_gaps() { - let one = [fk(10), fk(11)]; - let idx = RunCreateHeight::from_blocks([(7u32, one.as_slice())]); - assert!(matches!(idx, RunCreateHeight::Spans(_))); - assert_eq!(idx.get(fk(10)), Some(7)); - assert_eq!(idx.get(fk(11)), Some(7)); - assert_eq!(idx.get(fk(9)), None); - assert_eq!(idx.get(fk(12)), None); - - let low = [fk(10), fk(11)]; - let high = [fk(20), fk(21)]; - let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, high.as_slice())]); - assert_eq!(idx.get(fk(15)), None); - assert_eq!(idx.get(fk(10)), Some(1)); - assert_eq!(idx.get(fk(21)), Some(2)); - - let gapped = [fk(1), fk(2), fk(4)]; - let idx = RunCreateHeight::from_blocks([(3u32, gapped.as_slice())]); - assert!(matches!(idx, RunCreateHeight::Map(_))); - assert_eq!(idx.get(fk(3)), None); - assert_eq!(idx.get(fk(1)), Some(3)); - assert_eq!(idx.get(fk(2)), Some(3)); - assert_eq!(idx.get(fk(4)), Some(3)); - } + fn bip30_and_immature_coinbase_reject_from_facts() { + assert!(reject_bip30_unspent_prior(2, 2, true).is_ok()); + let bip30 = reject_bip30_unspent_prior(5, 1, true).unwrap_err(); + assert!( + matches!(bip30, ConsensusError::BadTx(s) if s.contains("BIP30")), + "{bip30:?}" + ); + assert!(reject_bip30_unspent_prior(5, 1, false).is_ok()); - #[test] - fn run_create_height_marks_each_block_first_fk_coinbase() { - let low = [fk(10), fk(11)]; - let high = [fk(12), fk(13)]; - let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, high.as_slice())]); - assert!(matches!(idx, RunCreateHeight::Spans(_))); - assert_eq!(idx.create(fk(10)), Some((1, true))); - assert_eq!(idx.create(fk(11)), Some((1, false))); - assert_eq!(idx.create(fk(12)), Some((2, true))); - assert_eq!(idx.create(fk(13)), Some((2, false))); - - let gapped = [fk(20), fk(22)]; - let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, gapped.as_slice())]); - assert!(matches!(idx, RunCreateHeight::Map(_))); - assert_eq!(idx.create(fk(10)), Some((1, true))); - assert_eq!(idx.create(fk(11)), Some((1, false))); - assert_eq!(idx.create(fk(20)), Some((2, true))); - assert_eq!(idx.create(fk(22)), Some((2, false))); - assert_eq!(idx.create(fk(21)), None); + let missing = reject_one_create(10, 100, None, false).unwrap_err(); + assert!( + matches!(missing, ConsensusError::BadTx(s) if s.contains("missingorspent")), + "{missing:?}" + ); + let forward = reject_one_create(10, 100, Some(11), false).unwrap_err(); + assert!( + matches!(forward, ConsensusError::MissingPrevout), + "{forward:?}" + ); + let immature = reject_one_create(10, 100, Some(1), true).unwrap_err(); + assert!( + matches!(immature, ConsensusError::BadTx(s) if s.contains("immature")), + "{immature:?}" + ); + assert!(reject_one_create(101, 100, Some(1), true).is_ok()); } } @@ -2715,5 +1372,3 @@ mod block_866342; mod finality_tests; #[cfg(test)] mod sigop_cost_tests; -#[cfg(test)] -mod structure_rule_tests; diff --git a/crates/rbitcoin-consensus/src/error.rs b/crates/rbitcoin-consensus/src/error.rs index 4ce4acf58e..7b45cece20 100644 --- a/crates/rbitcoin-consensus/src/error.rs +++ b/crates/rbitcoin-consensus/src/error.rs @@ -1,10 +1,11 @@ use rbitcoin_primitives::ScriptNumError; -use rbitcoin_store::StoreError; use std::fmt; #[derive(Debug)] pub enum ConsensusError { - Store(StoreError), + /// Programmer invariant inside a rule helper. Not a block reject and not a + /// store fault. The confirm pipeline must not cache it as a failed block. + Internal(&'static str), BadHeader(&'static str), BadBlock(&'static str), BadTx(&'static str), @@ -22,7 +23,7 @@ pub enum ConsensusError { impl fmt::Display for ConsensusError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ConsensusError::Store(e) => write!(f, "store: {e}"), + ConsensusError::Internal(s) => write!(f, "{s}"), ConsensusError::BadHeader(s) => write!(f, "bad header: {s}"), ConsensusError::BadBlock(s) => write!(f, "bad block: {s}"), ConsensusError::BadTx(s) => write!(f, "bad transaction: {s}"), @@ -37,14 +38,7 @@ impl fmt::Display for ConsensusError { } } -impl std::error::Error for ConsensusError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - ConsensusError::Store(e) => Some(e), - _ => None, - } - } -} +impl std::error::Error for ConsensusError {} impl From for ConsensusError { fn from(e: ScriptNumError) -> Self { @@ -52,27 +46,6 @@ impl From for ConsensusError { } } -impl From for ConsensusError { - fn from(e: StoreError) -> Self { - // Peer / untrusted blocks can hit archive stamp when a parent is simply - // missing (invalid block). That is consensus MissingPrevout, not store - // corruption — see docs/external_findings/002-store-corrupt-record-on-invalid-block.md. - match e { - StoreError::Cancelled(_) => ConsensusError::Cancelled, - StoreError::Corrupt(m) if m.contains("parent create_fk unresolved") => { - ConsensusError::MissingPrevout - } - other => ConsensusError::Store(other), - } - } -} - -impl ConsensusError { - pub fn is_uring_session_fault(&self) -> bool { - matches!(self, ConsensusError::Store(se) if se.is_uring_session_fault()) - } -} - /// Core `SCRIPT_VERIFY_*` parenthetical for `*-script-verify-flag-failed (…)`. pub fn script_flag_paren(token: &str) -> &str { let inner = token.split(" txid=").next().unwrap_or(token); @@ -147,35 +120,6 @@ pub fn block_reject_log_line(hash: impl std::fmt::Display, reason: &str) -> Stri mod tests { use super::*; - #[test] - fn uring_session_fault_covers_leftover_and_submit() { - for m in [ - "invariant: io_uring leftover cqe", - "io_uring submit failed", - "invariant: io_uring undrained", - ] { - let e = ConsensusError::Store(StoreError::Corrupt(m)); - assert!(e.is_uring_session_fault(), "{m}"); - } - assert!(!ConsensusError::Store(StoreError::NotFound).is_uring_session_fault()); - assert!(!ConsensusError::PrevoutSpent.is_uring_session_fault()); - } - - #[test] - fn archive_unresolved_parent_is_missing_prevout_not_corrupt() { - let e = ConsensusError::from(StoreError::Corrupt( - "archive: parent create_fk unresolved (contiguous batch required)", - )); - assert!(matches!(e, ConsensusError::MissingPrevout), "got {e:?}"); - assert!(!e.to_string().contains("corrupt")); - } - - #[test] - fn from_store_error() { - let e: ConsensusError = StoreError::NotFound.into(); - assert!(matches!(e, ConsensusError::Store(StoreError::NotFound))); - } - #[test] fn block_reject_reason_bip113_and_bip112_needles() { assert_eq!( diff --git a/crates/rbitcoin-consensus/src/header.rs b/crates/rbitcoin-consensus/src/header.rs index 8eb7f8f367..45c67bc0f2 100644 --- a/crates/rbitcoin-consensus/src/header.rs +++ b/crates/rbitcoin-consensus/src/header.rs @@ -3,65 +3,87 @@ use crate::params::{check_genesis_hash, ChainParams}; use bitcoin::block::Header; use bitcoin::hashes::Hash; use bitcoin::{CompactTarget, Target}; +pub use rbitcoin_primitives::median_time_past_times; use rbitcoin_primitives::Height; -use rbitcoin_query::Query; -/// Validate header linkage, MTP, difficulty bits, and proof-of-work. -pub fn validate_header( - query: &Query, - params: &ChainParams, - height: Height, - header: &Header, -) -> Result<(), ConsensusError> { - validate_header_hashed( - query, - params, - height, - header, - header.block_hash().to_byte_array(), - ) +/// Parent header facts for [`check_header`]. Copied so a height above zero can +/// use the same parent for linkage and for the work calculation. +#[derive(Clone, Copy)] +pub struct ParentHeader { + pub hash: [u8; 32], + pub time: u32, + pub bits: CompactTarget, } -/// [`validate_header`] using a caller-computed header hash (lookup already hashed). -pub(crate) fn validate_header_hashed( - query: &Query, +/// Header rules from the header bytes plus caller facts. No datadir. +/// +/// `mtp_times` ends at the parent (up to 11 timestamps). `period_first_time` +/// is the header `interval` blocks before a retarget height. `bits_at` supplies +/// earlier `nBits` for the testnet min-difficulty walk. +/// +/// On success the value is that parent median, sorted once. Genesis has no +/// parent window and returns `0`. +#[allow(clippy::too_many_arguments)] // each argument is one caller fact +pub fn check_header( params: &ChainParams, height: Height, header: &Header, hash: [u8; 32], -) -> Result<(), ConsensusError> { + parent: Option, + mtp_times: &[u32], + period_first_time: Option, + mut bits_at: impl FnMut(u32) -> Option, +) -> Result { let hash_bh = bitcoin::BlockHash::from_byte_array(hash); - if height.0 == 0 { if !check_genesis_hash(params, hash_bh) { return Err(ConsensusError::BadHeader("genesis hash mismatch")); } - } else { - let prev_height = Height(height.0 - 1); - let (_prev_fk, prev_rec) = query - .header_at_height(prev_height)? - .ok_or(ConsensusError::BadPrev)?; - if prev_rec.hash != header.prev_blockhash.to_byte_array() { - return Err(ConsensusError::BadPrev); + let expected = crate::params::genesis_block(params).header.bits; + if header.bits != expected { + return Err(ConsensusError::BadHeader("incorrect proof of work bits")); } + pow_hash_meets_target(hash, header.bits, params.pow_limit)?; + return Ok(0); + } - let mtp = median_time_past(query, prev_height)?; - if header.time <= mtp { - return Err(ConsensusError::BadHeader("timestamp <= median-time-past")); - } - check_header_version_and_future_time(params, height, header)?; + let parent = parent.ok_or(ConsensusError::BadPrev)?; + if parent.hash != header.prev_blockhash.to_byte_array() { + return Err(ConsensusError::BadPrev); } + let mtp = median_time_past_times(mtp_times) + .map_err(|_| ConsensusError::BadHeader("empty median time"))?; + if header.time <= mtp { + return Err(ConsensusError::BadHeader("timestamp <= median-time-past")); + } + check_header_version_and_future_time(params, height, header)?; - let expected_bits = expected_next_bits(query, params, height, header.time)?; - if header.bits != expected_bits { + let interval = params.difficulty_adjustment_interval(); + if interval > 0 + && height.0.is_multiple_of(interval) + && !params.no_pow_retargeting() + && period_first_time.is_none() + { + return Err(ConsensusError::BadHeader("missing retarget first header")); + } + let expected = next_work_bits( + params, + height.0, + parent.bits, + parent.time, + header.time, + period_first_time, + &mut bits_at, + ) + .ok_or(ConsensusError::BadPrev)?; + if header.bits != expected { return Err(ConsensusError::BadHeader("incorrect proof of work bits")); } - - pow_hash_meets_target(hash, header.bits, params.pow_limit) + pow_hash_meets_target(hash, header.bits, params.pow_limit)?; + Ok(mtp) } -/// Contextual checks when the parent is a known header, not necessarily on the -/// best chain (`header_at_height` would miss). +/// Validate header linkage, MTP, difficulty bits, and proof-of-work. pub fn validate_header_on_parent( params: &ChainParams, height: Height, @@ -84,7 +106,7 @@ pub fn validate_header_on_parent( } /// POW vs a **caller-computed** header hash (no second SHA256d). -pub(crate) fn pow_hash_meets_target( +pub fn pow_hash_meets_target( hash: [u8; 32], bits: CompactTarget, pow_limit: Target, @@ -103,7 +125,7 @@ pub(crate) fn pow_hash_meets_target( /// /// Core `ContextualCheckBlockHeader`. Assemble uses this instead of a second /// [`validate_header`] (MTP walk + header rehash). -pub(crate) fn check_header_version_and_future_time( +pub fn check_header_version_and_future_time( params: &ChainParams, height: Height, header: &Header, @@ -123,104 +145,12 @@ pub(crate) fn check_header_version_and_future_time( Ok(()) } -/// Median timestamp of up to 11 blocks ending at `height` (inclusive). -/// -/// **Confirm load path:** BIP68/BIP113 run during multi-block assemble while -/// mid-batch heights are not yet in `confirmed[]`. Prefer the durable confirmed -/// chain, then load-stage header plans (same hybrid as [`crate::confirm_run`] -/// header MTP). Heights still above tip with no plan are retryable load -/// incomplete — not permanent `BadPrev` (that silently split batches to n=1). -pub fn median_time_past(query: &Query, height: Height) -> Result { - if let Some((n, buf)) = query.store().mtp_times_at(height) { - return median_time_past_times(&buf[..n as usize]) - .map_err(|_| ConsensusError::BadHeader("empty median time")); - } - let mut times = Vec::with_capacity(11); - let start = height.0.saturating_sub(10); - let tip = query.tip_height().map(|h| h.0).unwrap_or(0); - for h in start..=height.0 { - if let Some((_fk, rec)) = query.header_at_height(Height(h))? { - times.push(rec.timestamp); - continue; - } - if let Some(plan) = query.confirm_parent_cache().get_header_plan(h) { - times.push(plan.header_rec.timestamp); - continue; - } - if h > tip { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "confirm: load incomplete (parent header plan missing above tip)", - ))); - } - return Err(ConsensusError::BadPrev); - } - median_time_past_times(×).map_err(|_| ConsensusError::BadHeader("empty median time")) -} - -/// MTP from the confirmed chain only (write structural). Tip-ahead heights -/// must be carried on [`crate::confirm_run`] `Prepared::prev_mtp`. -pub fn median_time_past_store(query: &Query, height: Height) -> Result { - if let Some((n, buf)) = query.store().mtp_times_at(height) { - return median_time_past_times(&buf[..n as usize]) - .map_err(|_| ConsensusError::BadHeader("empty median time")); - } - let mut times = Vec::with_capacity(11); - let start = height.0.saturating_sub(10); - for h in start..=height.0 { - if let Some((_fk, rec)) = query.header_at_height(Height(h))? { - times.push(rec.timestamp); - continue; - } - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "confirm: write MTP missing confirmed header (carry prev_mtp)", - ))); - } - median_time_past_times(×).map_err(|_| ConsensusError::BadHeader("empty median time")) -} - -pub use rbitcoin_primitives::median_time_past_times; - -/// Expected `nBits` for a new header at `height`. +/// Expected `nBits` for the header at `height`, from the parent and the period start. /// -/// `header_time` is the candidate block's timestamp (Core `pblock->GetBlockTime()`). -pub fn expected_next_bits( - query: &Query, - params: &ChainParams, - height: Height, - header_time: u32, -) -> Result { - if height.0 == 0 { - let g = crate::params::genesis_block(params); - return Ok(g.header.bits); - } - - let prev_height = Height(height.0 - 1); - let (_fk, prev_rec) = query - .header_at_height(prev_height)? - .ok_or(ConsensusError::BadPrev)?; - let prev_bits = CompactTarget::from_consensus(prev_rec.bits); - let period_first = period_first_time(query, params, height.0)?; - next_work_bits( - params, - height.0, - prev_bits, - prev_rec.timestamp, - header_time, - period_first, - |h| { - header_bits_at(query, Height(h)) - .ok() - .map(CompactTarget::from_consensus) - }, - ) - .ok_or(ConsensusError::BadPrev) -} - -/// Difficulty for the header at `height`, from the parent and the period start. -/// -/// `period_first_time` is the timestamp of the header at `height - interval` -/// when `height` is a retarget boundary. `bits_at` supplies earlier `nBits` -/// for the testnet min-difficulty walk. `None` means an ancestor is missing. +/// `period_first_time` is the timestamp `interval` blocks earlier when `height` +/// is a retarget boundary. `bits_at` supplies earlier `nBits` for the testnet +/// min-difficulty walk. `None` means an ancestor is missing. Median time is +/// [`median_time_past_times`], not this function. pub fn next_work_bits( params: &ChainParams, height: u32, @@ -256,21 +186,6 @@ pub fn next_work_bits( )) } -fn period_first_time( - query: &Query, - params: &ChainParams, - height: u32, -) -> Result, ConsensusError> { - let interval = params.difficulty_adjustment_interval(); - if interval == 0 || !height.is_multiple_of(interval) || params.no_pow_retargeting() { - return Ok(None); - } - let (_fk, first_rec) = query - .header_at_height(Height(height - interval))? - .ok_or(ConsensusError::BadHeader("missing retarget first header"))?; - Ok(Some(first_rec.timestamp)) -} - fn min_diff_bits( params: &ChainParams, height: u32, @@ -316,25 +231,10 @@ pub fn difficulty_from_bits(bits: u32) -> f64 { ddiff } -fn header_bits_at(query: &Query, height: Height) -> Result { - if let Some((_fk, rec)) = query.header_at_height(height)? { - return Ok(rec.bits); - } - if let Some(plan) = query.confirm_parent_cache().get_header_plan(height.0) { - return Ok(plan.header_rec.bits); - } - Err(ConsensusError::BadPrev) -} - #[cfg(test)] mod median_time_past_tests { use super::*; use bitcoin::block::Version; - use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::testutil::FixtureChain; - use rbitcoin_query::{Query, TxApply}; - use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, TxRecord}; - use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn max_target_bits_are_difficulty_one() { @@ -431,228 +331,6 @@ mod median_time_past_tests { assert_eq!(median_time_past_times(&[1, 2, 3, 4]).unwrap(), 3); } - fn temp_q() -> (std::path::PathBuf, Query) { - let n = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let dir = std::env::temp_dir().join(format!("rbitcoin-hdr-mtp-{n}")); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - let q = Query::open_or_create_tiny(dir.join("store")).unwrap(); - (dir, q) - } - - /// Write-gate-safe synthetic coinbase: non-null `prev_fk` must commit `parent_hash`. - fn coinbase(h: u32, prev: Fk, parent_hash: Option<[u8; 32]>) -> (HeaderRecord, TxApply) { - let version = 1; - let timestamp = 1_000 + h * 10; - let bits = 0x207fffff; - let nonce = h; - let mut merkle = [0u8; 32]; - merkle[0..4].copy_from_slice(&h.to_le_bytes()); - merkle[4] = 0xcd; - let hash = match parent_hash { - None => merkle, - Some(ph) => { - rbitcoin_store::block_header_hash(version, &ph, &merkle, timestamp, bits, nonce) - } - }; - let header = HeaderRecord { - prev_fk: prev, - version, - timestamp, - bits, - nonce, - merkle_root: merkle, - hash, - size: 0, - weight: 0, - }; - let mut txid = [0u8; 32]; - txid[0..4].copy_from_slice(&h.to_le_bytes()); - txid[31] = 0xcb; - let ta = TxApply { - tx: TxRecord { - txid, - version: 1, - locktime: 0, - input_start_fk: Fk::NULL, - input_count: 1, - output_start_fk: Fk::NULL, - output_count: 1, - }, - inputs: vec![InputRecord { - prev_txid: [0u8; 32], - parent: rbitcoin_store::InputParent::Coinbase, - sequence: u32::MAX, - script_sig: vec![h as u8], - witness: vec![], - }], - outputs: vec![OutputRecord::unspent(50, vec![0x51])], - }; - (header, ta) - } - - #[test] - fn mtp_from_confirmed_chain_and_missing_above_tip() { - let (dir, q) = temp_q(); - let mut prev = Fk::NULL; - let mut parent_hash: Option<[u8; 32]> = None; - for h in 0..3u32 { - let (hdr, ta) = coinbase(h, prev, parent_hash); - parent_hash = Some(hdr.hash); - prev = q.connect_block(Height(h), &hdr, &[ta]).unwrap(); - } - let mtp = median_time_past(&q, Height(2)).unwrap(); - // times: 1000, 1010, 1020 → middle 1010 - assert_eq!(mtp, 1010); - let (n, buf) = q - .store() - .mtp_times_at(Height(2)) - .expect("ring covers confirmed tip"); - assert_eq!(n, 3); - assert_eq!(median_time_past_times(&buf[..3]).unwrap(), 1010); - - // Height above tip with no plan → incomplete load error (not BadPrev). - let err = median_time_past(&q, Height(5)).unwrap_err(); - assert!( - format!("{err}").contains("load incomplete"), - "above tip must be the retryable load error, got {err:?}" - ); - - let (empty_dir, empty) = temp_q(); - let err = median_time_past(&empty, Height(0)).unwrap_err(); - assert!( - matches!(err, ConsensusError::BadPrev), - "a missing header at the tip is BadPrev, got {err:?}" - ); - let _ = std::fs::remove_dir_all(&empty_dir); - - // expected_next_bits: height 0 + regtest no-retarget. - let params = ChainParams::regtest(); - let gbits = expected_next_bits(&q, ¶ms, Height(0), 0).unwrap(); - assert_eq!(gbits, crate::params::genesis_block(¶ms).header.bits); - let b1 = expected_next_bits(&q, ¶ms, Height(1), 0).unwrap(); - let (_fk, rec0) = q.header_at_height(Height(0)).unwrap().unwrap(); - assert_eq!(b1.to_consensus(), rec0.bits); - - // Bad prev header height. - assert!(expected_next_bits(&q, ¶ms, Height(99), 0).is_err()); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn sequential_tip_mtp_matches_ring_and_survives_pop() { - let (dir, q) = temp_q(); - let mut prev = Fk::NULL; - let mut parent_hash: Option<[u8; 32]> = None; - let mut times = Vec::new(); - for h in 0..12u32 { - let (hdr, ta) = coinbase(h, prev, parent_hash); - times.push(hdr.timestamp); - parent_hash = Some(hdr.hash); - prev = q.connect_block(Height(h), &hdr, &[ta]).unwrap(); - } - let want11 = median_time_past_times(×[1..]).unwrap(); - assert_eq!(median_time_past_store(&q, Height(11)).unwrap(), want11); - assert_eq!(median_time_past(&q, Height(11)).unwrap(), want11); - let (n, buf) = q.store().mtp_times_at(Height(11)).expect("ring at tip"); - assert_eq!(n, 11); - assert_eq!(median_time_past_times(&buf[..11]).unwrap(), want11); - assert!( - q.store().mtp_times_at(Height(5)).is_none(), - "historical MTP is not the tip ring" - ); - assert_eq!( - median_time_past_store(&q, Height(5)).unwrap(), - median_time_past_times(×[0..=5]).unwrap() - ); - - q.disconnect_tip().unwrap(); - let want10 = median_time_past_times(×[0..=10]).unwrap(); - assert_eq!(median_time_past_store(&q, Height(10)).unwrap(), want10); - let (n, buf) = q - .store() - .mtp_times_at(Height(10)) - .expect("ring rebuilt after pop"); - assert_eq!(median_time_past_times(&buf[..n as usize]).unwrap(), want10); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn validate_header_genesis_and_bad_prev() { - let (dir, q) = temp_q(); - let params = ChainParams::regtest(); - let g = crate::params::genesis_block(¶ms); - // Wrong genesis hash at height 0. - let mut bad = g.header; - bad.nonce ^= 1; - let err = validate_header(&q, ¶ms, Height(0), &bad).unwrap_err(); - assert!(matches!(err, ConsensusError::BadHeader(_)), "{err:?}"); - - // Synthetic tip for prev linkage (child hash commits to parent). - let (h0, ta0) = coinbase(0, Fk::NULL, None); - let prev = q.connect_block(Height(0), &h0, &[ta0]).unwrap(); - let (h1, ta1) = coinbase(1, prev, Some(h0.hash)); - q.connect_block(Height(1), &h1, &[ta1]).unwrap(); - - // Header with wrong prev hash at height 1. - let mut hdr = g.header; - hdr.prev_blockhash = bitcoin::BlockHash::from_byte_array([0xee; 32]); - hdr.time = 2_000; - hdr.bits = bitcoin::CompactTarget::from_consensus(0x207fffff); - let err = validate_header(&q, ¶ms, Height(1), &hdr).unwrap_err(); - assert!( - matches!( - err, - ConsensusError::BadPrev - | ConsensusError::BadHeader(_) - | ConsensusError::BadVersion(_) - ), - "{err:?}" - ); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn testnet_min_difficulty_after_20_minute_gap() { - let (dir, q) = temp_q(); - let params = ChainParams::testnet(); - assert!(params.allow_min_difficulty_blocks()); - let (h0, ta0) = coinbase(0, Fk::NULL, None); - let prev_fk = q.connect_block(Height(0), &h0, &[ta0]).unwrap(); - let limit = params.pow_limit.to_compact_lossy(); - assert_ne!(CompactTarget::from_consensus(h0.bits), limit); - - let spacing = params.btc.pow_target_spacing as u32; - let gap = - expected_next_bits(&q, ¶ms, Height(1), h0.timestamp + 2 * spacing + 1).unwrap(); - assert_eq!(gap, limit); - let eq_boundary = - expected_next_bits(&q, ¶ms, Height(1), h0.timestamp + 2 * spacing).unwrap(); - assert_eq!(eq_boundary.to_consensus(), h0.bits); - - let (mut h1, ta1) = coinbase(1, prev_fk, Some(h0.hash)); - h1.timestamp = h0.timestamp + 2 * spacing + 1; - h1.bits = limit.to_consensus(); - h1.hash = rbitcoin_store::block_header_hash( - h1.version, - &h0.hash, - &h1.merkle_root, - h1.timestamp, - h1.bits, - h1.nonce, - ); - q.connect_block(Height(1), &h1, &[ta1]).unwrap(); - let walked = expected_next_bits(&q, ¶ms, Height(2), h1.timestamp + 100).unwrap(); - assert_eq!(walked.to_consensus(), h0.bits); - - let _ = std::fs::remove_dir_all(&dir); - } - #[test] fn next_work_bits_walks_back_past_min_difficulty() { let params = ChainParams::testnet(); @@ -703,6 +381,170 @@ mod median_time_past_tests { assert_eq!(kept, full); } + #[test] + fn check_header_rejects_without_a_store() { + let params = ChainParams::mainnet(); + let genesis = crate::params::genesis_block(¶ms); + let mut bad = genesis.header; + bad.nonce = bad.nonce.wrapping_add(1); + let err = check_header( + ¶ms, + Height(0), + &bad, + bad.block_hash().to_byte_array(), + None, + &[], + None, + |_| None, + ) + .unwrap_err(); + assert!( + matches!(err, ConsensusError::BadHeader(s) if s.contains("genesis")), + "{err:?}" + ); + + let parent = ParentHeader { + hash: [1u8; 32], + time: genesis.header.time, + bits: genesis.header.bits, + }; + let mut child = genesis.header; + child.time = parent.time + 1; + let missing = check_header( + ¶ms, + Height(1), + &child, + [0xff; 32], + None, + &[parent.time], + None, + |_| None, + ) + .unwrap_err(); + assert!(matches!(missing, ConsensusError::BadPrev), "{missing:?}"); + + child.prev_blockhash = bitcoin::BlockHash::from_byte_array([2u8; 32]); + let bad_prev = check_header( + ¶ms, + Height(1), + &child, + [0xff; 32], + Some(parent), + &[parent.time], + None, + |_| None, + ) + .unwrap_err(); + assert!(matches!(bad_prev, ConsensusError::BadPrev), "{bad_prev:?}"); + + child.prev_blockhash = bitcoin::BlockHash::from_byte_array(parent.hash); + child.time = parent.time; + let mtp = check_header( + ¶ms, + Height(1), + &child, + [0xff; 32], + Some(parent), + &[parent.time], + None, + |_| None, + ) + .unwrap_err(); + assert!( + matches!(mtp, ConsensusError::BadHeader(s) if s.contains("median-time-past")), + "{mtp:?}" + ); + + child.time = parent.time + 1; + child.bits = CompactTarget::from_consensus(0x1d00_eeee); + let bits = check_header( + ¶ms, + Height(1), + &child, + [0xff; 32], + Some(parent), + &[parent.time], + None, + |_| None, + ) + .unwrap_err(); + assert!( + matches!(bits, ConsensusError::BadHeader(s) if s.contains("proof of work bits")), + "{bits:?}" + ); + + child.bits = parent.bits; + let pow = check_header( + ¶ms, + Height(1), + &child, + [0xff; 32], + Some(parent), + &[parent.time], + None, + |_| None, + ) + .unwrap_err(); + assert!(matches!(pow, ConsensusError::InvalidPow), "{pow:?}"); + + let interval = params.difficulty_adjustment_interval(); + let retarget = check_header( + ¶ms, + Height(interval), + &child, + [0xff; 32], + Some(parent), + &[parent.time], + None, + |_| Some(parent.bits), + ) + .unwrap_err(); + assert!( + matches!(retarget, ConsensusError::BadHeader(s) if s.contains("missing retarget")), + "{retarget:?}" + ); + } + + #[test] + fn check_header_returns_the_median_it_checked() { + let params = ChainParams::regtest(); + let genesis = crate::params::genesis_block(¶ms); + let genesis_mtp = check_header( + ¶ms, + Height(0), + &genesis.header, + genesis.block_hash().to_byte_array(), + None, + &[], + None, + |_| None, + ) + .unwrap(); + assert_eq!(genesis_mtp, 0); + + let parent_time = genesis.header.time; + let block = + crate::regtest_pad::mine_empty_regtest(genesis.block_hash(), parent_time + 1, 1); + let times = [parent_time, parent_time - 1, parent_time + 2]; + let mtp = check_header( + ¶ms, + Height(1), + &block.header, + block.block_hash().to_byte_array(), + Some(ParentHeader { + hash: genesis.block_hash().to_byte_array(), + time: parent_time, + bits: genesis.header.bits, + }), + ×, + None, + |_| None, + ) + .unwrap(); + assert_eq!(mtp, median_time_past_times(×).unwrap()); + assert_eq!(mtp, parent_time); + } + #[test] fn check_header_version_and_future_time_regtest() { let params = ChainParams::regtest(); diff --git a/crates/rbitcoin-query/src/id_map.rs b/crates/rbitcoin-consensus/src/id_map.rs similarity index 100% rename from crates/rbitcoin-query/src/id_map.rs rename to crates/rbitcoin-consensus/src/id_map.rs diff --git a/crates/rbitcoin-consensus/src/lib.rs b/crates/rbitcoin-consensus/src/lib.rs index 034ee66cc8..e6143c11ca 100644 --- a/crates/rbitcoin-consensus/src/lib.rs +++ b/crates/rbitcoin-consensus/src/lib.rs @@ -1,21 +1,19 @@ //! Block and transaction validation / confirmability. -mod block; +pub mod block; mod clock; -mod confirm_run; -mod convert; mod error; -mod header; -mod index_rows; -mod index_writebehind; +pub mod header; +mod id_map; mod milestone; mod params; pub mod policy; mod regtest_pad; mod script; -mod script_pool; +pub mod script_pool; mod signet; pub mod silent_payments; +mod tx_precompute; pub(crate) use block::ScriptCheckJob; @@ -75,6 +73,7 @@ pub fn verify_tx_scripts_with_flags( .unwrap_or(Err(ConsensusError::BadBlock("script worker disconnected"))) } +pub use block::validate_block_structure_hashed; pub use block::{ bip34_height_script, bip68_active_for_tx, block_has_witness, block_mutated_without_coinbase, block_subsidy, check_block_wire, is_final_tx, sequence_locks_satisfied, tx_sigop_cost, @@ -82,409 +81,28 @@ pub use block::{ ScriptVerifyFlags, ValidationContext, BIP16_EXCEPTION_MAINNET, MAX_BLOCK_TX_COUNT, MAX_BLOCK_WEIGHT, MIN_TX_WEIGHT, TAPROOT_EXCEPTION_MAINNET, }; -pub(crate) use block::{validate_block_structure_hashed, TxPrecompute}; pub use clock::{with_now, NodeClock}; -pub use convert::header_to_record; -pub(crate) use convert::{block_to_apply, block_to_apply_with_txids_prev}; pub use error::{block_reject_log_line, block_reject_reason, script_flag_paren, ConsensusError}; pub use header::{ - difficulty_from_bits, expected_next_bits, median_time_past, next_work_bits, validate_header, - validate_header_on_parent, -}; -pub use index_writebehind::{ - build_indexes_released, index_tip_entry, prepare_live_indexes, spawn_index_writebehind, - IndexTipEntry, INDEX_STARTUP_GAP_HEIGHTS, + check_header, difficulty_from_bits, next_work_bits, validate_header_on_parent, ParentHeader, }; +pub use id_map::{IdMap, OutPointHasher, OutPointSet, TxidHasher, TxidMap, TxidSet}; pub use milestone::{Milestone, MilestoneAnchor}; pub use params::{ default_milestone_anchor, default_milestone_height, default_min_chain_work_be, genesis_block, - ChainParams, + ChainParams, MAINNET_MILESTONE_HASH, TESTNET_MILESTONE_HASH, }; pub use policy::PolicyResult; pub use regtest_pad::{ grind_regtest_pow, mine_empty_regtest, mine_op_true_signet_paying, mine_regtest_paying, - pad_empty_from, prepare_regtest_candidate, regtest_coinbase, REGTEST_BLOCK_SPACING, - REGTEST_POW_BITS, + prepare_regtest_candidate, regtest_coinbase, REGTEST_BLOCK_SPACING, REGTEST_POW_BITS, }; +pub use script_pool::unpark_script_publisher; pub use signet::{ default_signet_challenge, signet_challenge_transactions, signet_magic, validate_signet_block_solution, }; -pub use silent_payments::{ - taproot_matches_scan, tweak_from_tx, tweaks_for_height, TaprootOut, TxTweak, -}; - -use bitcoin::hashes::Hash; -use bitcoin::Block; -use rbitcoin_primitives::{Fk, Height}; -use rbitcoin_query::{Query, TxApply, WirePlanNeed}; -use rbitcoin_store::HeaderRecord; -use std::sync::Arc; - -/// Test-only assemble cold-why / batch TLS (window meters live on Query). -#[cfg(test)] -pub mod confirm_phase_stats { - use std::cell::Cell; - - thread_local! { - static TL_COLD_WHY: Cell<(u64, u64, u64, u64)> = - const { Cell::new((0, 0, 0, 0)) }; - static TL_BATCH_N: Cell = const { Cell::new(0) }; - static TL_COLD_N: Cell = const { Cell::new(0) }; - } - - #[inline] - pub fn tl_note_cold_why_txid_mismatch() { - TL_COLD_WHY.with(|c| { - let (a, b, c2, d) = c.get(); - c.set((a, b, c2 + 1, d)); - }); - TL_COLD_N.with(|c| c.set(c.get() + 1)); - } - #[inline] - pub fn tl_note_cold_why_vout_miss() { - TL_COLD_WHY.with(|c| { - let (a, b, c2, d) = c.get(); - c.set((a, b, c2, d + 1)); - }); - TL_COLD_N.with(|c| c.set(c.get() + 1)); - } - #[inline] - pub fn tl_note_batch_hit() { - TL_BATCH_N.with(|c| c.set(c.get() + 1)); - } - #[inline] - pub fn sample_tl_assemble_cold_why_and_reset() -> (u64, u64, u64, u64) { - TL_COLD_WHY.with(|c| c.replace((0, 0, 0, 0))) - } - #[inline] - pub fn sample_tl_batch_cold_n_and_reset() -> (u64, u64) { - let b = TL_BATCH_N.with(|c| c.replace(0)); - let cold = TL_COLD_N.with(|c| c.replace(0)); - (b, cold) - } -} - -/// Confirm a contiguous tip-extension run of wire blocks (sync all stages). -/// -/// See [`confirm_wire_run`]: lookup → load → scripts → write. IBD uses the split -/// phases for pipeline overlap. -pub use confirm_run::{ - confirm_bq_resolve_wave_capped, confirm_scripts_phase, confirm_wire_load_from_plan, - confirm_wire_load_phase, confirm_wire_load_phase_pipelined, confirm_wire_lookup_stamp, - confirm_wire_run, confirm_wire_run_preverified, confirm_write_phase, - confirm_write_phase_or_return, drive_script_waves_with, finish_post_commit_hashes, - parent_missing_from_chain, replay_spend_annotations, take_wave_items_for_load, BqResolveWave, - ConfirmLoadOutcome, ConfirmScriptOutcome, LoadedBatch, PlanStampOutcome, ScriptOkBatch, - ScriptPreverified, WireBlockIn, WireLoadPipeline, BQ_RESOLVE_WAVE_MAX_BLOCKS, - BQ_RESOLVE_WAVE_MAX_INPUTS, +pub use silent_payments::{taproot_matches_scan, tweak_from_tx, TaprootOut, TxTweak}; +pub use tx_precompute::{ + block_wire_len_from_pres, decode_block_precomputes, pres_for_tip, TxPrecompute, }; - -/// Wake the IBD scripts publisher (`ibd-confirm`) after `scriptq` send or close. -pub use script_pool::unpark_script_publisher; - -/// Accept + archive + confirm in one step (genesis / tip extension / tests). -/// -/// **Same path as IBD confirm:** structure + header checks, then Class A -/// archive, then [`confirm_wire_run`] (lookup → load pin denserels → scripts → -/// structural → Class C → abs spend annotate). No separate -/// `put_spend_batch_by_create`. -/// -/// Idempotent when `height` is already confirmed for this block hash. -/// Full script verify (no mempool skip) — use -/// [`accept_and_connect_block_preverified`] on tip follow with a live mempool. -pub fn accept_and_connect_block( - query: &Query, - params: &ChainParams, - height: Height, - block: &Block, - milestone: Milestone, -) -> Result { - accept_and_connect_block_preverified( - query, - params, - height, - Arc::new(block.clone()), - milestone, - &ScriptPreverified::new(), - None, - ) -} - -/// Like [`accept_and_connect_block`], skipping script verify for `preverified` -/// txids (tip follow: live mempool after accept). Reorg disconnect stays outside. -pub fn accept_and_connect_block_preverified( - query: &Query, - params: &ChainParams, - height: Height, - block: Arc, - milestone: Milestone, - preverified: &ScriptPreverified, - pres: Option>, -) -> Result { - let hash = block.block_hash().to_byte_array(); - if let Some(h) = query.height_of_hash(&hash).map_err(ConsensusError::from)? { - if h == height { - if let Some((fk, _)) = query - .get_header_by_hash(&hash) - .map_err(ConsensusError::from)? - { - confirm_run::finish_post_commit(query, height.0, &hash)?; - return Ok(fk); - } - } - } - - // Unified height-ordered path: wire → lookup → load (pin+assemble) → scripts → - // write (Class A + structural + Class C + annotate). No archive-then-reload. - let fks = confirm_wire_run_preverified( - query, - params, - milestone, - &[(height, block, pres)], - preverified, - )?; - if let Some(fk) = fks.into_iter().next() { - return Ok(fk); - } - // Write skipped heights ≤ tip (idempotent race). A body we just ran - // through lookup/load must have a header — missing is an invariant, not - // a soft NotFound (inflated confirmed[] used to hit this on tip+1). - query - .get_header_by_hash(&hash) - .map_err(ConsensusError::from)? - .map(|(fk, _)| fk) - .ok_or(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: confirm write skipped but header missing", - ))) -} - -fn class_a_header_and_txids( - query: &Query, - params: &ChainParams, - block: &Block, -) -> Result<(HeaderRecord, Vec<[u8; 32]>), ConsensusError> { - let ctx = ValidationContext::archive_structure(params); - let txids = validate_block_structure_hashed(block, &ctx)?; - let hash = block.header.block_hash().to_byte_array(); - crate::header::pow_hash_meets_target(hash, block.header.bits, params.pow_limit)?; - let prev = block.header.prev_blockhash; - let prev_fk = if prev.to_byte_array() == [0u8; 32] { - Fk::NULL - } else { - query - .get_header_by_hash(prev.as_byte_array()) - .map_err(ConsensusError::from)? - .map(|(fk, _)| fk) - .ok_or(ConsensusError::BadPrev)? - }; - Ok((header_to_record(prev_fk, &block.header, hash), txids)) -} - -/// Class A only (no tip / Class C). Crash and `plan=None` tests. -/// -/// Not a production IBD API — confirm write uses `archive_plan_batch_from_wire` -/// + commit (wire `seqsigwit` is encoded from the block and plan edges). -pub fn commit_class_a_block( - query: &Query, - params: &ChainParams, - height: Height, - block: &Block, - milestone: Milestone, -) -> Result<(), ConsensusError> { - let _ = (height, milestone); - let (header, txids) = class_a_header_and_txids(query, params, block)?; - let fk = query.ensure_header(&header).map_err(ConsensusError::from)?; - let arc = Arc::new(block.clone()); - query - .archive_class_a_from_wire(&[(fk, &arc, txids.as_slice())]) - .map_err(ConsensusError::from)?; - Ok(()) -} - -/// Class A for a contiguous run in one plan (same-batch parent stamp). -/// -/// Use this instead of N×[`commit_class_a_block`] when later blocks spend -/// earlier unconfirmed creates. -pub fn commit_class_a_run( - query: &Query, - params: &ChainParams, - blocks: &[(Height, Block)], - milestone: Milestone, -) -> Result<(), ConsensusError> { - let _ = milestone; - let mut owned = Vec::with_capacity(blocks.len()); - for (_, block) in blocks { - let (header, txids) = class_a_header_and_txids(query, params, block)?; - let fk = query.ensure_header(&header).map_err(ConsensusError::from)?; - owned.push((fk, Arc::new(block.clone()), txids)); - } - let refs: Vec> = owned - .iter() - .map(|(fk, b, ids)| (*fk, b, ids.as_slice())) - .collect(); - query - .archive_class_a_from_wire(&refs) - .map_err(ConsensusError::from)?; - Ok(()) -} - -/// CPU-side prep for Class A archive. -pub fn prepare_block_for_archive( - query: &Query, - params: &ChainParams, - block: &Block, -) -> Result<(HeaderRecord, Vec), ConsensusError> { - let hash = block.block_hash().to_byte_array(); - if query - .is_block_archived(&hash) - .map_err(ConsensusError::from)? - { - // Standalone archive helper (not confirm pipeline): one hash pass here. - return block_to_apply(query, &block.header, &block.txdata); - } - prepare_block_for_archive_new(query, params, block) -} - -fn prepare_block_for_archive_new( - query: &Query, - params: &ChainParams, - block: &Block, -) -> Result<(HeaderRecord, Vec), ConsensusError> { - let (header, txids) = class_a_header_and_txids(query, params, block)?; - let (_, txs) = - block_to_apply_with_txids_prev(header.prev_fk, &block.header, &block.txdata, &txids)?; - Ok((header, txs)) -} - -#[cfg(test)] -mod coverage_tests { - use super::*; - use bitcoin::absolute::LockTime; - use bitcoin::block::Version; - use bitcoin::hashes::Hash; - use bitcoin::script::ScriptBuf; - use bitcoin::transaction::Version as TxVersion; - use bitcoin::{Amount, OutPoint, Sequence, Target, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_primitives::Height; - use rbitcoin_query::Query; - use std::path::PathBuf; - fn temp_store() -> (PathBuf, Query) { - let path = std::env::temp_dir().join(format!( - "rbitcoin-consensus-cov-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - std::fs::create_dir_all(&path).unwrap(); - let q = Query::open_or_create_tiny(&path).expect("open store"); - (path, q) - } - - #[test] - fn verify_tx_scripts_detached_acs_job() { - let tx = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: bitcoin::Txid::from_byte_array([1; 32]), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(1), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }], - }; - let prevouts = vec![TxOut { - value: Amount::from_sat(10), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }]; - crate::verify_tx_scripts_detached(prevouts.clone(), tx.clone()).unwrap(); - let job = ScriptCheckJob::new( - prevouts, - tx, - crate::block::ScriptVerifyFlags::buried(true, true, true, true, true), - ); - crate::block::verify_one_script_job(&job).unwrap(); - } - - #[test] - fn verify_tx_scripts_detached_forks_op_true_and_op_return() { - let spend = |spk: Vec| { - let tx = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: bitcoin::Txid::from_byte_array([1; 32]), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(1), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }], - }; - let prevouts = vec![TxOut { - value: Amount::from_sat(10), - script_pubkey: ScriptBuf::from_bytes(spk), - }]; - crate::verify_tx_scripts_detached_forks(prevouts, tx, true, true, true, true, true) - }; - spend(vec![0x51]).unwrap(); - assert!(spend(vec![0x6a]).is_err()); - } - - #[test] - fn regtest_connect_archive_and_confirm_path() { - let (path, q) = temp_store(); - let params = ChainParams::regtest(); - let ms = Milestone::height(1_000_000); - let genesis = genesis_block(¶ms); - let gen_fk = accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, ms).unwrap(); - - let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); - // prepare helpers stay (CPU-side); confirm is sole Class A. - let (_hr, _txs) = prepare_block_for_archive(&q, ¶ms, &b1).unwrap(); - accept_and_connect_block(&q, ¶ms, Height(1), &b1, ms).unwrap(); - // already-have prepare after connect keeps the parent fk - let (hr, _) = prepare_block_for_archive(&q, ¶ms, &b1).unwrap(); - assert_eq!(hr.prev_fk, gen_fk); - - let _ = std::fs::remove_dir_all(&path); - } - - #[test] - fn assemble_second_block_rejects_stale_nversion() { - let (path, q) = temp_store(); - let params = ChainParams::regtest(); - let ms = Milestone::height(1_000_000); - let genesis = genesis_block(¶ms); - accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, ms).unwrap(); - - let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); - let mut b2 = mine_empty_regtest(b1.block_hash(), b1.header.time + 600, 2); - b2.header.version = Version::from_consensus(1); - let target = Target::from_compact(b2.header.bits); - for nonce in 0..u32::MAX { - b2.header.nonce = nonce; - if b2.header.validate_pow(target).is_ok() { - break; - } - } - let err = - confirm_wire_run(&q, ¶ms, ms, &[(Height(1), b1), (Height(2), b2)]).unwrap_err(); - assert!(matches!(err, ConsensusError::BadVersion(1)), "{err:?}"); - let _ = std::fs::remove_dir_all(&path); - } -} diff --git a/crates/rbitcoin-consensus/src/milestone.rs b/crates/rbitcoin-consensus/src/milestone.rs index fae3b4ac8f..6356b5cf04 100644 --- a/crates/rbitcoin-consensus/src/milestone.rs +++ b/crates/rbitcoin-consensus/src/milestone.rs @@ -67,31 +67,6 @@ impl Milestone { } } -/// Confirm-path gate. Notes `milestone_gate_ns` (`ibd: perf_dbg milestone_us`). -pub(crate) fn skips_on_query( - milestone: Milestone, - query: &rbitcoin_query::Query, - height: u32, - block: &[u8; 32], -) -> bool { - let t = std::time::Instant::now(); - let skip = milestone.skips_scripts(height, block, |h| query.milestone_header_at(h)); - rbitcoin_query::note_confirm( - &query.confirm_stats().milestone_gate_ns, - t.elapsed().as_nanos() as u64, - ); - skip -} - -pub(crate) fn check_scripts( - milestone: Milestone, - query: &rbitcoin_query::Query, - height: u32, - block: &[u8; 32], -) -> bool { - !skips_on_query(milestone, query, height, block) -} - impl Default for Milestone { fn default() -> Self { Self::NONE diff --git a/crates/rbitcoin-consensus/src/params.rs b/crates/rbitcoin-consensus/src/params.rs index 4493e430f4..59682f2b8b 100644 --- a/crates/rbitcoin-consensus/src/params.rs +++ b/crates/rbitcoin-consensus/src/params.rs @@ -138,7 +138,7 @@ impl ChainParams { /// True for the genesis coinbase txid, which is never a coin. #[inline] - pub(crate) fn is_genesis_coinbase(&self, txid: &[u8; 32]) -> bool { + pub fn is_genesis_coinbase(&self, txid: &[u8; 32]) -> bool { *txid == self.genesis_coinbase_txid } @@ -435,7 +435,6 @@ pub fn genesis_block(params: &ChainParams) -> bitcoin::block::Block { #[cfg(test)] mod tests { use super::*; - use rbitcoin_primitives::Height; /// Core `SigNetParams` / Inquisition signet: soft forks at height 1 (not 0). #[test] @@ -603,173 +602,4 @@ mod tests { let mut main = ChainParams::mainnet(); assert!(main.apply_test_activation_height("csv", 102).is_err()); } - - /// Overlay must change **confirm** BIP68, not just the getter. - #[test] - fn overlay_csv_102_is_lax_at_101_strict_at_102() { - use bitcoin::absolute::LockTime; - use bitcoin::script::ScriptBuf; - use bitcoin::transaction::Version as TxVersion; - use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_query::Query; - use std::time::{SystemTime, UNIX_EPOCH}; - - let n = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let dir = std::env::temp_dir().join(format!("rbtc-overlay-csv-{n}")); - let q = Query::open_or_create_tiny(&dir).unwrap(); - let mut params = ChainParams::regtest(); - params.apply_test_activation_height("csv", 102).unwrap(); - let genesis = constants::genesis_block(Network::Regtest); - crate::accept_and_connect_block( - &q, - ¶ms, - Height::GENESIS, - &genesis, - crate::Milestone::NONE, - ) - .unwrap(); - let (tip, tip_time, cbs) = crate::pad_empty_from( - &q, - ¶ms, - genesis.block_hash(), - genesis.header.time, - 1, - 100, - 1, - ); - let spk = ScriptBuf::from_bytes(vec![0x51]); - let parent = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: cbs[0], - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(50_0000_0000 - 1_000), - script_pubkey: spk.clone(), - }], - }; - let b101 = - crate::mine_regtest_paying(tip, tip_time + 600, 101, spk.clone(), vec![parent.clone()]); - crate::accept_and_connect_block(&q, ¶ms, Height(101), &b101, crate::Milestone::NONE) - .expect("parent spend at 101"); - - let child = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: parent.compute_txid(), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::from_consensus(10), - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(50_0000_0000 - 2_000), - script_pubkey: spk.clone(), - }], - }; - let b102 = crate::mine_regtest_paying( - b101.block_hash(), - b101.header.time + 600, - 102, - spk.clone(), - vec![child], - ); - let err = crate::accept_and_connect_block( - &q, - ¶ms, - Height(102), - &b102, - crate::Milestone::NONE, - ) - .unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("not final") || msg.contains("nonfinal") || msg.contains("BIP68"), - "csv@102 must reject BIP68-locked spend at height 102: {msg}" - ); - - let dir2 = std::env::temp_dir().join(format!("rbtc-overlay-csv-lax-{n}")); - let q2 = Query::open_or_create_tiny(&dir2).unwrap(); - let mut lax = ChainParams::regtest(); - lax.apply_test_activation_height("csv", 200).unwrap(); - crate::accept_and_connect_block( - &q2, - &lax, - Height::GENESIS, - &genesis, - crate::Milestone::NONE, - ) - .unwrap(); - let (tip2, time2, cbs2) = crate::pad_empty_from( - &q2, - &lax, - genesis.block_hash(), - genesis.header.time, - 1, - 100, - 1, - ); - let p_lax = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: cbs2[0], - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(50_0000_0000 - 1_000), - script_pubkey: spk.clone(), - }], - }; - let b101l = - crate::mine_regtest_paying(tip2, time2 + 600, 101, spk.clone(), vec![p_lax.clone()]); - crate::accept_and_connect_block(&q2, &lax, Height(101), &b101l, crate::Milestone::NONE) - .unwrap(); - let c_lax = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: p_lax.compute_txid(), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::from_consensus(10), - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(50_0000_0000 - 2_000), - script_pubkey: spk, - }], - }; - let b102l = crate::mine_regtest_paying( - b101l.block_hash(), - b101l.header.time + 600, - 102, - ScriptBuf::from_bytes(vec![0x51]), - vec![c_lax], - ); - crate::accept_and_connect_block(&q2, &lax, Height(102), &b102l, crate::Milestone::NONE) - .expect("csv@200: BIP68 not consensus at height 102"); - let _ = std::fs::remove_dir_all(&dir); - let _ = std::fs::remove_dir_all(&dir2); - } } diff --git a/crates/rbitcoin-consensus/src/regtest_pad.rs b/crates/rbitcoin-consensus/src/regtest_pad.rs index 8c4f161ef2..e35ae1783e 100644 --- a/crates/rbitcoin-consensus/src/regtest_pad.rs +++ b/crates/rbitcoin-consensus/src/regtest_pad.rs @@ -2,21 +2,15 @@ //! //! Prefer this over a local `for h in 1..=100 { mine_pow; connect }` loop. +use crate::block::apply_witness_commitment; +use crate::{bip34_height_script, block_has_witness, block_subsidy, ChainParams}; use bitcoin::absolute::LockTime; use bitcoin::block::{Header, Version}; use bitcoin::hashes::Hash; use bitcoin::transaction::Version as TxVersion; use bitcoin::{ Amount, Block, BlockHash, CompactTarget, OutPoint, ScriptBuf, Sequence, Target, Transaction, - TxIn, TxMerkleNode, TxOut, Txid, Witness, -}; -use rbitcoin_primitives::Height; -use rbitcoin_query::Query; - -use crate::block::apply_witness_commitment; -use crate::{ - accept_and_connect_block, bip34_height_script, block_has_witness, block_subsidy, ChainParams, - Milestone, + TxIn, TxMerkleNode, TxOut, Witness, }; pub const REGTEST_POW_BITS: u32 = 0x207f_ffff; @@ -291,60 +285,9 @@ pub fn regtest_coinbase(height: u32, value: Amount, script_pubkey: ScriptBuf) -> } } -/// Connect empty blocks `from_h..=last`. Returns tip hash/time and coinbase txids -/// for heights `1..=collect_coinbases` (empty if `collect_coinbases == 0`). -pub fn pad_empty_from( - query: &Query, - params: &ChainParams, - mut tip: BlockHash, - mut tip_time: u32, - from_h: u32, - last: u32, - collect_coinbases: u32, -) -> (BlockHash, u32, Vec) { - let ms = Milestone::NONE; - let mut cbs = Vec::new(); - for h in from_h..=last { - let b = mine_empty_regtest(tip, tip_time + 600, h); - if h >= 1 && h <= collect_coinbases { - cbs.push(b.txdata[0].compute_txid()); - } - accept_and_connect_block(query, params, Height(h), &b, ms).unwrap(); - tip = b.block_hash(); - tip_time = b.header.time; - } - query.apply_sh_pending().unwrap(); - (tip, tip_time, cbs) -} - #[cfg(test)] mod tests { use super::*; - use std::time::{SystemTime, UNIX_EPOCH}; - - #[test] - fn pad_collects_early_coinbases() { - let n = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let dir = std::env::temp_dir().join(format!("rbtc-pad-{n}")); - let q = Query::open_or_create_tiny(&dir).unwrap(); - let params = ChainParams::regtest(); - let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); - let (_tip, _t, cbs) = pad_empty_from( - &q, - ¶ms, - genesis.block_hash(), - genesis.header.time, - 1, - 3, - 2, - ); - assert_eq!(cbs.len(), 2); - let _ = std::fs::remove_dir_all(&dir); - } #[test] fn mine_regtest_paying_sets_coinbase_script() { diff --git a/crates/rbitcoin-consensus/src/script/interpreter.rs b/crates/rbitcoin-consensus/src/script/interpreter.rs index d1816ecfb0..b721698d13 100644 --- a/crates/rbitcoin-consensus/src/script/interpreter.rs +++ b/crates/rbitcoin-consensus/src/script/interpreter.rs @@ -201,7 +201,7 @@ pub(crate) struct EvalContext<'a> { /// Legacy midstate. Created on first use (WitnessV0 uses `pre`, tapscript `tapscript`). cache: RefCell>>, /// Structure/lookup midstates (WitnessV0 BIP143). - pre: Cow<'a, rbitcoin_query::TxPrecompute>, + pre: Cow<'a, crate::TxPrecompute>, /// BIP342 remaining validation weight (`50 + witness serialized size`). validation_weight_left: Cell, /// Tapscript only: per-input sighash facts from the script-path spend. @@ -250,7 +250,7 @@ impl<'a> EvalContext<'a> { bip65_active, bip112_active, bip66_active, - Cow::Owned(rbitcoin_query::TxPrecompute::from_tx(tx)), + Cow::Owned(crate::TxPrecompute::from_tx(tx)), ) } @@ -264,7 +264,7 @@ impl<'a> EvalContext<'a> { bip65_active: bool, bip112_active: bool, bip66_active: bool, - pre: Cow<'a, rbitcoin_query::TxPrecompute>, + pre: Cow<'a, crate::TxPrecompute>, ) -> Self { Self { tx, diff --git a/crates/rbitcoin-consensus/src/script/tests_verify.rs b/crates/rbitcoin-consensus/src/script/tests_verify.rs index 2c4195517a..325640c66c 100644 --- a/crates/rbitcoin-consensus/src/script/tests_verify.rs +++ b/crates/rbitcoin-consensus/src/script/tests_verify.rs @@ -1514,3 +1514,89 @@ fn legacy_sighash_single_past_last_output_is_uint256_one() { .unwrap(); assert_ne!(in_hash.to_byte_array(), one); } + +/// A taproot key-path signature with hashtype byte `0x00` is invalid when +/// taproot is on and ignored when the mainnet exception turns taproot off. +/// A P2SH redeem of `OP_0` matches the template hash either way. On the BIP16 +/// exception the redeem is not executed. On the next hash it is, and fails. +#[test] +fn exception_hashes_change_which_scripts_run() { + use crate::block::{ScriptVerifyFlags, BIP16_EXCEPTION_MAINNET, TAPROOT_EXCEPTION_MAINNET}; + use crate::params::ChainParams; + use bitcoin::transaction::Version as TxVersion; + + let main = Box::leak(Box::new(ChainParams::mainnet())); + + let mut program = vec![0x51, 0x20]; + program.extend_from_slice(&[0xff; 32]); + let mut sig = vec![0x11u8; 65]; + sig[64] = 0x00; + let tap_tx = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::from_slice(&[sig.as_slice()]), + }], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + }; + let tap_prev = vec![TxOut { + value: Amount::from_sat(2), + script_pubkey: ScriptBuf::from_bytes(program), + }]; + let tap_flags = |hash: &[u8; 32]| ScriptVerifyFlags::for_block(main, 692_261, hash, 0); + let accept = ScriptCheckJob::new( + tap_prev.clone(), + tap_tx.clone(), + tap_flags(&TAPROOT_EXCEPTION_MAINNET), + ); + script::verify_job_all_inputs(&accept).expect("taproot exception does not run the key path"); + let reject = ScriptCheckJob::new(tap_prev, tap_tx, tap_flags(&[1u8; 32])); + let err = script::verify_job_all_inputs(&reject).expect_err("neighbor enforces taproot"); + assert!( + format!("{err}").contains("sighash"), + "expected hashtype 0x00 reject, got {err}" + ); + + let redeem = [0x00u8]; + let redeem_hash = hash160::Hash::hash(&redeem); + let mut p2sh = vec![0xa9, 0x14]; + p2sh.extend_from_slice(redeem_hash.as_byte_array()); + p2sh.push(0x87); + let p2sh_tx = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::from_bytes(vec![0x01, 0x00]), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + }; + let p2sh_prev = vec![TxOut { + value: Amount::from_sat(2), + script_pubkey: ScriptBuf::from_bytes(p2sh), + }]; + let p2sh_flags = |hash: &[u8; 32]| ScriptVerifyFlags::for_block(main, 170_060, hash, 0); + let bare = ScriptCheckJob::new( + p2sh_prev.clone(), + p2sh_tx.clone(), + p2sh_flags(&BIP16_EXCEPTION_MAINNET), + ); + script::verify_job_all_inputs(&bare) + .expect("BIP16 exception checks the hash and does not run the redeem"); + let enforced = ScriptCheckJob::new(p2sh_prev, p2sh_tx, p2sh_flags(&[2u8; 32])); + assert!( + script::verify_job_all_inputs(&enforced).is_err(), + "neighbor executes the OP_0 redeem" + ); +} diff --git a/crates/rbitcoin-consensus/src/script_pool.rs b/crates/rbitcoin-consensus/src/script_pool.rs index ab27f99c44..bfe83f0692 100644 --- a/crates/rbitcoin-consensus/src/script_pool.rs +++ b/crates/rbitcoin-consensus/src/script_pool.rs @@ -23,7 +23,7 @@ use std::thread; /// Jobs claimed per steal. Amortizes `next` / `in_wave` / `Arc` traffic /// without a megachunk on mixed P2WPKH/P2WSH waves. -pub(crate) const STEAL_CHUNK: usize = 32; +pub const STEAL_CHUNK: usize = 32; use crate::error::ConsensusError; @@ -143,7 +143,7 @@ static PUBLISHERS: Mutex> = Mutex::new(Vec::new()); /// /// `None` removes the calling thread. Two stages in one process each /// register; a single slot dropped the first stage's wakes. -pub(crate) fn set_script_publisher(t: Option) { +pub fn set_script_publisher(t: Option) { let mut g = PUBLISHERS.lock().unwrap_or_else(|p| p.into_inner()); if let Some(t) = t { g.retain(|p| p.id() != t.id()); @@ -172,14 +172,14 @@ static STEAL_CLAIMS: AtomicUsize = AtomicUsize::new(0); /// so parallel wave tests do not inflate the counter. #[cfg(test)] static STEAL_CLAIMS_ON: AtomicBool = AtomicBool::new(false); -/// Serialize the two 256-job steal tests so they do not share [`STEAL_CLAIMS`]. -#[cfg(test)] +/// Serialize steal-wave tests so they do not interleave. Lives outside +/// `cfg(test)` because query tests link this crate without that cfg. static STEAL_TEST: Mutex<()> = Mutex::new(()); /// Hold across a test that publishes a steal wave, so pool tests do not /// interleave waves. -#[cfg(test)] -pub(crate) fn steal_test_gate() -> std::sync::MutexGuard<'static, ()> { +#[doc(hidden)] +pub fn steal_test_gate() -> std::sync::MutexGuard<'static, ()> { STEAL_TEST.lock().unwrap_or_else(|p| p.into_inner()) } @@ -205,7 +205,7 @@ fn steal_chunk() -> Option<(Arc, Range)> { } /// True when steal workers can still claim a foreground job. -pub(crate) fn fg_has_unclaimed() -> bool { +pub fn fg_has_unclaimed() -> bool { waves_snap().load().iter().any(|w| w.has_unclaimed()) } @@ -213,7 +213,7 @@ pub(crate) fn fg_has_unclaimed() -> bool { /// scripts stage thread to finish a wave tail instead of parking. /// /// A one-job index claim can run on `ibd-confirm` when the workers are busy. -pub(crate) fn help_steal() -> bool { +pub fn help_steal() -> bool { if on_steal_worker() { return false; } @@ -233,14 +233,14 @@ unsafe impl Send for ApplyCtx {} unsafe impl Sync for ApplyCtx {} /// Publisher-owned job list + live wave. Safe to move; heap allocation is stable. -pub(crate) struct OwnedWave { +pub struct OwnedWave { _items: Box<[T]>, _ctx: Box>, wave: Arc, } impl OwnedWave { - pub(crate) fn is_complete(&self) -> bool { + pub fn is_complete(&self) -> bool { self.wave.is_complete() } @@ -249,7 +249,7 @@ impl OwnedWave { self.wave.has_unclaimed() } - pub(crate) fn finish(self) -> Result<(), ConsensusError> { + pub fn finish(self) -> Result<(), ConsensusError> { self.wave.wait_done(); match self .wave @@ -279,7 +279,7 @@ fn unpublish_fg(wave: &Arc) { /// Publish `items` for steal workers without waiting. `None` = already done /// (empty or single-item ran inline). Claims [`STEAL_CHUNK`] jobs at a time. -pub(crate) fn start_for_each_owned( +pub fn start_for_each_owned( items: Vec, f: fn(&T) -> Result<(), ConsensusError>, ) -> Result>, ConsensusError> { @@ -288,7 +288,7 @@ pub(crate) fn start_for_each_owned( /// Same as [`start_for_each_owned`] with an explicit claim size. `chunk` of 0 /// is treated as 1. Empty and single-item lists still run inline. -pub(crate) fn start_for_each_owned_chunk( +pub fn start_for_each_owned_chunk( items: Vec, f: fn(&T) -> Result<(), ConsensusError>, chunk: usize, @@ -301,7 +301,7 @@ pub(crate) fn start_for_each_owned_chunk( /// [`start_for_each_owned`] runs a single item on the caller. The index wave /// must not do that: one filter would block `ibd-confirm` inside `start`. /// `chunk` of 0 is treated as 1. -pub(crate) fn start_for_each_pooled( +pub fn start_for_each_pooled( items: Vec, f: fn(&T) -> Result<(), ConsensusError>, chunk: usize, diff --git a/crates/rbitcoin-consensus/src/silent_payments.rs b/crates/rbitcoin-consensus/src/silent_payments.rs index 0745a51b39..17085c5882 100644 --- a/crates/rbitcoin-consensus/src/silent_payments.rs +++ b/crates/rbitcoin-consensus/src/silent_payments.rs @@ -11,15 +11,8 @@ use bitcoin::key::{Parity, XOnlyPublicKey}; use bitcoin::script::{Instruction, Script}; use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::{OutPoint, Transaction, TxOut, Witness}; -use rbitcoin_primitives::{Fk, Height}; -use rbitcoin_query::Query; -use rbitcoin_store::{IndexWindow, InputRecord, LoadedTweakTx, OutputRecord, StoreError}; -use std::collections::{BTreeMap, HashMap}; use std::sync::OnceLock; -use crate::error::ConsensusError; -use crate::params::ChainParams; - /// BIP341 NUMS internal key *H* (SHA256 of uncompressed *G* as x-only). const NUMS_H: [u8; 32] = [ 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e, @@ -88,217 +81,6 @@ pub fn tweak_from_tx(tx: &Transaction, prevouts: &[TxOut]) -> Option { }) } -/// Prefer the thin index (no parent peeks). Hole / no table → [`tweaks_for_height`]. -pub fn tweaks_at_height( - query: &Query, - params: &ChainParams, - height: Height, -) -> Result, ConsensusError> { - if !params.taproot_active_at(height.0) { - return Ok(BTreeMap::new()); - } - match query.load_thin_tweaks(height) { - Ok(Some(rows)) => { - let mut out = BTreeMap::new(); - for r in rows { - out.insert( - r.txid, - TxTweak { - tweak: r.tweak, - output_pubkeys: r - .p2tr - .into_iter() - .map(|(vout, xonly, value)| TaprootOut { vout, xonly, value }) - .collect(), - }, - ); - } - return Ok(out); - } - Ok(None) => { - rbitcoin_log::debug!( - "sp_tweaks: naive fallback h={} (hole or index off)", - height.0 - ); - } - Err(e) => return Err(e.into()), - } - tweaks_for_height(query, params, height) -} - -/// Confirmed height → eligible txid (internal order) → tweak. -/// -/// Pre-Taproot and missing heights return an empty map (no error). Does not -/// reconstruct a wire block. Walks packed bodies **and parent outs**. -pub fn tweaks_for_height( - query: &Query, - params: &ChainParams, - height: Height, -) -> Result, ConsensusError> { - query.require_sp_tweaks_unpruned()?; - if !params.taproot_active_at(height.0) { - return Ok(BTreeMap::new()); - } - let fks = match query.block_tx_fks(height) { - Ok(f) => f, - Err(StoreError::NotFound) => return Ok(BTreeMap::new()), - Err(e) => return Err(e.into()), - }; - if fks.is_empty() { - return Ok(BTreeMap::new()); - } - - let wave = rbitcoin_store::load_tweak_wave(&query.store().txs, &fks)?; - let mut by_fk: HashMap = HashMap::new(); - for t in &wave.txs { - by_fk.insert(t.fk, (t.rec.txid, &t.outs)); - } - for (fk, (txid, outs)) in &wave.parents { - by_fk.entry(Fk(*fk)).or_insert((*txid, outs)); - } - let txs: Vec<&LoadedTweakTx> = wave.txs.iter().collect(); - let tweaks = tweaks_for_txs(&txs, &|fk| by_fk.get(&fk).copied())?; - Ok(txs - .iter() - .zip(tweaks) - .filter_map(|(t, tw)| Some((t.rec.txid, tw?))) - .collect()) -} - -/// Tweak records of `window.blocks[i]` in block order (`None` = ineligible). -pub fn tweak_records_from_window( - window: &IndexWindow, - i: usize, -) -> Result>, ConsensusError> { - let txs: Vec<&LoadedTweakTx> = window.blocks[i].txs.iter().collect(); - let tweaks = tweaks_for_txs(&txs, &|fk| Some((window.txid(fk)?, window.outs(fk)?)))?; - Ok(tweaks.into_iter().map(|t| t.map(|t| t.tweak)).collect()) -} - -/// Txid and outputs of a spent create. -type ParentLookup<'a> = dyn Fn(Fk) -> Option<([u8; 32], &'a [OutputRecord])> + 'a; - -/// Per-tx tweak (`None` = ineligible) for `txs` in order. Only P2TR-output -/// txs with loaded inputs are candidates; EC math runs on the caller. -fn tweaks_for_txs<'a>( - txs: &[&'a LoadedTweakTx], - parent: &ParentLookup<'a>, -) -> Result>, ConsensusError> { - let mut out = vec![None; txs.len()]; - for (i, t) in txs.iter().enumerate() { - if !t.need_seqsigwit { - continue; - } - let Some(inputs) = t.inputs.as_ref() else { - continue; - }; - let (tx, prevouts) = build_tx_and_prevouts(inputs, &t.outs, parent)?; - out[i] = tweak_from_tx(&tx, &prevouts); - } - Ok(out) -} - -#[allow(clippy::type_complexity)] // packed row / pin / script-hash tuple is the on-disk shape -/// Tweaks-wire `TxTweak` from a stored 33-byte tweak + this tx’s packed outs. -/// -/// No parent IO. `Some(tweak)` with missing packed outs is corrupt. -pub fn tweaks_from_thin_and_body( - rows: &[([u8; 32], Option<[u8; 33]>, Option<&[OutputRecord]>)], -) -> Result, StoreError> { - let mut out = BTreeMap::new(); - for (txid, tweak, outs) in rows { - let Some(tweak) = tweak else { - continue; - }; - let Some(outs) = outs else { - return Err(StoreError::Corrupt( - "invariant: thin tweak missing packed body", - )); - }; - out.insert( - *txid, - TxTweak { - tweak: *tweak, - output_pubkeys: taproot_outs_from_records(outs), - }, - ); - } - Ok(out) -} - -fn taproot_outs_from_records(outputs: &[OutputRecord]) -> Vec { - let mut out = Vec::new(); - for (i, o) in outputs.iter().enumerate() { - if !is_p2tr(&o.script) { - continue; - } - let mut xonly = [0u8; 32]; - xonly.copy_from_slice(&o.script[2..34]); - let value = o.value; - out.push(TaprootOut { - vout: i as u32, - xonly, - value, - }); - } - out -} - -/// A spent parent missing from the lookup is an invariant break, not an -/// ineligible tx. -fn build_tx_and_prevouts( - inputs: &[InputRecord], - outputs: &[OutputRecord], - parent: &ParentLookup<'_>, -) -> Result<(Transaction, Vec), StoreError> { - let mut prevouts = Vec::with_capacity(inputs.len()); - let mut txins = Vec::with_capacity(inputs.len()); - for inp in inputs { - let (prev_txid, prev_script, prev_value) = if inp.is_coinbase() { - ([0u8; 32], Vec::new(), 0u64) - } else { - const MISSING: &str = "invariant: sp_tweaks spent parent missing"; - let (tid, outs) = parent(inp.create_fk()).ok_or(StoreError::Corrupt(MISSING))?; - let o = outs - .get(inp.prev_index() as usize) - .ok_or(StoreError::Corrupt(MISSING))?; - (tid, o.script.clone(), o.value) - }; - let wit_refs: Vec<&[u8]> = inp.witness.iter().map(|w| w.as_slice()).collect(); - txins.push(bitcoin::TxIn { - previous_output: OutPoint { - txid: bitcoin::Txid::from_byte_array(prev_txid), - vout: inp.prev_index(), - }, - script_sig: bitcoin::ScriptBuf::from_bytes(inp.script_sig.clone()), - sequence: bitcoin::Sequence::from_consensus(inp.sequence), - witness: Witness::from_slice(&wit_refs), - }); - let value = bitcoin::Amount::from_sat(prev_value); - prevouts.push(TxOut { - value, - script_pubkey: bitcoin::ScriptBuf::from_bytes(prev_script), - }); - } - let mut txouts = Vec::with_capacity(outputs.len()); - for o in outputs { - let value = bitcoin::Amount::from_sat(o.value); - txouts.push(TxOut { - value, - script_pubkey: bitcoin::ScriptBuf::from_bytes(o.script.clone()), - }); - } - Ok(( - Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: txins, - output: txouts, - }, - prevouts, - )) -} - fn input_hash_mul_a(tx: &Transaction, a: &PublicKey) -> Option<[u8; 33]> { let mut smallest = outpoint_bytes(&tx.input[0].previous_output); for vin in tx.input.iter().skip(1) { @@ -382,7 +164,7 @@ fn taproot_outs(tx: &Transaction) -> Vec { out } -pub(crate) fn is_p2tr(spk: &[u8]) -> bool { +pub fn is_p2tr(spk: &[u8]) -> bool { spk.len() == 34 && spk[0] == 0x51 && spk[1] == 0x20 } @@ -537,7 +319,6 @@ fn is_p2sh_p2wpkh_redeem(script_sig: &Script) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::Milestone; use bitcoin::absolute::LockTime; use bitcoin::consensus::encode::deserialize; use bitcoin::hashes::Hash; @@ -545,9 +326,6 @@ mod tests { use bitcoin::secp256k1::SecretKey; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, Sequence, TxIn}; - use rbitcoin_query::testutil::FixtureChain; - use rbitcoin_query::TxApply; - use rbitcoin_store::{HeaderRecord, TxRecord}; use serde_json::Value; use std::str::FromStr; #[test] @@ -591,37 +369,29 @@ mod tests { not_prog[1] = 0x15; assert!(!is_p2sh_p2wpkh_redeem(push(¬_prog).as_script())); - let zero = OutputRecord::unspent(0, { - let mut s = vec![0x51, 0x20]; - s.extend(std::iter::repeat_n(0xab, 32)); - s - }); - assert_eq!( - taproot_outs_from_records(std::slice::from_ref(&zero))[0].value, - 0 - ); - - let parent_outs = [OutputRecord::unspent(7, vec![0x51])]; - let inp = InputRecord { - prev_txid: [2u8; 32], - parent: rbitcoin_store::InputParent::Prevout { - create_fk: Fk(1), - vout: 0, - }, - sequence: 0, - script_sig: Vec::new(), - witness: Vec::new(), - }; - let (tx, prevouts) = build_tx_and_prevouts(&[inp], std::slice::from_ref(&zero), &|_| { - Some(([9u8; 32], parent_outs.as_slice())) - }) - .unwrap(); - assert_eq!(prevouts[0].value.to_sat(), 7); - assert_eq!(tx.output[0].value.to_sat(), 0); - let sk = SecretKey::from_slice(&[3u8; 32]).unwrap(); let pk = PublicKey::from_secret_key(secp(), &sk); - let mut tx = tx; + let mut tx = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: bitcoin::Txid::from_byte_array([9u8; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::from_bytes({ + let mut s = vec![0x51, 0x20]; + s.extend(std::iter::repeat_n(0xab, 32)); + s + }), + }], + }; tx.input.push(TxIn { previous_output: OutPoint { txid: bitcoin::Txid::from_byte_array([0u8; 32]), @@ -824,483 +594,4 @@ mod tests { }]; assert!(tweak_from_tx(&tx, &prev).is_none()); } - - fn tmp_store() -> (rbitcoin_query::testutil::TempDir, Query) { - rbitcoin_query::testutil::tiny_query_labeled("sp-tweaks") - } - - fn header(h: u32, prev_fk: Fk, prev_hash: Option<[u8; 32]>) -> HeaderRecord { - rbitcoin_query::testutil::header_with_merkle_marker(h, prev_fk, prev_hash, 0xec) - } - - #[test] - fn thin_compose_matches_engine_outs_without_parents() { - let (dir, q) = tmp_store(); - let params = ChainParams::regtest(); - // Reuse the P2WPKH→P2TR spend from the walk test via a tiny local chain. - use bitcoin::hashes::hash160; - use bitcoin::secp256k1::SecretKey; - let secp_ctx = Secp256k1::new(); - let sk = SecretKey::from_slice(&[2u8; 32]).unwrap(); - let pk = bitcoin::secp256k1::PublicKey::from_secret_key(&secp_ctx, &sk); - let ser = pk.serialize(); - let h160 = hash160::Hash::hash(&ser); - let mut p2wpkh = vec![0x00, 0x14]; - p2wpkh.extend_from_slice(h160.as_ref()); - let (xonly, _) = pk.x_only_public_key(); - let mut p2tr = vec![0x51, 0x20]; - p2tr.extend_from_slice(&xonly.serialize()); - let mut genesis_txid = [0u8; 32]; - genesis_txid[31] = 0xcb; - let h0 = header(0, Fk::NULL, None); - let fk0 = q - .connect_block( - Height(0), - &h0, - &[TxApply { - tx: TxRecord { - txid: genesis_txid, - version: 1, - locktime: 0, - input_start_fk: Fk::NULL, - input_count: 1, - output_start_fk: Fk::NULL, - output_count: 1, - }, - inputs: vec![InputRecord::coinbase(u32::MAX, vec![0x00], vec![])], - outputs: vec![OutputRecord::unspent(50_0000_0000, p2wpkh.clone())], - }], - ) - .unwrap(); - let create_fk = q.block_tx_fks(Height(0)).unwrap()[0]; - let mut spend_txid = [0u8; 32]; - spend_txid[0] = 0x11; - spend_txid[31] = 0xcd; - let h1 = header(1, fk0, Some(h0.hash)); - q.connect_block( - Height(1), - &h1, - &[TxApply { - tx: TxRecord { - txid: spend_txid, - version: 2, - locktime: 0, - input_start_fk: Fk::NULL, - input_count: 1, - output_start_fk: Fk::NULL, - output_count: 1, - }, - inputs: vec![InputRecord { - prev_txid: genesis_txid, - parent: rbitcoin_store::InputParent::Prevout { create_fk, vout: 0 }, - sequence: u32::MAX, - script_sig: vec![], - witness: vec![vec![0u8; 64], ser.to_vec()], - }], - outputs: vec![OutputRecord::unspent(49_0000_0000, p2tr.clone())], - }], - ) - .unwrap(); - - let naive = tweaks_for_height(&q, ¶ms, Height(1)).unwrap(); - let t = naive.get(&spend_txid).unwrap(); - let spend_fk = q.block_tx_fks(Height(1)).unwrap()[0]; - let (_rec, _ins, outs) = q.store().get_tx_full(spend_fk).unwrap(); - let composed = - tweaks_from_thin_and_body(&[(spend_txid, Some(t.tweak), Some(outs.as_slice()))]) - .unwrap(); - let c = composed.get(&spend_txid).unwrap(); - assert_eq!(c.tweak, t.tweak); - assert_eq!(c.output_pubkeys, t.output_pubkeys); - - let miss = tweaks_from_thin_and_body(&[(spend_txid, Some(t.tweak), None)]); - assert!( - matches!(miss, Err(StoreError::Corrupt(m)) if m.contains("invariant")), - "got {miss:?}" - ); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn tweaks_for_height_unknown_is_empty() { - let (dir, q) = tmp_store(); - let params = ChainParams::regtest(); - assert!(tweaks_for_height(&q, ¶ms, Height(0)) - .unwrap() - .is_empty()); - let main = ChainParams::mainnet(); - assert!(tweaks_for_height(&q, &main, Height(100)) - .unwrap() - .is_empty()); - let _ = std::fs::remove_dir_all(&dir); - } - - /// A connect with tweaks on seals that height. Disconnect truncates, and - /// the replacement block is sealed on the next connect. - #[test] - fn live_connect_seals_tweaks_and_reorg_truncates() { - let (dir, q) = tmp_store(); - let params = ChainParams::regtest(); - q.set_sptweaks_enabled(true, Height(0)).unwrap(); - crate::prepare_live_indexes(&q).unwrap(); - let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - crate::accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE) - .unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(1))); - let thin0 = q.load_thin_tweaks(Height(0)).unwrap().expect("indexed"); - assert!( - thin0.is_empty(), - "coinbase is not eligible — no Class A join" - ); - - let b1 = crate::mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); - crate::accept_and_connect_block(&q, ¶ms, Height(1), &b1, Milestone::NONE).unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(2))); - - q.disconnect_tip().unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(1))); - assert!(q.load_thin_tweaks(Height(1)).unwrap().is_none()); - - let b1b = crate::mine_empty_regtest(genesis.block_hash(), genesis.header.time + 601, 1); - crate::accept_and_connect_block(&q, ¶ms, Height(1), &b1b, Milestone::NONE).unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(2))); - assert!(q.load_thin_tweaks(Height(1)).unwrap().is_some()); - let _ = std::fs::remove_dir_all(&dir); - } - - /// Contiguous connects seal tweaks. The materialize does not rewrite them. - #[test] - fn live_connect_seals_tweaks_builder_leaves_them() { - let (dir, q) = tmp_store(); - let params = ChainParams::regtest(); - q.set_sptweaks_enabled(true, Height(0)).unwrap(); - crate::prepare_live_indexes(&q).unwrap(); - let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - crate::accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE) - .unwrap(); - let b1 = crate::mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); - crate::accept_and_connect_block(&q, ¶ms, Height(1), &b1, Milestone::NONE).unwrap(); - let b2 = crate::mine_empty_regtest(b1.block_hash(), b1.header.time + 600, 2); - crate::accept_and_connect_block(&q, ¶ms, Height(2), &b2, Milestone::NONE).unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(3))); - - q.release_index_writebehind(Height(2)); - crate::build_indexes_released(&q).unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(3))); - for h in 0..=2 { - assert!( - q.load_thin_tweaks(Height(h)).unwrap().is_some(), - "height {h}" - ); - } - let _ = std::fs::remove_dir_all(&dir); - } - - struct SpKeys { - p2wpkh: Vec, - p2tr: Vec, - pubkey: Vec, - } - - fn sp_keys() -> SpKeys { - use bitcoin::hashes::hash160; - let sk = SecretKey::from_slice(&[2u8; 32]).unwrap(); - let pk = PublicKey::from_secret_key(secp(), &sk); - let ser = pk.serialize(); - let mut p2wpkh = vec![0x00, 0x14]; - p2wpkh.extend_from_slice(hash160::Hash::hash(&ser).as_ref()); - let mut p2tr = vec![0x51, 0x20]; - p2tr.extend_from_slice(&pk.x_only_public_key().0.serialize()); - SpKeys { - p2wpkh, - p2tr, - pubkey: ser.to_vec(), - } - } - - fn sp_spend( - tag: u8, - prev: ([u8; 32], Fk, u32), - witness: Vec>, - outs: Vec, - ) -> TxApply { - let mut txid = [0u8; 32]; - txid[0] = tag; - TxApply { - tx: TxRecord { - txid, - version: 2, - locktime: 0, - input_start_fk: Fk::NULL, - input_count: 1, - output_start_fk: Fk::NULL, - output_count: outs.len() as u32, - }, - inputs: vec![InputRecord { - prev_txid: prev.0, - parent: rbitcoin_store::InputParent::Prevout { - create_fk: prev.1, - vout: prev.2, - }, - sequence: u32::MAX, - script_sig: vec![], - witness, - }], - outputs: outs, - } - } - - fn seal_tweaks(q: &Query, through: u32) { - q.release_index_writebehind(Height(through)); - crate::build_indexes_released(q).unwrap(); - assert_eq!(q.sptweaks_next_height(), Some(Height(through + 1))); - } - - /// Height 1 carries two eligible P2WPKH→P2TR spends around a spend with - /// fat seqsigwit and a fat output script. The naive walk, the index - /// window reader, the engine on the rebuilt wire tx, and the served - /// index all give the same tweaks, and the served read spans the - /// middle txout but not its seqsigwit. - fn pin_tweaks_agree_and_thin_serve_skips_fat_witness( - q: &Query, - params: &ChainParams, - keys: &SpKeys, - a: [u8; 32], - b: [u8; 32], - ) { - let naive = tweaks_for_height(q, params, Height(1)).unwrap(); - assert_eq!(naive.len(), 2, "the OP_TRUE spend is not eligible"); - let fks = q.block_tx_fks(Height(1)).unwrap(); - let wire_a = q.reconstruct_tx(fks[0]).unwrap(); - let engine_prev = [TxOut { - value: Amount::from_sat(20_0000_0000), - script_pubkey: ScriptBuf::from_bytes(keys.p2wpkh.clone()), - }]; - let engine = tweak_from_tx(&wire_a, &engine_prev).unwrap(); - assert_eq!(naive[&a].tweak, engine.tweak); - assert_eq!(naive[&a].output_pubkeys, engine.output_pubkeys); - - let want = vec![Some(naive[&a].tweak), None, Some(naive[&b].tweak)]; - for start in [0, 1] { - let heights = q.index_heights(start, 1, Some(0)).unwrap(); - let window = q.read_index_window(&heights).unwrap(); - let i = window.blocks.len() - 1; - assert_eq!( - tweak_records_from_window(&window, i).unwrap(), - want, - "window from {start}: the parent inside or outside the window" - ); - } - - let mid_seqsigwit = q.store().tx_seqsigwit_range(fks[1]).unwrap().1; - let mid_txout = q.store().txs.body_range(fks[1]).unwrap().1; - let _ = q.sample_reset_thin_tweak_body_bytes(); - let rows = q.load_thin_tweaks(Height(1)).unwrap().expect("indexed"); - let read = q.sample_reset_thin_tweak_body_bytes(); - assert_eq!( - rows.iter().map(|r| r.txid).collect::>(), - [a, b], - "ineligible txs are not joined from Class A" - ); - assert_eq!((rows[0].p2tr.len(), rows[1].p2tr.len()), (2, 1)); - assert!( - read >= mid_txout, - "the txout span covers the ineligible hole (read={read} txout={mid_txout})" - ); - assert!( - read < mid_seqsigwit, - "the fat seqsigwit stays out (read={read} seqsigwit={mid_seqsigwit})" - ); - let indexed = tweaks_at_height(q, params, Height(1)).unwrap(); - for txid in [a, b] { - assert_eq!(indexed[&txid].tweak, naive[&txid].tweak); - assert_eq!(indexed[&txid].output_pubkeys, naive[&txid].output_pubkeys); - } - } - - fn pin_thin_range_limits_and_hole(q: &Query, a: [u8; 32], c: [u8; 32]) { - use rbitcoin_query::ThinTweakRangeLimits; - - let all = q - .load_thin_tweaks_range(Height(0), ThinTweakRangeLimits::default()) - .unwrap(); - assert_eq!( - all.iter() - .map(|(h, rows)| (h.0, rows.len())) - .collect::>(), - [(0, 0), (1, 2), (2, 1)] - ); - assert_eq!(all[2].1[0].txid, c); - for (h, rows) in &all { - let single = q.load_thin_tweaks(*h).unwrap().expect("indexed"); - assert_eq!(single.len(), rows.len()); - for (s, r) in single.iter().zip(rows) { - assert_eq!((s.txid, s.tweak, &s.p2tr), (r.txid, r.tweak, &r.p2tr)); - } - } - let limited = |max_heights, max_eligible| { - q.load_thin_tweaks_range( - Height(1), - ThinTweakRangeLimits { - max_heights, - max_eligible, - ..ThinTweakRangeLimits::default() - }, - ) - .unwrap() - }; - assert_eq!(limited(1, 8192).len(), 1); - assert_eq!( - limited(10, 1).len(), - 1, - "the eligible budget stops after the first height that spends it" - ); - assert!(q - .load_thin_tweaks_range(Height(3), ThinTweakRangeLimits::default()) - .unwrap() - .is_empty()); - let only2 = q - .load_thin_tweaks_range(Height(2), ThinTweakRangeLimits::default()) - .unwrap(); - assert_eq!(only2.len(), 1); - assert_eq!(only2[0].0, Height(2)); - - let cut = |cut_through| { - q.load_thin_tweaks_range( - Height(1), - ThinTweakRangeLimits { - cut_through, - ..ThinTweakRangeLimits::default() - }, - ) - .unwrap() - }; - let kept = cut(true); - let row_a = kept[0].1.iter().find(|r| r.txid == a).unwrap(); - assert_eq!( - row_a.p2tr.iter().map(|o| o.0).collect::>(), - [1], - "cut-through drops the P2TR output spent at height 2" - ); - let hist = cut(false); - assert_eq!( - hist[0].1.iter().find(|r| r.txid == a).unwrap().p2tr.len(), - 2 - ); - } - - /// One synthetic chain through the index builder: coinbase at 0, two - /// eligible spends around a fat ineligible one at 1, a spend of one of - /// those P2TR outputs plus a third eligible spend at 2 (sealed with 1 in - /// one window), and at 3 the spend that leaves the first tx all spent. - #[test] - fn sp_tweaks_confirm_life() { - let (dir, q) = tmp_store(); - let params = ChainParams::regtest(); - let keys = sp_keys(); - q.set_sptweaks_enabled(true, Height(0)).unwrap(); - let sig_wit = || vec![vec![0u8; 64], keys.pubkey.clone()]; - let p2tr = |sat| OutputRecord::unspent(sat, keys.p2tr.clone()); - - let mut cb_txid = [0u8; 32]; - cb_txid[31] = 0xcb; - let h0 = header(0, Fk::NULL, None); - let fk0 = q - .connect_block( - Height(0), - &h0, - &[TxApply { - tx: TxRecord { - txid: cb_txid, - version: 1, - locktime: 0, - input_start_fk: Fk::NULL, - input_count: 1, - output_start_fk: Fk::NULL, - output_count: 4, - }, - inputs: vec![InputRecord::coinbase(u32::MAX, vec![0x00], vec![])], - outputs: vec![ - OutputRecord::unspent(20_0000_0000, keys.p2wpkh.clone()), - OutputRecord::unspent(20_0000_0000, keys.p2wpkh.clone()), - OutputRecord::unspent(5_0000_0000, vec![0x51]), - OutputRecord::unspent(5_0000_0000, keys.p2wpkh.clone()), - ], - }], - ) - .unwrap(); - seal_tweaks(&q, 0); - assert!( - q.load_thin_tweaks(Height(0)).unwrap().unwrap().is_empty(), - "a coinbase is not eligible" - ); - let cb = q.block_tx_fks(Height(0)).unwrap()[0]; - - let spend_a = sp_spend( - 0xaa, - (cb_txid, cb, 0), - sig_wit(), - vec![p2tr(12_0000_0000), p2tr(7_0000_0000)], - ); - let fat = sp_spend( - 0xfe, - (cb_txid, cb, 2), - vec![vec![0u8; 16_384]], - vec![OutputRecord::unspent(4_0000_0000, vec![0x51; 4096])], - ); - let spend_b = sp_spend(0xbb, (cb_txid, cb, 1), sig_wit(), vec![p2tr(19_0000_0000)]); - let (a, b) = (spend_a.tx.txid, spend_b.tx.txid); - let h1 = header(1, fk0, Some(h0.hash)); - let fk1 = q - .connect_block(Height(1), &h1, &[spend_a, fat, spend_b]) - .unwrap(); - let a_fk = q.block_tx_fks(Height(1)).unwrap()[0]; - - let spend_c = sp_spend(0xcc, (cb_txid, cb, 3), sig_wit(), vec![p2tr(4_0000_0000)]); - let c = spend_c.tx.txid; - let spend_a0 = sp_spend( - 0xd0, - (a, a_fk, 0), - vec![vec![0u8; 64]], - vec![OutputRecord::unspent(11_0000_0000, keys.p2wpkh.clone())], - ); - let h2 = header(2, fk1, Some(h1.hash)); - let fk2 = q - .connect_block(Height(2), &h2, &[spend_c, spend_a0]) - .unwrap(); - assert_eq!( - q.sptweaks_next_height(), - Some(Height(1)), - "confirm writes none" - ); - seal_tweaks(&q, 2); - - pin_tweaks_agree_and_thin_serve_skips_fat_witness(&q, ¶ms, &keys, a, b); - pin_thin_range_limits_and_hole(&q, a, c); - - let spend_a1 = sp_spend( - 0xd1, - (a, a_fk, 1), - vec![vec![0u8; 64]], - vec![OutputRecord::unspent(6_0000_0000, vec![0x51])], - ); - let h3 = header(3, fk2, Some(h2.hash)); - q.connect_block(Height(3), &h3, &[spend_a1]).unwrap(); - seal_tweaks(&q, 3); - let gone = q - .load_thin_tweaks_range( - Height(1), - rbitcoin_query::ThinTweakRangeLimits { - cut_through: true, - ..rbitcoin_query::ThinTweakRangeLimits::default() - }, - ) - .unwrap(); - assert_eq!( - gone[0].1.iter().map(|r| r.txid).collect::>(), - [b], - "an all-spent eligible tx is omitted" - ); - let _ = std::fs::remove_dir_all(&dir); - } } diff --git a/crates/rbitcoin-query/src/tx_precompute.rs b/crates/rbitcoin-consensus/src/tx_precompute.rs similarity index 95% rename from crates/rbitcoin-query/src/tx_precompute.rs rename to crates/rbitcoin-consensus/src/tx_precompute.rs index 50c582d047..ea75a38977 100644 --- a/crates/rbitcoin-query/src/tx_precompute.rs +++ b/crates/rbitcoin-consensus/src/tx_precompute.rs @@ -965,55 +965,6 @@ mod tests { assert!(block.txdata[0].output.is_empty()); } - /// Peer intake walks the wire and lookup decodes it. A payload one side - /// accepts and the other refuses either strands a queued body or drops an - /// honest one, so every truncation and single-byte rewrite must agree. - #[test] - fn block_wire_walk_agrees_with_decode() { - use bitcoin::consensus::encode::serialize; - let zero_in = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![TxOut { - value: Amount::from_sat(1), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }], - }; - // Segwit with one empty and one non-empty witness decodes; rewriting - // the non-empty stack count to 0 hits the all-empty refusal. - let mut mixed = p2wpkh_like(); - mixed.input.insert(0, legacy_1in().input[0].clone()); - mixed.input[1].witness = Witness::from_slice(&[[0x01]]); - let mut raw = vec![0u8; 80]; - raw.push(5); - for tx in [legacy_1in(), p2wpkh_like(), zero_in, mixed] { - raw.extend_from_slice(&serialize(&tx)); - } - raw.extend_from_slice(&FLAG_ZERO_TX); - let agree = |p: &[u8]| { - let decoded = super::decode_block_precomputes(p, false) - .map(|(b, _, _)| b.txdata.iter().map(|t| t.input.len() as u32).sum()); - assert_eq!( - rbitcoin_store::block_wire_input_count(p), - decoded, - "payload {p:02x?}" - ); - decoded - }; - assert_eq!(agree(&raw), Some(4)); - for n in 0..raw.len() { - agree(&raw[..n]); - } - for i in 80..raw.len() { - for v in [0x00, 0x01, 0x02, 0xfd, 0xfe, 0xff] { - let mut p = raw.clone(); - p[i] = v; - agree(&p); - } - } - } - /// Core's `UnserializeTransaction` reads an empty vin and flag 0 as a tx /// with no inputs and no outputs; the locktime follows. const FLAG_ZERO_TX: [u8; 10] = [2, 0, 0, 0, 0x00, 0x00, 7, 0, 0, 0]; diff --git a/crates/rbitcoin-electrum/src/electrum_sh_journey.rs b/crates/rbitcoin-electrum/src/electrum_sh_journey.rs index 2e268f3649..7e8a7add06 100644 --- a/crates/rbitcoin-electrum/src/electrum_sh_journey.rs +++ b/crates/rbitcoin-electrum/src/electrum_sh_journey.rs @@ -2,7 +2,8 @@ use bitcoin::absolute::LockTime; use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; -use rbitcoin_consensus::{accept_and_connect_block, Milestone}; +use rbitcoin_consensus::{Milestone}; +use rbitcoin_query::{accept_and_connect_block}; use rbitcoin_query::TxApply; use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, TxRecord}; @@ -503,7 +504,7 @@ async fn electrum_sh_join() { let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); const N_SPENDS: u32 = 4; - let (tip, _tip_time, coinbase_txids) = rbitcoin_consensus::pad_empty_from( + let (tip, _tip_time, coinbase_txids) = rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), diff --git a/crates/rbitcoin-electrum/src/server_tests.rs b/crates/rbitcoin-electrum/src/server_tests.rs index 40dacc977b..b4ac92d4fd 100644 --- a/crates/rbitcoin-electrum/src/server_tests.rs +++ b/crates/rbitcoin-electrum/src/server_tests.rs @@ -543,8 +543,9 @@ async fn electrum_fee_reports_follow_configured_and_enforced_floor() { /// a real blocking store query (`blockchain.block.headers`). #[tokio::test(flavor = "current_thread")] async fn ping_overlaps_blocking_headers_on_one_worker() { - use rbitcoin_consensus::{accept_and_connect_block, Milestone}; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; use std::sync::atomic::AtomicU64; let (dir, q) = tmp_store(); @@ -552,7 +553,7 @@ async fn ping_overlaps_blocking_headers_on_one_worker() { let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); // Enough headers that a serial walk stays in-flight after ping is scheduled. - let _ = rbitcoin_consensus::pad_empty_from( + let _ = rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), diff --git a/crates/rbitcoin-electrum/src/silent_scan.rs b/crates/rbitcoin-electrum/src/silent_scan.rs index 9f36107f31..7d9205e97b 100644 --- a/crates/rbitcoin-electrum/src/silent_scan.rs +++ b/crates/rbitcoin-electrum/src/silent_scan.rs @@ -2,8 +2,9 @@ use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::Network; -use rbitcoin_consensus::{taproot_matches_scan, tweaks_for_height, ChainParams}; +use rbitcoin_consensus::{taproot_matches_scan, ChainParams}; use rbitcoin_primitives::{hex_decode, Height}; +use rbitcoin_query::tweaks_for_height; use rbitcoin_query::Query; use serde_json::{json, Value}; diff --git a/crates/rbitcoin-electrum/src/tweaks.rs b/crates/rbitcoin-electrum/src/tweaks.rs index f8774df11a..deefd8ffd0 100644 --- a/crates/rbitcoin-electrum/src/tweaks.rs +++ b/crates/rbitcoin-electrum/src/tweaks.rs @@ -8,10 +8,11 @@ //! `historicalMode=false` omits confirmed-spent P2TR outs. //! Serve-time `--sptweaks-dust` omits P2TR outs with `value <=` the floor. -use rbitcoin_consensus::{tweaks_for_height, ChainParams, TxTweak}; +use rbitcoin_consensus::{ChainParams, TxTweak}; #[cfg(test)] use rbitcoin_primitives::hex_encode; use rbitcoin_primitives::Height; +use rbitcoin_query::tweaks_for_height; use rbitcoin_query::{Query, ThinTweakRangeLimits, ThinTweakRow}; #[cfg(test)] use serde_json::Map; diff --git a/crates/rbitcoin-esplora/src/internal.rs b/crates/rbitcoin-esplora/src/internal.rs index 333377e3f7..31d1befbcc 100644 --- a/crates/rbitcoin-esplora/src/internal.rs +++ b/crates/rbitcoin-esplora/src/internal.rs @@ -416,9 +416,10 @@ mod tests { use crate::server::{run_esplora, EsploraConfig, EsploraListen}; use bitcoin::absolute::LockTime; use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_net::MempoolHub; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_query::Query; use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -475,7 +476,7 @@ mod tests { let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); - let (_tip, _time, cbs) = rbitcoin_consensus::pad_empty_from( + let (_tip, _time, cbs) = rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), diff --git a/crates/rbitcoin-net/Cargo.toml b/crates/rbitcoin-net/Cargo.toml index d8b995b8b7..86d35fefeb 100644 --- a/crates/rbitcoin-net/Cargo.toml +++ b/crates/rbitcoin-net/Cargo.toml @@ -27,6 +27,8 @@ sha3 = "0.12" # Relay tests pin a full mempool slot table. Dependents of this crate do # not turn the feature on. rbitcoin-mempool = { workspace = true, features = ["test-hooks"] } +# Side-block accept arms one cancelled header read. Dependents do not. +rbitcoin-query = { workspace = true, features = ["test-hooks"] } [target.'cfg(target_os = "linux")'.dependencies] # POLLRDHUP: peer FIN with unread bytes still in the receive buffer. diff --git a/crates/rbitcoin-net/src/chain.rs b/crates/rbitcoin-net/src/chain.rs index 945a61d2af..7bfcabc55e 100644 --- a/crates/rbitcoin-net/src/chain.rs +++ b/crates/rbitcoin-net/src/chain.rs @@ -12,14 +12,17 @@ use bitcoin::{ Work, }; use rbitcoin_consensus::{ - accept_and_connect_block_preverified, confirm_wire_load_from_plan as consensus_load_from_plan, - confirm_wire_load_phase_pipelined, confirm_write_phase, genesis_block, header_to_record, - mine_op_true_signet_paying, mine_regtest_paying, validate_header, validate_header_on_parent, - ChainParams, Milestone, PlanStampOutcome, ScriptOkBatch, ScriptPreverified, WireLoadPipeline, + genesis_block, mine_op_true_signet_paying, mine_regtest_paying, validate_header_on_parent, + ChainParams, Milestone, }; use rbitcoin_log::{debug, info, warn}; use rbitcoin_primitives::{Fk, Height}; use rbitcoin_query::Query; +use rbitcoin_query::{ + accept_and_connect_block_preverified, confirm_wire_load_from_plan as consensus_load_from_plan, + confirm_wire_load_phase_pipelined, confirm_write_phase, header_to_record, validate_header, + PlanStampOutcome, ScriptOkBatch, ScriptPreverified, WireLoadPipeline, +}; use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering}; @@ -288,16 +291,23 @@ fn connect_failed_for_header(hash: [u8; 32], e: NetError) -> NetError { } } -fn header_reject(header: &Header, e: &rbitcoin_consensus::ConsensusError) -> NetError { - if let rbitcoin_consensus::ConsensusError::Store(se) = e { - return NetError::store(se); +fn header_reject(header: &Header, e: &rbitcoin_query::ConfirmError) -> NetError { + if e.is_cancelled() { + return NetError::Cancelled; } - let reason = rbitcoin_consensus::block_reject_reason(e); + let rules = match e { + rbitcoin_query::ConfirmError::Store(se) => return NetError::store(se), + rbitcoin_query::ConfirmError::Rules(rbitcoin_consensus::ConsensusError::Internal(m)) => { + return NetError::store(m); + } + rbitcoin_query::ConfirmError::Rules(r) => r, + }; + let reason = rbitcoin_consensus::block_reject_reason(rules); rbitcoin_log::info!( "{}", rbitcoin_consensus::block_reject_log_line(header.block_hash(), &reason) ); - NetError::Consensus(e.to_string()) + NetError::Consensus(rules.to_string()) } /// Inputs a connected block spends, excluding the coinbase null prevout. @@ -378,6 +388,10 @@ pub struct ChainHub { chain_work_prefix: RwLock>, /// Once the relay-inhibited latch clears, stay out. finished_ibd: AtomicBool, + /// Initial `getheaders` queued by follow sessions since process start. + initial_getheaders_sent: AtomicU64, + /// `headers` messages applied since process start (including empty). + headers_replies: AtomicU64, /// A follow-session block inv during IBD. The walk asks its header peer. header_inv_challenge: AtomicBool, #[cfg(test)] @@ -493,6 +507,8 @@ impl ChainHub { asked_blocks: RwLock::new(HashSet::new()), chain_work_prefix: RwLock::new(Vec::new()), finished_ibd: AtomicBool::new(false), + initial_getheaders_sent: AtomicU64::new(0), + headers_replies: AtomicU64::new(0), header_inv_challenge: AtomicBool::new(false), #[cfg(test)] block_at_height_calls: AtomicU64::new(0), @@ -1154,6 +1170,22 @@ impl ChainHub { best } + pub fn note_initial_getheaders_sent(&self) { + self.initial_getheaders_sent.fetch_add(1, Ordering::Relaxed); + } + + pub fn initial_getheaders_sent(&self) -> u64 { + self.initial_getheaders_sent.load(Ordering::Relaxed) + } + + pub fn note_headers_reply(&self) { + self.headers_replies.fetch_add(1, Ordering::Relaxed); + } + + pub fn headers_reply_count(&self) -> u64 { + self.headers_replies.load(Ordering::Relaxed) + } + fn note_header_tip(&self, header: &Header) { let hash = header.block_hash(); if self.is_connected(&hash) { @@ -1349,7 +1381,7 @@ impl ChainHub { return Err("bad-prevblk".into()); } if let Some(ph) = self.query.height_of_hash(&prev_bytes).ok().flatten() { - if let Ok(mtp) = rbitcoin_consensus::median_time_past(self.query.as_ref(), ph) { + if let Ok(mtp) = rbitcoin_query::median_time_past(self.query.as_ref(), ph) { if header.time <= mtp { return Err("time-too-old".into()); } @@ -1500,7 +1532,7 @@ impl ChainHub { mtp, expected, ) - .map_err(|e| header_reject(header, &e)) + .map_err(|e| header_reject(header, &rbitcoin_query::ConfirmError::from(e))) } fn sync_parent_header( @@ -1689,7 +1721,7 @@ impl ChainHub { &self, stamped: PlanStampOutcome, pipeline: Option<&WireLoadPipeline>, - ) -> Result { + ) -> Result { consensus_load_from_plan( &self.query, &self.params, @@ -1698,7 +1730,7 @@ impl ChainHub { pipeline, &ScriptPreverified::new(), ) - .map_err(NetError::from_consensus) + .map_err(NetError::from_confirm) } /// Unified lookup+load from raw wire blocks (no Class-A wire rebuild). @@ -1713,7 +1745,7 @@ impl ChainHub { pub fn confirm_wire_load_phase( &self, blocks: &[(Height, Block)], - ) -> Result, NetError> { + ) -> Result, NetError> { self.confirm_wire_load_phase_pipelined(blocks, None) } @@ -1724,7 +1756,7 @@ impl ChainHub { &self, blocks: &[(Height, Block)], pipeline: Option<&WireLoadPipeline>, - ) -> Result, NetError> { + ) -> Result, NetError> { let Some(contig) = self.confirm_wire_contig(blocks, pipeline) else { return Ok(None); }; @@ -1736,20 +1768,20 @@ impl ChainHub { &ScriptPreverified::new(), pipeline, ) - .map_err(NetError::from_consensus)?; + .map_err(NetError::from_confirm)?; Ok(Some(ok)) } /// WRITE stage: structural + Class C + spend annotate (ordered). pub fn confirm_write(&self, batch: ScriptOkBatch) -> Result, NetError> { - let headers = batch.wire_headers().map_err(NetError::from_consensus)?; + let headers = batch.wire_headers().map_err(NetError::from_confirm)?; let meta: Vec<(u32, BlockHash)> = batch .heights_hashes() .into_iter() .map(|(h, raw)| (h, BlockHash::from_byte_array(raw))) .collect(); confirm_write_phase(&self.query, &self.params, self.milestone, batch) - .map_err(NetError::from_consensus)?; + .map_err(NetError::from_confirm)?; self.note_confirmed_tip(&meta, &headers)?; Ok(meta .iter() @@ -1763,11 +1795,9 @@ impl ChainHub { headers: &[Header], ) -> Result<(), NetError> { if headers.len() != need_meta.len() { - return Err(NetError::from_consensus( - rbitcoin_consensus::ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( - "invariant: wire headers length", - )), - )); + return Err(NetError::store(rbitcoin_store::StoreError::Corrupt( + "invariant: wire headers length", + ))); } if let Some(mp) = self.mempool() { mp.clear_recent_rejects(); @@ -1807,7 +1837,7 @@ impl ChainHub { /// Block time is `max(MTP+1, now)`. fn generate_block_time(&self, tip_h: u32, tip_time: u32) -> u32 { let now = self.clock.now_secs() as u32; - let mtp = rbitcoin_consensus::median_time_past(self.query.as_ref(), Height(tip_h)) + let mtp = rbitcoin_query::median_time_past(self.query.as_ref(), Height(tip_h)) .unwrap_or(tip_time); now.max(mtp.saturating_add(1)) } @@ -1951,7 +1981,7 @@ impl ChainHub { if let Err(e) = self.query.apply_sh_pending() { rbitcoin_log::warn!("generate: SH write-behind drain: {e}"); } - if let Err(e) = rbitcoin_consensus::build_indexes_released(&self.query) { + if let Err(e) = rbitcoin_query::build_indexes_released(&self.query) { rbitcoin_log::warn!("generate: index write-behind drain: {e}"); } Ok(hashes) @@ -2516,7 +2546,7 @@ impl ChainHub { &batch, )?; validate_header_on_parent(&self.params, Height(height), &b.header, mtp, expected) - .map_err(|e| header_reject(&b.header, &e)) + .map_err(|e| header_reject(&b.header, &rbitcoin_query::ConfirmError::from(e))) }; if let Err(e) = checked { return Err(connect_failed_for_header(b.block_hash().to_byte_array(), e)); @@ -3028,12 +3058,14 @@ impl ChainHub { } }) .map_err(|e| match e { - rbitcoin_consensus::ConsensusError::Cancelled => NetError::Cancelled, - rbitcoin_consensus::ConsensusError::Store(se) => { - rbitcoin_log::warn!("tip connect {hash} @ {height}: store fault: {se}"); - NetError::store(se) + e if e.is_cancelled() => NetError::Cancelled, + rbitcoin_query::ConfirmError::Rules( + rbitcoin_consensus::ConsensusError::Internal(m), + ) => { + rbitcoin_log::warn!("tip connect {hash} @ {height}: pipeline fault: {m}"); + NetError::store(m) } - e => { + rbitcoin_query::ConfirmError::Rules(e) => { let reason = rbitcoin_consensus::block_reject_reason(&e); rbitcoin_log::info!( "{}", @@ -3045,6 +3077,10 @@ impl ChainHub { NetError::Consensus(reason) } } + rbitcoin_query::ConfirmError::Store(se) => { + rbitcoin_log::warn!("tip connect {hash} @ {height}: store fault: {se}"); + NetError::store(se) + } })?; self.header_tips.write().unwrap().remove(&hash); let t_mp = std::time::Instant::now(); @@ -3326,13 +3362,13 @@ pub fn check_block_proposal_with( } let height = tip_h.0.saturating_add(1); let expected = - rbitcoin_consensus::expected_next_bits(query, params, Height(height), block.header.time) + rbitcoin_query::expected_next_bits(query, params, Height(height), block.header.time) .map(|c| c.to_consensus()) .unwrap_or(tip_rec.bits); if block.header.bits.to_consensus() != expected { return Err("bad-diffbits".into()); } - let mtp = rbitcoin_consensus::median_time_past(query, tip_h).unwrap_or(tip_rec.timestamp); + let mtp = rbitcoin_query::median_time_past(query, tip_h).unwrap_or(tip_rec.timestamp); // Core is `<=` MTP. Proposal uses `<` so a template stamped at the // parent's mediantime+1 still validates after that parent is submitted // (new MTP often equals that stamp on an incrementing cache). @@ -3907,8 +3943,9 @@ mod tests { use bitcoin::{ Amount, CompactTarget, OutPoint, Sequence, Target, Transaction, TxIn, TxOut, Witness, }; - use rbitcoin_consensus::{confirm_scripts_phase, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_mempool::UtxoProvider; + use rbitcoin_query::confirm_scripts_phase; fn tmp_hub() -> (rbitcoin_query::testutil::TempDir, ChainHub) { super::tiny_regtest_hub_labeled("chain") @@ -4289,7 +4326,7 @@ mod tests { #[test] fn wire_load_skips_a_block_below_the_pipeline_floor() { - use rbitcoin_consensus::WireLoadPipeline; + use rbitcoin_query::WireLoadPipeline; let (dir, hub) = tmp_hub(); hub.ensure_genesis().unwrap(); @@ -5902,7 +5939,7 @@ mod tests { /// Regression: hub used store tip+1 only → Ok(None) "empty outcome" thrash. #[test] fn wire_prep_ahead_of_store_tip_with_pipeline() { - use rbitcoin_consensus::WireLoadPipeline; + use rbitcoin_query::WireLoadPipeline; let (dir, hub) = tmp_hub(); hub.ensure_genesis().unwrap(); @@ -6214,7 +6251,7 @@ mod tests { .block_at_height(0) .unwrap() .expect("genesis after ensure"); - rbitcoin_consensus::pad_empty_from( + rbitcoin_query::pad_empty_from( hub.query.as_ref(), &hub.params, genesis.block_hash(), @@ -6296,7 +6333,7 @@ mod tests { .block_at_height(0) .unwrap() .expect("genesis after ensure"); - let (_tip, _time, cbs) = rbitcoin_consensus::pad_empty_from( + let (_tip, _time, cbs) = rbitcoin_query::pad_empty_from( hub.query.as_ref(), &hub.params, genesis.block_hash(), @@ -6746,7 +6783,7 @@ mod tests { )); assert!(hub.held_body(&stale.block_hash()).is_some()); - rbitcoin_consensus::pad_empty_from( + rbitcoin_query::pad_empty_from( hub.query.as_ref(), &hub.params, b1.block_hash(), @@ -7605,6 +7642,30 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + /// A side block whose parent header read is cancelled is not `BLOCK_FAILED`. + #[test] + fn cancelled_header_read_on_a_side_block_is_not_cached_invalid() { + let (dir, hub) = tmp_hub(); + hub.ensure_genesis().unwrap(); + let gen = hub.tip_hash().unwrap(); + let b1 = mine(gen, 1_300_050_000, 1); + hub.accept_block(b1.clone()).unwrap(); + let side = mine_distinct(gen, 1_300_050_001, 1, &[b1.block_hash()]); + hub.query.fail_next_header_read_cancelled(); + let err = hub + .accept_received_block(side.clone()) + .expect_err("a cancelled header read is not an accept"); + assert!( + err.is_local_fault(), + "cancel must stay a store/engine result, got {err}" + ); + assert!( + !hub.is_block_invalid(&side.block_hash()), + "a cancelled header read must not be BLOCK_FAILED, err={err}" + ); + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn store_fault_during_header_check_is_not_a_failed_block() { let hash = [0x11; 32]; diff --git a/crates/rbitcoin-net/src/error.rs b/crates/rbitcoin-net/src/error.rs index aaf9ec7b6e..c242e7aabe 100644 --- a/crates/rbitcoin-net/src/error.rs +++ b/crates/rbitcoin-net/src/error.rs @@ -87,11 +87,19 @@ impl NetError { match e { rbitcoin_consensus::ConsensusError::Cancelled => NetError::Cancelled, rbitcoin_consensus::ConsensusError::BadPrev => NetError::BadPrev, - rbitcoin_consensus::ConsensusError::Store(se) => NetError::store(se), + // Pool-adapted store text. Not a block verdict. + rbitcoin_consensus::ConsensusError::Internal(m) => NetError::store(m), other => NetError::Consensus(other.to_string()), } } + pub fn from_confirm(e: rbitcoin_query::ConfirmError) -> Self { + match e { + rbitcoin_query::ConfirmError::Rules(r) => Self::from_consensus(r), + rbitcoin_query::ConfirmError::Store(se) => NetError::store(se), + } + } + /// `BLOCK_MUTATED`: the body may not be the one the header commits to, /// so the block hash must not be cached as failed. pub fn is_mutated(&self) -> bool { diff --git a/crates/rbitcoin-net/src/ibd/archive.rs b/crates/rbitcoin-net/src/ibd/archive.rs index 477b8f1547..f18ac7ec20 100644 --- a/crates/rbitcoin-net/src/ibd/archive.rs +++ b/crates/rbitcoin-net/src/ibd/archive.rs @@ -442,7 +442,7 @@ mod class_a_rehydrate_tests { let b = mine(tip, time + h * 600, h); hub.ensure_header(&b.header).unwrap(); let (rec, txs) = - rbitcoin_consensus::prepare_block_for_archive(&hub.query, &hub.params, &b).unwrap(); + rbitcoin_query::prepare_block_for_archive(&hub.query, &hub.params, &b).unwrap(); hub.query.commit_class_a_only(&rec, &txs).unwrap(); hashes.push(b.block_hash()); tip = b.block_hash(); diff --git a/crates/rbitcoin-net/src/ibd/confirm/mod.rs b/crates/rbitcoin-net/src/ibd/confirm/mod.rs index 7a15ed865c..2dd2975446 100644 --- a/crates/rbitcoin-net/src/ibd/confirm/mod.rs +++ b/crates/rbitcoin-net/src/ibd/confirm/mod.rs @@ -5,8 +5,8 @@ use super::status::LoopStats; use crate::chain::ChainHub; use bitcoin::hashes::Hash; use bitcoin::BlockHash; -use rbitcoin_consensus::WireLoadPipeline; use rbitcoin_log::{debug, info, warn}; +use rbitcoin_query::WireLoadPipeline; use std::cell::Cell; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; @@ -392,12 +392,12 @@ impl ConfirmRejectClass { pub(crate) fn from_consensus(err: &rbitcoin_consensus::ConsensusError) -> Self { use rbitcoin_consensus::ConsensusError; - use rbitcoin_store::StoreError; match err { ConsensusError::Cancelled => Self::Cancelled, ConsensusError::BadPrev => Self::SoftWire, ConsensusError::BadBlock(r) if crate::chain::reject_is_mutated(r) => Self::SoftWire, ConsensusError::BadHeader("missing retarget first header") => Self::SoftWire, + ConsensusError::Internal(m) => Self::from_err_str(m), ConsensusError::BadBlock(_) | ConsensusError::BadTx(_) | ConsensusError::Script(_) @@ -406,11 +406,19 @@ impl ConfirmRejectClass { | ConsensusError::BadVersion(_) | ConsensusError::BadHeader(_) | ConsensusError::MissingPrevout => Self::ConsensusInvalid, - ConsensusError::Store(StoreError::Cancelled(_)) => Self::Cancelled, - ConsensusError::Store(e) if e.is_uring_session_fault() => Self::EngineFault, - ConsensusError::Store(StoreError::Io { .. }) => Self::EngineFault, - ConsensusError::Store(StoreError::Corrupt(m)) => Self::from_err_str(m), - ConsensusError::Store(_) => Self::EngineFault, + } + } + + pub(crate) fn from_confirm(err: &rbitcoin_query::ConfirmError) -> Self { + use rbitcoin_query::ConfirmError; + use rbitcoin_store::StoreError; + match err { + ConfirmError::Rules(e) => Self::from_consensus(e), + ConfirmError::Store(StoreError::Cancelled(_)) => Self::Cancelled, + ConfirmError::Store(e) if e.is_uring_session_fault() => Self::EngineFault, + ConfirmError::Store(StoreError::Io { .. }) => Self::EngineFault, + ConfirmError::Store(StoreError::Corrupt(m)) => Self::from_err_str(m), + ConfirmError::Store(_) => Self::EngineFault, } } @@ -463,10 +471,10 @@ impl ConfirmRejectClass { fn finish_connected_write_after_session_fault( query: &rbitcoin_query::Query, items: &[(u32, [u8; 32])], -) -> Result<(), rbitcoin_consensus::ConsensusError> { +) -> Result<(), rbitcoin_query::ConfirmError> { query.uring_recover_or_abort("ibd-confirm-write"); loop { - match rbitcoin_consensus::finish_post_commit_hashes(query, items) { + match rbitcoin_query::finish_post_commit_hashes(query, items) { Ok(()) => return Ok(()), Err(e) if e.is_uring_session_fault() => { query.uring_recover_or_abort("ibd-confirm-write"); @@ -749,9 +757,11 @@ impl LookupFaultPolicy { self.consecutive = 0; } - pub(crate) fn on_err(&mut self, e: &rbitcoin_consensus::ConsensusError) -> LookupFaultAction { - use rbitcoin_consensus::ConsensusError; - let backpressure = matches!(e, ConsensusError::Store(se) if se.is_io_backpressure()); + pub(crate) fn on_err(&mut self, e: &rbitcoin_query::ConfirmError) -> LookupFaultAction { + let backpressure = matches!( + e, + rbitcoin_query::ConfirmError::Store(se) if se.is_io_backpressure() + ); if backpressure { return LookupFaultAction::Ignore; } @@ -1637,7 +1647,7 @@ fn check_missing_parent(hub: &ChainHub, feed: &ConfirmFeed, block: &bitcoin::Blo } // Same HDD gate as lookup and load: this read scans `tx.head`. hub.query.store().spindle_wait(); - let read = rbitcoin_consensus::parent_missing_from_chain(&hub.query, block, stop); + let read = rbitcoin_query::parent_missing_from_chain(&hub.query, block, stop); if stop() || !ready() || hub.query.take_disconnect(&mut disconnects).is_some() { return ParentCheck::OffTip; } @@ -1655,14 +1665,14 @@ fn check_missing_parent(hub: &ChainHub, feed: &ConfirmFeed, block: &bitcoin::Blo /// to one block at a time. A lone block's miss takes the class of its /// [`ParentCheck`]; an engine fault goes back once, and a repeat halts IBD. fn stamp_reject_class( - e: &rbitcoin_consensus::ConsensusError, + e: &rbitcoin_query::ConfirmError, check: Option, ) -> ConfirmRejectClass { match check { Some(ParentCheck::Missing(_)) => ConfirmRejectClass::ConsensusInvalid, Some(ParentCheck::OffTip) => ConfirmRejectClass::Cascade, Some(ParentCheck::Engine) => ConfirmRejectClass::EngineFault, - None => ConfirmRejectClass::from_consensus(e), + None => ConfirmRejectClass::from_confirm(e), } } @@ -1692,14 +1702,14 @@ pub(crate) fn stamp_reject_operator_msg(err: &str, stats: &rbitcoin_query::Confi /// leftover is returned for the next write iteration (ordered scripts should /// never hit that path). fn drain_script_ok_write_queue( - first: rbitcoin_consensus::ScriptOkBatch, - rx: &std::sync::mpsc::Receiver, + first: rbitcoin_query::ScriptOkBatch, + rx: &std::sync::mpsc::Receiver, max_parts: usize, - mut on_extra: impl FnMut(&rbitcoin_consensus::ScriptOkBatch), + mut on_extra: impl FnMut(&rbitcoin_query::ScriptOkBatch), ) -> ( - rbitcoin_consensus::ScriptOkBatch, + rbitcoin_query::ScriptOkBatch, usize, - Option, + Option, ) { let mut batch = first; let mut parts = 1usize; @@ -1870,10 +1880,10 @@ pub(crate) fn spawn_confirm_engine( ) -> (std::thread::JoinHandle<()>, Arc) { let queues = ConfirmQueueDepths::new(); let caps = confirm_queue_caps(); - type ScriptsIn = (rbitcoin_consensus::LoadedBatch, u64); + type ScriptsIn = (rbitcoin_query::LoadedBatch, u64); let (mat_tx, mat_rx) = std::sync::mpsc::sync_channel::(caps.script); let (write_tx, write_rx) = - std::sync::mpsc::sync_channel::(caps.write); + std::sync::mpsc::sync_channel::(caps.write); let (load_tx, load_rx) = std::sync::mpsc::sync_channel::(caps.load); // A write fault that does not re-arm lookup: plan drops reserved fks + // last_loaded so re-lookup after Class A partial commit does not drift @@ -1895,7 +1905,7 @@ pub(crate) fn spawn_confirm_engine( info!("ibd: confirm write on dedicated OS thread"); let stats = hub_wb.query.confirm_stats_arc(); // Non-contig leftover already note_write_recv'd; write it next iter. - let mut leftover: Option = None; + let mut leftover: Option = None; loop { let t_recv = Instant::now(); let first = match leftover.take() { @@ -1951,7 +1961,7 @@ pub(crate) fn spawn_confirm_engine( .iter() .map(|&(h, raw)| (h, BlockHash::from_byte_array(raw))) .collect(); - match rbitcoin_consensus::confirm_write_phase_or_return( + match rbitcoin_query::confirm_write_phase_or_return( &hub_wb.query, &hub_wb.params, hub_wb.milestone, @@ -2011,8 +2021,7 @@ pub(crate) fn spawn_confirm_engine( confirm_thr_stats::add_write_work(&stats, t0.elapsed()); let mut err = e; let msg = err.to_string(); - if matches!(err, rbitcoin_consensus::ConsensusError::Cancelled) - || feed_wb.stopped() + if err.is_cancelled() || feed_wb.stopped() { info!("ibd: confirm write aborted: {msg}"); break; @@ -2124,7 +2133,7 @@ pub(crate) fn spawn_confirm_engine( warn!( "ibd: confirm write reject @ {height} batch_parts={parts}: {err}" ); - let class = ConfirmRejectClass::from_consensus(&err); + let class = ConfirmRejectClass::from_confirm(&err); let t_rearm = Instant::now(); let wave: Vec<(u32, BlockHash, &bitcoin::Block)> = meta .iter() @@ -2161,7 +2170,7 @@ pub(crate) fn spawn_confirm_engine( .spawn(move || { info!("ibd: confirm scripts on dedicated OS thread (publish waves; steal pool verifies)"); let stats = hub_sc.query.confirm_stats_arc(); - rbitcoin_consensus::drive_script_waves_with( + rbitcoin_query::drive_script_waves_with( &mat_rx, |batch, wait| { confirm_thr_stats::add_script_recv_wait(&stats, wait); @@ -2208,14 +2217,13 @@ pub(crate) fn spawn_confirm_engine( |e, meta, dropped| { confirm_thr_stats::add_script_work(&stats, Duration::ZERO); let msg = e.to_string(); - if matches!(e, rbitcoin_consensus::ConsensusError::Cancelled) - || feed_sc.stopped() + if e.is_cancelled() || feed_sc.stopped() { info!("ibd: confirm scripts aborted: {msg}"); return false; } let session_fault = e.is_uring_session_fault(); - let class = ConfirmRejectClass::from_consensus(&e); + let class = ConfirmRejectClass::from_confirm(&e); // A retried, engine-faulted, or session-faulted wave goes // back with the batches dropped behind it; a one-block // verdict drops its block. @@ -2447,8 +2455,7 @@ pub(crate) fn spawn_confirm_engine( Ok(s) => s, Err(e) => { let msg = e.to_string(); - if matches!(e, rbitcoin_consensus::ConsensusError::Cancelled) - || feed_load.stopped() + if e.is_cancelled() || feed_load.stopped() { drop(mat_tx); rbitcoin_consensus::unpark_script_publisher(); @@ -2458,7 +2465,9 @@ pub(crate) fn spawn_confirm_engine( let session_fault = e.is_uring_session_fault(); let parent_check = match (&e, wire_batch.as_slice()) { ( - rbitcoin_consensus::ConsensusError::MissingPrevout, + rbitcoin_query::ConfirmError::Rules( + rbitcoin_consensus::ConsensusError::MissingPrevout, + ), [(_, _, w)], ) => { let t_check = Instant::now(); @@ -2778,11 +2787,11 @@ pub(crate) fn spawn_confirm_engine( } else { remaining .saturating_mul(run_max) - .min(rbitcoin_consensus::BQ_RESOLVE_WAVE_MAX_BLOCKS) + .min(rbitcoin_query::BQ_RESOLVE_WAVE_MAX_BLOCKS) }; let max_inputs = (remaining as u32) .saturating_mul(confirm_batch_max_inputs()) - .min(rbitcoin_consensus::BQ_RESOLVE_WAVE_MAX_INPUTS); + .min(rbitcoin_query::BQ_RESOLVE_WAVE_MAX_INPUTS); let wave_h = hub .query .block_queue_unresolved_heights(path_lo, &skip, max_blocks); @@ -2882,7 +2891,7 @@ pub(crate) fn spawn_confirm_engine( // Take before the send: a take that loses to a // re-arm leaves its rows queued and is not sent. if !matches!( - rbitcoin_consensus::take_wave_items_for_load( + rbitcoin_query::take_wave_items_for_load( &hub.query, &batch.items, taken_gen, @@ -3057,10 +3066,10 @@ pub(super) fn lookup_bulk_wave( heights: &[u32], max_blocks: usize, max_inputs: u32, -) -> Result { +) -> Result { query.store().spindle_wait(); note_confirm_bulk_read(); - rbitcoin_consensus::confirm_bq_resolve_wave_capped( + rbitcoin_query::confirm_bq_resolve_wave_capped( query, params, milestone, heights, max_blocks, max_inputs, ) } @@ -3070,12 +3079,12 @@ pub(super) fn load_bulk_stamp( query: &rbitcoin_query::Query, params: &rbitcoin_consensus::ChainParams, milestone: rbitcoin_consensus::Milestone, - blocks: &[rbitcoin_consensus::WireBlockIn], - pipeline: Option<&rbitcoin_consensus::WireLoadPipeline>, -) -> Result { + blocks: &[rbitcoin_query::WireBlockIn], + pipeline: Option<&rbitcoin_query::WireLoadPipeline>, +) -> Result { query.store().spindle_wait(); note_confirm_bulk_read(); - rbitcoin_consensus::confirm_wire_lookup_stamp(query, params, milestone, blocks, pipeline) + rbitcoin_query::confirm_wire_lookup_stamp(query, params, milestone, blocks, pipeline) } #[cfg(test)] diff --git a/crates/rbitcoin-net/src/ibd/confirm/tests.rs b/crates/rbitcoin-net/src/ibd/confirm/tests.rs index 3dc24e036b..46dbee5cf3 100644 --- a/crates/rbitcoin-net/src/ibd/confirm/tests.rs +++ b/crates/rbitcoin-net/src/ibd/confirm/tests.rs @@ -71,7 +71,7 @@ fn spindle_hold_blocks_lookup_and_load_bulk_reads() { #[test] fn index_startup_gap_matches_one_write_drain() { assert_eq!( - rbitcoin_consensus::INDEX_STARTUP_GAP_HEIGHTS as usize, + rbitcoin_query::INDEX_STARTUP_GAP_HEIGHTS as usize, write_drain_max_parts(write_queue_cap()) * CONFIRM_RUN_MAX_BLOCKS ); } @@ -1527,22 +1527,27 @@ fn plan_epoch_stale_after_clear() { #[test] fn write_session_fault_is_engine_fault() { - use rbitcoin_consensus::ConsensusError; use rbitcoin_store::StoreError; - let undrained = ConsensusError::Store(StoreError::Corrupt("invariant: io_uring undrained")); + let undrained = + rbitcoin_query::ConfirmError::Store(StoreError::Corrupt("invariant: io_uring undrained")); assert_eq!( - super::ConfirmRejectClass::from_consensus(&undrained), + super::ConfirmRejectClass::from_confirm(&undrained), super::ConfirmRejectClass::EngineFault ); - let leftover = ConsensusError::Store(StoreError::Corrupt("invariant: io_uring leftover cqe")); + let leftover = rbitcoin_query::ConfirmError::Store(StoreError::Corrupt( + "invariant: io_uring leftover cqe", + )); assert_eq!( - super::ConfirmRejectClass::from_consensus(&leftover), + super::ConfirmRejectClass::from_confirm(&leftover), super::ConfirmRejectClass::EngineFault ); - let io = ConsensusError::Store(StoreError::io("/tmp/x", std::io::Error::other("disk"))); + let io = rbitcoin_query::ConfirmError::Store(StoreError::io( + "/tmp/x", + std::io::Error::other("disk"), + )); assert_eq!( - super::ConfirmRejectClass::from_consensus(&io), + super::ConfirmRejectClass::from_confirm(&io), super::ConfirmRejectClass::EngineFault ); } @@ -1626,13 +1631,15 @@ fn requeue_on_uring_recover_rearms_lookup_and_offers_the_wave_back() { #[test] fn lookup_fault_policy_io_halt() { use super::{LookupFaultAction, LookupFaultPolicy}; - use rbitcoin_consensus::ConsensusError; use rbitcoin_store::StoreError; let mut p = LookupFaultPolicy::default(); - let bp = ConsensusError::Store(StoreError::BudgetFull("io_uring SQ")); + let bp = rbitcoin_query::ConfirmError::Store(StoreError::BudgetFull("io_uring SQ")); assert_eq!(p.on_err(&bp), LookupFaultAction::Ignore); - let io = ConsensusError::Store(StoreError::io("/tmp/x", std::io::Error::other("disk"))); + let io = rbitcoin_query::ConfirmError::Store(StoreError::io( + "/tmp/x", + std::io::Error::other("disk"), + )); for _ in 0..7 { assert_eq!(p.on_err(&io), LookupFaultAction::Warn); } @@ -1697,10 +1704,9 @@ fn ibd_confirm_pin_fault() { use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; - use rbitcoin_consensus::{ - mine_empty_regtest, mine_regtest_paying, pad_empty_from, ChainParams, - }; + use rbitcoin_consensus::{mine_empty_regtest, mine_regtest_paying, ChainParams}; use rbitcoin_primitives::Height; + use rbitcoin_query::pad_empty_from; use rbitcoin_query::testutil::FixtureChain as _; use rbitcoin_query::{ArchiveWritePlan, TxApply}; use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, TxRecord}; diff --git a/crates/rbitcoin-net/src/ibd/events/confirm_reject_tests.rs b/crates/rbitcoin-net/src/ibd/events/confirm_reject_tests.rs index 1dfcbf6a13..7a5ef0b38f 100644 --- a/crates/rbitcoin-net/src/ibd/events/confirm_reject_tests.rs +++ b/crates/rbitcoin-net/src/ibd/events/confirm_reject_tests.rs @@ -144,7 +144,9 @@ fn confirm_reject_class_matches_substring_table() { ConfirmRejectClass::Cancelled ); assert_eq!( - ConfirmRejectClass::from_consensus(&ConsensusError::from(StoreError::Cancelled("stop"))), + ConfirmRejectClass::from_confirm(&rbitcoin_query::ConfirmError::from( + StoreError::Cancelled("stop"), + )), ConfirmRejectClass::Cancelled ); assert_eq!( @@ -152,21 +154,21 @@ fn confirm_reject_class_matches_substring_table() { ConfirmRejectClass::ConsensusInvalid ); assert_eq!( - ConfirmRejectClass::from_consensus(&ConsensusError::Store(StoreError::Corrupt( - "tx put_full_batch fk mismatch (plan not committed in order)" - ))), + ConfirmRejectClass::from_confirm(&rbitcoin_query::ConfirmError::Store( + StoreError::Corrupt("tx put_full_batch fk mismatch (plan not committed in order)") + )), ConfirmRejectClass::Cascade ); assert_eq!( - ConfirmRejectClass::from_consensus(&ConsensusError::Store(StoreError::Corrupt( - "invariant: spend annotate missing pin denserels/abs" - ))), + ConfirmRejectClass::from_confirm(&rbitcoin_query::ConfirmError::Store( + StoreError::Corrupt("invariant: spend annotate missing pin denserels/abs") + )), ConfirmRejectClass::EngineFault ); assert_eq!( - ConfirmRejectClass::from_consensus(&ConsensusError::Store(StoreError::Corrupt( - "invariant: io_uring undrained" - ))), + ConfirmRejectClass::from_confirm(&rbitcoin_query::ConfirmError::Store( + StoreError::Corrupt("invariant: io_uring undrained") + )), ConfirmRejectClass::EngineFault ); assert_eq!( @@ -1875,7 +1877,8 @@ fn wire_pad() -> &'static WirePad { use std::sync::OnceLock; static PAD: OnceLock = OnceLock::new(); PAD.get_or_init(|| { - use rbitcoin_consensus::{pad_empty_from, ChainParams}; + use rbitcoin_consensus::ChainParams; + use rbitcoin_query::pad_empty_from; let (dir, hub0) = crate::chain::tiny_regtest_hub_labeled("wire-pad"); hub0.query.enter_direct_index_mode().unwrap(); let hub = std::sync::Arc::new(hub0); @@ -1931,7 +1934,8 @@ impl WireRig { /// A fresh regtest pad like [`wire_pad`], on a store whose `tx.head` /// segments roll every 204 creates. fn new_small_head(label: &str, peers: usize) -> Self { - use rbitcoin_consensus::{pad_empty_from, ChainParams}; + use rbitcoin_consensus::ChainParams; + use rbitcoin_query::pad_empty_from; let dir = rbitcoin_query::testutil::TempDir::labeled(label).expect("small-head dir"); drop(rbitcoin_store::testutil::create_tiny_with_head_bits( dir.path(), diff --git a/crates/rbitcoin-net/src/ibd/path.rs b/crates/rbitcoin-net/src/ibd/path.rs index 9f67dd74fe..f9d95caa44 100644 --- a/crates/rbitcoin-net/src/ibd/path.rs +++ b/crates/rbitcoin-net/src/ibd/path.rs @@ -670,7 +670,7 @@ mod tests { Amount, Block, CompactTarget, OutPoint, Sequence, Target, Transaction, TxIn, TxOut, Witness, }; - use rbitcoin_consensus::prepare_block_for_archive; + use rbitcoin_query::prepare_block_for_archive; let (dir, hub) = crate::chain::tiny_regtest_hub_labeled("path-seed-arch"); hub.ensure_genesis().unwrap(); diff --git a/crates/rbitcoin-net/src/peer.rs b/crates/rbitcoin-net/src/peer.rs index e94c6c2fe4..58170cdc1e 100644 --- a/crates/rbitcoin-net/src/peer.rs +++ b/crates/rbitcoin-net/src/peer.rs @@ -2111,6 +2111,7 @@ fn maybe_queue_initial_getheaders( if started { let h = hub.tip_height().unwrap_or(0); rbitcoin_log::info!("{}", crate::chain::initial_getheaders_log(h, session.id)); + hub.note_initial_getheaders_sent(); let _ = queue_getheaders(out, hub, Some(session), true, None); } started @@ -3660,6 +3661,7 @@ fn on_headers( headers: &[bitcoin::block::Header], ) -> Result<(), NetError> { let n = headers.len(); + hub.note_headers_reply(); let _ = session.is_some_and(|s| s.take_awaiting_headers()); let mut ignored_below_floor = false; if n == 0 { diff --git a/crates/rbitcoin-net/src/peer_tests.rs b/crates/rbitcoin-net/src/peer_tests.rs index c89d22fc3c..2238e8d0b4 100644 --- a/crates/rbitcoin-net/src/peer_tests.rs +++ b/crates/rbitcoin-net/src/peer_tests.rs @@ -4711,7 +4711,7 @@ fn headers_poll_expires_a_stale_tx_off_the_tokio_worker() { let (dir, hub) = crate::chain::tiny_regtest_hub_labeled("hdr-poll-expire"); hub.ensure_genesis().unwrap(); let tip = hub.tip_header().unwrap(); - let (_tip, _time, cbs) = rbitcoin_consensus::pad_empty_from( + let (_tip, _time, cbs) = rbitcoin_query::pad_empty_from( &hub.query, &ChainParams::regtest(), hub.tip_hash().unwrap(), diff --git a/crates/rbitcoin-net/src/tip_accept.rs b/crates/rbitcoin-net/src/tip_accept.rs index 3d877c4839..23611c3f73 100644 --- a/crates/rbitcoin-net/src/tip_accept.rs +++ b/crates/rbitcoin-net/src/tip_accept.rs @@ -1,7 +1,7 @@ //! Process-wide `tip-accept` thread: P2P/RPC connect off tokio workers. //! //! Jobs are 1-block (or one `accept_branch` run). Confirm still uses -//! [`rbitcoin_consensus::confirm_wire_run_preverified`] (lookup → load → +//! [`rbitcoin_query::confirm_wire_run_preverified`] (lookup → load → //! `rbtc-scripts-*` steal → write). Not the IBD body-queue pipeline. use std::future::Future; diff --git a/crates/rbitcoin-net/src/tx_relay.rs b/crates/rbitcoin-net/src/tx_relay.rs index 0afb058a74..2c3ae31f22 100644 --- a/crates/rbitcoin-net/src/tx_relay.rs +++ b/crates/rbitcoin-net/src/tx_relay.rs @@ -310,11 +310,8 @@ impl UtxoProvider for QueryUtxoProvider<'_> { if let Some(m) = self.meter_create_mtp { m.fetch_add(1, Ordering::Relaxed); } - rbitcoin_consensus::median_time_past( - self.query, - Height(create_height.saturating_sub(1)), - ) - .unwrap_or(0) + rbitcoin_query::median_time_past(self.query, Height(create_height.saturating_sub(1))) + .unwrap_or(0) }; ChainPrevout::Unspent(Coin { txout: TxOut { @@ -1767,7 +1764,7 @@ impl MempoolHub { /// Confirmed tip snapshot for mempool structural checks (height + BIP113 MTP). fn chain_tip_ctx(&self) -> ChainTipCtx { - use rbitcoin_consensus::median_time_past; + use rbitcoin_query::median_time_past; let height = self.query.tip_height().map(|h| h.0).unwrap_or(0); let fk = self.query.tip_header_fk().ok().flatten(); if let Some(fk) = fk { @@ -5325,9 +5322,10 @@ mod tests { /// prevout unavailable. `Ok(false)` on height 0 is not a non-coinbase coin. #[test] fn coinbase_table_error_makes_the_prevout_unavailable() { - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_mempool::{ChainPrevout, UtxoProvider}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; let dir = tmp(); let q = Query::open_or_create_tiny(&dir).unwrap(); @@ -5369,8 +5367,9 @@ mod tests { let (store, owned_q, owned_cbs) = copy_maturity_pad(3); let q = &owned_q; let cbs = owned_cbs.as_slice(); - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_store::script_hash; let params = ChainParams::regtest(); @@ -6062,10 +6061,9 @@ mod tests { /// drops it. The graph filter is what decides, including a tx that is gone. #[test] fn purge_confirmed_drops_relay_maps() { - use rbitcoin_consensus::{ - accept_and_connect_block, mine_regtest_paying, ChainParams, Milestone, - }; + use rbitcoin_consensus::{mine_regtest_paying, ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; let (store, q, cbs) = copy_maturity_pad(3); let params = ChainParams::regtest(); @@ -6828,15 +6826,16 @@ mod tests { } fn build_maturity_pad(n: u32) -> MaturityPad { - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; let dir = tmp(); let q = Query::open_or_create_tiny(&dir).unwrap(); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); let last = n + params.coinbase_maturity(); - let (_tip, _t, cbs) = rbitcoin_consensus::pad_empty_from( + let (_tip, _t, cbs) = rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), @@ -7824,14 +7823,15 @@ mod tests { #[test] fn fee_history_preload_reuses_heights_it_already_holds() { - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::accept_and_connect_block; let store_dir = tmp(); let q = Query::open_or_create_tiny(&store_dir).unwrap(); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); - rbitcoin_consensus::pad_empty_from( + rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), @@ -7879,14 +7879,15 @@ mod tests { #[test] fn fee_history_file_survives_a_restart_and_drops_heights_off_the_chain() { - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::accept_and_connect_block; let store_dir = tmp(); let q = Query::open_or_create_tiny(&store_dir).unwrap(); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); - let (tip, tip_time, _) = rbitcoin_consensus::pad_empty_from( + let (tip, tip_time, _) = rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), @@ -7904,7 +7905,7 @@ mod tests { assert_eq!(first.total_targets, 11); assert_eq!(first.ready_targets, 0); // a connect after the preload goes to the journal - rbitcoin_consensus::pad_empty_from(&q, ¶ms, tip, tip_time, 6, 6, 0); + rbitcoin_query::pad_empty_from(&q, ¶ms, tip, tip_time, 6, 6, 0); hub.note_block_fee_history(Height(6)); let journaled = fee_history_file::load(&mp_dir).unwrap().unwrap(); assert_eq!( diff --git a/crates/rbitcoin-node/src/run.rs b/crates/rbitcoin-node/src/run.rs index d0b4c7855c..edcc944e78 100644 --- a/crates/rbitcoin-node/src/run.rs +++ b/crates/rbitcoin-node/src/run.rs @@ -277,7 +277,7 @@ pub fn run_node(config: NodeConfig) -> Result { config.store_layout(), config.check_blocks_window(), )?; - rbitcoin_consensus::replay_spend_annotations(&query) + rbitcoin_query::replay_spend_annotations(&query) .map_err(|e| NodeError::Init(format!("spend annotation replay: {e}")))?; Ok(NodeHandle { config, @@ -321,7 +321,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { ); } apply_startup_index_mode(&handle.query, &config, params.taproot_height())?; - rbitcoin_consensus::prepare_live_indexes(&handle.query) + rbitcoin_query::prepare_live_indexes(&handle.query) .map_err(|e| crate::error::NodeError::Init(format!("index startup repair failed: {e}")))?; let bind = config.listen.start_p2p_bind(config.network); @@ -724,14 +724,14 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { tip_follow_ready = gates.tip_follow_ready; sh_tip_ready = gates.sh_tip_ready; if tip_follow_ready && !shutdown.requested() { - let index_entry = rbitcoin_consensus::index_tip_entry(&node.hub.query); + let index_entry = rbitcoin_query::index_tip_entry(&node.hub.query); if index_entry.advertise_filters { info!("blockfilter: already at tip; advertising NODE_COMPACT_FILTERS"); rbitcoin_net::set_compact_filters_service(true); } if index_entry.spawn { let advertise_later = !index_entry.advertise_filters; - index_writebehind = Some(rbitcoin_consensus::spawn_index_writebehind( + index_writebehind = Some(rbitcoin_query::spawn_index_writebehind( Arc::clone(&node.hub.query), Arc::clone(&shutdown.flag), { @@ -795,6 +795,10 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { } } if tip_follow_ready && !shutdown.requested() { + let header_mark = ( + node.hub.initial_getheaders_sent(), + node.hub.headers_reply_count(), + ); let follow_n = targets.len().min(max_out.min(3)); if catch_up.dial_failed_all() { for peer in targets.iter().take(follow_n) { @@ -839,6 +843,9 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { // without a live session, including ones past `follow_n`. Hostnames // were registered before catch-up (`set_connect_hosts`). node.peers.set_connect_addrs(config.listen.connect.clone()); + // Materialize can outlast the miner. Do not publish Electrum until + // the follow `getheaders` round is connected and indexed. + wait_wallet_history(&node, config.shindex, &shutdown, header_mark).await; } let (mut electrum_handles, mut electrum_bridge, electrum_onion) = start_electrum_if_ready( @@ -1937,6 +1944,38 @@ async fn start_i2p_named_forward( Ok(sam) } +/// Wallet bind waits out one follow `getheaders` round. +/// +/// Scripthash materialize holds the process before dial. The miner can add +/// a block in that window. Opening Electrum on the pre-dial tip serves a +/// history that does not include it. +async fn wait_wallet_history(node: &P2PNode, shindex: bool, shutdown: &Shutdown, mark: (u64, u64)) { + if node.follow_live_count() == 0 { + return; + } + let (sent0, reply0) = mark; + let started = Instant::now(); + let deadline = Duration::from_secs(20); + let unsent_limit = Duration::from_secs(2); + loop { + if shutdown.requested() || started.elapsed() >= deadline { + return; + } + let sent = node.hub.initial_getheaders_sent().saturating_sub(sent0); + let replies = node.hub.headers_reply_count().saturating_sub(reply0); + let tip = node.tip_height().unwrap_or(0); + let best = node.hub.best_header_height(); + let sh_ok = !shindex || node.hub.query.sh_is_tip_ready(); + if sent > 0 && replies >= sent && best <= tip && sh_ok { + return; + } + if sent == 0 && started.elapsed() >= unsent_limit { + return; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } +} + #[allow(clippy::too_many_arguments)] // call-site args stay unbundled async fn start_electrum_if_ready( sh_tip_ready: bool, diff --git a/crates/rbitcoin-primitives/Cargo.toml b/crates/rbitcoin-primitives/Cargo.toml index 76bd4e9883..0b783369af 100644 --- a/crates/rbitcoin-primitives/Cargo.toml +++ b/crates/rbitcoin-primitives/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true description = "Shared types for the rbitcoin node" [dependencies] +bitcoin_hashes = { workspace = true } [lints] workspace = true diff --git a/crates/rbitcoin-primitives/src/lib.rs b/crates/rbitcoin-primitives/src/lib.rs index 0337d2c314..e7d98252dd 100644 --- a/crates/rbitcoin-primitives/src/lib.rs +++ b/crates/rbitcoin-primitives/src/lib.rs @@ -7,6 +7,7 @@ mod compact; mod hex; mod loc_simd; mod median_time; +mod merkle; mod script_sigops; mod scriptnum; @@ -20,6 +21,9 @@ pub use hex::{ }; pub use loc_simd::{deinterleave_pairs_u8x8, inclusive_u8x8_times_8}; pub use median_time::median_time_past_times; +pub use merkle::{ + merkle_branch, merkle_root_from_branch, merkle_root_from_txids, merkle_root_mutated, +}; pub use script_sigops::script_sigop_count; pub use scriptnum::{ scriptnum_decode, scriptnum_decode_width, scriptnum_encode, scriptnum_is_minimal, diff --git a/crates/rbitcoin-primitives/src/merkle.rs b/crates/rbitcoin-primitives/src/merkle.rs new file mode 100644 index 0000000000..24c6d9e59b --- /dev/null +++ b/crates/rbitcoin-primitives/src/merkle.rs @@ -0,0 +1,139 @@ +//! Bitcoin block merkle root from leaf txids (internal byte order). +//! +//! The mutation flag is Core's `ComputeMerkleRoot` check (CVE-2012-2459): +//! some level held two equal hashes in one pair before odd padding, so a +//! shorter tx list has the same root. + +use bitcoin_hashes::{sha256, Hash, HashEngine}; + +/// Bitcoin block merkle root from leaf txids (internal byte order). +pub fn merkle_root_from_txids(leaves: &[[u8; 32]]) -> [u8; 32] { + merkle_root_mutated(leaves).0 +} + +/// [`merkle_root_from_txids`] plus Core's `ComputeMerkleRoot` mutation flag +/// (CVE-2012-2459). +pub fn merkle_root_mutated(leaves: &[[u8; 32]]) -> ([u8; 32], bool) { + if leaves.is_empty() { + return ([0u8; 32], false); + } + let mut mutated = false; + let mut level: Vec<[u8; 32]> = leaves.to_vec(); + while level.len() > 1 { + mutated |= level.chunks_exact(2).any(|pair| pair[0] == pair[1]); + level = merkle_parent_level(level); + } + (level[0], mutated) +} + +/// Sibling hashes from `leaves[index]` up to the root, deepest first. Empty for +/// one leaf. Folding the leaf with them (left when the index bit is 0) +/// reproduces [`merkle_root_from_txids`]. The leaf's own value feeds no entry. +pub fn merkle_branch(leaves: &[[u8; 32]], mut index: usize) -> Vec<[u8; 32]> { + let mut branch = Vec::new(); + let mut level: Vec<[u8; 32]> = leaves.to_vec(); + while level.len() > 1 { + let sibling = (index ^ 1).min(level.len() - 1); + branch.push(level[sibling]); + level = merkle_parent_level(level); + index /= 2; + } + branch +} + +/// Root reached by folding `leaf` at `index` with its [`merkle_branch`]. The +/// leaf is the left child when the index bit at that depth is 0. +pub fn merkle_root_from_branch(leaf: [u8; 32], branch: &[[u8; 32]], index: usize) -> [u8; 32] { + branch + .iter() + .enumerate() + .fold(leaf, |acc, (depth, sibling)| { + if (index >> depth) & 1 == 0 { + hash256_concat(&acc, sibling) + } else { + hash256_concat(sibling, &acc) + } + }) +} + +fn merkle_parent_level(mut level: Vec<[u8; 32]>) -> Vec<[u8; 32]> { + if level.len() % 2 == 1 { + level.push(level[level.len() - 1]); + } + level + .chunks_exact(2) + .map(|pair| hash256_concat(&pair[0], &pair[1])) + .collect() +} + +fn hash256_concat(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] { + let mut eng = sha256::HashEngine::default(); + eng.input(a); + eng.input(b); + let mid = sha256::Hash::from_engine(eng); + let mut eng2 = sha256::HashEngine::default(); + eng2.input(mid.as_byte_array()); + sha256::Hash::from_engine(eng2).to_byte_array() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merkle_root_single_and_pair() { + let a = [1u8; 32]; + assert_eq!(merkle_root_from_txids(&[a]), a); + let b = [2u8; 32]; + let root = merkle_root_from_txids(&[a, b]); + assert_eq!(root, hash256_concat(&a, &b)); + } + + /// CVE-2012-2459: a repeated tail at any level keeps the root and is + /// flagged. The shorter list and an unpaired repeat are not. + #[test] + fn merkle_mutation_flags_a_repeated_tail_at_any_level() { + let l: Vec<[u8; 32]> = (1..=6u8).map(|n| [n; 32]).collect(); + let (root3, mutated) = merkle_root_mutated(&l[..3]); + assert!(!mutated); + assert_eq!( + merkle_root_mutated(&[l[0], l[1], l[2], l[2]]), + (root3, true) + ); + assert!(!merkle_root_mutated(&[l[0], l[1], l[1]]).1); + + let (root6, mutated) = merkle_root_mutated(&l); + assert!(!mutated); + let mut tail = l.clone(); + tail.extend_from_slice(&l[4..]); + assert_eq!(merkle_root_mutated(&tail), (root6, true)); + assert_eq!(merkle_root_mutated(&[]), ([0u8; 32], false)); + } + + #[test] + fn merkle_path_folds_to_root_at_every_index() { + let (a, b, c) = ([1u8; 32], [2u8; 32], [3u8; 32]); + assert!(merkle_branch(&[a], 0).is_empty()); + assert_eq!(merkle_branch(&[a, b], 0), vec![b]); + assert_eq!( + merkle_branch(&[a, b, c], 0), + vec![b, hash256_concat(&c, &c)] + ); + assert_eq!( + merkle_branch(&[a, b, c], 2), + vec![c, hash256_concat(&a, &b)] + ); + for n in 1..=9u8 { + let leaves: Vec<[u8; 32]> = (0..n).map(|i| [i + 10; 32]).collect(); + let root = merkle_root_from_txids(&leaves); + for (index, leaf) in leaves.iter().enumerate() { + let branch = merkle_branch(&leaves, index); + assert_eq!( + merkle_root_from_branch(*leaf, &branch, index), + root, + "n={n} index={index}" + ); + } + } + } +} diff --git a/crates/rbitcoin-query/Cargo.toml b/crates/rbitcoin-query/Cargo.toml index 7ab42549e3..42f15acea8 100644 --- a/crates/rbitcoin-query/Cargo.toml +++ b/crates/rbitcoin-query/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true description = "Query API over the rbitcoin store" [dependencies] +rbitcoin-consensus = { workspace = true } rbitcoin-primitives = { workspace = true } rbitcoin-store = { workspace = true } rbitcoin-log = { workspace = true } @@ -16,6 +17,9 @@ arc-swap = { workspace = true } # Attribute only. cargo-mutants reads it; it does not change compiled code. mutants = "0.0.4" +[features] +test-hooks = [] + [dev-dependencies] # Height-index tests write a raw confirmed slot. Dependents of this crate # do not turn the feature on. diff --git a/crates/rbitcoin-query/src/batch_parents.rs b/crates/rbitcoin-query/src/batch_parents.rs index d5ccb531b4..9c0f65d7ce 100644 --- a/crates/rbitcoin-query/src/batch_parents.rs +++ b/crates/rbitcoin-query/src/batch_parents.rs @@ -52,6 +52,12 @@ pub struct PinnedOuts { vout: u32, } +impl rbitcoin_consensus::block::PinParts for PinOuts { + fn parts(&self, vout: u32) -> Option<(u64, &[u8])> { + self.get_parts(vout) + } +} + impl SharedPrevoutScript { /// Amount and script for a pinned vout. `None` for [`Self::Wire`] (the /// block lives on the script job) and when the vout is absent. @@ -61,6 +67,18 @@ impl SharedPrevoutScript { Self::Pinned(p) => p.outs.get_parts(p.vout), } } + + /// Same allocation the pin published. The script bytes are not copied. + pub fn into_shared_prevout(self) -> rbitcoin_consensus::block::SharedPrevout { + use rbitcoin_consensus::block::SharedPrevout; + match self { + Self::Wire { tx_index, vout } => SharedPrevout::Wire { tx_index, vout }, + Self::Pinned(p) => SharedPrevout::Pinned { + outs: p.outs, + vout: p.vout, + }, + } + } } const CB_UNKNOWN: u8 = 0; diff --git a/crates/rbitcoin-query/src/bip68_store_tests.rs b/crates/rbitcoin-query/src/bip68_store_tests.rs new file mode 100644 index 0000000000..e1e2a25318 --- /dev/null +++ b/crates/rbitcoin-query/src/bip68_store_tests.rs @@ -0,0 +1,266 @@ +//! BIP68 checks that read a confirmed median from the store. +#![cfg(test)] + +use bitcoin::absolute::LockTime; +use bitcoin::script::ScriptBuf; +use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; +use rbitcoin_consensus::ValidationContext; + +fn bare_tx(version: i32, lock_time: LockTime, sequence: Sequence) -> Transaction { + Transaction { + version: bitcoin::transaction::Version(version), + lock_time, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + } +} + +#[test] +fn bip68_height_zero_time_lock_uses_the_median() { + let type_flag = 1u32 << 22; + let mtp = 1_231_006_505u32; + // The median comes from the confirmed genesis header through + // `structural_bip68`, not from a caller-supplied MTP slice. + let (_dir, query) = crate::testutil::tiny_query_labeled("bip68-h0"); + let header_fk = query + .store() + .put_header(&rbitcoin_store::HeaderRecord { + timestamp: mtp, + hash: [0x11; 32], + ..Default::default() + }) + .unwrap(); + query + .store() + .confirmed + .set(rbitcoin_primitives::Height(0), header_fk) + .unwrap(); + query.store().rebuild_height_fence().unwrap(); + let params = Box::leak(Box::new(rbitcoin_consensus::ChainParams::regtest())); + let ctx = ValidationContext::at( + params, + rbitcoin_primitives::Height(1), + rbitcoin_consensus::Milestone::NONE, + ); + let create = rbitcoin_primitives::Fk(1); + let mut create_heights = crate::FkMap::default(); + create_heights.insert(create, 0); + let spends = [([0u8; 32], 0u32, rbitcoin_primitives::Fk::NULL, create, 0u32)]; + let block = height_zero_timelock_block(type_flag); + crate::block_pipeline::structural_bip68( + &query, + &block, + &ctx, + &spends, + &create_heights, + &mut crate::U32Map::default(), + ) + .expect("height 0 time lock uses the genesis median"); + let too_far = height_zero_timelock_block(type_flag | 1); + let err = crate::block_pipeline::structural_bip68( + &query, + &too_far, + &ctx, + &spends, + &create_heights, + &mut crate::U32Map::default(), + ) + .expect_err("512s relative to the genesis median is not final at height 1"); + assert!( + format!("{err}").contains("nonfinal"), + "structural path must apply the median, got {err}" + ); +} + +fn height_zero_timelock_block(sequence: u32) -> bitcoin::Block { + use bitcoin::block::{Header, Version}; + use bitcoin::hashes::Hash; + use bitcoin::{Block, BlockHash, CompactTarget, TxMerkleNode}; + let spend = bare_tx(2, LockTime::ZERO, Sequence::from_consensus(sequence)); + let coinbase = bare_tx(1, LockTime::ZERO, Sequence::MAX); + let mut block = Block { + header: Header { + version: Version::from_consensus(1), + prev_blockhash: BlockHash::from_byte_array([0; 32]), + merkle_root: TxMerkleNode::from_byte_array([0; 32]), + time: 1_231_006_505, + bits: CompactTarget::from_consensus(0x1d00_ffff), + nonce: 0, + }, + txdata: vec![coinbase, spend], + }; + block.header.merkle_root = block.compute_merkle_root().unwrap(); + block +} + +fn seq_tx(seqs: &[u32]) -> Transaction { + Transaction { + version: bitcoin::transaction::Version(2), + lock_time: LockTime::ZERO, + input: seqs + .iter() + .map(|seq| TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(*seq), + witness: Witness::new(), + }) + .collect(), + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + } +} + +fn csv_block(txs: Vec) -> bitcoin::Block { + use bitcoin::block::{Header, Version}; + use bitcoin::hashes::Hash; + use bitcoin::{Block, BlockHash, CompactTarget, TxMerkleNode}; + let mut block = Block { + header: Header { + version: Version::from_consensus(1), + prev_blockhash: BlockHash::from_byte_array([0; 32]), + merkle_root: TxMerkleNode::from_byte_array([0; 32]), + time: 1_231_006_505, + bits: CompactTarget::from_consensus(0x1d00_ffff), + nonce: 0, + }, + txdata: txs, + }; + block.header.merkle_root = block.compute_merkle_root().unwrap(); + block +} + +/// Three two-input txs must connect. `si * n_in` false-rejects the third. +#[test] +fn structural_bip68_multi_input_block_connects() { + let params = Box::leak(Box::new(rbitcoin_consensus::ChainParams::regtest())); + let ctx = ValidationContext::at( + params, + rbitcoin_primitives::Height(1), + rbitcoin_consensus::Milestone::NONE, + ); + let (_dir, query) = crate::testutil::tiny_query_labeled("bip68-multi"); + let coinbase = bare_tx(1, LockTime::ZERO, Sequence::MAX); + let spend = |seq: u32| seq_tx(&[seq, seq]); + let block = csv_block(vec![ + coinbase, + spend(u32::MAX), + spend(u32::MAX), + spend(u32::MAX), + ]); + let spends = vec![ + ( + [0u8; 32], + 0u32, + rbitcoin_primitives::Fk::NULL, + rbitcoin_primitives::Fk::NULL, + 0u32 + ); + 6 + ]; + let mut cache = crate::U32Map::default(); + cache.insert(0, 1_000); + crate::block_pipeline::structural_bip68( + &query, + &block, + &ctx, + &spends, + &crate::FkMap::default(), + &mut cache, + ) + .expect("a well-formed multi-input block passes the spend cursor"); +} + +/// Height-type CSV does not prefetch a median. A missing parent header must +/// still pass when the block's own previous median is already cached. +#[test] +fn structural_bip68_height_lock_skips_missing_coin_mtp() { + let params = Box::leak(Box::new(rbitcoin_consensus::ChainParams::regtest())); + let ctx = ValidationContext::at( + params, + rbitcoin_primitives::Height(6), + rbitcoin_consensus::Milestone::NONE, + ); + let (_dir, query) = crate::testutil::tiny_query_labeled("bip68-height-mtp"); + let create = rbitcoin_primitives::Fk(3); + let mut create_heights = crate::FkMap::default(); + create_heights.insert(create, 5); + let block = csv_block(vec![ + bare_tx(1, LockTime::ZERO, Sequence::MAX), + seq_tx(&[0]), + ]); + let spends = [([0u8; 32], 0u32, rbitcoin_primitives::Fk::NULL, create, 0u32)]; + let mut cache = crate::U32Map::default(); + cache.insert(5, 1_500_000); + crate::block_pipeline::structural_bip68( + &query, + &block, + &ctx, + &spends, + &create_heights, + &mut cache, + ) + .expect("height-type lock must not read the coin median"); +} + +/// BIP68 walks `txdata[1..]`. A coinbase whose relative lock would fail still connects. +#[test] +fn bip68_skips_a_coinbase_whose_relative_lock_would_fail() { + use rbitcoin_consensus::{ + mine_empty_regtest, prepare_regtest_candidate, ConsensusError, Milestone, + }; + use rbitcoin_primitives::Height; + + let (_dir, q) = crate::testutil::tiny_query_labeled("bip68-coinbase"); + let params = rbitcoin_consensus::ChainParams::regtest(); + let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); + crate::accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE) + .unwrap(); + let mut block = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); + block.txdata[0].version = bitcoin::transaction::Version::TWO; + block.txdata[0].input[0].sequence = Sequence::from_consensus(200); + prepare_regtest_candidate(&mut block, genesis.block_hash(), genesis.header.time + 600); + crate::accept_and_connect_block(&q, ¶ms, Height(1), &block, Milestone::NONE) + .expect("coinbase sequence is not a BIP68 input"); + assert_eq!(q.tip_height(), Some(Height(1))); + + let params = Box::leak(Box::new(rbitcoin_consensus::ChainParams::regtest())); + let ctx = ValidationContext::at(params, Height(6), Milestone::NONE); + let (_dir, query) = crate::testutil::tiny_query_labeled("bip68-noncb"); + let create = rbitcoin_primitives::Fk(3); + let mut create_heights = crate::FkMap::default(); + create_heights.insert(create, 5); + let block = csv_block(vec![ + bare_tx(1, LockTime::ZERO, Sequence::MAX), + bare_tx(2, LockTime::ZERO, Sequence::from_consensus(200)), + ]); + let spends = [([0u8; 32], 0u32, rbitcoin_primitives::Fk::NULL, create, 0u32)]; + let mut cache = crate::U32Map::default(); + cache.insert(5, 1_500_000); + let err = crate::block_pipeline::structural_bip68( + &query, + &block, + &ctx, + &spends, + &create_heights, + &mut cache, + ) + .expect_err("the same lock rejects a non-coinbase"); + assert!( + matches!( + err, + crate::ConfirmError::Rules(ConsensusError::BadTx(s)) if s.contains("nonfinal") + ), + "{err:?}" + ); +} diff --git a/crates/rbitcoin-query/src/block_pipeline.rs b/crates/rbitcoin-query/src/block_pipeline.rs new file mode 100644 index 0000000000..0458e0f6ca --- /dev/null +++ b/crates/rbitcoin-query/src/block_pipeline.rs @@ -0,0 +1,1483 @@ +//! Assemble and spentness for confirm. This module loads rows and calls the +//! consensus rule functions. It does not decide a header, script, or spend +//! rule on its own. +use bitcoin::block::Block; +use bitcoin::hashes::Hash; +use bitcoin::ScriptBuf; +use bitcoin::{Amount, OutPoint, Transaction, TxOut}; +use rbitcoin_consensus::block::{ + JobPrevouts, PendingCreates, ScriptCheckJob, ScriptVerifyFlags, TxidMap, +}; +use rbitcoin_consensus::ChainParams; +use rbitcoin_primitives::Height; +use std::hash::BuildHasherDefault; +use std::sync::Arc; +use std::time::Instant; + +use crate::{ConfirmError, FkMap, Query, U32Map, U64Map}; + +#[cfg(test)] +pub(crate) mod test_phase { + use std::cell::Cell; + thread_local! { + static TL_COLD_WHY: Cell<(u64, u64, u64, u64)> = const { Cell::new((0, 0, 0, 0)) }; + static TL_BATCH_N: Cell = const { Cell::new(0) }; + static TL_COLD_N: Cell = const { Cell::new(0) }; + } + #[inline] + pub fn tl_note_cold_why_txid_mismatch() { + TL_COLD_WHY.with(|c| { + let (a, b, c2, d) = c.get(); + c.set((a, b, c2 + 1, d)); + }); + TL_COLD_N.with(|c| c.set(c.get() + 1)); + } + #[inline] + pub fn tl_note_cold_why_vout_miss() { + TL_COLD_WHY.with(|c| { + let (a, b, c2, d) = c.get(); + c.set((a, b, c2, d + 1)); + }); + TL_COLD_N.with(|c| c.set(c.get() + 1)); + } + #[inline] + pub fn tl_note_batch_hit() { + TL_BATCH_N.with(|c| c.set(c.get() + 1)); + } + #[inline] + pub fn sample_tl_assemble_cold_why_and_reset() -> (u64, u64, u64, u64) { + TL_COLD_WHY.with(|c| c.replace((0, 0, 0, 0))) + } + #[inline] + pub fn sample_tl_batch_cold_n_and_reset() -> (u64, u64) { + let batch = TL_BATCH_N.with(|c| c.replace(0)); + let cold = TL_COLD_N.with(|c| c.replace(0)); + (batch, cold) + } +} + +/// Block-local prevout path counts; the confirm pipeline flushes once. +#[derive(Default)] +pub(crate) struct AsmPrevoutAcc { + in_n: u64, + same_n: u64, + batch_n: u64, + cold_n: u64, + cold_null_fk_n: u64, + cold_not_pin_n: u64, + cold_txid_mismatch_n: u64, + cold_vout_miss_n: u64, +} + +impl AsmPrevoutAcc { + pub(crate) fn flush(&self, stats: &crate::ConfirmStats) { + crate::note_confirm(&stats.asm_in_n, self.in_n); + crate::note_confirm(&stats.asm_prev_same_n, self.same_n); + crate::note_confirm(&stats.asm_prev_batch_n, self.batch_n); + crate::note_confirm(&stats.asm_prev_cold_n, self.cold_n); + crate::note_confirm(&stats.asm_prev_cold_null_fk_n, self.cold_null_fk_n); + crate::note_confirm(&stats.asm_prev_cold_not_pin_n, self.cold_not_pin_n); + crate::note_confirm( + &stats.asm_prev_cold_txid_mismatch_n, + self.cold_txid_mismatch_n, + ); + crate::note_confirm(&stats.asm_prev_cold_vout_miss_n, self.cold_vout_miss_n); + } +} + +pub(crate) struct AssembleMeters { + pub prev_ns: u64, + pub job_ns: u64, + pub acc: AsmPrevoutAcc, +} + +#[allow(clippy::too_many_arguments)] // call-site args stay unbundled +#[allow(clippy::type_complexity)] // packed row / pin / script-hash tuple is the on-disk shape +/// Sequential assemble: resolve prevout **content**, build script jobs, collect spends. +/// +/// Confirm IBD path: no durable spentness / maturity / BIP68 create-height +/// resolution — those run in [`structural_validate_spends`] after scripts (load +/// must not walk create height per parent). Absolute nLockTime finality +/// (BIP113 MTP of prev block) still runs here — it only needs header MTP. +/// +/// `pending_spent`: pack-local double-spend (early reject before scripts). +/// `pending_creates`: pack-local `txid → create_fk` (not per-vout). +/// Same-block outs use `txid_index` (this block, `pj < ti`); meters flush +/// once per block (no per-input Instant / atomics). +/// +/// Prevouts resolve from per-batch [`crate::BatchParents`] + +/// [`crate::SpendEdges`]. Optimistic miss is `invariant:` (no head recover). +/// +/// Returns `(script_jobs, spends, fees, tx_fees, meters)`. `fees` is the block sum +/// for the coinbase subsidy check. `tx_fees` is one satoshi fee per tx +/// (coinbase is 0) for the `txstat` stamp. `meters` is flushed by the caller. +/// +/// `prev_mtp` / `block_hash` / `bip16_active` must be computed **once** by the +/// caller (assemble_run header window) — no re-walk of headers and no rehash. +/// +/// `wire`: when `Some`, script jobs share that Arc (no `Transaction` clone). +/// When `None` (unit-test connect), jobs own a deep clone of each non-cb tx. +/// `build_script_jobs` is the caller's milestone decision. +pub fn assemble_block_prevouts( + block: &Block, + ctx: &rbitcoin_consensus::ValidationContext<'_>, + archived_tx_fks: Option<&[rbitcoin_primitives::Fk]>, + pending_spent: &mut rbitcoin_consensus::OutPointSet, + pending_creates: &mut PendingCreates, + batch_parents: &crate::BatchParents, + spend_edges: &crate::SpendEdges, + create_txids: &[[u8; 32]], + prev_mtp: u32, + block_hash: &[u8; 32], + bip16_active: bool, + wire: Option<&Arc>, + pres: Option<&Arc<[rbitcoin_consensus::TxPrecompute]>>, + build_script_jobs: bool, +) -> Result< + ( + Vec, + Vec<( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )>, + i64, + Vec, + AssembleMeters, + ), + ConfirmError, +> { + assemble_prevout_guards(block, archived_tx_fks, create_txids)?; + // Caller-supplied BIP16 must match hash+prev_mtp (no silent re-resolve). + debug_assert_eq!( + bip16_active, + rbitcoin_consensus::block::bip16_active_from_prev_mtp( + ctx.params, + ctx.height.0, + block_hash, + prev_mtp + ) + ); + let flags = + rbitcoin_consensus::block::ScriptVerifyFlags::consensus_at(ctx, block_hash, bip16_active); + + let n_tx = block.txdata.len(); + let mut txid_index: TxidMap = + TxidMap::with_capacity_and_hasher(n_tx, Default::default()); + for (i, id) in create_txids.iter().enumerate() { + txid_index.insert(*id, i); + } + let mut acc = AsmPrevoutAcc::default(); + let mut clk_job = 0u64; + let mut fees = 0i64; + let mut tx_fees = vec![0u64; n_tx]; + let mut script_jobs: Vec = if build_script_jobs { + Vec::with_capacity(n_tx.saturating_sub(1)) + } else { + Vec::new() + }; + let mut block_sigops_cost = match pres.and_then(|p| p.first()) { + Some(p) => p.sigops.saturating_mul(4), + None => rbitcoin_consensus::block::legacy_sigop_count(&block.txdata[0]).saturating_mul(4), + }; + let mut spends: Vec<( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )> = Vec::with_capacity(n_tx.saturating_mul(2)); + + let t_loop = Instant::now(); + + let lock_time_cutoff = assemble_lock_time_cutoff(ctx, block, prev_mtp); + + for (ti, tx) in block.txdata.iter().enumerate() { + let spend_fk = archived_tx_fks.map(|fks| fks[ti]); + let txid = create_txids[ti]; + + rbitcoin_consensus::block::reject_non_final(tx, ctx.height.0, lock_time_cutoff)?; + rbitcoin_consensus::block::reject_tx_without_outputs(tx)?; + if ti > 0 { + tx_fees[ti] = assemble_non_cb_tx( + ctx.params, + block, + tx, + ti, + txid, + spend_fk, + flags, + bip16_active, + wire, + pres, + &txid_index, + pending_spent, + pending_creates, + batch_parents, + spend_edges, + &mut acc, + &mut script_jobs, + &mut spends, + &mut fees, + &mut block_sigops_cost, + build_script_jobs, + &mut clk_job, + )?; + } + + let create_fk = spend_fk.unwrap_or(rbitcoin_primitives::Fk::NULL); + if !create_fk.is_null() { + pending_creates.insert(txid, create_fk); + } + } + + let clk_prev = t_loop.elapsed().as_nanos() as u64; + Ok(( + script_jobs, + spends, + fees, + tx_fees, + AssembleMeters { + prev_ns: clk_prev, + job_ns: clk_job, + acc, + }, + )) +} + +fn assemble_prevout_guards( + block: &Block, + archived_tx_fks: Option<&[rbitcoin_primitives::Fk]>, + create_txids: &[[u8; 32]], +) -> Result<(), ConfirmError> { + rbitcoin_consensus::block::reject_assemble_block_shape( + block.txdata.len(), + create_txids.len(), + archived_tx_fks.map(|fks| fks.len()), + block.txdata.first().is_some_and(|tx| tx.is_coinbase()), + )?; + Ok(()) +} + +pub(crate) fn assemble_lock_time_cutoff( + ctx: &rbitcoin_consensus::ValidationContext<'_>, + block: &Block, + prev_mtp: u32, +) -> u32 { + // Height 0 has no MTP history, so the cutoff is the block time even + // when CSV is active. + if ctx.height.0 == 0 || !ctx.params.csv_active_at(ctx.height.0) { + block.header.time + } else { + prev_mtp + } +} + +#[allow(clippy::too_many_arguments)] // one non-coinbase assemble; call site stays unbundled +fn assemble_non_cb_tx( + params: &ChainParams, + block: &Block, + tx: &Transaction, + ti: usize, + txid: [u8; 32], + spend_fk: Option, + flags: ScriptVerifyFlags, + bip16_active: bool, + wire: Option<&Arc>, + pres: Option<&Arc<[rbitcoin_consensus::TxPrecompute]>>, + txid_index: &TxidMap, + pending_spent: &mut rbitcoin_consensus::OutPointSet, + pending_creates: &PendingCreates, + batch_parents: &crate::BatchParents, + spend_edges: &crate::SpendEdges, + acc: &mut AsmPrevoutAcc, + script_jobs: &mut Vec, + spends: &mut Vec<( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )>, + fees: &mut i64, + block_sigops_cost: &mut u64, + build_script_jobs: bool, + clk_job: &mut u64, +) -> Result { + rbitcoin_consensus::block::reject_tx_without_inputs(tx)?; + let (value_in, prevouts, tx_in_sigops) = assemble_non_cb_inputs( + params, + block, + tx, + ti, + spend_fk, + flags, + bip16_active, + txid_index, + pending_spent, + pending_creates, + batch_parents, + spend_edges, + wire, + acc, + spends, + build_script_jobs, + )?; + let tx_legacy_sigops = match pres.and_then(|p| p.get(ti)) { + Some(p) => p.sigops.saturating_mul(4), + None => rbitcoin_consensus::block::legacy_sigop_count(tx).saturating_mul(4), + }; + *block_sigops_cost = + rbitcoin_consensus::block::add_sigops(*block_sigops_cost, tx_legacy_sigops, tx_in_sigops)?; + let value_out = rbitcoin_consensus::block::assemble_tx_value_out(tx, ti, pres)?; + let fee = rbitcoin_consensus::block::fee_from_values(value_in, value_out)?; + let fee_sat = rbitcoin_consensus::block::fee_sat(fee)?; + *fees = rbitcoin_consensus::block::add_fee(*fees, fee)?; + if build_script_jobs { + let t_job = Instant::now(); + let mut job = match (wire, prevouts) { + (Some(w), rbitcoin_consensus::block::JobPrevouts::Shared(v)) => { + rbitcoin_consensus::block::ScriptCheckJob::from_parts( + txid, + rbitcoin_consensus::block::JobPrevouts::Shared(v), + rbitcoin_consensus::block::JobTx::shared(Arc::clone(w), ti), + flags, + ) + } + (None, rbitcoin_consensus::block::JobPrevouts::Owned(v)) => { + rbitcoin_consensus::block::ScriptCheckJob::with_txid(txid, v, tx.clone(), flags) + } + _ => { + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: script job prevouts", + ))); + } + }; + if let Some(ps) = pres { + if rbitcoin_consensus::block::should_use_pres(ti, ps.len()) { + job = job.with_pre_slice(Arc::clone(ps), ti); + } + } + script_jobs.push(job); + *clk_job = clk_job.saturating_add(t_job.elapsed().as_nanos() as u64); + } + Ok(fee_sat) +} + +#[allow(clippy::too_many_arguments)] // one non-coinbase input walk; call site stays unbundled +fn assemble_non_cb_inputs( + params: &ChainParams, + block: &Block, + tx: &Transaction, + ti: usize, + spend_fk: Option, + flags: ScriptVerifyFlags, + bip16_active: bool, + txid_index: &TxidMap, + pending_spent: &mut rbitcoin_consensus::OutPointSet, + pending_creates: &PendingCreates, + batch_parents: &crate::BatchParents, + spend_edges: &crate::SpendEdges, + wire: Option<&Arc>, + acc: &mut AsmPrevoutAcc, + spends: &mut Vec<( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )>, + build_script_jobs: bool, +) -> Result<(i64, JobPrevouts, u64), ConfirmError> { + let mut value_in = 0i64; + let mut prevouts = + if build_script_jobs && wire.is_some() { + rbitcoin_consensus::block::JobPrevouts::Shared(Vec::with_capacity(tx.input.len())) + } else { + rbitcoin_consensus::block::JobPrevouts::owned(Vec::with_capacity( + if build_script_jobs { tx.input.len() } else { 0 }, + )) + }; + let edges = spend_fk.and_then(|fk| fk.get().and_then(|id| spend_edges.get(&id))); + let mut tx_in_sigops = 0u64; + for (ii, input) in tx.input.iter().enumerate() { + let op = input.previous_output; + let key = (op.txid.to_byte_array(), op.vout); + // The genesis coinbase stays indexed for RPC and Electrum, but it is + // not a coin (Core `ConnectBlock` genesis early return). + rbitcoin_consensus::block::reject_genesis_coinbase_spend( + params.is_genesis_coinbase(&key.0), + )?; + rbitcoin_consensus::block::reject_duplicate_input(pending_spent.insert(key))?; + if let Some(&pj) = txid_index.get(&key.0) { + rbitcoin_consensus::block::reject_same_block_create(pj, ti)?; + } + let prev_fk = edges + .as_ref() + .and_then(|t| t.get(ii)) + .and_then(|e| e.create_fk.get().map(|_| e.create_fk)) + .or_else(|| pending_creates.get(&key.0).copied()); + let prev_out = resolve_prevout( + block, + op, + input, + prev_fk, + txid_index, + ti, + batch_parents, + bip16_active, + flags.witness_active, + build_script_jobs, + wire, + acc, + )?; + let create_fk = prev_out.create_fk; + tx_in_sigops = tx_in_sigops.saturating_add(prev_out.input_sigops); + spends.push(( + key.0, + key.1, + spend_fk.unwrap_or(rbitcoin_primitives::Fk::NULL), + create_fk, + ii as u32, + )); + value_in = + rbitcoin_consensus::block::add_value_in(value_in, prev_out.txout.value.to_sat())?; + if build_script_jobs { + match &mut prevouts { + rbitcoin_consensus::block::JobPrevouts::Shared(v) => { + let shared = prev_out.shared.ok_or(ConfirmError::Store( + rbitcoin_store::StoreError::Corrupt( + "invariant: wire confirm prevout shares script bytes", + ), + ))?; + v.push(shared); + } + rbitcoin_consensus::block::JobPrevouts::Owned(v) => v.push(prev_out.txout), + } + } + } + Ok((value_in, prevouts, tx_in_sigops)) +} + +/// Local wall times for one block's structural pass (write path diagnostics). +/// +/// Measured with `Instant` — **not** deltas of window atomics (those race with +/// `sample_and_reset` mid-batch and produced false `spent=0` on slow writes). +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct StructuralPhaseNs { + pub spent_ns: u64, + /// Pin abs collect + bulk on-disk 8-byte spender meta pread. + pub spent_abs_ns: u64, + /// `is_confirmed_strong_at` on non-null fields (still durable authority). + pub spent_strong_ns: u64, + /// Cold unspent_create_vouts / null-create probes. + pub spent_cold_ns: u64, + /// pending_spent order gate (CPU). + pub spent_pending_ns: u64, + pub create_h_ns: u64, + pub bip68_ns: u64, +} + +/// Post-script structural checks: durable spentness, maturity, BIP68, coinbase subsidy. +/// +/// Runs in height order on the write path (after scripts). `pending_spent` is +/// write-local across a multi-height run. +/// +/// **BIP68** create-height lives here (not optimistic load assemble) so confirm +/// load does not walk create height for every parent. Heights: bulk fence. +/// Coin MTP only for time-type relative locks on version ≥2 txs (v1 skipped). +/// +/// Write-path spend annotate slots: abs edge plus the meta structural already read. +/// +/// Filled during spentness and passed to annotate. No second edge copy. +#[derive(Clone, Debug, Default)] +pub(crate) struct AnnotateSlots { + pub abs_edges: Vec<( + u64, + rbitcoin_primitives::Fk, + u32, + rbitcoin_primitives::Fk, + u32, + )>, + pub known: Vec<(rbitcoin_primitives::Fk, u8, u32)>, +} + +impl AnnotateSlots { + pub(crate) fn push( + &mut self, + edge: ( + u64, + rbitcoin_primitives::Fk, + u32, + rbitcoin_primitives::Fk, + u32, + ), + known: (rbitcoin_primitives::Fk, u8, u32), + ) { + self.abs_edges.push(edge); + self.known.push(known); + } +} + +/// Per-block spentness scratch. Capacity stays across blocks in one write batch. +/// [`AnnotateSlots`] accumulate for the whole batch. +pub(crate) struct StructuralScratch { + abs_jobs: Vec, + unique_fks: Vec, + pub(crate) height_by_id: U64Map, + skip_n: std::collections::HashMap< + (u64, u32), + u32, + BuildHasherDefault, + >, + skip: OverlayMetaSkip, + disk_jobs: Vec, + abs_offs: Vec, + field_fks: Vec, + field_seen: crate::U64Set, + pub(crate) field_h_by_id: U64Map, + durable_spent: DurableSpentSet, + height_list: Vec, + create_height_by_fk: FkMap, + pub slots: AnnotateSlots, +} + +impl Default for StructuralScratch { + fn default() -> Self { + Self { + abs_jobs: Vec::new(), + unique_fks: Vec::new(), + height_by_id: U64Map::default(), + skip_n: std::collections::HashMap::with_hasher(Default::default()), + skip: OverlayMetaSkip::with_hasher(Default::default()), + disk_jobs: Vec::new(), + abs_offs: Vec::new(), + field_fks: Vec::new(), + field_seen: crate::U64Set::default(), + field_h_by_id: U64Map::default(), + durable_spent: DurableSpentSet::with_hasher(Default::default()), + height_list: Vec::new(), + create_height_by_fk: FkMap::default(), + slots: AnnotateSlots::default(), + } + } +} + +impl StructuralScratch { + /// Drop annotate slots from the previous write batch. Per-block buffers + /// stay for [`Self::begin_block`]; slots accumulate inside one batch. + pub(crate) fn begin_batch(&mut self) { + self.slots.abs_edges.clear(); + self.slots.known.clear(); + } + + fn begin_block(&mut self) { + self.abs_jobs.clear(); + self.unique_fks.clear(); + self.height_by_id.clear(); + self.skip_n.clear(); + self.skip.clear(); + self.disk_jobs.clear(); + self.abs_offs.clear(); + self.field_fks.clear(); + self.field_seen.clear(); + self.field_h_by_id.clear(); + self.durable_spent.clear(); + self.height_list.clear(); + self.create_height_by_fk.clear(); + } +} + +#[allow(clippy::too_many_arguments)] // call-site args stay unbundled +/// **Spentness:** pin denserels → abs + bulk 8-byte meta. Sparse durable-**spent** +/// set (not unspent). Missing abs / short meta is hard `Err`. **Multi-list** after +/// reorg annotate is a protocol cold walk (`has_confirmed_strong_spender_create`) +/// — not a hard fail (tip-follow reorgs leave multi flags by design). Emits +/// Annotate slots on `scratch` for pure-write annotate. +pub fn structural_validate_spends( + query: &Query, + block: &Block, + ctx: &rbitcoin_consensus::ValidationContext<'_>, + archived_tx_fks: Option<&[rbitcoin_primitives::Fk]>, + spends: &[( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )], + fees: i64, + pending_spent: &mut rbitcoin_consensus::OutPointSet, + batch_parents: &crate::BatchParents, + mtp_cache: &mut U32Map, + run_create_height: &RunCreateHeight, + class_a_wave: &ClassAWave, + scratch: &mut StructuralScratch, + abs_jobs: &[StructuralAbsJob], +) -> Result { + use std::time::Instant; + + scratch.begin_block(); + let maturity = ctx.params.coinbase_maturity(); + // BIP30's txid batch is inside `spent_ns` (signet runs it on every block). + let t_spent = Instant::now(); + reject_bip30_unspent_overwrite(query, block, ctx)?; + let t_abs = Instant::now(); + structural_abs_heights(query, run_create_height, scratch, abs_jobs)?; + let tip = query.tip_height().map(|h| h.0); + let mut spent_strong_ns = 0u64; + let mut multi_list_ns = 0u64; + if !scratch.abs_jobs.is_empty() { + fill_overlay_skip(spends, class_a_wave, scratch); + let loaded = structural_load_durable_spent(query, tip, scratch)?; + multi_list_ns = loaded.0; + spent_strong_ns = loaded.1; + } + let spent_abs_ns = (t_abs.elapsed().as_nanos() as u64).saturating_sub(spent_strong_ns); + let spent_cold_ns = multi_list_ns; + let t_pending = Instant::now(); + structural_mark_pending(spends, pending_spent, &scratch.durable_spent)?; + let spent_pending_ns = t_pending.elapsed().as_nanos() as u64; + let spent_ns = t_spent.elapsed().as_nanos() as u64; + let t_create = Instant::now(); + structural_create_heights( + query, + batch_parents, + run_create_height, + ctx.height.0, + maturity, + scratch, + )?; + let create_h_ns = t_create.elapsed().as_nanos() as u64; + let t_bip68 = Instant::now(); + structural_bip68( + query, + block, + ctx, + spends, + &scratch.create_height_by_fk, + mtp_cache, + )?; + let bip68_ns = t_bip68.elapsed().as_nanos() as u64; + let _ = archived_tx_fks; + rbitcoin_consensus::block::check_coinbase_subsidy(block, ctx, fees)?; + Ok(StructuralPhaseNs { + spent_ns, + spent_abs_ns, + spent_strong_ns, + spent_cold_ns, + spent_pending_ns, + create_h_ns, + bip68_ns, + }) +} + +pub(crate) type StructuralAbsJob = (u64, u32, u64, rbitcoin_primitives::Fk, u32); + +/// Creates this write batch's Class A append committed. +/// +/// Only these spent slots carry the same-batch sole-spender overlay. A run +/// create archived by an earlier wave has whatever that wave and later +/// annotates left on disk, so structural reads and annotates it. +#[derive(Default)] +pub(crate) struct ClassAWave(Vec); + +impl ClassAWave { + pub(crate) fn new(mut fks: Vec) -> Self { + fks.sort_unstable_by_key(|f| f.0); + Self(fks) + } + + fn contains(&self, fk: rbitcoin_primitives::Fk) -> bool { + self.0.binary_search_by_key(&fk.0, |f| f.0).is_ok() + } +} + +/// Create heights for one write batch. +/// +/// Contiguous per-block fk spans are the IBD shape (`first` + count). A gap +/// or overlap uses the map so a fk between spans does not inherit a height. +/// Each block's first fk is its coinbase: a span's `first`, or the map's flag. +pub(crate) enum RunCreateHeight { + Spans(Vec<(u64, u64, u32)>), + Map(FkMap<(u32, bool)>), +} + +impl RunCreateHeight { + pub(crate) fn from_blocks<'a>( + blocks: impl IntoIterator, + ) -> Self { + let blocks: Vec<(u32, &'a [rbitcoin_primitives::Fk])> = blocks.into_iter().collect(); + if let Some(spans) = contiguous_create_spans(&blocks) { + Self::Spans(spans) + } else { + let mut map = FkMap::default(); + for (height, fks) in blocks { + for (i, fk) in fks.iter().enumerate() { + map.insert(*fk, (height, i == 0)); + } + } + Self::Map(map) + } + } + + pub(crate) fn get(&self, fk: rbitcoin_primitives::Fk) -> Option { + self.create(fk).map(|(height, _)| height) + } + + /// Height of a create in this batch and whether it is its block's coinbase. + pub(crate) fn create(&self, fk: rbitcoin_primitives::Fk) -> Option<(u32, bool)> { + match self { + Self::Map(map) => map.get(&fk).copied(), + Self::Spans(spans) => { + let id = fk.get()?; + let i = spans.partition_point(|span| span.0 <= id); + let (first, end, height) = spans.get(i.checked_sub(1)?)?; + (*end > id).then_some((*height, *first == id)) + } + } + } +} + +/// `Some` when every non-empty block is `[first, first+n)` and spans do not overlap. +/// +/// A null fk, a gap, or an id that would wrap is `None` so the caller uses the map. +/// A wrapped end must not cover fks that are not in the block. +fn contiguous_create_spans( + blocks: &[(u32, &[rbitcoin_primitives::Fk])], +) -> Option> { + let mut spans = Vec::with_capacity(blocks.len()); + for &(height, fks) in blocks { + if fks.is_empty() { + continue; + } + let mut ids = fks.iter().map(|fk| fk.get()); + let first = ids.next().flatten()?; + let mut prev = first; + for id in ids { + let id = id?; + let next = prev.checked_add(1)?; + if id != next { + return None; + } + prev = id; + } + let end = prev.checked_add(1)?; + spans.push((first, end, height)); + } + spans.sort_unstable_by_key(|span| span.0); + for pair in spans.windows(2) { + if pair[0].1 > pair[1].0 { + return None; + } + } + Some(spans) +} +type DurableSpentSet = + std::collections::HashSet<(u64, u32), BuildHasherDefault>; +type OverlayMetaSkip = std::collections::HashMap< + (u64, u32), + (rbitcoin_primitives::Fk, u32), + BuildHasherDefault, +>; + +fn fill_overlay_skip( + spends: &[( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )], + class_a_wave: &ClassAWave, + scratch: &mut StructuralScratch, +) { + for &(_, vout, sfk, cfk, vin) in spends { + if !class_a_wave.contains(cfk) { + continue; + } + let Some(id) = cfk.get() else { + continue; + }; + let key = (id, vout); + *scratch.skip_n.entry(key).or_insert(0) += 1; + scratch.skip.entry(key).or_insert((sfk, vin)); + } + let skip_n = &scratch.skip_n; + scratch + .skip + .retain(|k, _| skip_n.get(k).copied() == Some(1)); +} + +fn overlay_meta_is_skip( + id: u64, + vout: u32, + sfk: rbitcoin_primitives::Fk, + vin: u32, + skip: &OverlayMetaSkip, +) -> bool { + skip.get(&(id, vout)) == Some(&(sfk, vin)) +} + +fn structural_abs_heights( + query: &Query, + run_create_height: &RunCreateHeight, + scratch: &mut StructuralScratch, + abs_jobs: &[StructuralAbsJob], +) -> Result<(), ConfirmError> { + scratch.abs_jobs.extend_from_slice(abs_jobs); + scratch.unique_fks.extend( + scratch + .abs_jobs + .iter() + .map(|(id, _, _, _, _)| rbitcoin_primitives::Fk(*id)), + ); + scratch.unique_fks.sort_unstable_by_key(|f| f.0); + scratch.unique_fks.dedup(); + let durable_heights = query + .store() + .tx_height_get_batch(&scratch.unique_fks) + .map_err(ConfirmError::from)?; + for (fk, h) in scratch.unique_fks.iter().zip(durable_heights) { + let Some(id) = fk.get() else { + continue; + }; + let Some(h) = h.or_else(|| run_create_height.get(*fk)) else { + continue; + }; + scratch.height_by_id.insert(id, h); + } + Ok(()) +} + +fn structural_load_durable_spent( + query: &Query, + tip: Option, + scratch: &mut StructuralScratch, +) -> Result<(u64, u64), ConfirmError> { + use std::time::Instant; + let mut ovl_n = 0u64; + for &job in &scratch.abs_jobs { + let (id, vout, abs, sfk, vin) = job; + if overlay_meta_is_skip(id, vout, sfk, vin, &scratch.skip) { + ovl_n = ovl_n.saturating_add(1); + continue; + } + scratch.disk_jobs.push(job); + scratch.abs_offs.push(abs); + } + crate::note_confirm(&query.confirm_stats().spend_overlay_skip_n, ovl_n); + if scratch.abs_offs.is_empty() { + return Ok((0, 0)); + } + let meta_backend = rbitcoin_store::spend_meta_backend(); + let t_meta = Instant::now(); + let metas = query + .store() + .get_spender_meta_at_abs_batch_backend(&scratch.abs_offs, meta_backend) + .map_err(ConfirmError::from)?; + let meta_ns = t_meta.elapsed().as_nanos() as u64; + crate::note_confirm(&query.confirm_stats().spend_meta_ns, meta_ns); + crate::note_confirm( + &query.confirm_stats().spend_meta_n, + scratch.abs_offs.len() as u64, + ); + let _ = meta_backend; + if metas.len() != scratch.disk_jobs.len() { + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: structural meta batch length", + ))); + } + let t_strong = Instant::now(); + for row in &metas { + let Some((field, _, _)) = row else { + continue; + }; + if field.is_null() { + continue; + } + if let Some(fid) = field.get() { + if scratch.field_seen.insert(fid) { + scratch.field_fks.push(*field); + } + } + } + let field_heights = query + .store() + .tx_height_get_batch(&scratch.field_fks) + .map_err(ConfirmError::from)?; + for (fk, h) in scratch.field_fks.iter().zip(field_heights) { + if let Some((id, h)) = fk.get().zip(h) { + scratch.field_h_by_id.insert(id, h); + } + } + let jobs = std::mem::take(&mut scratch.disk_jobs); + let applied = jobs + .iter() + .copied() + .zip(metas) + .try_fold(0u64, |acc, (job, meta)| { + let (id, vout, abs, sfk, vin) = job; + let ns = structural_apply_one_meta(query, meta, id, vout, abs, sfk, vin, tip, scratch)?; + Ok::(acc.saturating_add(ns)) + }); + scratch.disk_jobs = jobs; + let multi_list_ns = applied?; + let spent_strong_ns = t_strong + .elapsed() + .as_nanos() + .saturating_sub(multi_list_ns as u128) as u64; + Ok((multi_list_ns, spent_strong_ns)) +} + +#[allow(clippy::too_many_arguments)] // one spent-slot apply; call site stays unbundled +pub(crate) fn structural_apply_one_meta( + query: &Query, + meta: Option<(rbitcoin_primitives::Fk, u8, u32)>, + id: u64, + vout: u32, + abs: u64, + sfk: rbitcoin_primitives::Fk, + vin: u32, + tip: Option, + scratch: &mut StructuralScratch, +) -> Result { + use std::time::Instant; + let Some((field, flags, field_vin)) = meta else { + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: structural spender meta short/OOB (cold forbidden)", + ))); + }; + scratch.slots.push( + (abs, rbitcoin_primitives::Fk(id), vout, sfk, vin), + (field, flags, field_vin), + ); + let multi = flags & rbitcoin_store::output_flags::MULTI_SPENDER != 0; + if multi { + let t_m = Instant::now(); + let spent = query + .store() + .has_confirmed_strong_spender_create(rbitcoin_primitives::Fk(id), vout, None) + .map_err(ConfirmError::from)?; + let ns = t_m.elapsed().as_nanos() as u64; + if spent { + scratch.durable_spent.insert((id, vout)); + } + return Ok(ns); + } + if field.is_null() { + return Ok(0); + } + let strong = query + .store() + .is_confirmed_strong_at(field, tip) + .map_err(ConfirmError::from)?; + if !strong { + return Ok(0); + } + let create_h = scratch.height_by_id.get(&id).copied(); + let spend_h = field + .get() + .and_then(|fid| scratch.field_h_by_id.get(&fid).copied()); + if let (Some(ch), Some(sh)) = (create_h, spend_h) { + if sh < ch { + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: confirmed spender below its create height", + ))); + } + } + scratch.durable_spent.insert((id, vout)); + Ok(0) +} + +fn structural_mark_pending( + spends: &[( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )], + pending_spent: &mut rbitcoin_consensus::OutPointSet, + durable_spent: &DurableSpentSet, +) -> Result<(), ConfirmError> { + for &(prev_txid, vout, _spend_fk, create_fk, _vin) in spends { + let key = (prev_txid, vout); + let spent = if create_fk.is_null() { + false + } else if let Some(id) = create_fk.get() { + durable_spent.contains(&(id, vout)) + } else { + false + }; + rbitcoin_consensus::block::reject_prevout_spent(spent || !pending_spent.insert(key))?; + } + Ok(()) +} + +fn structural_create_heights( + query: &Query, + batch_parents: &crate::BatchParents, + run_create_height: &RunCreateHeight, + spend_height: u32, + maturity: u32, + scratch: &mut StructuralScratch, +) -> Result<(), ConfirmError> { + // `confirmed[h]` for a height in this batch is written at Class C, after + // structural; the batch index owns coinbase identity there. + scratch.height_list.extend( + scratch + .height_by_id + .iter() + .filter(|&(&id, &h)| run_create_height.get(rbitcoin_primitives::Fk(id)) != Some(h)) + .map(|(_, &h)| h), + ); + scratch.height_list.sort_unstable(); + scratch.height_list.dedup(); + let coinbase_fk_by_height = query + .store() + .coinbase_fk_at_heights(&scratch.height_list) + .map_err(ConfirmError::from)?; + let n_unique = scratch.unique_fks.len(); + for i in 0..n_unique { + let create_fk = scratch.unique_fks[i]; + let Some(id) = create_fk.get() else { + continue; + }; + let Some(&durable_h) = scratch.height_by_id.get(&id) else { + return rbitcoin_consensus::block::reject_one_create( + spend_height, + maturity, + None, + false, + ) + .map_err(ConfirmError::from); + }; + let pin_cb = batch_parents.get_parent_coinbase(create_fk); + let is_cb = match (pin_cb, run_create_height.create(create_fk)) { + (Some(cb), _) => cb, + (None, Some((h, cb))) if h == durable_h => cb, + (None, _) => { + let cb = coinbase_fk_by_height + .get(&durable_h) + .ok_or(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: no coinbase fk at confirmed create height", + )))?; + *cb == create_fk + } + }; + // Core connects a run one block at a time: a create from a later + // block of this batch is not yet a coin when this block spends it. + rbitcoin_consensus::block::reject_one_create( + spend_height, + maturity, + Some(durable_h), + is_cb, + )?; + scratch.create_height_by_fk.insert(create_fk, durable_h); + } + Ok(()) +} + +pub(crate) fn structural_bip68( + query: &Query, + block: &Block, + ctx: &rbitcoin_consensus::ValidationContext<'_>, + spends: &[( + [u8; 32], + u32, + rbitcoin_primitives::Fk, + rbitcoin_primitives::Fk, + u32, + )], + create_height_by_fk: &FkMap, + mtp_cache: &mut U32Map, +) -> Result<(), ConfirmError> { + if !ctx.params.csv_active_at(ctx.height.0) { + return Ok(()); + } + const DISABLE: u32 = 1 << 31; + const TYPE_FLAG: u32 = 1 << 22; + let prev_mtp = if ctx.height.0 == 0 { + 0 + } else { + mtp_at(query, Height(ctx.height.0 - 1), mtp_cache)? + }; + let mut si = 0usize; + let mut prev_heights: Vec = Vec::new(); + let mut coin_mtps: Vec = Vec::new(); + for tx in block.txdata.iter().skip(1) { + let n_in = tx.input.len(); + rbitcoin_consensus::block::reject_spend_cursor(si + n_in <= spends.len())?; + let tx_spends = &spends[si..si + n_in]; + si += n_in; + if !rbitcoin_consensus::block::bip68_active_for_tx(tx) { + continue; + } + prev_heights.clear(); + coin_mtps.clear(); + prev_heights.reserve(n_in); + coin_mtps.reserve(n_in); + for (inp, &(_ptid, _vout, _sfk, create_fk, _vin)) in tx.input.iter().zip(tx_spends.iter()) { + let ch = if create_fk.is_null() { + ctx.height.0 + } else { + rbitcoin_consensus::block::reject_csv_create_height( + create_height_by_fk.get(&create_fk).copied(), + )? + }; + prev_heights.push(ch); + let seq = inp.sequence.to_consensus_u32(); + let need_mtp = seq & DISABLE == 0 && seq & TYPE_FLAG != 0; + // Height 0 is the genesis coin: one median lookup of block 0. + // A missing create height is not height 0. + let mtp = if !need_mtp { + 0 + } else { + mtp_at(query, Height(ch.saturating_sub(1)), mtp_cache)? + }; + coin_mtps.push(mtp); + } + rbitcoin_consensus::block::reject_sequence_locks( + tx, + &prev_heights, + &coin_mtps, + ctx.height.0, + prev_mtp, + )?; + } + rbitcoin_consensus::block::reject_spend_cursor(si == spends.len())?; + Ok(()) +} + +/// MTP for write structural. Prefers assemble-carried `prev_mtp` (seeded into +/// `cache`). Misses go to durable headers only — never `get_header_plan`. +/// BIP30: a connected instance with any unspent spendable output may not be +/// overwritten. +/// Skipped for the two mainnet repeats, and when the header at BIP34 height +/// is this network's BIP34 hash and the block is below +/// [`crate::params::BIP34_IMPLIES_BIP30_LIMIT`]. Signet and regtest have no +/// BIP34 hash, so every block is checked. Just-archived self is unconnected. +fn reject_bip30_unspent_overwrite( + query: &Query, + block: &Block, + ctx: &rbitcoin_consensus::ValidationContext<'_>, +) -> Result<(), ConfirmError> { + if ctx.params.is_bip30_repeat(ctx.height.0, block.block_hash()) + || bip34_ancestry_skips_bip30(query, ctx) + { + return Ok(()); + } + let create_txids: Vec<[u8; 32]> = block + .txdata + .iter() + .map(|tx| tx.compute_txid().to_byte_array()) + .collect(); + let hits = query + .store() + .get_fk_by_txid_batch(&create_txids) + .map_err(ConfirmError::from)?; + for (_txid, row) in hits { + let Some((old_fk, _)) = row else { + continue; + }; + // Connected instance at *this* height is ourselves (re-validate / + // already-confirmed fixture). BIP30 is an earlier unspent sibling. + let prior_height = query + .store() + .tx_height_get(old_fk) + .map_err(ConfirmError::from)?; + if prior_height == Some(ctx.height.0) { + continue; + } + let (_, outs) = query + .store() + .get_tx_meta_and_outputs(old_fk) + .map_err(ConfirmError::from)?; + let mut unspent = false; + for (v, out) in (0u32..).zip(&outs) { + // Core's AddCoins never stores an unspendable output, so it is + // not a coin that the overwrite could clobber. + if rbitcoin_consensus::policy::is_unspendable(&out.script) { + continue; + } + let spent = query + .store() + .has_confirmed_strong_spender_create(old_fk, v, None) + .map_err(ConfirmError::from)?; + if !spent { + unspent = true; + break; + } + } + if unspent { + rbitcoin_consensus::block::reject_bip30_unspent_prior( + ctx.height.0, + prior_height.unwrap_or(u32::MAX), + true, + )?; + } + } + Ok(()) +} + +fn bip34_ancestry_skips_bip30( + query: &Query, + ctx: &rbitcoin_consensus::ValidationContext<'_>, +) -> bool { + let height = ctx.height.0; + if ctx.params.bip34_hash.is_none() { + return false; + } + if height <= ctx.params.btc.bip34_height { + return false; + } + let ancestor = query + .header_at_height(Height(ctx.params.btc.bip34_height)) + .ok() + .flatten() + .map(|(_, rec)| bitcoin::BlockHash::from_byte_array(rec.hash)); + ctx.params + .bip30_skipped_for_bip34_ancestry(height, ancestor) +} + +fn mtp_at(query: &Query, height: Height, cache: &mut U32Map) -> Result { + if let Some(&t) = cache.get(&height.0) { + return Ok(t); + } + let t = crate::median_time_past_store(query, height)?; + cache.insert(height.0, t); + Ok(t) +} + +pub(crate) struct ResolvedPrevout { + txout: TxOut, + /// Set when the script bytes stay in a block or pin `Arc`. + shared: Option, + /// P2SH+witness sigop cost for this input's prevout script (not legacy). + input_sigops: u64, + /// Class A create fk for this prevout (or `NULL` for same-block). Load pin + /// denserels must carry identity matching wire `prev_txid` for this fk. + create_fk: rbitcoin_primitives::Fk, +} + +#[allow(clippy::too_many_arguments)] // call-site args stay unbundled +pub(crate) fn resolve_prevout( + block: &Block, + op: OutPoint, + inp: &bitcoin::TxIn, + prev_fk_hint: Option, + txid_index: &TxidMap, + spend_ti: usize, + batch_parents: &crate::BatchParents, + bip16: bool, + witness: bool, + need_script_buf: bool, + wire: Option<&Arc>, + acc: &mut AsmPrevoutAcc, +) -> Result { + let prev_txid = op.txid.to_byte_array(); + + if let Some(&pj) = txid_index.get(&prev_txid) { + if pj < spend_ti { + let v = op.vout as usize; + let o = rbitcoin_consensus::block::require_same_block_output(block.txdata.get(pj), v)?; + // Core never puts this script in the coin view. The returned + // `txout` is often empty; the wire output holds the real bytes. + rbitcoin_consensus::block::reject_unspendable_prevout( + rbitcoin_consensus::policy::is_unspendable(o.script_pubkey.as_bytes()), + )?; + acc.in_n = acc.in_n.saturating_add(1); + acc.same_n = acc.same_n.saturating_add(1); + let shared = if need_script_buf { + wire.map(|_| rbitcoin_consensus::block::SharedPrevout::Wire { + tx_index: pj as u32, + vout: op.vout, + }) + } else { + None + }; + return Ok(ResolvedPrevout { + txout: if shared.is_some() || !need_script_buf { + TxOut { + value: o.value, + script_pubkey: ScriptBuf::new(), + } + } else { + o.clone() + }, + shared, + input_sigops: rbitcoin_consensus::block::prevout_spk_sigops( + inp, + o.script_pubkey.as_bytes(), + bip16, + witness, + ), + create_fk: rbitcoin_primitives::Fk::NULL, + }); + } + } + + // Batch pin first (no TxRecord clone — A3). Pin identity/vout misses are + // hard invariants (load must fill schema-13 identity + denserels). + if let Some(prev_fk) = prev_fk_hint { + if let Some((pinned, parent_txid)) = batch_parents.share_parent_prevout(prev_fk, op.vout) { + if parent_txid != prev_txid { + acc.cold_txid_mismatch_n = acc.cold_txid_mismatch_n.saturating_add(1); + #[cfg(test)] + crate::block_pipeline::test_phase::tl_note_cold_why_txid_mismatch(); + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: pin parent create identity mismatch wire prev_txid", + ))); + } + let (value, script) = + pinned + .parts() + .ok_or(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: pin incomplete outs for spent parent vout", + )))?; + // Same rule as the same-block arm. `txout.script_pubkey` below is + // empty; `script` is the archived bytes, including when scripts + // are not executed. + rbitcoin_consensus::block::reject_unspendable_prevout( + rbitcoin_consensus::policy::is_unspendable(script), + )?; + let input_sigops = + rbitcoin_consensus::block::prevout_spk_sigops(inp, script, bip16, witness); + acc.in_n = acc.in_n.saturating_add(1); + acc.batch_n = acc.batch_n.saturating_add(1); + #[cfg(test)] + crate::block_pipeline::test_phase::tl_note_batch_hit(); + return Ok(ResolvedPrevout { + txout: TxOut { + value: Amount::from_sat(value), + script_pubkey: ScriptBuf::new(), + }, + shared: need_script_buf.then(|| pinned.into_shared_prevout()), + input_sigops, + create_fk: prev_fk, + }); + } else if batch_parents.contains(prev_fk) { + acc.cold_vout_miss_n = acc.cold_vout_miss_n.saturating_add(1); + #[cfg(test)] + crate::block_pipeline::test_phase::tl_note_cold_why_vout_miss(); + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: pin incomplete outs for spent parent vout", + ))); + } + } + + Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: lookup stage miss (assemble parent create_fk)", + ))) +} + +#[cfg(test)] +mod overlay_meta_skip_tests { + use super::*; + use rbitcoin_primitives::Fk; + + fn spends(rows: &[(u32, Fk, Fk, u32)]) -> Vec<([u8; 32], u32, Fk, Fk, u32)> { + rows.iter() + .map(|&(vout, sfk, cfk, vin)| ([0u8; 32], vout, sfk, cfk, vin)) + .collect() + } + + #[test] + fn overlay_meta_skip_omits_matching_abs() { + let wave = ClassAWave::new(vec![Fk(10)]); + let mut map = FkMap::default(); + map.insert(Fk(10), (5, false)); + let run = RunCreateHeight::Map(map); + assert_eq!(run.create(Fk(10)), Some((5, false))); + let spends = spends(&[(0, Fk(11), Fk(10), 0)]); + let mut scratch = StructuralScratch::default(); + fill_overlay_skip(&spends, &wave, &mut scratch); + assert!(overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip)); + assert!(!overlay_meta_is_skip(10, 0, Fk(11), 1, &scratch.skip)); + assert!(!overlay_meta_is_skip(10, 1, Fk(11), 0, &scratch.skip)); + assert!(!overlay_meta_is_skip(99, 0, Fk(11), 0, &scratch.skip)); + } + + #[test] + fn overlay_meta_skip_keeps_conflicting_spender_on_disk_list() { + let wave = ClassAWave::new(vec![Fk(10)]); + let mut map = FkMap::default(); + map.insert(Fk(10), (5, false)); + let run = RunCreateHeight::Map(map); + assert_eq!(run.create(Fk(10)), Some((5, false))); + let spends = spends(&[(0, Fk(11), Fk(10), 0), (0, Fk(12), Fk(10), 0)]); + let mut scratch = StructuralScratch::default(); + fill_overlay_skip(&spends, &wave, &mut scratch); + assert!(!overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip)); + assert!(!overlay_meta_is_skip(10, 0, Fk(12), 0, &scratch.skip)); + } + + #[test] + fn overlay_meta_skip_historical_create_stays_on_disk() { + let wave = ClassAWave::default(); + let spends = spends(&[(0, Fk(11), Fk(10), 0)]); + let mut scratch = StructuralScratch::default(); + fill_overlay_skip(&spends, &wave, &mut scratch); + assert!(!overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip)); + } +} + +#[cfg(test)] +mod run_create_height_tests { + use super::RunCreateHeight; + use rbitcoin_primitives::Fk; + + fn fk(id: u64) -> Fk { + Fk(id) + } + + #[test] + fn run_create_height_span_misses_gaps() { + let one = [fk(10), fk(11)]; + let idx = RunCreateHeight::from_blocks([(7u32, one.as_slice())]); + assert!(matches!(idx, RunCreateHeight::Spans(_))); + assert_eq!(idx.get(fk(10)), Some(7)); + assert_eq!(idx.get(fk(11)), Some(7)); + assert_eq!(idx.get(fk(9)), None); + assert_eq!(idx.get(fk(12)), None); + + let low = [fk(10), fk(11)]; + let high = [fk(20), fk(21)]; + let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, high.as_slice())]); + assert_eq!(idx.get(fk(15)), None); + assert_eq!(idx.get(fk(10)), Some(1)); + assert_eq!(idx.get(fk(21)), Some(2)); + + let gapped = [fk(1), fk(2), fk(4)]; + let idx = RunCreateHeight::from_blocks([(3u32, gapped.as_slice())]); + assert!(matches!(idx, RunCreateHeight::Map(_))); + assert_eq!(idx.get(fk(3)), None); + assert_eq!(idx.get(fk(1)), Some(3)); + assert_eq!(idx.get(fk(2)), Some(3)); + assert_eq!(idx.get(fk(4)), Some(3)); + } + + #[test] + fn run_create_height_marks_each_block_first_fk_coinbase() { + let low = [fk(10), fk(11)]; + let high = [fk(12), fk(13)]; + let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, high.as_slice())]); + assert!(matches!(idx, RunCreateHeight::Spans(_))); + assert_eq!(idx.create(fk(10)), Some((1, true))); + assert_eq!(idx.create(fk(11)), Some((1, false))); + assert_eq!(idx.create(fk(12)), Some((2, true))); + assert_eq!(idx.create(fk(13)), Some((2, false))); + + let gapped = [fk(20), fk(22)]; + let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, gapped.as_slice())]); + assert!(matches!(idx, RunCreateHeight::Map(_))); + assert_eq!(idx.create(fk(10)), Some((1, true))); + assert_eq!(idx.create(fk(11)), Some((1, false))); + assert_eq!(idx.create(fk(20)), Some((2, true))); + assert_eq!(idx.create(fk(22)), Some((2, false))); + assert_eq!(idx.create(fk(21)), None); + } +} diff --git a/crates/rbitcoin-query/src/chain_view.rs b/crates/rbitcoin-query/src/chain_view.rs index 3a0c32f241..e19b216fd0 100644 --- a/crates/rbitcoin-query/src/chain_view.rs +++ b/crates/rbitcoin-query/src/chain_view.rs @@ -214,6 +214,14 @@ impl Query { &self, height: Height, ) -> Result, QueryError> { + // One-shot armed by tests. The IBD stop flag must not fail later reads. + #[cfg(feature = "test-hooks")] + if self + .header_read_cancel + .swap(false, std::sync::atomic::Ordering::SeqCst) + { + return Err(StoreError::Cancelled("header read")); + } note_header_lookup(); #[cfg(test)] note_header_io_under_index_lock(); diff --git a/crates/rbitcoin-query/src/confirm_error.rs b/crates/rbitcoin-query/src/confirm_error.rs new file mode 100644 index 0000000000..211c1511d2 --- /dev/null +++ b/crates/rbitcoin-query/src/confirm_error.rs @@ -0,0 +1,137 @@ +//! Confirm pipeline error. Rule rejects stay [`ConsensusError`]. Disk, io_uring, +//! and cancellation stay [`StoreError`] and must not be cached as a failed block. + +use rbitcoin_consensus::ConsensusError; +use rbitcoin_store::StoreError; +use std::fmt; + +#[derive(Debug)] +pub enum ConfirmError { + Rules(ConsensusError), + Store(StoreError), +} + +impl fmt::Display for ConfirmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfirmError::Rules(e) => write!(f, "{e}"), + ConfirmError::Store(e) => write!(f, "store: {e}"), + } + } +} + +impl std::error::Error for ConfirmError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ConfirmError::Rules(e) => Some(e), + ConfirmError::Store(e) => Some(e), + } + } +} + +impl From for ConfirmError { + fn from(e: ConsensusError) -> Self { + ConfirmError::Rules(e) + } +} + +impl From for ConfirmError { + fn from(e: StoreError) -> Self { + // A missing parent on an untrusted block is not store corruption. + // Cancellation stays a store result. Callers must not cache it. + match e { + StoreError::Corrupt(m) if m.contains("parent create_fk unresolved") => { + ConfirmError::from(rbitcoin_consensus::block::unresolved_parent_create()) + } + other => ConfirmError::Store(other), + } + } +} + +impl ConfirmError { + pub fn is_uring_session_fault(&self) -> bool { + matches!(self, ConfirmError::Store(se) if se.is_uring_session_fault()) + } + + pub fn rules(&self) -> Option<&ConsensusError> { + match self { + ConfirmError::Rules(e) => Some(e), + ConfirmError::Store(_) => None, + } + } + + /// Cooperative abort. Not a verdict on the block. + pub fn is_cancelled(&self) -> bool { + matches!( + self, + ConfirmError::Store(StoreError::Cancelled(_)) + | ConfirmError::Rules(ConsensusError::Cancelled) + ) + } + + /// Script-pool jobs only return [`ConsensusError`]. A store fault becomes + /// [`ConsensusError::Internal`] so the pool does not grow a second taxonomy. + /// io_uring text stays recognizable to [`StoreError::is_uring_session_fault_msg`]. + pub(crate) fn into_pool(self) -> ConsensusError { + match self { + ConfirmError::Rules(e) => e, + ConfirmError::Store(StoreError::Cancelled(_)) => ConsensusError::Cancelled, + ConfirmError::Store(StoreError::Corrupt(m)) + if StoreError::is_uring_session_fault_msg(m) => + { + ConsensusError::Internal("io_uring submit failed") + } + ConfirmError::Store(StoreError::Corrupt(m)) => ConsensusError::Internal(m), + ConfirmError::Store(_) => ConsensusError::Internal("index assemble store fault"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uring_session_fault_covers_leftover_and_submit() { + for m in [ + "invariant: io_uring leftover cqe", + "io_uring submit failed", + "invariant: io_uring undrained", + ] { + let e = ConfirmError::Store(StoreError::Corrupt(m)); + assert!(e.is_uring_session_fault(), "{m}"); + } + assert!(!ConfirmError::Store(StoreError::NotFound).is_uring_session_fault()); + assert!(!ConfirmError::Rules(ConsensusError::PrevoutSpent).is_uring_session_fault()); + } + + #[test] + fn archive_unresolved_parent_is_missing_prevout_not_corrupt() { + let e = ConfirmError::from(StoreError::Corrupt( + "archive: parent create_fk unresolved (contiguous batch required)", + )); + assert!( + matches!(e, ConfirmError::Rules(ConsensusError::MissingPrevout)), + "got {e:?}" + ); + assert!(!e.to_string().contains("corrupt")); + } + + #[test] + fn from_store_error_keeps_not_found() { + let e = ConfirmError::from(StoreError::NotFound); + assert!(matches!(e, ConfirmError::Store(StoreError::NotFound))); + } + + #[test] + fn cancelled_stays_a_store_error() { + let e = ConfirmError::from(StoreError::Cancelled("stop")); + assert!(matches!( + e, + ConfirmError::Store(StoreError::Cancelled("stop")) + )); + assert!(e.is_cancelled()); + assert_eq!(e.to_string(), "store: cancelled: stop"); + assert!(matches!(e.into_pool(), ConsensusError::Cancelled)); + } +} diff --git a/crates/rbitcoin-consensus/src/confirm_run/bq_resolve.rs b/crates/rbitcoin-query/src/confirm_run/bq_resolve.rs similarity index 98% rename from crates/rbitcoin-consensus/src/confirm_run/bq_resolve.rs rename to crates/rbitcoin-query/src/confirm_run/bq_resolve.rs index 7c49b6e84b..392eaab095 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/bq_resolve.rs +++ b/crates/rbitcoin-query/src/confirm_run/bq_resolve.rs @@ -3,14 +3,11 @@ //! Pack/hold uses enqueue-stamped Σ inputs (no `Block` decode). Emit runs one //! [`Store::get_fk_by_txid_batch`] (TipOnly) across the selected heights //! (soft **64000** inputs / hard **1080** blocks). Hits ride the wave as -//! [`rbitcoin_query::BatchParentIds`] (fk + body_range + spent_range). Does not +//! [`crate::BatchParentIds`] (fk + body_range + spent_range). Does not //! claim, structure, or stamp. Same-wave creates are omitted from TipOnly need. use super::*; -use crate::milestone::Milestone; -use rbitcoin_query::{ - BatchParentIds, IdMap, ResolvedWire, TxPrecompute, TxidHasher, U32Map, U64Map, -}; +use crate::{BatchParentIds, IdMap, ResolvedWire, TxPrecompute, TxidHasher, U32Map, U64Map}; use std::collections::{HashMap, HashSet}; use std::hash::BuildHasherDefault; use std::sync::Arc; @@ -194,7 +191,7 @@ pub fn confirm_bq_resolve_wave_capped( heights: &[u32], max_blocks: usize, max_inputs: u32, -) -> Result { +) -> Result { let t0 = Instant::now(); let mut stats = BqResolveWaveStats::default(); let mut done: Vec = Vec::new(); @@ -291,9 +288,9 @@ pub fn confirm_bq_resolve_wave_capped( continue; }; let t_dec = Instant::now(); - let decoded = rbitcoin_query::decode_block_precomputes( + let decoded = crate::decode_block_precomputes( &payload[..], - !crate::milestone::skips_on_query(milestone, query, h, &hash), + !crate::milestone_gate::skips_on_query(milestone, query, h, &hash), ); let wall = t_dec.elapsed().as_nanos() as u64; let Some((block, pres_vec, pre_ns)) = decoded else { @@ -362,7 +359,7 @@ pub fn confirm_bq_resolve_wave_capped( let rows = query .store() .get_fk_by_txid_batch(&need) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; for (txid, row) in rows { if let Some((fk, pair)) = row { layer.insert(txid, (fk, pair.txout)); @@ -393,7 +390,7 @@ pub fn confirm_bq_resolve_wave_capped( if !promote.is_empty() { query .block_queue_promote_wave(promote) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; } stats.heights = items.len() as u32; stats.work_ns = t0.elapsed().as_nanos() as u64; @@ -422,7 +419,7 @@ pub fn take_wave_items_for_load( query: &Query, items: &[(u32, [u8; 32], ResolvedWire)], gen: u32, -) -> Result { +) -> Result { let Some(&(last, _, _)) = items.last() else { return Ok(true); }; @@ -432,7 +429,7 @@ pub fn take_wave_items_for_load( for (h, _, _) in items { query .block_queue_dequeue_height(*h) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; } Ok(true) } @@ -441,7 +438,6 @@ pub fn take_wave_items_for_load( mod tests { use super::*; use crate::accept_and_connect_block; - use crate::regtest_pad::mine_empty_regtest; use bitcoin::absolute::LockTime; use bitcoin::block::{Header, Version}; use bitcoin::consensus::encode::serialize; @@ -451,8 +447,9 @@ mod tests { Amount, BlockHash, CompactTarget, OutPoint, ScriptBuf, Sequence, Target, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, }; - fn tmp_query() -> (rbitcoin_query::testutil::TempDir, Query) { - rbitcoin_query::testutil::tiny_query_labeled("bq-resolve") + use rbitcoin_consensus::mine_empty_regtest; + fn tmp_query() -> (crate::testutil::TempDir, Query) { + crate::testutil::tiny_query_labeled("bq-resolve") } fn take_emitted(q: &Query, wave: &BqResolveWave) { @@ -503,7 +500,7 @@ mod tests { let mut ss = if height == 0 { vec![0x00] } else { - crate::bip34_height_script(height) + rbitcoin_consensus::bip34_height_script(height) }; while ss.len() < 2 { ss.push(0x00); @@ -1072,7 +1069,7 @@ mod tests { #[test] fn resolve_wave_emits_a_promoted_height() { - use rbitcoin_query::{ResolvedWire, TxPrecompute}; + use crate::{ResolvedWire, TxPrecompute}; use std::sync::Arc; let (path, q) = tmp_query(); @@ -1133,7 +1130,7 @@ mod tests { /// height is confirmed so stamp does not MissingPrevout (931147 / 933474). #[test] fn stamp_uses_inflight_until_tip_covers_parent_height() { - use rbitcoin_query::InFlight; + use crate::InFlight; use rbitcoin_store::{OutputRecord, TxRecord}; let (path, q) = tmp_query(); let params = ChainParams::regtest(); @@ -1141,7 +1138,7 @@ mod tests { accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); let parent_txid = [0x22u8; 32]; let parent_fk = rbitcoin_primitives::Fk(99); - let pin = rbitcoin_query::CreatePinInner::records( + let pin = crate::CreatePinInner::records( TxRecord { txid: parent_txid, version: 1, @@ -1207,7 +1204,7 @@ mod tests { /// would drop the parent (mainnet 945952 leftover_n=3546 hit=2811). #[test] fn stamp_uses_inflight_when_confirmed_tip_leads_fence() { - use rbitcoin_query::InFlight; + use crate::InFlight; use rbitcoin_store::{OutputRecord, TxRecord}; let (path, q) = tmp_query(); let params = ChainParams::regtest(); @@ -1227,7 +1224,7 @@ mod tests { let parent_txid = [0x33u8; 32]; let parent_fk = rbitcoin_primitives::Fk(99); - let pin = rbitcoin_query::CreatePinInner::records( + let pin = crate::CreatePinInner::records( TxRecord { txid: parent_txid, version: 1, diff --git a/crates/rbitcoin-consensus/src/confirm_run/head_drain.rs b/crates/rbitcoin-query/src/confirm_run/head_drain.rs similarity index 100% rename from crates/rbitcoin-consensus/src/confirm_run/head_drain.rs rename to crates/rbitcoin-query/src/confirm_run/head_drain.rs diff --git a/crates/rbitcoin-consensus/src/confirm_run/index.rs b/crates/rbitcoin-query/src/confirm_run/index.rs similarity index 95% rename from crates/rbitcoin-consensus/src/confirm_run/index.rs rename to crates/rbitcoin-query/src/confirm_run/index.rs index 2dc6f3a0d6..04a9f45d8d 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/index.rs +++ b/crates/rbitcoin-query/src/confirm_run/index.rs @@ -11,15 +11,15 @@ use std::collections::HashMap; use std::sync::{Arc, OnceLock}; use std::time::Instant; +use crate::{ConfirmError, ParentPinView, Query}; use bitcoin::hashes::Hash; use bitcoin::{Amount, Block, ScriptBuf, Transaction, TxOut}; use rbitcoin_primitives::{Fk, Height}; -use rbitcoin_query::{ParentPinView, Query}; -use rbitcoin_store::StoreError; use crate::index_rows::{self, IndexRows}; -use crate::silent_payments::{is_p2tr, tweak_from_tx}; -use crate::ConsensusError; +use rbitcoin_consensus::silent_payments::{is_p2tr, tweak_from_tx}; +use rbitcoin_consensus::ConsensusError; +use rbitcoin_store::StoreError; use super::{LoadedBatch, Prepared}; @@ -93,7 +93,7 @@ pub(super) fn index_want(query: &Query) -> IndexWant { } } -pub(super) fn prepare_index_jobs(batch: &LoadedBatch) -> Result { +pub(super) fn prepare_index_jobs(batch: &LoadedBatch) -> Result { let want = &batch.index_want; if !want.filters && want.tweak_origin.is_none() { return Ok(IndexJobs { @@ -181,7 +181,7 @@ pub(super) fn prepare_index_jobs(batch: &LoadedBatch) -> Result Result<(), ConsensusError> { - match job { + let result = match job { IndexJob::Filter(work) => { let _ = work.started.set(Instant::now()); apply_filter(work) @@ -190,10 +190,11 @@ pub(super) fn apply_index_job(job: &IndexJob) -> Result<(), ConsensusError> { let _ = work.started.set(Instant::now()); apply_tweak_range(work, txs) } - } + }; + result.map_err(ConfirmError::into_pool) } -fn apply_tweak_range(work: &BlockWork, txs: &[usize]) -> Result<(), ConsensusError> { +fn apply_tweak_range(work: &BlockWork, txs: &[usize]) -> Result<(), ConfirmError> { for &ti in txs { let tx = &work.block.txdata[ti]; let prevouts = prevouts_cached(work, ti)?; @@ -205,7 +206,7 @@ fn apply_tweak_range(work: &BlockWork, txs: &[usize]) -> Result<(), ConsensusErr Ok(()) } -fn apply_filter(work: &BlockWork) -> Result<(), ConsensusError> { +fn apply_filter(work: &BlockWork) -> Result<(), ConfirmError> { let mut prevouts = Vec::with_capacity(work.block.txdata.len()); for ti in 0..work.block.txdata.len() { prevouts.push(prevouts_cached(work, ti)?.clone()); @@ -220,7 +221,7 @@ fn apply_filter(work: &BlockWork) -> Result<(), ConsensusError> { Ok(()) } -pub(super) fn rows_from_heights(slots: &[HeightSlots]) -> Result { +pub(super) fn rows_from_heights(slots: &[HeightSlots]) -> Result { let mut rows = IndexRows::default(); for height in slots { if let Some(slot) = &height.filter { @@ -250,7 +251,7 @@ fn tx_has_taproot_out(tx: &Transaction) -> bool { .any(|out| is_p2tr(out.script_pubkey.as_bytes())) } -fn group_spends(prep: &Prepared, n_tx: usize) -> Result>, ConsensusError> { +fn group_spends(prep: &Prepared, n_tx: usize) -> Result>, ConfirmError> { let mut index_of: HashMap = HashMap::with_capacity(prep.tx_fks.len()); for (i, fk) in prep.tx_fks.iter().copied().enumerate() { if i >= n_tx { @@ -283,7 +284,7 @@ fn basic_filter_from_wire( hash: &[u8; 32], block: &Block, prevouts: &[Vec], -) -> Result { +) -> Result { let outputs = block .txdata .iter() @@ -292,10 +293,10 @@ fn basic_filter_from_wire( .iter() .flatten() .map(|prev| prev.script_pubkey.as_bytes()); - rbitcoin_query::basic_filter_from_scripts(hash, outputs, spent).map_err(ConsensusError::from) + crate::basic_filter_from_scripts(hash, outputs, spent).map_err(ConfirmError::from) } -fn prevouts_cached(work: &BlockWork, ti: usize) -> Result<&Vec, ConsensusError> { +fn prevouts_cached(work: &BlockWork, ti: usize) -> Result<&Vec, ConfirmError> { if let Some(got) = work.prevouts[ti].get() { return Ok(got); } @@ -310,7 +311,7 @@ fn prevouts_for_tx( work: &BlockWork, ti: usize, tx: &Transaction, -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { if tx.is_coinbase() { return Ok(Vec::new()); } @@ -325,7 +326,7 @@ fn prevouts_for_tx( Ok(out) } -fn resolve_prevout(work: &BlockWork, spend: &SpendRef) -> Result { +fn resolve_prevout(work: &BlockWork, spend: &SpendRef) -> Result { if spend.create_fk.is_null() { let same_block = work.same_block.get_or_init(|| { let mut map = HashMap::with_capacity(work.block.txdata.len()); @@ -356,7 +357,7 @@ fn resolve_prevout(work: &BlockWork, spend: &SpendRef) -> Result Result { +pub(super) fn seal_live_indexes(query: &Query, seal: IndexSeal) -> Result { if seal.filters.is_empty() && seal.tweaks.is_empty() { return Ok(0); } @@ -367,25 +368,25 @@ pub(super) fn seal_live_indexes(query: &Query, seal: IndexSeal) -> Result ConsensusError { - ConsensusError::Store(StoreError::Corrupt(msg)) +fn corrupt(msg: &'static str) -> ConfirmError { + ConfirmError::Store(StoreError::Corrupt(msg)) } #[cfg(test)] mod tests { use super::*; + use crate::testutil::tiny_query_labeled; + use crate::Query; use bitcoin::absolute::LockTime; use bitcoin::hashes::{hash160, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, TxIn, TxOut, Witness}; - use rbitcoin_query::testutil::tiny_query_labeled; - use rbitcoin_query::Query; - use crate::silent_payments::tweak_records_from_window; - use crate::{ - accept_and_connect_block, confirm_wire_run, genesis_block, mine_empty_regtest, - mine_regtest_paying, ChainParams, Milestone, + use crate::sp_read::tweak_records_from_window; + use crate::{accept_and_connect_block, confirm_wire_run}; + use rbitcoin_consensus::{ + genesis_block, mine_empty_regtest, mine_regtest_paying, ChainParams, Milestone, }; fn keys() -> (Vec, bitcoin::ScriptBuf, bitcoin::ScriptBuf) { @@ -436,7 +437,7 @@ mod tests { /// P2TR once that coinbase is mature, and the blocks between them. #[test] fn live_batch_matches_window_and_reaches_tip() { - let _gate = crate::script_pool::steal_test_gate(); + let _gate = rbitcoin_consensus::script_pool::steal_test_gate(); let (_dir, q) = tiny_query_labeled("idx-live"); enable(&q); go_live(&q); @@ -510,7 +511,7 @@ mod tests { #[test] fn far_watermark_does_not_advance() { - let _gate = crate::script_pool::steal_test_gate(); + let _gate = rbitcoin_consensus::script_pool::steal_test_gate(); let (_dir, q) = tiny_query_labeled("idx-gap"); let params = ChainParams::regtest(); let ms = Milestone::height(u32::MAX); @@ -539,7 +540,7 @@ mod tests { #[test] fn startup_seals_a_short_gap_then_the_batch_appends() { - let _gate = crate::script_pool::steal_test_gate(); + let _gate = rbitcoin_consensus::script_pool::steal_test_gate(); let (_dir, q) = tiny_query_labeled("idx-repair"); let params = ChainParams::regtest(); let ms = Milestone::height(u32::MAX); @@ -560,7 +561,7 @@ mod tests { /// A limit that fits only the tweak hole does not turn live append on. #[test] fn startup_repairs_each_fitting_index() { - let _gate = crate::script_pool::steal_test_gate(); + let _gate = rbitcoin_consensus::script_pool::steal_test_gate(); let (_dir, q) = tiny_query_labeled("idx-split"); let params = ChainParams::regtest(); let ms = Milestone::height(u32::MAX); @@ -759,14 +760,14 @@ mod tests { LoadedBatch { prepared: vec![p0, p1], wire_blocks: vec![Arc::new(b0), Arc::new(b1)], - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: super::super::ScriptPreverified::new(), archive_plan: None, index_want: IndexWant { filters: true, tweak_origin: Some(0), }, - stats: Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: Arc::new(crate::ConfirmStats::default()), } } diff --git a/crates/rbitcoin-consensus/src/confirm_run/lookup.rs b/crates/rbitcoin-query/src/confirm_run/lookup.rs similarity index 84% rename from crates/rbitcoin-consensus/src/confirm_run/lookup.rs rename to crates/rbitcoin-query/src/confirm_run/lookup.rs index c25b8aff0d..11705f3d6d 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/lookup.rs +++ b/crates/rbitcoin-query/src/confirm_run/lookup.rs @@ -5,14 +5,14 @@ use super::*; /// Lookup-stamped external parent material for load body denserels. /// /// **Lookup** fills this via `tx.head` / `create.loc` / `txid.body` (never `txout.body`). -/// **Load** denserels by range using only [`rbitcoin_query::ParentIdent`] (+ plan offline pins). +/// **Load** denserels by range using only [`crate::ParentIdent`] (+ plan offline pins). /// Integer create_fk map uses [`U64Map`] (identity hasher) — pack-scale win over SipHash. #[derive(Debug, Default, Clone)] pub struct ParentPinStamp { /// prev_txid → create_fk_id (plan=None edges; empty after `take_from_plan`). - pub resolved: rbitcoin_query::TxidMap, + pub resolved: crate::TxidMap, /// create_fk_id → identity (txid + optional body/spent/pin). - pub idents: rbitcoin_query::U64Map, + pub idents: crate::U64Map, /// create_fk_id → spent need-vouts (packed at lookup; load pin reuses). pub parent_vouts: U64Map>, } @@ -21,9 +21,9 @@ impl ParentPinStamp { /// Move plan stamp identity into the load stamp (no 100k-entry clone). /// /// `resolved` stays empty: plan path pins from packed `create_fk`. - pub(crate) fn take_from_plan(plan: &mut rbitcoin_query::ArchiveWritePlan) -> Self { + pub(crate) fn take_from_plan(plan: &mut crate::ArchiveWritePlan) -> Self { Self { - resolved: rbitcoin_query::TxidMap::with_hasher(Default::default()), + resolved: crate::TxidMap::with_hasher(Default::default()), idents: std::mem::take(&mut plan.external_parents), parent_vouts: std::mem::take(&mut plan.external_parent_vouts), } @@ -53,18 +53,14 @@ impl ParentPinStamp { } #[inline] - pub(super) fn create_pin(&self, create_fk_id: u64) -> Option<&rbitcoin_query::CreatePin> { + pub(super) fn create_pin(&self, create_fk_id: u64) -> Option<&crate::CreatePin> { self.idents.get(&create_fk_id).and_then(|p| p.pin.as_ref()) } } -pub type WireBlockIn = ( - Height, - Arc, - Option>, -); +pub type WireBlockIn = (Height, Arc, Option>); type LookupPhaseOut = ( - Option, + Option, Vec, Vec>, u64, @@ -75,7 +71,7 @@ type LookupPhaseOut = ( /// **No `tx.body` denserels on lookup.** Load denserels by range from /// [`ParentPinStamp`] / plan ranges. Handoff is owned plan + parent pin stamp. pub struct PlanStampOutcome { - pub plan: Option, + pub plan: Option, /// External parent fk/range/txid stamped at lookup (always; including plan=None). pub parent_pin: ParentPinStamp, /// Wall ns for structure + plan_batch (head stamp). @@ -100,14 +96,14 @@ impl PlanStampOutcome { pub(super) fn archived_create_pairs( metas: &[BodyMeta], -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { let mut pairs = Vec::new(); for m in metas { if m.tx_fks.is_empty() { continue; } if m.tx_fks.len() != m.txids.len() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: archived stamp tx_fks/txids length", ))); } @@ -134,9 +130,9 @@ pub fn confirm_wire_lookup_stamp( milestone: Milestone, blocks: &[WireBlockIn], pipeline: Option<&WireLoadPipeline>, -) -> Result { +) -> Result { let t0 = Instant::now(); - query.on_load_pack().map_err(ConsensusError::from)?; + query.on_load_pack().map_err(ConfirmError::from)?; let (mut plan, metas, wire_blocks, plan_ns) = wire_lookup_phase(query, params, milestone, blocks, pipeline)?; let ifo = pipeline.map(|p| p.in_flight); @@ -156,10 +152,10 @@ pub fn confirm_wire_lookup_stamp( (pin, pairs) } }; - rbitcoin_query::note_confirm(&query.confirm_stats().lookup_blocks, blocks.len() as u64); - rbitcoin_query::note_confirm(&query.confirm_stats().lookup_head_ns, plan_ns); + crate::note_confirm(&query.confirm_stats().lookup_blocks, blocks.len() as u64); + crate::note_confirm(&query.confirm_stats().lookup_head_ns, plan_ns); let work_ns = t0.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().lookup_total_ns, work_ns); + crate::note_confirm(&query.confirm_stats().lookup_total_ns, work_ns); Ok(PlanStampOutcome { plan, parent_pin, @@ -180,11 +176,11 @@ pub(super) fn stamp_parent_pin_archived( params: &ChainParams, metas: &[BodyMeta], wire_blocks: &[Arc], - in_flight: Option<&rbitcoin_query::InFlight>, - skeleton: Option<&rbitcoin_query::BatchParentIds>, + in_flight: Option<&crate::InFlight>, + skeleton: Option<&crate::BatchParentIds>, carried_need: Option<&[[u8; 32]]>, -) -> Result { - let mut same_batch: rbitcoin_query::TxidMap = rbitcoin_query::TxidMap::default(); +) -> Result { + let mut same_batch: crate::TxidMap = crate::TxidMap::default(); for m in metas { for (tid, fk) in m.txids.iter().zip(m.tx_fks.iter()) { if let Some(id) = fk.get() { @@ -193,7 +189,7 @@ pub(super) fn stamp_parent_pin_archived( } } // Value: a wire input spends this txid (BIP30 probes are `false`). - let mut need_external: rbitcoin_query::TxidMap = rbitcoin_query::TxidMap::default(); + let mut need_external: crate::TxidMap = crate::TxidMap::default(); if skeleton.is_some() { for &prev in carried_need.unwrap_or(&[]) { if same_batch.contains_key(&prev) { @@ -228,25 +224,25 @@ pub(super) fn stamp_parent_pin_archived( } } } - let empty = rbitcoin_query::InFlight::new(); + let empty = crate::InFlight::new(); let ifo = in_flight.unwrap_or(&empty); let need_vec: Vec<[u8; 32]> = need_external.keys().copied().collect(); - let ext = rbitcoin_query::stamp_external_parents( + let ext = crate::stamp_external_parents( query.store(), &need_vec, ifo, skeleton, query.confirm_stats(), ) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; if need_external .iter() .any(|(txid, &spent)| spent && !ext.resolved.contains_key(txid)) { - return Err(ConsensusError::MissingPrevout); + rbitcoin_consensus::block::reject_unresolved_spend(true)?; } let mut stamp = ParentPinStamp { - resolved: rbitcoin_query::TxidMap::with_capacity_and_hasher( + resolved: crate::TxidMap::with_capacity_and_hasher( ext.resolved.len().saturating_add(same_batch.len()), Default::default(), ), @@ -263,23 +259,23 @@ pub(super) fn stamp_parent_pin_archived( stamp .idents .entry(id) - .or_insert_with(|| rbitcoin_query::ParentIdent::new(tid)) + .or_insert_with(|| crate::ParentIdent::new(tid)) .txid = tid; } // Identities are stamped from wire prev_txid at insert time — never soft-fill // from txid.body here (that would be a dual path after lookup promised identity). if skeleton.is_none() { - rbitcoin_query::fill_missing_parent_ranges( + crate::fill_missing_parent_ranges( query.store(), ifo, &mut stamp.idents, query.confirm_stats(), ) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; } for ident in stamp.idents.values() { if ident.txid == [0u8; 32] { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: plan=None parent stamp zero create identity", ))); } @@ -300,8 +296,8 @@ pub fn parent_missing_from_chain( query: &Query, block: &Block, stop: impl Fn() -> bool, -) -> Result, ConsensusError> { - let created: rbitcoin_query::TxidSet = block +) -> Result, ConfirmError> { + let created: crate::TxidSet = block .txdata .iter() .map(|tx| tx.compute_txid().to_byte_array()) @@ -317,10 +313,10 @@ pub fn parent_missing_from_chain( need.sort_unstable(); need.dedup(); let unsynced_from = query.store().head_unsynced_first_fk(); - let hits: rbitcoin_query::TxidSet = query + let hits: crate::TxidSet = query .store() .get_fk_by_txid_batch(&need) - .map_err(ConsensusError::from)? + .map_err(ConfirmError::from)? .into_iter() .filter_map(|(txid, row)| row.map(|_| txid)) .collect(); @@ -333,7 +329,7 @@ pub fn parent_missing_from_chain( .connected_in_unsynced_head(unsynced_from, &need, stop)? .is_empty() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: tx.head misses a connected create", ))); } @@ -352,7 +348,7 @@ pub fn confirm_wire_load_from_plan( stamped: PlanStampOutcome, pipeline: Option<&WireLoadPipeline>, preverified: &ScriptPreverified, -) -> Result { +) -> Result { let t_work = Instant::now(); let t_load = Instant::now(); let PlanStampOutcome { @@ -376,7 +372,7 @@ pub fn confirm_wire_load_from_plan( ifo, )?; - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().load_ns, t_load.elapsed().as_nanos() as u64, ); @@ -398,12 +394,12 @@ pub fn confirm_wire_load_from_plan( if let Some(ref mut plan) = plan { if !plan.packed.is_empty() { if tx_fees.len() != plan.packed.len() { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: assemble tx fees length", ))); } if tx_sizes.len() != plan.packed.len() { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: txstat size length", ))); } @@ -434,7 +430,7 @@ pub(super) fn wire_lookup_phase( milestone: Milestone, blocks: &[WireBlockIn], pipeline: Option<&WireLoadPipeline>, -) -> Result { +) -> Result { lookup_require_contiguous(blocks)?; let mut wire_blocks: Vec> = Vec::with_capacity(blocks.len()); @@ -457,11 +453,15 @@ pub(super) fn wire_lookup_phase( let hash = block.block_hash().to_byte_array(); let ctx = ValidationContext::at(params, *height, milestone); let t_struct = Instant::now(); - let pres = crate::block::validate_block_structure_with_pres( + let stats = query.confirm_stats(); + let mut note = |txid_ns, wtxid_ns, walk_ns| { + stats.note_struct_parts(txid_ns, wtxid_ns, walk_ns); + }; + let pres = rbitcoin_consensus::block::validate_block_structure_with_pres( block.as_ref(), &ctx, caller_pres.as_ref().map(Arc::clone), - Some(query.confirm_stats()), + Some(&mut note), )?; let txids: Vec<[u8; 32]> = pres.iter().map(|p| p.txid).collect(); struct_ns = struct_ns.saturating_add(t_struct.elapsed().as_nanos() as u64); @@ -487,16 +487,17 @@ pub(super) fn wire_lookup_phase( if block.header.prev_blockhash.to_byte_array() == [0u8; 32] { rbitcoin_primitives::Fk::NULL } else { - query - .get_header_by_hash(block.header.prev_blockhash.as_byte_array()) - .map_err(ConsensusError::from)? - .map(|(fk, _)| fk) - .ok_or(ConsensusError::BadPrev)? + rbitcoin_consensus::block::require_linked_parent( + query + .get_header_by_hash(block.header.prev_blockhash.as_byte_array()) + .map_err(ConfirmError::from)? + .map(|(fk, _)| fk), + )? } } else { metas[i - 1].header_fk }; - let header_rec = crate::header_to_record(prev_fk, &block.header, hash); + let header_rec = crate::convert::header_to_record(prev_fk, &block.header, hash); prepare_ns = prepare_ns.saturating_add(t_prep.elapsed().as_nanos() as u64); let t_put = Instant::now(); let header_fk = stamp_header_fk(query, &header_rec, carried.map(|(fk, _)| fk))?; @@ -525,14 +526,14 @@ pub(super) fn wire_lookup_phase( fn check_carried_header_lens( pipeline: Option<&WireLoadPipeline>, n_blocks: usize, -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { let Some(p) = pipeline else { return Ok(()); }; if (!p.carried_header_fks.is_empty() || !p.carried_header_hashes.is_empty()) && (p.carried_header_fks.len() != n_blocks || p.carried_header_hashes.len() != n_blocks) { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: carried header length", ))); } @@ -565,24 +566,23 @@ fn stamp_check_header_link( hash: [u8; 32], carried: Option<(rbitcoin_primitives::Fk, [u8; 32])>, prev_meta_hash: Option<[u8; 32]>, -) -> Result<(), ConsensusError> { - if i == 0 && height.0 != path_lo { - return Err(ConsensusError::BadPrev); +) -> Result<(), ConfirmError> { + if i == 0 { + rbitcoin_consensus::block::reject_batch_starts_at(height.0, path_lo)?; } let prev_bytes = header.prev_blockhash.to_byte_array(); if let Some((_, expect)) = carried { - if hash != expect { - return Err(ConsensusError::BadBlock("carried header hash mismatch")); - } + rbitcoin_consensus::block::reject_carried_header_hash(&hash, &expect)?; if i == 0 { if path_lo != store_path_lo { let expect_prev = pipeline.and_then(|p| p.parent_hash).unwrap_or([0u8; 32]); - if prev_bytes != expect_prev { - return Err(ConsensusError::BadPrev); - } + rbitcoin_consensus::block::reject_prev_link(&prev_bytes, &expect_prev)?; } - } else if prev_bytes != prev_meta_hash.unwrap_or([0u8; 32]) { - return Err(ConsensusError::BadPrev); + } else { + rbitcoin_consensus::block::reject_prev_link( + &prev_bytes, + &prev_meta_hash.unwrap_or([0u8; 32]), + )?; } return Ok(()); } @@ -591,16 +591,12 @@ fn stamp_check_header_link( validate_header_hashed(query, params, height, header, hash)?; } else { let expect_prev = pipeline.and_then(|p| p.parent_hash).unwrap_or([0u8; 32]); - if prev_bytes != expect_prev { - return Err(ConsensusError::BadPrev); - } + rbitcoin_consensus::block::reject_prev_link(&prev_bytes, &expect_prev)?; pow_hash_meets_target(hash, header.bits, params.pow_limit)?; } return Ok(()); } - if prev_bytes != prev_meta_hash.unwrap_or([0u8; 32]) { - return Err(ConsensusError::BadPrev); - } + rbitcoin_consensus::block::reject_prev_link(&prev_bytes, &prev_meta_hash.unwrap_or([0u8; 32]))?; pow_hash_meets_target(hash, header.bits, params.pow_limit)?; Ok(()) } @@ -609,40 +605,35 @@ fn stamp_header_fk( query: &Query, header_rec: &rbitcoin_store::HeaderRecord, carried: Option, -) -> Result { +) -> Result { if let Some(fk) = carried { match query.store().get_header(fk) { Ok(rec) if rec.hash == header_rec.hash => { - rbitcoin_query::note_confirm(&query.confirm_stats().phase_prep_header_skip_n, 1); + crate::note_confirm(&query.confirm_stats().phase_prep_header_skip_n, 1); return Ok(fk); } Ok(_) | Err(StoreError::NotFound) => {} - Err(e) => return Err(ConsensusError::from(e)), + Err(e) => return Err(ConfirmError::from(e)), } } if let Some((fk, _)) = query .get_header_by_hash(&header_rec.hash) - .map_err(ConsensusError::from)? + .map_err(ConfirmError::from)? { Ok(fk) } else { query .store() .put_header(header_rec) - .map_err(ConsensusError::from) + .map_err(ConfirmError::from) } } -fn lookup_require_contiguous(blocks: &[WireBlockIn]) -> Result<(), ConsensusError> { - if blocks.is_empty() { - return Err(ConsensusError::BadBlock("empty confirm batch")); - } - for w in blocks.windows(2) { - if w[1].0 .0 != w[0].0 .0.saturating_add(1) { - return Err(ConsensusError::BadBlock("confirm run not contiguous")); - } - } - Ok(()) +fn lookup_require_contiguous(blocks: &[WireBlockIn]) -> Result<(), ConfirmError> { + rbitcoin_consensus::block::reject_confirm_batch( + &blocks.iter().map(|(h, _, _)| h.0).collect::>(), + ) + .map_err(ConfirmError::from) } fn lookup_note_stamp( @@ -659,23 +650,23 @@ fn lookup_note_stamp( .note_stamp(struct_ns, prepare_ns, filter_ns, batch_ns); // `note_confirm` already ignores a zero add. A `> 0` gate here is the // same call, and `>` versus `==` would drop a real sample. - rbitcoin_query::note_confirm(&query.confirm_stats().phase_prep_struct_ns, struct_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().phase_prep_header_ns, header_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().phase_prep_prepare_ns, prepare_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().phase_prep_filter_plan_ns, plan_ns); + crate::note_confirm(&query.confirm_stats().phase_prep_struct_ns, struct_ns); + crate::note_confirm(&query.confirm_stats().phase_prep_header_ns, header_ns); + crate::note_confirm(&query.confirm_stats().phase_prep_prepare_ns, prepare_ns); + crate::note_confirm(&query.confirm_stats().phase_prep_filter_plan_ns, plan_ns); } fn lookup_bind_have_body( query: &Query, metas: &mut [BodyMeta], wire_blocks: &[Arc], -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { for (i, m) in metas.iter_mut().enumerate() { if let Some(list) = query .store() .header_txs .get_list(m.header_fk) - .map_err(ConsensusError::from)? + .map_err(ConfirmError::from)? { m.tx_fks = list; } @@ -697,13 +688,13 @@ fn lookup_bind_need_body<'a>( metas: &'a mut [BodyMeta], wire_blocks: &'a [Arc], need_fks: &[rbitcoin_primitives::Fk], -) -> Result { +) -> Result { let mut need = Vec::with_capacity(need_fks.len()); for fk in need_fks { let i = metas .iter() .position(|m| m.header_fk == *fk) - .ok_or(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + .ok_or(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: need-body header_fk not in batch", )))?; need.push((*fk, &wire_blocks[i], metas[i].txids.as_slice())); @@ -717,16 +708,16 @@ fn lookup_bind_need_body<'a>( p.skeleton.as_ref(), p.skeleton.is_some().then_some(p.carried_need.as_slice()), ) - .map_err(ConsensusError::from)?, + .map_err(ConfirmError::from)?, None => query .archive_plan_batch_from_wire( &need, query.tx_body_count().saturating_add(1).max(1), - &rbitcoin_query::InFlight::new(), + &crate::InFlight::new(), None, None, ) - .map_err(ConsensusError::from)?, + .map_err(ConfirmError::from)?, }; let by_header = create_fks_from_header_ranges(&plan.per_header_ranges); for (i, m) in metas.iter_mut().enumerate() { @@ -752,12 +743,12 @@ fn lookup_bind_archive_plan( pipeline: Option<&WireLoadPipeline>, metas: &mut [BodyMeta], wire_blocks: &[Arc], -) -> Result<(Option, u64, u64), ConsensusError> { +) -> Result<(Option, u64, u64), ConfirmError> { let t_filter = Instant::now(); let header_fks: Vec = metas.iter().map(|m| m.header_fk).collect(); let need_fks = query .archive_filter_need_header_fks(&header_fks) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; confirm_archive_kind(header_fks.len(), need_fks.len())?; let filter_ns = t_filter.elapsed().as_nanos() as u64; let t_batch = Instant::now(); @@ -786,13 +777,13 @@ pub(super) enum ConfirmArchiveKind { pub(super) fn confirm_archive_kind( n_headers: usize, n_need: usize, -) -> Result { +) -> Result { if n_need == 0 { Ok(ConfirmArchiveKind::AllHaveBody) } else if n_need == n_headers { Ok(ConfirmArchiveKind::AllNeedBody) } else { - Err(ConsensusError::Store(StoreError::Corrupt( + Err(ConfirmError::Store(StoreError::Corrupt( "invariant: confirm batch mixed archived", ))) } @@ -818,6 +809,7 @@ pub(super) fn create_fks_from_header_ranges( #[cfg(test)] mod tests { use super::*; + use crate::IdMap; use bitcoin::absolute::LockTime; use bitcoin::hashes::Hash; use bitcoin::transaction::Version as TxVersion; @@ -825,10 +817,9 @@ mod tests { Amount, Block, CompactTarget, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, }; - use rbitcoin_query::IdMap; use rbitcoin_store::HeaderRecord; - fn tmp_query() -> (rbitcoin_query::testutil::TempDir, Query) { - rbitcoin_query::testutil::tiny_query_labeled("stamp-archived") + fn tmp_query() -> (crate::testutil::TempDir, Query) { + crate::testutil::tiny_query_labeled("stamp-archived") } fn spend_block(prev: [u8; 32]) -> Block { @@ -864,7 +855,7 @@ mod tests { /// plan=None rehydrate and the query helper stamp the same skeleton parent. #[test] fn archived_stamp_matches_shared_helper_on_skeleton() { - use rbitcoin_query::BatchParentIds; + use crate::BatchParentIds; let (path, q) = tmp_query(); let params = ChainParams::regtest(); let parent_txid = { @@ -876,15 +867,15 @@ mod tests { m.insert(parent_txid, (rbitcoin_primitives::Fk(88), (4000, 32))); let skel = BatchParentIds { ids: std::sync::Arc::new(m), - spent: std::sync::Arc::new(rbitcoin_query::U64Map::default()), + spent: std::sync::Arc::new(crate::U64Map::default()), n_out: Default::default(), - need_vouts: rbitcoin_query::U64Map::default(), + need_vouts: crate::U64Map::default(), }; - let helper = rbitcoin_query::stamp_external_parents( + let helper = crate::stamp_external_parents( q.store(), &[parent_txid], - &rbitcoin_query::InFlight::new(), + &crate::InFlight::new(), Some(&skel), q.confirm_stats(), ) @@ -949,8 +940,8 @@ mod tests { #[test] fn stamp_uses_caller_pres() { use crate::accept_and_connect_block; - use crate::regtest_pad::mine_empty_regtest; - use rbitcoin_query::TxPrecompute; + use crate::TxPrecompute; + use rbitcoin_consensus::mine_empty_regtest; let (path, q) = tmp_query(); let params = ChainParams::regtest(); @@ -1003,7 +994,7 @@ mod tests { #[test] fn stamp_skips_header_ensure_when_pipeline_carries_fk() { use crate::accept_and_connect_block; - use crate::regtest_pad::mine_empty_regtest; + use rbitcoin_consensus::mine_empty_regtest; use std::sync::atomic::Ordering; let (path, q) = tmp_query(); @@ -1020,7 +1011,7 @@ mod tests { let rec = crate::header_to_record(genesis_fk, &b1.header, hash); let hfk = q.store().put_header(&rec).unwrap(); let n_headers = q.store().headers.count(); - let inflight = rbitcoin_query::InFlight::new(); + let inflight = crate::InFlight::new(); let pipe = WireLoadPipeline { path_lo: 1, parent_hash: None, @@ -1060,7 +1051,7 @@ mod tests { carried_header_hashes: vec![[0x11; 32]], }; match confirm_wire_lookup_stamp(&q, ¶ms, Milestone::NONE, &items, Some(&pipe_bad)) { - Err(ConsensusError::BadBlock("carried header hash mismatch")) => {} + Err(ConfirmError::Rules(ConsensusError::BadBlock("carried header hash mismatch"))) => {} Err(e) => panic!("expected hash mismatch, got {e}"), Ok(_) => panic!("expected hash mismatch, got Ok"), } @@ -1070,7 +1061,7 @@ mod tests { #[test] fn stamp_does_not_bind_carried_fk_when_store_hash_differs() { use crate::accept_and_connect_block; - use crate::regtest_pad::mine_empty_regtest; + use rbitcoin_consensus::mine_empty_regtest; use std::sync::atomic::Ordering; let (path, q) = tmp_query(); @@ -1085,7 +1076,7 @@ mod tests { let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); let hash = b1.block_hash().to_byte_array(); let n_headers = q.store().headers.count(); - let inflight = rbitcoin_query::InFlight::new(); + let inflight = crate::InFlight::new(); let pipe = WireLoadPipeline { path_lo: 1, parent_hash: None, @@ -1117,7 +1108,7 @@ mod tests { #[test] fn stamp_rejects_carried_header_length_mismatch() { use crate::accept_and_connect_block; - use crate::regtest_pad::mine_empty_regtest; + use rbitcoin_consensus::mine_empty_regtest; let (path, q) = tmp_query(); let params = ChainParams::regtest(); @@ -1125,7 +1116,7 @@ mod tests { accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); let hash = b1.block_hash().to_byte_array(); - let inflight = rbitcoin_query::InFlight::new(); + let inflight = crate::InFlight::new(); let pipe = WireLoadPipeline { path_lo: 1, parent_hash: None, @@ -1148,14 +1139,12 @@ mod tests { }; let items = [(Height(1), Arc::new(b1), None)]; match confirm_wire_lookup_stamp(&q, ¶ms, Milestone::NONE, &items, Some(&pipe)) { - Err(ConsensusError::Store(StoreError::Corrupt("invariant: carried header length"))) => { - } + Err(ConfirmError::Store(StoreError::Corrupt("invariant: carried header length"))) => {} Err(e) => panic!("expected carried header length, got {e}"), Ok(_) => panic!("expected carried header length, got Ok"), } match confirm_wire_lookup_stamp(&q, ¶ms, Milestone::NONE, &items, Some(&fks_only)) { - Err(ConsensusError::Store(StoreError::Corrupt("invariant: carried header length"))) => { - } + Err(ConfirmError::Store(StoreError::Corrupt("invariant: carried header length"))) => {} Err(e) => panic!("one carried side must still be checked, got {e}"), Ok(_) => panic!("empty fks with a hash must fail the length check"), } @@ -1192,7 +1181,7 @@ mod tests { fn archived_create_pairs_len_mismatch_is_corrupt() { let meta = meta_with(vec![[1u8; 32], [2u8; 32]], vec![rbitcoin_primitives::Fk(1)]); match archived_create_pairs(std::slice::from_ref(&meta)) { - Err(ConsensusError::Store(StoreError::Corrupt(m))) => { + Err(ConfirmError::Store(StoreError::Corrupt(m))) => { assert_eq!(m, "invariant: archived stamp tx_fks/txids length"); } other => panic!("expected length Corrupt, got {other:?}"), diff --git a/crates/rbitcoin-consensus/src/confirm_run/mod.rs b/crates/rbitcoin-query/src/confirm_run/mod.rs similarity index 90% rename from crates/rbitcoin-consensus/src/confirm_run/mod.rs rename to crates/rbitcoin-query/src/confirm_run/mod.rs index 2f209da7f4..6dfd8bdec0 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/mod.rs +++ b/crates/rbitcoin-query/src/confirm_run/mod.rs @@ -26,21 +26,16 @@ //! in height order once both waves have finished. Steal workers unpark the //! publisher when a wave completes. -use crate::block::{ - assemble_block_prevouts, block_has_witness_from_pres, structural_validate_spends, - ScriptCheckJob, ValidationContext, -}; -use crate::error::ConsensusError; -use crate::header::{ - check_header_version_and_future_time, median_time_past_times, pow_hash_meets_target, - validate_header_hashed, -}; -use crate::milestone::Milestone; -use crate::params::{genesis_block, ChainParams}; +use crate::block_pipeline::{assemble_block_prevouts, structural_validate_spends}; +use crate::header_read::validate_header_hashed; +use crate::ConfirmError; +use crate::{FkMap, Query, U32Map, U64Map}; use bitcoin::hashes::Hash; use bitcoin::Block; +use rbitcoin_consensus::block::{block_has_witness_from_pres, ScriptCheckJob, ValidationContext}; +use rbitcoin_consensus::header::pow_hash_meets_target; +use rbitcoin_consensus::{ChainParams, ConsensusError, Milestone}; use rbitcoin_primitives::Height; -use rbitcoin_query::{FkMap, Query, U32Map, U64Map}; use rbitcoin_store::{StoreError, WriteIoBackend}; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -104,7 +99,7 @@ struct BodyMeta { /// at structure/entry (plan or archived load). Assemble must use these only. txids: Vec<[u8; 32]>, /// Same walk as `txids` (lookup/structure). Script jobs reuse these. - pres: std::sync::Arc<[rbitcoin_query::TxPrecompute]>, + pres: std::sync::Arc<[crate::TxPrecompute]>, } /// Assemble output for one height (held through scripts → write). @@ -151,9 +146,9 @@ pub struct WireLoadPipeline<'a> { /// Inclusive create-fk start for [`Query::archive_plan_batch_from_wire`]. pub next_tx_start: u64, /// Prior uncommitted packs (load-thread map; stamp/pin borrow). - pub in_flight: &'a rbitcoin_query::InFlight, + pub in_flight: &'a crate::InFlight, /// Lookup-filled parent identity for this load batch (IBD skeleton). - pub skeleton: Option, + pub skeleton: Option, /// External prev_txids from the BQ input walk (IBD skeleton stamp; no second wire collect). pub carried_need: Vec<[u8; 32]>, /// BQ `header_fk` per block (empty = stamp ensure/put). Length matches `carried_header_hashes`. @@ -174,14 +169,14 @@ pub struct LoadedBatch { /// Shared wire (Arc) so load→scripts→write does not deep-clone full blocks. wire_blocks: Vec>, /// Per-batch pin map: load → assemble → write structural, then drop. - batch_parents: rbitcoin_query::BatchParents, + batch_parents: crate::BatchParents, /// Mempool preverified txids for scripts stage (tip follow); empty on IBD. script_preverified: ScriptPreverified, /// Planned Class A write from wire lookup/load (committed in write stage). - pub archive_plan: Option, + pub archive_plan: Option, /// Indexes to assemble. Copied from `index_live` at load. index_want: index::IndexWant, - stats: Arc, + stats: Arc, } /// Script-verified batch ready for ordered commit (Class A + structural + C). @@ -190,8 +185,8 @@ pub struct LoadedBatch { pub struct ScriptOkBatch { prepared: Vec, wire_blocks: Vec>, - batch_parents: rbitcoin_query::BatchParents, - pub archive_plan: Option, + batch_parents: crate::BatchParents, + pub archive_plan: Option, /// Filter bytes and tweak vecs for this batch. Empty when the indexes are off. index_seal: index::IndexSeal, } @@ -234,7 +229,7 @@ pub fn confirm_wire_load_phase( milestone: Milestone, blocks: &[(Height, Block)], preverified: &ScriptPreverified, -) -> Result { +) -> Result { confirm_wire_load_phase_pipelined(query, params, milestone, blocks, preverified, None) } @@ -249,7 +244,7 @@ fn wire_blocks_to_arcs(query: &Query, blocks: &[(Height, Block)]) -> Vec 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().phase_prep_wire_arc_ns, ns); + crate::note_confirm(&query.confirm_stats().phase_prep_wire_arc_ns, ns); } arcs } @@ -265,15 +260,10 @@ pub fn confirm_wire_load_phase_pipelined( blocks: &[(Height, Block)], preverified: &ScriptPreverified, pipeline: Option<&WireLoadPipeline>, -) -> Result { - if blocks.is_empty() { - return Err(ConsensusError::BadBlock("empty confirm batch")); - } - for w in blocks.windows(2) { - if w[1].0 .0 != w[0].0 .0.saturating_add(1) { - return Err(ConsensusError::BadBlock("confirm run not contiguous")); - } - } +) -> Result { + rbitcoin_consensus::block::reject_confirm_batch( + &blocks.iter().map(|(h, _)| h.0).collect::>(), + )?; let arcs = wire_blocks_to_arcs(query, blocks); let stamped = confirm_wire_lookup_stamp(query, params, milestone, &arcs, pipeline)?; confirm_wire_load_from_plan(query, params, milestone, stamped, pipeline, preverified) @@ -285,7 +275,7 @@ pub fn confirm_wire_run( params: &ChainParams, milestone: Milestone, blocks: &[(Height, Block)], -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { let arcs: Vec = blocks .iter() .map(|(h, b)| (*h, Arc::new(b.clone()), None)) @@ -307,10 +297,8 @@ pub fn confirm_wire_run_preverified( milestone: Milestone, blocks: &[WireBlockIn], preverified: &ScriptPreverified, -) -> Result, ConsensusError> { - if blocks.is_empty() { - return Err(ConsensusError::BadBlock("empty confirm batch")); - } +) -> Result, ConfirmError> { + rbitcoin_consensus::block::reject_empty_confirm_batch(blocks.is_empty())?; let stamped = confirm_wire_lookup_stamp(query, params, milestone, blocks, None)?; let mat = confirm_wire_load_from_plan(query, params, milestone, stamped, None, preverified)?; let ok = confirm_scripts_phase(mat.batch)?; @@ -372,9 +360,9 @@ impl ScriptOkBatch { } /// Wire headers in the same order as [`Self::heights_hashes`]. - pub fn wire_headers(&self) -> Result, ConsensusError> { + pub fn wire_headers(&self) -> Result, ConfirmError> { if self.wire_blocks.len() != self.prepared.len() { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: wire headers length", ))); } diff --git a/crates/rbitcoin-consensus/src/confirm_run/phases.rs b/crates/rbitcoin-query/src/confirm_run/phases.rs similarity index 63% rename from crates/rbitcoin-consensus/src/confirm_run/phases.rs rename to crates/rbitcoin-query/src/confirm_run/phases.rs index eff71757e2..8c5ee2f826 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/phases.rs +++ b/crates/rbitcoin-query/src/confirm_run/phases.rs @@ -1,65 +1,60 @@ //! Shared confirm phase helpers (load/assemble/structural/script/commit). use super::*; -use rbitcoin_query::FkMap; +use crate::FkMap; fn assemble_parent_mtp_and_bits( query: &Query, params: &ChainParams, height: Height, header: &bitcoin::block::Header, -) -> Result<(u32, Vec), ConsensusError> { + hash: [u8; 32], +) -> Result<(u32, Vec), ConfirmError> { let prev_h = Height(height.0 - 1); let start = prev_h.0.saturating_sub(10); - let prev_hash = header.prev_blockhash.to_byte_array(); let mut times = Vec::with_capacity(11); - let mut prev_bits_raw: Option = None; - let mut prev_time: Option = None; + let mut parent: Option = None; for h in start..=prev_h.0 { - if let Some(plan) = query.confirm_parent_cache().get_header_plan(h) { - times.push(plan.header_rec.timestamp); - if h == prev_h.0 { - if plan.header_rec.hash != prev_hash { - return Err(ConsensusError::BadPrev); - } - prev_bits_raw = Some(plan.header_rec.bits); - prev_time = Some(plan.header_rec.timestamp); - } - } else if let Some((_fk, rec)) = query - .header_at_height(Height(h)) - .map_err(ConsensusError::from)? - { - times.push(rec.timestamp); - if h == prev_h.0 { - if rec.hash != prev_hash { - return Err(ConsensusError::BadPrev); - } - prev_bits_raw = Some(rec.bits); - prev_time = Some(rec.timestamp); - } - } else { - return Err(ConsensusError::Store(StoreError::Corrupt( - "confirm: load incomplete (parent header plan missing above tip)", - ))); + let (rec_hash, timestamp, bits) = + if let Some(plan) = query.confirm_parent_cache().get_header_plan(h) { + ( + plan.header_rec.hash, + plan.header_rec.timestamp, + plan.header_rec.bits, + ) + } else if let Some((_fk, rec)) = query + .header_at_height(Height(h)) + .map_err(ConfirmError::from)? + { + (rec.hash, rec.timestamp, rec.bits) + } else { + return Err(ConfirmError::Store(StoreError::Corrupt( + "confirm: load incomplete (parent header plan missing above tip)", + ))); + }; + times.push(timestamp); + if h == prev_h.0 { + parent = Some(rbitcoin_consensus::ParentHeader { + hash: rec_hash, + time: timestamp, + bits: bitcoin::CompactTarget::from_consensus(bits), + }); } } - let mtp = median_time_past_times(×) - .map_err(|_| ConsensusError::BadHeader("empty median time"))?; - if header.time <= mtp { - return Err(ConsensusError::BadHeader("timestamp <= median-time-past")); - } - check_header_version_and_future_time(params, height, header)?; - let (Some(prev_bits_raw), Some(prev_time)) = (prev_bits_raw, prev_time) else { - return Err(ConsensusError::Store(StoreError::Corrupt( - "confirm: load incomplete (parent header plan missing above tip)", - ))); - }; - let prev_bits = bitcoin::CompactTarget::from_consensus(prev_bits_raw); - let expected = - expected_bits_extending(query, params, height, prev_bits, prev_time, header.time)?; - if header.bits != expected { - return Err(ConsensusError::BadHeader("incorrect proof of work bits")); - } + let parent = parent.ok_or(ConfirmError::Store(StoreError::Corrupt( + "confirm: load incomplete (parent header plan missing above tip)", + )))?; + let period_first = retarget_first_time(query, params, height.0)?; + let mtp = rbitcoin_consensus::check_header( + params, + height, + header, + hash, + Some(parent), + ×, + period_first, + |h| header_bits_plan_or_store(query, h), + )?; Ok((mtp, times)) } @@ -68,24 +63,60 @@ fn assemble_chained_header( params: &ChainParams, height: Height, header: &bitcoin::block::Header, + hash: [u8; 32], prev: &Prepared, time_window: &[u32], -) -> Result { - if header.prev_blockhash.to_byte_array() != prev.hash { - return Err(ConsensusError::BadPrev); - } - let mtp = median_time_past_times(time_window) - .map_err(|_| ConsensusError::BadHeader("empty median time"))?; - if header.time <= mtp { - return Err(ConsensusError::BadHeader("timestamp <= median-time-past")); +) -> Result { + let period_first = retarget_first_time(query, params, height.0)?; + Ok(rbitcoin_consensus::check_header( + params, + height, + header, + hash, + Some(rbitcoin_consensus::ParentHeader { + hash: prev.hash, + time: prev.time, + bits: prev.bits, + }), + time_window, + period_first, + |h| header_bits_plan_or_store(query, h), + )?) +} + +fn header_bits_plan_or_store(query: &Query, h: u32) -> Option { + if let Some((_fk, rec)) = query.header_at_height(Height(h)).ok().flatten() { + return Some(bitcoin::CompactTarget::from_consensus(rec.bits)); } - check_header_version_and_future_time(params, height, header)?; - let expected = - expected_bits_extending(query, params, height, prev.bits, prev.time, header.time)?; - if header.bits != expected { - return Err(ConsensusError::BadHeader("incorrect proof of work bits")); + query + .confirm_parent_cache() + .get_header_plan(h) + .map(|plan| bitcoin::CompactTarget::from_consensus(plan.header_rec.bits)) +} + +fn retarget_first_time( + query: &Query, + params: &ChainParams, + height: u32, +) -> Result, ConfirmError> { + let interval = params.difficulty_adjustment_interval(); + let on_boundary = interval > 0 && height.is_multiple_of(interval); + if !on_boundary || params.no_pow_retargeting() { + return Ok(None); } - Ok(mtp) + let first_height = Height(height - interval); + let first = if let Some((_fk, rec)) = query.header_at_height(first_height)? { + Some(rec.timestamp) + } else { + query + .confirm_parent_cache() + .get_header_plan(first_height.0) + .map(|plan| plan.header_rec.timestamp) + }; + // Missing period-start is the header rule, decided in consensus. + Ok(rbitcoin_consensus::block::require_retarget_first( + true, false, first, + )?) } /// Median-time-past uses the last 11 timestamps. Older ones drop. @@ -107,12 +138,12 @@ pub(super) fn assemble_run( milestone: Milestone, metas: Vec, wire_blocks: &[Arc], - batch_parents: &rbitcoin_query::BatchParents, - spend_edges: &rbitcoin_query::SpendEdges, -) -> Result { + batch_parents: &crate::BatchParents, + spend_edges: &crate::SpendEdges, +) -> Result { // Provisional same-run double-spend only (not durable spentness). - let mut pending_spent: rbitcoin_query::OutPointSet = Default::default(); - let mut pending_creates = crate::block::PendingCreates::default(); + let mut pending_spent: crate::OutPointSet = Default::default(); + let mut pending_creates = rbitcoin_consensus::block::PendingCreates::default(); let mut time_window: Vec = Vec::with_capacity(11); let mut prepared: Vec = Vec::with_capacity(metas.len()); let mut tx_fees: Vec = Vec::new(); @@ -131,7 +162,7 @@ pub(super) fn assemble_run( if i == 0 { if height.0 >= 1 { let (mtp, times) = - assemble_parent_mtp_and_bits(query, params, height, &block.header)?; + assemble_parent_mtp_and_bits(query, params, height, &block.header, block_hash)?; prev_mtp = mtp; time_window = times; } else { @@ -144,28 +175,35 @@ pub(super) fn assemble_run( params, height, &block.header, + block_hash, &prepared[i - 1], &time_window, )?; } - if block_has_witness_from_pres(&meta.pres) && !params.segwit_active_at(height.0) { - return Err(ConsensusError::BadBlock("unexpected witness before segwit")); - } + rbitcoin_consensus::block::reject_witness_before_segwit( + block_has_witness_from_pres(&meta.pres), + params.segwit_active_at(height.0), + )?; // BIP325: full signet challenge on tip confirm only. if height.0 > 0 { if let Some(challenge) = params.signet_challenge.as_ref() { - crate::signet::validate_signet_block_solution(block, challenge.as_script())?; + rbitcoin_consensus::validate_signet_block_solution(block, challenge.as_script())?; } } - let bip16_active = - crate::block::bip16_active_from_prev_mtp(params, height.0, &block_hash, prev_mtp); + let bip16_active = rbitcoin_consensus::block::bip16_active_from_prev_mtp( + params, + height.0, + &block_hash, + prev_mtp, + ); let t_connect = Instant::now(); - let (script_jobs, spends, fees, block_fees) = assemble_block_prevouts( - query, + let build_script_jobs = + !crate::milestone_gate::skips_on_query(milestone, query, height.0, &block_hash); + let (script_jobs, spends, fees, block_fees, meters) = assemble_block_prevouts( block.as_ref(), &ctx, Some(&meta.tx_fks), @@ -179,9 +217,13 @@ pub(super) fn assemble_run( bip16_active, Some(block), Some(&meta.pres), + build_script_jobs, )?; + meters.acc.flush(query.confirm_stats()); + crate::note_confirm(&query.confirm_stats().asm_prevout_ns, meters.prev_ns); + crate::note_confirm(&query.confirm_stats().asm_job_ns, meters.job_ns); if meta.pres.len() != block.txdata.len() { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: txstat size pres", ))); } @@ -193,7 +235,7 @@ pub(super) fn assemble_run( tx_sizes.push((base, total)); } tx_fees.extend(block_fees); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().connect_ns, t_connect.elapsed().as_nanos() as u64, ); @@ -208,7 +250,12 @@ pub(super) fn assemble_run( jobs: script_jobs, spends, fees, - check_scripts: crate::milestone::check_scripts(milestone, query, height.0, &block_hash), + check_scripts: crate::milestone_gate::check_scripts( + milestone, + query, + height.0, + &block_hash, + ), time: block.header.time, bits: block.header.bits, hash: block_hash, @@ -225,8 +272,8 @@ pub(super) fn assemble_run( /// Scratch reused by the confirm write thread. Cleared at each batch. #[derive(Default)] pub(super) struct StructuralReuse { - pub scratch: crate::block::StructuralScratch, - pub pending: rbitcoin_query::OutPointSet, + pub scratch: crate::block_pipeline::StructuralScratch, + pub pending: crate::OutPointSet, } /// Durable spentness + maturity + subsidy after scripts (height order). @@ -237,14 +284,20 @@ pub(super) fn structural_run( milestone: Milestone, prepared: &[Prepared], wire_blocks: &[Arc], - batch_parents: &rbitcoin_query::BatchParents, - abs_jobs: &[Vec], - class_a_wave: &crate::block::ClassAWave, + batch_parents: &crate::BatchParents, + abs_jobs: &[Vec], + class_a_wave: &crate::block_pipeline::ClassAWave, reuse: &mut StructuralReuse, -) -> Result<(crate::block::StructuralPhaseNs, crate::block::AnnotateSlots), ConsensusError> { - use crate::block::StructuralPhaseNs; +) -> Result< + ( + crate::block_pipeline::StructuralPhaseNs, + crate::block_pipeline::AnnotateSlots, + ), + ConfirmError, +> { + use crate::block_pipeline::StructuralPhaseNs; if abs_jobs.len() != prepared.len() { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: spend abs jobs length", ))); } @@ -252,7 +305,7 @@ pub(super) fn structural_run( // Slots and the pack-local double-spend set are this batch only. reuse.scratch.begin_batch(); reuse.pending.clear(); - let index = crate::block::RunCreateHeight::from_blocks( + let index = crate::block_pipeline::RunCreateHeight::from_blocks( prepared.iter().map(|p| (p.height.0, p.tx_fks.as_slice())), ); let mut mtp_cache: U32Map = U32Map::default(); @@ -288,47 +341,47 @@ pub(super) fn structural_run( tot.bip68_ns = tot.bip68_ns.saturating_add(ph.bip68_ns); } // Window counters (may race with sampler; last-write uses `tot` instead). - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().structural_ns, t0.elapsed().as_nanos() as u64, ); - rbitcoin_query::note_confirm(&query.confirm_stats().structural_spent_ns, tot.spent_ns); - rbitcoin_query::note_confirm( + crate::note_confirm(&query.confirm_stats().structural_spent_ns, tot.spent_ns); + crate::note_confirm( &query.confirm_stats().structural_spent_abs_ns, tot.spent_abs_ns, ); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().structural_spent_strong_ns, tot.spent_strong_ns, ); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().structural_spent_cold_ns, tot.spent_cold_ns, ); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().structural_spent_pending_ns, tot.spent_pending_ns, ); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().structural_create_h_ns, tot.create_h_ns, ); - rbitcoin_query::note_confirm(&query.confirm_stats().structural_bip68_ns, tot.bip68_ns); + crate::note_confirm(&query.confirm_stats().structural_bip68_ns, tot.bip68_ns); Ok((tot, std::mem::take(&mut reuse.scratch.slots))) } pub(super) fn class_c_commit( query: &Query, prepared: &mut [Prepared], - write_create_pins: &FkMap, -) -> Result, ConsensusError> { + write_create_pins: &FkMap, +) -> Result, ConfirmError> { use std::sync::atomic::Ordering as QOrd; let strong0 = query.confirm_stats().strong_ns.load(QOrd::Relaxed); let tip0 = query.confirm_stats().tip_ns.load(QOrd::Relaxed); - let items: Vec = prepared + let items: Vec = prepared .iter_mut() - .map(|p| rbitcoin_query::ConfirmPrepared { + .map(|p| crate::ConfirmPrepared { height: p.height, header_fk: p.header_fk, tx_fks: std::mem::take(&mut p.tx_fks), @@ -342,7 +395,7 @@ pub(super) fn class_c_commit( }; let out = query .confirm_blocks_run_with_create_pins(&items, pins) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; let strong_d = query .confirm_stats() .strong_ns @@ -353,7 +406,7 @@ pub(super) fn class_c_commit( .tip_ns .load(QOrd::Relaxed) .saturating_sub(tip0); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().class_c_ns, strong_d.saturating_add(tip_d), ); @@ -365,8 +418,8 @@ pub(super) fn class_c_commit( /// Pure-write annotate from structural slots (no pin `get_spender_abs`). pub(super) fn post_commit( query: &Query, - slots: &crate::block::AnnotateSlots, -) -> Result { + slots: &crate::block_pipeline::AnnotateSlots, +) -> Result { let t_spent = Instant::now(); if query.spend_index_enabled() && !slots.abs_edges.is_empty() { let _utxo_view = query.store().hold_utxo_view(); @@ -375,33 +428,34 @@ pub(super) fn post_commit( let cold = query .store() .put_spend_batch_by_abs_meta_known(&slots.abs_edges, &slots.known, backend) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; if !cold.is_empty() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: spend annotate abs cold (OOB or IO); load/layout bug", ))); } let ann_ns = t_ann.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().spend_ann_ns, ann_ns); - rbitcoin_query::note_confirm( + crate::note_confirm(&query.confirm_stats().spend_ann_ns, ann_ns); + crate::note_confirm( &query.confirm_stats().spend_ann_n, slots.abs_edges.len() as u64, ); let _ = backend; - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().spend_annotate_ranged, slots.abs_edges.len() as u64, ); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().spend_ann_pread_skip, slots.abs_edges.len() as u64, ); } let spend_ann_ns = t_spent.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().utxo_apply_ns, spend_ann_ns); + crate::note_confirm(&query.confirm_stats().utxo_apply_ns, spend_ann_ns); Ok(spend_ann_ns) } +#[cfg(test)] pub(super) fn expected_bits_extending( query: &Query, params: &ChainParams, @@ -409,60 +463,34 @@ pub(super) fn expected_bits_extending( prev_bits: bitcoin::CompactTarget, prev_time: u32, header_time: u32, -) -> Result { - use bitcoin::CompactTarget; +) -> Result { + use rbitcoin_consensus::genesis_block; if height.0 == 0 { return Ok(genesis_block(params).header.bits); } - let interval = params.difficulty_adjustment_interval(); - let on_boundary = interval > 0 && height.0.is_multiple_of(interval); - let period_first = if on_boundary && !params.no_pow_retargeting() { - // Period-start may still be above confirmed tip during tip-ahead - // multi-block load (i>0). Lookup/load already put_header_plan. - let first_height = Height(height.0 - interval); - let first_ts = if let Some((_fk, rec)) = query - .header_at_height(first_height) - .map_err(ConsensusError::from)? - { - rec.timestamp - } else if let Some(plan) = query.confirm_parent_cache().get_header_plan(first_height.0) { - plan.header_rec.timestamp - } else { - return Err(ConsensusError::BadHeader("missing retarget first header")); - }; - Some(first_ts) - } else { - None - }; - crate::header::next_work_bits( + let period_first = retarget_first_time(query, params, height.0)?; + rbitcoin_consensus::block::require_next_work(rbitcoin_consensus::header::next_work_bits( params, height.0, prev_bits, prev_time, header_time, period_first, - |h| { - if let Some((_fk, rec)) = query.header_at_height(Height(h)).ok().flatten() { - return Some(CompactTarget::from_consensus(rec.bits)); - } - query - .confirm_parent_cache() - .get_header_plan(h) - .map(|plan| CompactTarget::from_consensus(plan.header_rec.bits)) - }, - ) - .ok_or(ConsensusError::BadPrev) + |h| header_bits_plan_or_store(query, h), + )) + .map_err(ConfirmError::from) } #[cfg(test)] mod tests { use super::*; - use crate::regtest_pad::mine_empty_regtest; - use crate::{accept_and_connect_block, ChainParams, Milestone}; + use crate::accept_and_connect_block; + use crate::TxPrecompute; use bitcoin::blockdata::constants; use bitcoin::ScriptBuf; + use rbitcoin_consensus::mine_empty_regtest; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::TxPrecompute; use rbitcoin_store::HeaderRecord; use std::sync::Arc; @@ -480,8 +508,8 @@ mod tests { assert_eq!(window, (4..=14).collect::>()); } - fn tiny() -> (rbitcoin_query::testutil::TempDir, rbitcoin_query::Query) { - rbitcoin_query::testutil::tiny_query_labeled("phases-pin") + fn tiny() -> (crate::testutil::TempDir, crate::Query) { + crate::testutil::tiny_query_labeled("phases-pin") } #[test] @@ -528,8 +556,8 @@ mod tests { Milestone::NONE, vec![meta], std::slice::from_ref(&block), - &rbitcoin_query::BatchParents::new(), - &rbitcoin_query::SpendEdges::default(), + &crate::BatchParents::new(), + &crate::SpendEdges::default(), ) { Ok(_) => panic!("height 1 with a challenge must reject a block that has no solution"), Err(e) => e, diff --git a/crates/rbitcoin-consensus/src/confirm_run/pin.rs b/crates/rbitcoin-query/src/confirm_run/pin.rs similarity index 80% rename from crates/rbitcoin-consensus/src/confirm_run/pin.rs rename to crates/rbitcoin-query/src/confirm_run/pin.rs index baf7e40f52..1f491d3f89 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/pin.rs +++ b/crates/rbitcoin-query/src/confirm_run/pin.rs @@ -2,19 +2,19 @@ use super::*; -type CreatePin = rbitcoin_query::CreatePin; +type CreatePin = crate::CreatePin; struct PlanSpend<'a> { - spend_edges: std::borrow::Cow<'a, rbitcoin_query::SpendEdges>, + spend_edges: std::borrow::Cow<'a, crate::SpendEdges>, parent_vouts: U64Map>, vouts_from_stamp: bool, batch_pin_by_id: U64Map<&'a CreatePin>, } fn spend_edges_from_plan<'a>( - plan: &'a rbitcoin_query::ArchiveWritePlan, + plan: &'a crate::ArchiveWritePlan, parent_pin: &mut ParentPinStamp, -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { let mut batch_pin_by_id: U64Map<&CreatePin> = U64Map::default(); // Reachable plans store the same pin Arc in `packed` and `batch_pin`. // Indexing `packed` is that pin either way. @@ -24,7 +24,7 @@ fn spend_edges_from_plan<'a>( } } if plan.edges.is_empty() && !plan.planned_fks.is_empty() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: plan spend edges empty", ))); } @@ -59,8 +59,8 @@ fn spend_edges_from_stamp( parent_pin: &ParentPinStamp, metas: &[BodyMeta], wire_blocks: &[Arc], -) -> (rbitcoin_query::SpendEdges, U64Map>) { - let mut spend_edges = rbitcoin_query::SpendEdges::default(); +) -> (crate::SpendEdges, U64Map>) { + let mut spend_edges = crate::SpendEdges::default(); let mut parent_vouts: U64Map> = U64Map::default(); for (m, block) in metas.iter().zip(wire_blocks.iter()) { for (ti, tx) in block.txdata.iter().enumerate() { @@ -71,7 +71,7 @@ fn spend_edges_from_stamp( for (vin, inp) in tx.input.iter().enumerate() { let vin = vin as u32; if inp.previous_output.is_null() { - edges.push(rbitcoin_query::SpendEdge { + edges.push(crate::SpendEdge { prev_txid: [0u8; 32], vout: u32::MAX, spend_fk: rbitcoin_primitives::Fk(sfk), @@ -83,7 +83,7 @@ fn spend_edges_from_stamp( let prev_txid = inp.previous_output.txid.to_byte_array(); let vout = inp.previous_output.vout; if let Some(&pid) = parent_pin.resolved.get(&prev_txid) { - edges.push(rbitcoin_query::SpendEdge { + edges.push(crate::SpendEdge { prev_txid, vout, spend_fk: rbitcoin_primitives::Fk(sfk), @@ -93,7 +93,7 @@ fn spend_edges_from_stamp( parent_vouts.entry(pid).or_default().push(vout); continue; } - edges.push(rbitcoin_query::SpendEdge { + edges.push(crate::SpendEdge { prev_txid, vout, spend_fk: rbitcoin_primitives::Fk(sfk), @@ -111,8 +111,8 @@ fn fill_pins( parent_vouts: &U64Map>, batch_pin_by_id: &U64Map<&CreatePin>, parent_pin: &ParentPinStamp, - in_flight: Option<&rbitcoin_query::InFlight>, - stats: &rbitcoin_query::ConfirmStats, + in_flight: Option<&crate::InFlight>, + stats: &crate::ConfirmStats, ) -> U64Map { let mut plan_by_id: U64Map = U64Map::default(); if let Some(ifo) = in_flight { @@ -148,7 +148,7 @@ fn fill_pins( plan_by_id.insert(*id, pin); } let recent_outs_ns = t_recent.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&stats.pin_recent_outs_ns, recent_outs_ns); + crate::note_confirm(&stats.pin_recent_outs_ns, recent_outs_ns); plan_by_id } @@ -166,7 +166,7 @@ fn apply_plan_pins( parent_vouts: &U64Map>, plan_by_id: &U64Map, parent_pin: &ParentPinStamp, - batch_parents: &mut rbitcoin_query::BatchParents, + batch_parents: &mut crate::BatchParents, still_need: &mut U64Map>, ) -> u64 { let mut n_plan_pin = 0u64; @@ -217,13 +217,13 @@ fn reject_vout_past_n_out( parent_vouts: &U64Map>, parent_pin: &ParentPinStamp, plan_by_id: &U64Map, -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { for (id, need) in parent_vouts { let Some(&max_vout) = need.iter().max() else { continue; }; let wire_n_out = || match plan_by_id.get(id).map(|p| &***p) { - Some(rbitcoin_query::CreatePinInner::Wire { + Some(crate::CreatePinInner::Wire { block, tx_index, .. }) => block .txdata @@ -232,9 +232,7 @@ fn reject_vout_past_n_out( _ => None, }; let n_out = parent_pin.n_out(*id).map(u64::from).or_else(wire_n_out); - if n_out.is_some_and(|n| u64::from(max_vout) >= n) { - return Err(ConsensusError::MissingPrevout); - } + rbitcoin_consensus::block::reject_vout_past_n_out(max_vout, n_out)?; } Ok(()) } @@ -244,8 +242,8 @@ fn denserels_by_stamped_range( query: &Query, parent_pin: &ParentPinStamp, still_need: &mut U64Map>, - batch_parents: &mut rbitcoin_query::BatchParents, -) -> Result<(u64, u64), ConsensusError> { + batch_parents: &mut crate::BatchParents, +) -> Result<(u64, u64), ConfirmError> { let mut range_jobs: Vec<(rbitcoin_primitives::Fk, (u64, u64), [u8; 32], u32, Vec)> = Vec::new(); let pending = std::mem::take(still_need); @@ -260,7 +258,7 @@ fn denserels_by_stamped_range( }; let tid = parent_pin.create_txid(id); let Some(tid) = tid else { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: lookup stage miss (load parent create identity not stamped)", ))); }; @@ -273,31 +271,31 @@ fn denserels_by_stamped_range( let (decoded, body_ns, dec_ns, extend_n, body_sqe_n, guess_full_n) = query .store() .get_outs_by_range_batch(&range_jobs) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; let rng_ns = body_ns.saturating_add(dec_ns); // Zero adds are a no-op inside `note_confirm`. - rbitcoin_query::note_confirm(&query.confirm_stats().cold_io_ns, rng_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_ns, rng_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_body_ns, body_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_decode_ns, dec_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_n, n_range); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_extend_n, extend_n); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_body_sqe_n, body_sqe_n); - rbitcoin_query::note_confirm(&query.confirm_stats().cold_range_guess_full_n, guess_full_n); - rbitcoin_query::note_confirm(&query.confirm_stats().body_tx_reads, n_range); - rbitcoin_query::note_confirm(&query.confirm_stats().pin_new, n_range); + crate::note_confirm(&query.confirm_stats().cold_io_ns, rng_ns); + crate::note_confirm(&query.confirm_stats().cold_range_ns, rng_ns); + crate::note_confirm(&query.confirm_stats().cold_range_body_ns, body_ns); + crate::note_confirm(&query.confirm_stats().cold_range_decode_ns, dec_ns); + crate::note_confirm(&query.confirm_stats().cold_range_n, n_range); + crate::note_confirm(&query.confirm_stats().cold_range_extend_n, extend_n); + crate::note_confirm(&query.confirm_stats().cold_range_body_sqe_n, body_sqe_n); + crate::note_confirm(&query.confirm_stats().cold_range_guess_full_n, guess_full_n); + crate::note_confirm(&query.confirm_stats().body_tx_reads, n_range); + crate::note_confirm(&query.confirm_stats().pin_new, n_range); let t_range_fill = Instant::now(); for ((fk, range, _tid, _n_out, need), row) in range_jobs.into_iter().zip(decoded) { let Some(id) = fk.get() else { continue; }; let Some((mut tx, live, sparse)) = row else { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: load denserels by range returned none for stamped parent", ))); }; if live.len() != need.len() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: load denserels by range incomplete outs for need_vouts", ))); } @@ -305,7 +303,7 @@ fn denserels_by_stamped_range( tx.txid = parent_pin .create_txid(id) - .ok_or(ConsensusError::Store(StoreError::Corrupt( + .ok_or(ConfirmError::Store(StoreError::Corrupt( "invariant: lookup stage miss (load parent create identity not stamped)", )))?; } @@ -318,7 +316,7 @@ fn denserels_by_stamped_range( } let range_fill_ns = t_range_fill.elapsed().as_nanos() as u64; // `note_confirm` ignores a zero add, so a `> 0` gate is the same call. - rbitcoin_query::note_confirm(&query.confirm_stats().pin_range_fill_ns, range_fill_ns); + crate::note_confirm(&query.confirm_stats().pin_range_fill_ns, range_fill_ns); Ok((n_range, rng_ns)) } @@ -330,18 +328,12 @@ fn denserels_by_stamped_range( /// `spent_range` onto pins. Write [`collect_spend_abs_after_fill`] is abs-or-Corrupt. pub(super) fn pin_for_wire_batch<'a>( query: &Query, - plan: Option<&'a rbitcoin_query::ArchiveWritePlan>, + plan: Option<&'a crate::ArchiveWritePlan>, parent_pin: &mut ParentPinStamp, metas: &[BodyMeta], wire_blocks: &[Arc], - in_flight: Option<&rbitcoin_query::InFlight>, -) -> Result< - ( - rbitcoin_query::BatchParents, - std::borrow::Cow<'a, rbitcoin_query::SpendEdges>, - ), - ConsensusError, -> { + in_flight: Option<&crate::InFlight>, +) -> Result<(crate::BatchParents, std::borrow::Cow<'a, crate::SpendEdges>), ConfirmError> { let t_pin = Instant::now(); let t_thin = Instant::now(); @@ -379,9 +371,9 @@ pub(super) fn pin_for_wire_batch<'a>( ); reject_vout_past_n_out(&parent_vouts, parent_pin, &plan_by_id)?; - let mut batch_parents = rbitcoin_query::BatchParents::with_capacity(parent_vouts.len()); + let mut batch_parents = crate::BatchParents::with_capacity(parent_vouts.len()); let thin_ns = t_thin.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().thin_ns, thin_ns); + crate::note_confirm(&query.confirm_stats().thin_ns, thin_ns); let mut still_need: U64Map> = U64Map::default(); let t_plan = Instant::now(); @@ -404,7 +396,7 @@ pub(super) fn pin_for_wire_batch<'a>( } if !still_need.is_empty() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: lookup stage miss (load parent without body_range denserels)", ))); } @@ -427,12 +419,12 @@ pub(super) fn pin_for_wire_batch<'a>( let contract_ns = t_contract.elapsed().as_nanos() as u64; let n_unique = parent_vouts.len() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().parent_unique, n_unique); - rbitcoin_query::note_confirm(&query.confirm_stats().utxo_parents, n_unique); - rbitcoin_query::note_confirm(&query.confirm_stats().pin_plan, n_plan_pin); - rbitcoin_query::note_confirm(&query.confirm_stats().pin_cache_body, n_plan_pin); - rbitcoin_query::note_confirm(&query.confirm_stats().plan_pin_ns, plan_pin_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().pin_contract_ns, contract_ns); + crate::note_confirm(&query.confirm_stats().parent_unique, n_unique); + crate::note_confirm(&query.confirm_stats().utxo_parents, n_unique); + crate::note_confirm(&query.confirm_stats().pin_plan, n_plan_pin); + crate::note_confirm(&query.confirm_stats().pin_cache_body, n_plan_pin); + crate::note_confirm(&query.confirm_stats().plan_pin_ns, plan_pin_ns); + crate::note_confirm(&query.confirm_stats().pin_contract_ns, contract_ns); query.confirm_stats().note_last_pin( plan_pin_ns, cold_range_batch_ns, @@ -441,11 +433,11 @@ pub(super) fn pin_for_wire_batch<'a>( n_range_new, ); let pin_ns = t_pin.elapsed().as_nanos() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().parent_pin_ns, pin_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().pin_body_ns, pin_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().load_win_ns, pin_ns); + crate::note_confirm(&query.confirm_stats().parent_pin_ns, pin_ns); + crate::note_confirm(&query.confirm_stats().pin_body_ns, pin_ns); + crate::note_confirm(&query.confirm_stats().load_win_ns, pin_ns); let n_blks = metas.len() as u64; - rbitcoin_query::note_confirm(&query.confirm_stats().load_blocks, n_blks); + crate::note_confirm(&query.confirm_stats().load_blocks, n_blks); Ok((batch_parents, spend_edges)) } @@ -456,9 +448,9 @@ pub(super) fn pin_for_wire_batch<'a>( /// before `tx.head` insert and passes the jobs to structural, which does not /// look them up again. Null create fk is skipped; duplicate abs is one job. pub(super) fn collect_spend_abs_after_fill( - batch_parents: &rbitcoin_query::BatchParents, + batch_parents: &crate::BatchParents, prepared: &[Prepared], -) -> Result>, ConsensusError> { +) -> Result>, ConfirmError> { let mut out = Vec::with_capacity(prepared.len()); for p in prepared { let jobs = batch_parents @@ -468,7 +460,7 @@ pub(super) fn collect_spend_abs_after_fill( .map(|&(_txid, vout, sfk, cfk, vin)| (cfk, vout, sfk, vin)), ) .map_err(|_| { - ConsensusError::Store(StoreError::Corrupt( + ConfirmError::Store(StoreError::Corrupt( "invariant: ensure denserels/abs incomplete for spend edge", )) })?; @@ -487,16 +479,16 @@ pub(super) fn collect_spend_abs_after_fill( /// [`collect_spend_abs_after_fill`] is the write-phase post-condition. #[cfg(test)] pub(super) fn ensure_spend_abs_layouts( - batch_parents: &rbitcoin_query::BatchParents, + batch_parents: &crate::BatchParents, prepared: &[Prepared], -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { for p in prepared { for &(_txid, vout, sfk, cfk, _vin) in &p.spends { if sfk.is_null() || cfk.is_null() { continue; } if batch_parents.get_spender_abs(cfk, vout).is_none() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: ensure denserels/abs incomplete for spend edge", ))); } @@ -508,12 +500,12 @@ pub(super) fn ensure_spend_abs_layouts( #[cfg(test)] mod tests { use super::*; + use crate::{CreatePin, ParentIdent, U64Map}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{CreatePin, ParentIdent, U64Map}; use rbitcoin_store::{OutputRecord, TxRecord}; fn pin_with(txid: [u8; 32], inputs: u32) -> CreatePin { - rbitcoin_query::CreatePinInner::records( + crate::CreatePinInner::records( TxRecord { txid, version: 1, @@ -531,7 +523,7 @@ mod tests { fn plan_pin_coinbase_flag_follows_input_count() { let one = pin_with([0x31; 32], 1); let two = pin_with([0x32; 32], 2); - let mut parents = rbitcoin_query::BatchParents::new(); + let mut parents = crate::BatchParents::new(); parents.insert_owned( Fk(1), one.tx().clone(), @@ -566,7 +558,7 @@ mod tests { "a one-input pin still publishes a body range" ); - let mut fresh = rbitcoin_query::BatchParents::new(); + let mut fresh = crate::BatchParents::new(); let mut by_two: U64Map = U64Map::default(); by_two.insert(2, two); let mut need: U64Map> = U64Map::default(); @@ -585,7 +577,7 @@ mod tests { empty_need.insert(3, Vec::new()); let mut by_empty: U64Map = U64Map::default(); by_empty.insert(3, pin_with([0x33; 32], 1)); - let mut dest = rbitcoin_query::BatchParents::new(); + let mut dest = crate::BatchParents::new(); let mut still = U64Map::default(); apply_plan_pins( &empty_need, diff --git a/crates/rbitcoin-consensus/src/confirm_run/scripts.rs b/crates/rbitcoin-query/src/confirm_run/scripts.rs similarity index 91% rename from crates/rbitcoin-consensus/src/confirm_run/scripts.rs rename to crates/rbitcoin-query/src/confirm_run/scripts.rs index e87216cbc8..7ff5a9f985 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/scripts.rs +++ b/crates/rbitcoin-query/src/confirm_run/scripts.rs @@ -2,8 +2,8 @@ use super::index::{self, IndexJob}; use super::*; -use crate::block::{verify_one_script_job, ScriptCheckJob}; -use crate::script_pool::{ +use rbitcoin_consensus::block::{verify_one_script_job, ScriptCheckJob}; +use rbitcoin_consensus::script_pool::{ fg_has_unclaimed, help_steal, set_script_publisher, start_for_each_owned, start_for_each_pooled, OwnedWave, }; @@ -20,7 +20,7 @@ const SCRIPT_WAVES_MAX: usize = 4; fn take_script_jobs( prepared: &mut [Prepared], preverified: &ScriptPreverified, - stats: &rbitcoin_query::ConfirmStats, + stats: &crate::ConfirmStats, ) -> Vec { let mut jobs = Vec::new(); let mut n_skip = 0u64; @@ -40,9 +40,9 @@ fn take_script_jobs( p.jobs.shrink_to_fit(); } if n_skip > 0 { - rbitcoin_query::note_confirm(&stats.script_skip_mempool, n_skip); + crate::note_confirm(&stats.script_skip_mempool, n_skip); } - rbitcoin_query::note_confirm(&stats.script_jobs, jobs.len() as u64); + crate::note_confirm(&stats.script_jobs, jobs.len() as u64); jobs } @@ -53,10 +53,10 @@ fn outcome_from( seal: super::index::IndexSeal, idx_asm_ns: u64, ) -> ConfirmScriptOutcome { - rbitcoin_query::note_confirm(&batch.stats.script_ns, work_ns); + crate::note_confirm(&batch.stats.script_ns, work_ns); if idx_asm_ns > 0 { - rbitcoin_query::note_confirm(&batch.stats.idx_asm_ns, idx_asm_ns); - rbitcoin_query::note_confirm(&batch.stats.blockfilter_ns, idx_asm_ns); + crate::note_confirm(&batch.stats.idx_asm_ns, idx_asm_ns); + crate::note_confirm(&batch.stats.blockfilter_ns, idx_asm_ns); } ConfirmScriptOutcome { batch: ScriptOkBatch { @@ -94,7 +94,7 @@ impl Inflight { fn start( mut batch: LoadedBatch, mat_ns: u64, - ) -> Result { + ) -> Result { let t0 = Instant::now(); let meta = ScriptsBatchMeta::from_batch(&batch, mat_ns); let scripts = @@ -106,11 +106,11 @@ impl Inflight { let index_started = index_jobs.started; let script_wave = match start_for_each_owned(scripts, apply_script) { Ok(w) => w, - Err(e) => return Err((e, meta)), + Err(e) => return Err((ConfirmError::from(e), meta)), }; let index_wave = match start_for_each_pooled(index_jobs.jobs, index::apply_index_job, 1) { Ok(w) => w, - Err(e) => return Err((e, meta)), + Err(e) => return Err((ConfirmError::from(e), meta)), }; let inf = Self { batch, @@ -158,7 +158,7 @@ impl Inflight { scripts_done && index_done } - fn finish(mut self) -> Result<(ConfirmScriptOutcome, ScriptsBatchMeta), ConsensusError> { + fn finish(mut self) -> Result<(ConfirmScriptOutcome, ScriptsBatchMeta), ConfirmError> { let script_res = match self.script_wave.take() { Some(w) => w.finish(), None => Ok(()), @@ -189,7 +189,7 @@ impl Inflight { } } -pub fn confirm_scripts_phase(batch: LoadedBatch) -> Result { +pub fn confirm_scripts_phase(batch: LoadedBatch) -> Result { match Inflight::start(batch, 0) { Ok(inf) => inf.finish().map(|(o, _)| o), Err((e, _)) => Err(e), @@ -206,7 +206,7 @@ pub fn drive_script_waves_with( mat_rx: &std::sync::mpsc::Receiver<(LoadedBatch, u64)>, mut on_take: impl FnMut(&LoadedBatch, Duration), mut on_ok: impl FnMut(ConfirmScriptOutcome, ScriptsBatchMeta) -> bool, - mut on_err: impl FnMut(ConsensusError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, + mut on_err: impl FnMut(ConfirmError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, mut should_stop: impl FnMut() -> bool, ) { struct ClearPublisher; @@ -263,7 +263,7 @@ fn drop_inflight_tail(inflight: &mut VecDeque) -> Vec, on_ok: &mut impl FnMut(ConfirmScriptOutcome, ScriptsBatchMeta) -> bool, - on_err: &mut impl FnMut(ConsensusError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, + on_err: &mut impl FnMut(ConfirmError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, ) -> bool { while inflight.front().is_some_and(Inflight::is_complete) { let front = inflight.pop_front().expect("front"); @@ -289,7 +289,7 @@ fn drive_try_start( inflight: &mut VecDeque, mat_rx: &std::sync::mpsc::Receiver<(LoadedBatch, u64)>, on_take: &mut impl FnMut(&LoadedBatch, Duration), - on_err: &mut impl FnMut(ConsensusError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, + on_err: &mut impl FnMut(ConfirmError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, ) -> DriveStart { if fg_has_unclaimed() || inflight.len() >= SCRIPT_WAVES_MAX { return DriveStart::Idle; @@ -327,7 +327,7 @@ fn drive_blocking_start( inflight: &mut VecDeque, mat_rx: &std::sync::mpsc::Receiver<(LoadedBatch, u64)>, on_take: &mut impl FnMut(&LoadedBatch, Duration), - on_err: &mut impl FnMut(ConsensusError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, + on_err: &mut impl FnMut(ConfirmError, ScriptsBatchMeta, &[ScriptsBatchMeta]) -> bool, ) -> DriveStart { let t_recv = Instant::now(); match mat_rx.recv() { diff --git a/crates/rbitcoin-consensus/src/confirm_run/spend_durable_tests.rs b/crates/rbitcoin-query/src/confirm_run/spend_durable_tests.rs similarity index 92% rename from crates/rbitcoin-consensus/src/confirm_run/spend_durable_tests.rs rename to crates/rbitcoin-query/src/confirm_run/spend_durable_tests.rs index e0f04fe614..4a3d8cf719 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/spend_durable_tests.rs +++ b/crates/rbitcoin-query/src/confirm_run/spend_durable_tests.rs @@ -11,8 +11,9 @@ use bitcoin::{ Txid, Witness, }; -use crate::block::bip34_height_script; -use crate::{accept_and_connect_block, ChainParams, ConsensusError, Milestone}; +use crate::{accept_and_connect_block, ConfirmError}; +use rbitcoin_consensus::block::bip34_height_script; +use rbitcoin_consensus::{ChainParams, ConsensusError, Milestone}; use rbitcoin_primitives::Height; use rbitcoin_store::spent_abs; @@ -89,7 +90,7 @@ fn mine(prev: bitcoin::BlockHash, time: u32, height: u32, extra: Vec 0, @@ -508,8 +518,8 @@ fn failed_post_tip_stage_does_not_let_the_next_block_respend() { /// over the reconnected height, or the next open does not replay its spends. #[test] fn checkpoint_after_reorg_does_not_skip_the_reconnected_spends() { - let _gate = crate::script_pool::steal_test_gate(); - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("spend-reorg-snapshot"); + let _gate = rbitcoin_consensus::script_pool::steal_test_gate(); + let (dir, q) = crate::testutil::tiny_query_labeled("spend-reorg-snapshot"); let params = ChainParams::regtest(); let ms = Milestone::NONE; let maturity = params.coinbase_maturity(); @@ -549,7 +559,7 @@ fn checkpoint_after_reorg_does_not_skip_the_reconnected_spends() { assert!( matches!( err, - ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: live index commit" )) ), @@ -558,11 +568,11 @@ fn checkpoint_after_reorg_does_not_skip_the_reconnected_spends() { assert_eq!(q.tip_height(), Some(Height(hy)), "Class C committed Y"); let q = std::sync::Arc::new(q); - rbitcoin_query::SpendSync::spawn(std::sync::Arc::clone(&q)).shutdown(); + crate::SpendSync::spawn(std::sync::Arc::clone(&q)).shutdown(); let store = q.store().path().to_path_buf(); drop(q); - let q = rbitcoin_query::Query::open_or_create_tiny(&store).unwrap(); + let q = crate::Query::open_or_create_tiny(&store).unwrap(); crate::replay_spend_annotations(&q).unwrap(); assert!( q.is_outpoint_spent(c1.as_byte_array(), 0).unwrap(), @@ -577,7 +587,10 @@ fn checkpoint_after_reorg_does_not_skip_the_reconnected_spends() { let err = accept_and_connect_block(&q, ¶ms, Height(hy + 1), &respend, ms) .expect_err("an output Y spent must not be spent again"); assert_eq!(q.tip_height(), Some(Height(hy)), "{err}"); - assert!(matches!(err, ConsensusError::PrevoutSpent), "{err}"); + assert!( + matches!(err, ConfirmError::Rules(ConsensusError::PrevoutSpent)), + "{err}" + ); let _ = dir; } @@ -594,7 +607,7 @@ fn marker_heights(store: &std::path::Path) -> Option<(u32, u32)> { } fn connect_until( - q: &rbitcoin_query::Query, + q: &crate::Query, params: &ChainParams, ms: Milestone, tip: &mut bitcoin::BlockHash, @@ -622,7 +635,7 @@ fn shrink_input_loc_hwm(store: &std::path::Path, keep: u64) { f.write_all(&logical.to_le_bytes()).unwrap(); } -fn input_body_off(q: &rbitcoin_query::Query, spend_fk: rbitcoin_primitives::Fk) -> u64 { +fn input_body_off(q: &crate::Query, spend_fk: rbitcoin_primitives::Fk) -> u64 { let mut before = 0u64; for id in 1..spend_fk.0 { let (_, ins, _) = q.store().get_tx_full(rbitcoin_primitives::Fk(id)).unwrap(); @@ -632,8 +645,8 @@ fn input_body_off(q: &rbitcoin_query::Query, spend_fk: rbitcoin_primitives::Fk) } fn sealed_spend_chain() -> ( - rbitcoin_query::testutil::TempDir, - rbitcoin_query::Query, + crate::testutil::TempDir, + crate::Query, ChainParams, Milestone, u32, @@ -641,7 +654,7 @@ fn sealed_spend_chain() -> ( Block, u32, ) { - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("spend-edge-loss"); + let (dir, q) = crate::testutil::tiny_query_labeled("spend-edge-loss"); q.set_spend_index(true); let params = ChainParams::regtest(); let ms = Milestone::NONE; @@ -672,7 +685,7 @@ fn short_input_hwm_shrinks_below_the_edgeless_create() { drop(q); shrink_input_loc_hwm(&store, keep); - let q = rbitcoin_query::Query::open_or_create_tiny(&store).unwrap(); + let q = crate::Query::open_or_create_tiny(&store).unwrap(); q.set_spend_index(true); let tip_h = q.tip_height().expect("a prefix of the chain stays"); assert!( @@ -709,7 +722,7 @@ fn short_input_hwm_shrinks_below_the_edgeless_create() { let err = accept_and_connect_block(&q, ¶ms, Height(tip_h.0 + 2), &respend, ms) .expect_err("reconnecting the spend keeps the output spent"); assert!( - matches!(err, ConsensusError::PrevoutSpent), + matches!(err, ConfirmError::Rules(ConsensusError::PrevoutSpent)), "respend after reconnect: {err}" ); let _ = dir; @@ -735,7 +748,7 @@ fn zeroed_input_body_shrinks_and_is_not_a_coinbase() { f.write_all(&[0u8; 8]).unwrap(); } - let q = rbitcoin_query::Query::open_or_create_tiny(&store).unwrap(); + let q = crate::Query::open_or_create_tiny(&store).unwrap(); q.set_spend_index(true); let tip_h = q.tip_height().unwrap(); assert!( @@ -778,7 +791,7 @@ fn replay_rejects_coinbase_edge_on_a_non_coinbase() { assert!( matches!( err, - ConsensusError::Store(rbitcoin_store::StoreError::Corrupt(_)) + ConfirmError::Store(rbitcoin_store::StoreError::Corrupt(_)) ), "{err}" ); @@ -788,11 +801,8 @@ fn replay_rejects_coinbase_edge_on_a_non_coinbase() { let _ = dir; } -fn chain_at_height( - label: &str, - height: u32, -) -> (rbitcoin_query::testutil::TempDir, rbitcoin_query::Query) { - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled(label); +fn chain_at_height(label: &str, height: u32) -> (crate::testutil::TempDir, crate::Query) { + let (dir, q) = crate::testutil::tiny_query_labeled(label); let params = ChainParams::regtest(); let ms = Milestone::NONE; let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); diff --git a/crates/rbitcoin-consensus/src/confirm_run/write.rs b/crates/rbitcoin-query/src/confirm_run/write.rs similarity index 85% rename from crates/rbitcoin-consensus/src/confirm_run/write.rs rename to crates/rbitcoin-query/src/confirm_run/write.rs index 903ec4e87f..7a4959c80f 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/write.rs +++ b/crates/rbitcoin-query/src/confirm_run/write.rs @@ -63,7 +63,7 @@ fn submit_queued<'a>( let drain = super::head_drain::submit_head_insert(query.store(), queued); let head_sub_ns = t_head.elapsed().as_nanos() as u64; if head_sub_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().write_head_sub_ns, head_sub_ns); + crate::note_confirm(&query.confirm_stats().write_head_sub_ns, head_sub_ns); } drain } @@ -108,7 +108,7 @@ pub(super) fn write_batch_vs_tip( fn finish_already_committed_write( query: &Query, batch: &ScriptOkBatch, -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { let items: Vec<(u32, [u8; 32])> = batch .prepared .iter() @@ -122,8 +122,8 @@ fn finish_already_committed_write( } struct ArchivePlanNs { - pins: FkMap, - class_a_wave: crate::block::ClassAWave, + pins: FkMap, + class_a_wave: crate::block_pipeline::ClassAWave, class_a_ns: u64, ensure_ns: u64, plan_take_ns: u64, @@ -133,10 +133,10 @@ struct ArchivePlanNs { fn apply_archive_plan( query: &Query, batch: &mut ScriptOkBatch, -) -> Result { +) -> Result { let mut ns = ArchivePlanNs { pins: FkMap::default(), - class_a_wave: crate::block::ClassAWave::default(), + class_a_wave: crate::block_pipeline::ClassAWave::default(), class_a_ns: 0, ensure_ns: 0, plan_take_ns: 0, @@ -151,11 +151,11 @@ fn apply_archive_plan( let t_take = Instant::now(); let planned_fks = plan.planned_fks.clone(); if plan.batch_pin.len() != plan.planned_fks.len() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: batch_pin length", ))); } - let packed_pins: Vec = std::mem::take(&mut plan.batch_pin); + let packed_pins: Vec = std::mem::take(&mut plan.batch_pin); ns.plan_take_ns = t_take.elapsed().as_nanos() as u64; let t_ca = Instant::now(); let (committed, loc) = { @@ -163,7 +163,7 @@ fn apply_archive_plan( spindle_mark("class-a"); query .archive_commit_plan_defer_head_parents(plan) - .map_err(ConsensusError::from)? + .map_err(ConfirmError::from)? }; ns.class_a_ns = t_ca.elapsed().as_nanos() as u64; if !committed { @@ -195,7 +195,7 @@ fn apply_archive_plan( // The overlay is proven only for rows this commit wrote. A plan trimmed // at commit wrote fewer rows than `planned_fks` names. if loc.len() == planned_fks.len() { - ns.class_a_wave = crate::block::ClassAWave::new(planned_fks); + ns.class_a_wave = crate::block_pipeline::ClassAWave::new(planned_fks); } Ok(ns) } @@ -208,13 +208,13 @@ fn apply_archive_plan( /// **Class A never leads tip** (no dual-track archive-ahead / body DONTNEED lead). /// /// Accrues window timers on [`Query::confirm_stats`] and snapshots the last batch -/// for slow-write logs via [`rbitcoin_query::ConfirmStats::last_write_phases`]. +/// for slow-write logs via [`crate::ConfirmStats::last_write_phases`]. pub fn confirm_write_phase( query: &Query, params: &ChainParams, milestone: Milestone, batch: ScriptOkBatch, -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { confirm_write_phase_or_return(query, params, milestone, batch).map_err(|(e, _)| e) } @@ -226,7 +226,7 @@ pub fn confirm_write_phase_or_return( params: &ChainParams, milestone: Milestone, mut batch: ScriptOkBatch, -) -> Result, (ConsensusError, ScriptOkBatch)> { +) -> Result, (ConfirmError, ScriptOkBatch)> { write_phase(query, params, milestone, &mut batch).map_err(|e| (e, batch)) } @@ -235,12 +235,12 @@ fn write_phase( params: &ChainParams, milestone: Milestone, batch: &mut ScriptOkBatch, -) -> Result, ConsensusError> { +) -> Result, ConfirmError> { let tip = query.tip_height().map(|h| h.0); match write_batch_vs_tip(tip, batch.prepared.iter().map(|p| p.height.0)) { WriteBatchVsTip::AllOld => return finish_already_committed_write(query, batch), WriteBatchVsTip::SpansTip => { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: write batch spans tip", ))); } @@ -262,16 +262,16 @@ fn write_phase( let abs_jobs = super::collect_spend_abs_after_fill(&batch.batch_parents, &batch.prepared)?; ensure_ns = ensure_ns.saturating_add(t_ens.elapsed().as_nanos() as u64); if class_a_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().class_a_ns, class_a_ns); + crate::note_confirm(&query.confirm_stats().class_a_ns, class_a_ns); } if ensure_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().ensure_layout_ns, ensure_ns); + crate::note_confirm(&query.confirm_stats().ensure_layout_ns, ensure_ns); } if plan_take_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().write_plan_take_ns, plan_take_ns); + crate::note_confirm(&query.confirm_stats().write_plan_take_ns, plan_take_ns); } if create_map_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().write_create_map_ns, create_map_ns); + crate::note_confirm(&query.confirm_stats().write_create_map_ns, create_map_ns); } // Flag off: tx.head insert overlaps structural, Class C, and annotate. @@ -285,7 +285,7 @@ fn write_phase( Some(submit_queued(query, queued.take().unwrap())) }; - let overlap = (|| -> Result<_, ConsensusError> { + let overlap = (|| -> Result<_, ConfirmError> { // Local Instant totals (not atomic deltas) — sample_and_reset races mid-batch. let t_struct = Instant::now(); let (struct_ph, slots) = WRITE_REUSE.with(|reuse| { @@ -319,7 +319,7 @@ fn write_phase( .saturating_sub(cc0); let class_c_join_ns = class_c_wall_ns.saturating_sub(class_c_ns); if class_c_join_ns > 0 { - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().write_class_c_join_ns, class_c_join_ns, ); @@ -328,8 +328,8 @@ fn write_phase( let seal = std::mem::take(&mut batch.index_seal); let idx_put_ns = super::index::seal_live_indexes(query, seal)?; if idx_put_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().idx_put_ns, idx_put_ns); - rbitcoin_query::note_confirm(&query.confirm_stats().blockfilter_ns, idx_put_ns); + crate::note_confirm(&query.confirm_stats().idx_put_ns, idx_put_ns); + crate::note_confirm(&query.confirm_stats().blockfilter_ns, idx_put_ns); } let spend_ann_ns = { @@ -365,13 +365,13 @@ fn write_phase( let (drain_res, restore) = drain.join_restore(); let drain_join_ns = t_join.elapsed().as_nanos() as u64; if drain_join_ns > 0 { - rbitcoin_query::note_confirm(&query.confirm_stats().write_drain_join_ns, drain_join_ns); + crate::note_confirm(&query.confirm_stats().write_drain_join_ns, drain_join_ns); } if drain_res.is_err() { query.store().txs.head_note_pending(&restore); } let (out, n_blocks, structural_ns, struct_ph, class_c_ns, spend_ann_ns) = overlap?; - drain_res.map_err(ConsensusError::from)?; + drain_res.map_err(ConfirmError::from)?; if let Some(fk) = drain_max_fk { query.note_head_drain_fk(fk); } @@ -380,10 +380,10 @@ fn write_phase( if let Some(h) = batch.prepared.iter().map(|p| p.height.0).max() { query.prune_write_create_loc(h); } - rbitcoin_query::note_confirm(&query.confirm_stats().phase_blocks, n_blocks as u64); + crate::note_confirm(&query.confirm_stats().phase_blocks, n_blocks as u64); query .confirm_stats() - .note_last_write(rbitcoin_query::LastWritePhases { + .note_last_write(crate::LastWritePhases { n_blocks: n_blocks as u32, wall_ns: t_wall.elapsed().as_nanos() as u64, class_a_ns, @@ -406,7 +406,7 @@ pub(crate) fn finish_post_commit( query: &Query, height: u32, hash: &[u8; 32], -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { finish_post_commit_hashes(query, &[(height, *hash)]) } @@ -415,7 +415,7 @@ pub(crate) fn finish_post_commit( pub fn finish_post_commit_hashes( query: &Query, items: &[(u32, [u8; 32])], -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { let queued = query.store().txs.take_pending_queued(); if queued.is_empty() && !query.spend_index_enabled() { return Ok(()); @@ -431,11 +431,11 @@ pub fn finish_post_commit_hashes( let annotate_res = { let _gate = hold_spindle(query); spindle_mark("ann-begin"); - let res = (|| -> Result<(), ConsensusError> { + let res = (|| -> Result<(), ConfirmError> { if !query.spend_index_enabled() { return Ok(()); } - let mut slots = crate::block::AnnotateSlots::default(); + let mut slots = crate::block_pipeline::AnnotateSlots::default(); for &(height, hash) in items { annotate_slots_from_connected_hash(query, height, &hash, &mut slots)?; } @@ -464,7 +464,7 @@ pub fn finish_post_commit_hashes( query.store().txs.head_note_pending(&restore); } annotate_res?; - drain_res.map_err(ConsensusError::from)?; + drain_res.map_err(ConfirmError::from)?; if let Some(fk) = drain_max_fk { query.note_head_drain_fk(fk); } @@ -483,7 +483,7 @@ const REPLAY_STATUS_MS: u64 = 10_000; /// above genesis is rewritten before the marker is published at the surviving tip. /// Returns how many heights were rewritten. Call this on process open after /// tip-window revalidation. -pub fn replay_spend_annotations(query: &Query) -> Result { +pub fn replay_spend_annotations(query: &Query) -> Result { let Some(tip) = query.tip_height().map(|h| h.0) else { return Ok(0); }; @@ -494,17 +494,17 @@ pub fn replay_spend_annotations(query: &Query) -> Result { let annotated = query .store() .spend_annotated_through() - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; if annotated.is_some_and(|h| h > tip) { query .store() .clamp_spend_durable() - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; } let annotated = query .store() .spend_annotated_through() - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; let a = match annotated { Some(h) => h.min(tip), None => { @@ -518,16 +518,16 @@ pub fn replay_spend_annotations(query: &Query) -> Result { rewrite_spend_heights(query, a, tip) } -fn publish_spend_marker(query: &Query, tip: u32) -> Result<(), ConsensusError> { +fn publish_spend_marker(query: &Query, tip: u32) -> Result<(), ConfirmError> { let sync_ns = query .store() .sync_spend_durable(tip) - .map_err(ConsensusError::from)?; - rbitcoin_query::note_confirm(&query.confirm_stats().spend_durable_ns, sync_ns); + .map_err(ConfirmError::from)?; + crate::note_confirm(&query.confirm_stats().spend_durable_ns, sync_ns); Ok(()) } -fn rewrite_spend_heights(query: &Query, annotated: u32, tip: u32) -> Result { +fn rewrite_spend_heights(query: &Query, annotated: u32, tip: u32) -> Result { rbitcoin_log::info!("store: replay spend annotations ({annotated}, {tip}]"); let replayed = annotate_spend_heights(query, annotated + 1, tip)?; publish_spend_marker(query, tip)?; @@ -538,7 +538,7 @@ fn rewrite_spend_heights(query: &Query, annotated: u32, tip: u32) -> Result Result<(), ConsensusError> { +fn annotate_pending_spends(query: &Query) -> Result<(), ConfirmError> { let token = query.store().spend_annotate_token(); let Some(from) = query.store().spend_annotate_pending() else { return Ok(()); @@ -549,7 +549,7 @@ fn annotate_pending_spends(query: &Query) -> Result<(), ConsensusError> { ); let t = Instant::now(); annotate_spend_heights(query, from, tip)?; - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().spend_replay_ns, t.elapsed().as_nanos() as u64, ); @@ -558,7 +558,7 @@ fn annotate_pending_spends(query: &Query) -> Result<(), ConsensusError> { Ok(()) } -fn annotate_spend_heights(query: &Query, start: u32, tip: u32) -> Result { +fn annotate_spend_heights(query: &Query, start: u32, tip: u32) -> Result { let heights: Vec = (start..=tip).collect(); let replayed = heights.len() as u32; let started = std::time::Instant::now(); @@ -569,9 +569,9 @@ fn annotate_spend_heights(query: &Query, start: u32, tip: u32) -> Result Result<(), ConsensusError> { - match query.height_of_hash(hash).map_err(ConsensusError::from)? { + slots: &mut crate::block_pipeline::AnnotateSlots, +) -> Result<(), ConfirmError> { + match query.height_of_hash(hash).map_err(ConfirmError::from)? { Some(h) if h.0 == height => {} _ => return Ok(()), } - let Some((hfk, _)) = query - .get_header_by_hash(hash) - .map_err(ConsensusError::from)? - else { + let Some((hfk, _)) = query.get_header_by_hash(hash).map_err(ConfirmError::from)? else { return Ok(()); }; let Some(tx_fks) = query .store() .header_txs .get_list(hfk) - .map_err(ConsensusError::from)? + .map_err(ConfirmError::from)? else { return Ok(()); }; @@ -617,11 +614,11 @@ fn annotate_slots_from_connected_hash( let (_meta, ins, _outs) = query .store() .get_tx_full(spend_fk) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; if tx_i == 0 { let coinbase = ins.len() == 1 && ins[0].is_coinbase(); if !coinbase { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: coinbase input edge", ))); } @@ -629,19 +626,19 @@ fn annotate_slots_from_connected_hash( } for (inp_i, inp) in ins.into_iter().enumerate() { let rbitcoin_store::InputParent::Prevout { create_fk, vout } = inp.parent else { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: non-coinbase input edge", ))); }; let (off, len) = query .store() .tx_spent_range(create_fk) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; let abs = rbitcoin_store::spent_abs(off, vout); if abs.saturating_add(rbitcoin_store::OutputRecord::SPENT_SLOT_LEN as u64) > off.saturating_add(len) { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: finish_post_commit spent slot OOB", ))); } @@ -649,7 +646,7 @@ fn annotate_slots_from_connected_hash( .store() .txs .get_output_spender_meta(create_fk, vout) - .map_err(ConsensusError::from)?; + .map_err(ConfirmError::from)?; let flags = if multi { rbitcoin_store::output_flags::MULTI_SPENDER } else { @@ -669,12 +666,12 @@ fn annotate_slots_from_connected_hash( /// Write never preads `create.loc`. pub(super) fn fill_planned_create_layout_after_commit( query: &Query, - batch_parents: &mut rbitcoin_query::BatchParents, + batch_parents: &mut crate::BatchParents, planned_fks: &[rbitcoin_primitives::Fk], loc: &[rbitcoin_store::CreateLocPair], - packed: &[rbitcoin_query::CreatePin], + packed: &[crate::CreatePin], prepared: &[Prepared], -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { let mut need: U64Map> = U64Map::default(); for p in prepared { for &(_txid, vout, sfk, cfk, _vin) in &p.spends { @@ -694,7 +691,7 @@ pub(super) fn fill_planned_create_layout_after_commit( } if !planned_fks.is_empty() { if loc.len() != planned_fks.len() || packed.len() != planned_fks.len() { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: append loc length", ))); } @@ -702,7 +699,7 @@ pub(super) fn fill_planned_create_layout_after_commit( let Some(id) = fk.get() else { continue }; let Some(vouts) = need.get(&id) else { continue }; if pair.n_out != pin.n_out() as u32 { - return Err(ConsensusError::Store(StoreError::Corrupt( + return Err(ConfirmError::Store(StoreError::Corrupt( "invariant: append loc length", ))); } diff --git a/crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs b/crates/rbitcoin-query/src/confirm_run/write_idempotent_tests.rs similarity index 91% rename from crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs rename to crates/rbitcoin-query/src/confirm_run/write_idempotent_tests.rs index aafb08b416..d3dffdb263 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs +++ b/crates/rbitcoin-query/src/confirm_run/write_idempotent_tests.rs @@ -4,10 +4,10 @@ use super::{ confirm_archive_kind, write_batch_vs_tip, write_height_needed, ConfirmArchiveKind, WriteBatchVsTip, }; -use rbitcoin_query::testutil::FixtureChain; +use crate::testutil::FixtureChain; -fn tmp_query() -> (rbitcoin_query::testutil::TempDir, rbitcoin_query::Query) { - rbitcoin_query::testutil::tiny_query_labeled("write-idemp") +fn tmp_query() -> (crate::testutil::TempDir, crate::Query) { + crate::testutil::tiny_query_labeled("write-idemp") } #[test] @@ -85,10 +85,10 @@ fn script_ok_append_contiguous_and_gap() { fn batch_one(h: u32) -> ScriptOkBatch { ScriptOkBatch { prepared: vec![empty_prepared(h, h as u8)], - wire_blocks: vec![Arc::new(crate::params::genesis_block( - &crate::params::ChainParams::regtest(), + wire_blocks: vec![Arc::new(rbitcoin_consensus::genesis_block( + &rbitcoin_consensus::ChainParams::regtest(), ))], - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), archive_plan: None, index_seal: super::index::IndexSeal::default(), } @@ -111,7 +111,7 @@ fn script_ok_append_contiguous_and_gap() { .append_contiguous(ScriptOkBatch { prepared: vec![], wire_blocks: vec![], - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), archive_plan: None, index_seal: super::index::IndexSeal::default(), }) @@ -122,7 +122,7 @@ fn script_ok_append_contiguous_and_gap() { let mut empty = ScriptOkBatch { prepared: vec![], wire_blocks: vec![], - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), archive_plan: None, index_seal: super::index::IndexSeal::default(), }; @@ -142,9 +142,9 @@ fn script_ok_append_contiguous_and_gap() { // archive_plan merge: Some+Some concatenates; mixed polarity is leftover. let mut with_plan = batch_one(70); - with_plan.archive_plan = Some(rbitcoin_query::ArchiveWritePlan::empty()); + with_plan.archive_plan = Some(crate::ArchiveWritePlan::empty()); let mut next = batch_one(71); - next.archive_plan = Some(rbitcoin_query::ArchiveWritePlan::empty()); + next.archive_plan = Some(crate::ArchiveWritePlan::empty()); assert!(with_plan.append_contiguous(next).is_ok()); assert!(with_plan.archive_plan.is_some()); let mut only_other = batch_one(72); @@ -157,7 +157,7 @@ fn script_ok_append_contiguous_and_gap() { assert!(with_plan.archive_plan.is_some()); let mut no_plan = batch_one(80); let mut has = batch_one(81); - has.archive_plan = Some(rbitcoin_query::ArchiveWritePlan::empty()); + has.archive_plan = Some(crate::ArchiveWritePlan::empty()); let err = no_plan .append_contiguous(has) .expect_err("None+Some polarity"); @@ -201,11 +201,11 @@ fn three_stage_write_filter_and_scripts_surface() { let batch = LoadedBatch { prepared: Vec::new(), wire_blocks: Vec::new(), - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: ScriptPreverified::new(), archive_plan: None, index_want: super::index::IndexWant::default(), - stats: std::sync::Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: std::sync::Arc::new(crate::ConfirmStats::default()), }; assert!(batch.is_empty()); assert_eq!(batch.approx_wire_bytes(), 0); @@ -214,11 +214,11 @@ fn three_stage_write_filter_and_scripts_surface() { let mut metered = LoadedBatch { prepared: Vec::new(), wire_blocks: vec![std::sync::Arc::new(genesis)], - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: ScriptPreverified::new(), archive_plan: None, index_want: super::index::IndexWant::default(), - stats: std::sync::Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: std::sync::Arc::new(crate::ConfirmStats::default()), }; assert!(metered.approx_wire_bytes() > 0); let mut txid = [0u8; 32]; @@ -280,7 +280,7 @@ fn confirm_archive_kind_refuses_mixed() { ); let err = confirm_archive_kind(3, 2).unwrap_err(); match err { - crate::error::ConsensusError::Store(rbitcoin_store::StoreError::Corrupt(m)) => { + crate::ConfirmError::Store(rbitcoin_store::StoreError::Corrupt(m)) => { assert_eq!(m, "invariant: confirm batch mixed archived"); } other => panic!("expected mixed archived, got {other:?}"), @@ -293,11 +293,11 @@ fn empty_loaded_batch() -> super::LoadedBatch { super::LoadedBatch { prepared: Vec::new(), wire_blocks: Vec::new(), - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: super::ScriptPreverified::new(), archive_plan: None, index_want: super::index::IndexWant::default(), - stats: std::sync::Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: std::sync::Arc::new(crate::ConfirmStats::default()), } } @@ -365,7 +365,7 @@ fn drive_script_waves_ordered_without_coordinator_threads() { fn prepared_at( height: u32, hash: [u8; 32], - jobs: Vec, + jobs: Vec, check_scripts: bool, ) -> super::Prepared { use bitcoin::CompactTarget; @@ -388,21 +388,21 @@ fn prepared_at( fn loaded_at( height: u32, hash: [u8; 32], - jobs: Vec, + jobs: Vec, check_scripts: bool, ) -> super::LoadedBatch { super::LoadedBatch { prepared: vec![prepared_at(height, hash, jobs, check_scripts)], wire_blocks: Vec::new(), - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: super::ScriptPreverified::new(), archive_plan: None, index_want: super::index::IndexWant::default(), - stats: std::sync::Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: std::sync::Arc::new(crate::ConfirmStats::default()), } } -fn bad_p2pkh_job() -> crate::block::ScriptCheckJob { +fn bad_p2pkh_job() -> rbitcoin_consensus::block::ScriptCheckJob { use bitcoin::absolute::LockTime; use bitcoin::hashes::Hash; use bitcoin::script::ScriptBuf; @@ -433,11 +433,11 @@ fn bad_p2pkh_job() -> crate::block::ScriptCheckJob { }], }; let tid = tx.compute_txid().to_byte_array(); - crate::block::ScriptCheckJob::with_txid( + rbitcoin_consensus::block::ScriptCheckJob::with_txid( tid, prevouts, tx, - crate::block::ScriptVerifyFlags::buried(true, true, true, true, true), + rbitcoin_consensus::block::ScriptVerifyFlags::buried(true, true, true, true, true), ) } @@ -517,9 +517,10 @@ fn script_jobs_shrink_after_take() { #[test] fn tip_plus_one_after_trailing_null_heal_is_not_notfound() { use crate::accept_and_connect_block; - use crate::milestone::Milestone; - use crate::params::ChainParams; - use crate::regtest_pad::{mine_empty_regtest, pad_empty_from}; + use crate::pad_empty_from; + use rbitcoin_consensus::mine_empty_regtest; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::Height; let (path, q) = tmp_query(); let params = ChainParams::regtest(); @@ -548,7 +549,7 @@ fn tip_plus_one_after_trailing_null_heal_is_not_notfound() { raw[8..16].copy_from_slice(&new_logical.to_le_bytes()); std::fs::write(&conf, &raw).unwrap(); - let q = rbitcoin_query::Query::open_or_create_tiny(&path).unwrap(); + let q = crate::Query::open_or_create_tiny(&path).unwrap(); assert_eq!(q.tip_height().map(|h| h.0), Some(3)); let nxt = mine_empty_regtest(tip, tip_time + 600, 4); let r = accept_and_connect_block(&q, ¶ms, Height(4), &nxt, Milestone::NONE); @@ -570,8 +571,8 @@ fn tip_plus_one_after_trailing_null_heal_is_not_notfound() { #[test] fn expected_bits_extending_height0_and_no_retarget() { use super::expected_bits_extending; - use crate::params::ChainParams; use bitcoin::CompactTarget; + use rbitcoin_consensus::ChainParams; use rbitcoin_primitives::Height; let (path, q) = tmp_query(); let params = ChainParams::regtest(); @@ -584,7 +585,10 @@ fn expected_bits_extending_height0_and_no_retarget() { 0, ) .unwrap(); - assert_eq!(gbits, crate::params::genesis_block(¶ms).header.bits); + assert_eq!( + gbits, + rbitcoin_consensus::genesis_block(¶ms).header.bits + ); // No-pow-retargeting: any height returns prev_bits. let prev = CompactTarget::from_consensus(0x207f_ffff); let b = expected_bits_extending(&q, ¶ms, Height(2016), prev, 100, 0).unwrap(); @@ -595,11 +599,11 @@ fn expected_bits_extending_height0_and_no_retarget() { let loaded = LoadedBatch { prepared: Vec::new(), wire_blocks: Vec::new(), - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: ScriptPreverified::new(), archive_plan: None, index_want: super::index::IndexWant::default(), - stats: std::sync::Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: std::sync::Arc::new(crate::ConfirmStats::default()), }; let ok = confirm_scripts_phase(loaded).unwrap(); assert!(ok.batch.is_empty()); @@ -642,7 +646,7 @@ fn expected_bits_extending_height0_and_no_retarget() { }, txdata: vec![cb], }; - assert!(crate::block::check_bip34_coinbase(&block.txdata[0], 17).is_err()); + assert!(rbitcoin_consensus::block::check_bip34_coinbase(&block.txdata[0], 17).is_err()); let _ = std::fs::remove_dir_all(&path); } @@ -660,8 +664,8 @@ fn expected_bits_extending_height0_and_no_retarget() { #[test] fn expected_bits_extending_uses_header_plan_when_period_start_above_tip() { use super::expected_bits_extending; - use crate::params::ChainParams; use bitcoin::CompactTarget; + use rbitcoin_consensus::ChainParams; use rbitcoin_primitives::{Fk, Height}; use rbitcoin_store::HeaderRecord; let (path, q) = tmp_query(); @@ -727,12 +731,12 @@ fn expected_bits_extending_uses_header_plan_when_period_start_above_tip() { #[test] fn script_wave_skips_preverified_txids() { use super::{confirm_scripts_phase, LoadedBatch, Prepared, ScriptPreverified}; - use crate::block::ScriptCheckJob; use bitcoin::absolute::LockTime; use bitcoin::hashes::Hash; use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, CompactTarget, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; + use rbitcoin_consensus::block::ScriptCheckJob; use rbitcoin_primitives::{Fk, Height}; let prevouts = vec![TxOut { @@ -769,7 +773,7 @@ fn script_wave_skips_preverified_txids() { tid, prevouts, tx, - crate::block::ScriptVerifyFlags::buried(true, true, true, true, true), + rbitcoin_consensus::block::ScriptVerifyFlags::buried(true, true, true, true, true), ); let prepared = Prepared { height: Height(1), @@ -787,24 +791,24 @@ fn script_wave_skips_preverified_txids() { let batch = LoadedBatch { prepared: vec![prepared], wire_blocks: vec![], - batch_parents: rbitcoin_query::BatchParents::new(), + batch_parents: crate::BatchParents::new(), script_preverified: pre, archive_plan: None, index_want: super::index::IndexWant::default(), - stats: std::sync::Arc::new(rbitcoin_query::ConfirmStats::default()), + stats: std::sync::Arc::new(crate::ConfirmStats::default()), }; confirm_scripts_phase(batch).expect("preverified skip avoids bad script fail"); } -fn tiny_query() -> (rbitcoin_query::testutil::TempDir, rbitcoin_query::Query) { +fn tiny_query() -> (crate::testutil::TempDir, crate::Query) { let (path, q) = tmp_query(); q.enter_direct_index_mode().unwrap(); (path, q) } -fn fill_edges_from_packed(plan: &mut rbitcoin_query::ArchiveWritePlan) { +fn fill_edges_from_packed(plan: &mut crate::ArchiveWritePlan) { + use crate::SpendEdge; use rbitcoin_primitives::Fk; - use rbitcoin_query::SpendEdge; if !plan.edges.is_empty() { return; } @@ -854,8 +858,8 @@ fn pin_and_ensure_journey() { use super::{ ensure_spend_abs_layouts, pin_for_wire_batch, post_commit, ParentPinStamp, Prepared, }; + use crate::{ArchiveWritePlan, BatchParents}; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::{ArchiveWritePlan, BatchParents}; use rbitcoin_store::{InputRecord, OutputRecord}; let (path, q) = tiny_query(); @@ -863,10 +867,7 @@ fn pin_and_ensure_journey() { let missing_parent = Fk(999_999); let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - rec_tx(0xAA, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(rec_tx(0xAA, 1), vec![OutputRecord::unspent(1, vec![0x51])]), vec![InputRecord { prev_txid: [0xBB; 32], parent: rbitcoin_store::InputParent::Prevout { @@ -914,7 +915,7 @@ fn pin_and_ensure_journey() { "unexpected err: {msg}" ); - post_commit(&q, &crate::block::AnnotateSlots::default()) + post_commit(&q, &crate::block_pipeline::AnnotateSlots::default()) .expect("empty annotate list does not consult BatchParents"); let parent_tx = rec_tx(0x11, 1); @@ -943,16 +944,13 @@ fn pin_and_ensure_journey() { }]; let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - rec_tx(0x22, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(rec_tx(0x22, 1), vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins.clone(), )]; plan.planned_fks = vec![Fk(2)]; plan.external_parents.insert( parent_id, - rbitcoin_query::ParentIdent::with_loc(parent_tx.txid, range, (spent_off, spent_len), 1), + crate::ParentIdent::with_loc(parent_tx.txid, range, (spent_off, spent_len), 1), ); let mut stamp = ParentPinStamp::take_from_plan(&mut plan); fill_edges_from_packed(&mut plan); @@ -968,16 +966,13 @@ fn pin_and_ensure_journey() { let mut plan2 = ArchiveWritePlan::empty(); plan2.packed = vec![( - rbitcoin_query::CreatePinInner::records( - rec_tx(0x22, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(rec_tx(0x22, 1), vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins.clone(), )]; plan2.planned_fks = vec![Fk(2)]; plan2.external_parents.insert( parent_id, - rbitcoin_query::ParentIdent::with_body(parent_tx.txid, range), + crate::ParentIdent::with_body(parent_tx.txid, range), ); let mut empty_stamp = ParentPinStamp::default(); fill_edges_from_packed(&mut plan2); @@ -1052,16 +1047,13 @@ fn pin_and_ensure_journey() { let mut plan3 = ArchiveWritePlan::empty(); plan3.packed = vec![( - rbitcoin_query::CreatePinInner::records( - rec_tx(0x22, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(rec_tx(0x22, 1), vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins, )]; plan3.planned_fks = vec![Fk(2)]; plan3.external_parents.insert( parent_id, - rbitcoin_query::ParentIdent { + crate::ParentIdent { txid: parent_tx.txid, body: Some(range), spent: Some((spent_off, spent_len)), @@ -1087,14 +1079,14 @@ fn pin_and_ensure_journey() { let mut plan4 = ArchiveWritePlan::empty(); plan4.packed = vec![ ( - rbitcoin_query::CreatePinInner::records( + crate::CreatePinInner::records( rec_tx(0x32, 1), vec![OutputRecord::unspent(1, vec![0x51])], ), vec![InputRecord::coinbase(u32::MAX, vec![0x01], vec![])], ), ( - rbitcoin_query::CreatePinInner::records( + crate::CreatePinInner::records( rec_tx(0x33, 1), vec![OutputRecord::unspent(1, vec![0x51])], ), @@ -1162,19 +1154,15 @@ fn pin_and_ensure_journey() { #[test] fn fill_same_batch_abs_from_append_loc_ram() { use super::{ensure_spend_abs_layouts, fill_planned_create_layout_after_commit, Prepared}; + use crate::BatchParents; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::BatchParents; use rbitcoin_store::{InputRecord, OutputRecord}; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x32, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); - let child_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x33, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let parent_pin = + crate::CreatePinInner::records(rec_tx(0x32, 1), vec![OutputRecord::unspent(1, vec![0x51])]); + let child_pin = + crate::CreatePinInner::records(rec_tx(0x33, 1), vec![OutputRecord::unspent(1, vec![0x51])]); let parent_ins = vec![InputRecord::coinbase(u32::MAX, vec![0x01], vec![])]; let child_ins = vec![InputRecord { prev_txid: [0x32; 32], @@ -1243,10 +1231,8 @@ fn fill_same_batch_abs_from_append_loc_ram() { let mut multi_tx = rec_tx(0x44, 1); multi_tx.input_count = 2; - let multi_pin = rbitcoin_query::CreatePinInner::records( - multi_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let multi_pin = + crate::CreatePinInner::records(multi_tx, vec![OutputRecord::unspent(1, vec![0x51])]); let multi_fk = Fk(50); let multi_loc = rbitcoin_store::CreateLocPair { txout: (10, 8), @@ -1352,9 +1338,8 @@ fn fill_same_batch_abs_from_append_loc_ram() { /// Overlay slot already in Class A: structural must not meta-pread it. #[test] fn structural_same_batch_overlay_skips_meta_pread() { - use crate::block::{structural_validate_spends, RunCreateHeight}; - use crate::milestone::Milestone; - use crate::params::ChainParams; + use crate::block_pipeline::{structural_validate_spends, RunCreateHeight}; + use crate::{BatchParents, FkMap, OutPointSet}; use bitcoin::absolute::LockTime; use bitcoin::block::{Header, Version}; use bitcoin::hashes::Hash; @@ -1364,23 +1349,22 @@ fn structural_same_batch_overlay_skips_meta_pread() { Amount, Block, BlockHash, CompactTarget, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness, }; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::{BatchParents, FkMap, OutPointSet}; use rbitcoin_store::{InputRecord, OutputRecord}; use std::sync::atomic::Ordering; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( + let parent_pin = crate::CreatePinInner::records( rec_tx(0x32, 2), vec![ OutputRecord::unspent(7, vec![0x51]), OutputRecord::unspent(8, vec![0x52]), ], ); - let child_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x33, 1), - vec![OutputRecord::unspent(5, vec![0x51])], - ); + let child_pin = + crate::CreatePinInner::records(rec_tx(0x33, 1), vec![OutputRecord::unspent(5, vec![0x51])]); let parent_ins = vec![InputRecord::coinbase(u32::MAX, vec![0x01], vec![])]; let child_ins = vec![InputRecord { prev_txid: [0x32; 32], @@ -1474,11 +1458,11 @@ fn structural_same_batch_overlay_skips_meta_pread() { map.insert(fks[1], (1, false)); let run = RunCreateHeight::Map(map); let params = ChainParams::regtest(); - let ctx = crate::block::ValidationContext::at(¶ms, Height(1), Milestone::NONE); + let ctx = rbitcoin_consensus::block::ValidationContext::at(¶ms, Height(1), Milestone::NONE); let mut pending = OutPointSet::default(); - let mut mtp = rbitcoin_query::U32Map::::default(); + let mut mtp = crate::U32Map::::default(); mtp.insert(0, 1_300_000_000); - let mut scratch = crate::block::StructuralScratch::default(); + let mut scratch = crate::block_pipeline::StructuralScratch::default(); let meta0 = q.confirm_stats().spend_meta_n.load(Ordering::Relaxed); let ovl0 = q .confirm_stats() @@ -1495,7 +1479,7 @@ fn structural_same_batch_overlay_skips_meta_pread() { &bp, &mut mtp, &run, - &crate::block::ClassAWave::new(fks.clone()), + &crate::block_pipeline::ClassAWave::new(fks.clone()), &mut scratch, &abs_jobs, ) @@ -1530,9 +1514,8 @@ fn structural_same_batch_overlay_skips_meta_pread() { #[test] fn structural_scratch_second_block_does_not_replay_first_slots() { - use crate::block::{structural_validate_spends, RunCreateHeight, StructuralScratch}; - use crate::milestone::Milestone; - use crate::params::ChainParams; + use crate::block_pipeline::{structural_validate_spends, RunCreateHeight, StructuralScratch}; + use crate::{BatchParents, OutPointSet}; use bitcoin::absolute::LockTime; use bitcoin::block::{Header, Version}; use bitcoin::hashes::Hash; @@ -1542,12 +1525,13 @@ fn structural_scratch_second_block_does_not_replay_first_slots() { Amount, Block, BlockHash, CompactTarget, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness, }; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::{BatchParents, OutPointSet}; use rbitcoin_store::{InputRecord, OutputRecord}; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( + let parent_pin = crate::CreatePinInner::records( rec_tx(0x41, 2), vec![ OutputRecord::unspent(1, vec![0x51]), @@ -1624,9 +1608,9 @@ fn structural_scratch_second_block_does_not_replay_first_slots() { }; block.header.merkle_root = block.compute_merkle_root().unwrap(); let params = ChainParams::regtest(); - let ctx = crate::block::ValidationContext::at(¶ms, Height(1), Milestone::NONE); + let ctx = rbitcoin_consensus::block::ValidationContext::at(¶ms, Height(1), Milestone::NONE); let mut pending = OutPointSet::default(); - let mut mtp = rbitcoin_query::U32Map::::default(); + let mut mtp = crate::U32Map::::default(); mtp.insert(0, 1_300_000_000); let mut scratch = StructuralScratch::default(); let run = RunCreateHeight::Spans(Vec::new()); @@ -1650,7 +1634,7 @@ fn structural_scratch_second_block_does_not_replay_first_slots() { &bp, &mut mtp, &run, - &crate::block::ClassAWave::default(), + &crate::block_pipeline::ClassAWave::default(), &mut scratch, &abs_jobs, ) @@ -1675,16 +1659,14 @@ fn structural_scratch_second_block_does_not_replay_first_slots() { #[test] fn post_commit_spend_index_off_leaves_slots_unwritten() { use super::post_commit; - use crate::block::AnnotateSlots; + use crate::block_pipeline::AnnotateSlots; use rbitcoin_primitives::Fk; use rbitcoin_store::OutputRecord; use std::sync::atomic::Ordering; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x61, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let parent_pin = + crate::CreatePinInner::records(rec_tx(0x61, 1), vec![OutputRecord::unspent(1, vec![0x51])]); let (fks, _loc) = q .store() .put_tx_full_batch_from_pins( @@ -1722,15 +1704,13 @@ fn post_commit_spend_index_off_leaves_slots_unwritten() { #[test] fn fill_just_written_survives_until_last_started_write() { use super::{ensure_spend_abs_layouts, fill_planned_create_layout_after_commit, Prepared}; + use crate::BatchParents; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::BatchParents; use rbitcoin_store::OutputRecord; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x32, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let parent_pin = + crate::CreatePinInner::records(rec_tx(0x32, 1), vec![OutputRecord::unspent(1, vec![0x51])]); let (fks, loc) = q .store() @@ -1796,16 +1776,14 @@ fn fill_just_written_survives_until_last_started_write() { #[test] fn fill_stamp_spent_hole_from_write_tls() { use super::{ensure_spend_abs_layouts, fill_planned_create_layout_after_commit, Prepared}; + use crate::{stamp_external_parents, BatchParentIds, BatchParents, InFlight}; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::{stamp_external_parents, BatchParentIds, BatchParents, InFlight}; use rbitcoin_store::OutputRecord; use std::sync::atomic::Ordering; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x32, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let parent_pin = + crate::CreatePinInner::records(rec_tx(0x32, 1), vec![OutputRecord::unspent(1, vec![0x51])]); let (fks, loc) = q .store() @@ -1875,15 +1853,13 @@ fn fill_stamp_spent_hole_from_write_tls() { #[test] fn pin_and_ensure_from_pin_loc_without_tls() { use super::{ensure_spend_abs_layouts, pin_for_wire_batch, ParentPinStamp, Prepared}; + use crate::{stamp_external_parents, ArchiveWritePlan, InFlight}; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::{stamp_external_parents, ArchiveWritePlan, InFlight}; use rbitcoin_store::{InputRecord, OutputRecord}; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x41, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let parent_pin = + crate::CreatePinInner::records(rec_tx(0x41, 1), vec![OutputRecord::unspent(1, vec![0x51])]); let (fks, loc) = q .store() .put_tx_full_batch_from_pins( @@ -1908,7 +1884,7 @@ fn pin_and_ensure_from_pin_loc_without_tls() { q.store(), &[parent_pin.tx().txid], &inflight, - Some(&rbitcoin_query::BatchParentIds::default()), + Some(&crate::BatchParentIds::default()), q.confirm_stats(), ) .expect("pin loc without disk"); @@ -1928,10 +1904,7 @@ fn pin_and_ensure_from_pin_loc_without_tls() { }]; let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - rec_tx(0x42, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(rec_tx(0x42, 1), vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins, )]; plan.planned_fks = vec![Fk(2)]; @@ -1964,8 +1937,8 @@ fn pin_and_ensure_from_pin_loc_without_tls() { #[test] fn pin_creates_only_ibd_skeleton_miss_is_lookup_stage_miss() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::{ArchiveWritePlan, ParentIdent}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{ArchiveWritePlan, ParentIdent}; use rbitcoin_store::{InputRecord, OutputRecord}; let (path, q) = tiny_query(); @@ -1982,10 +1955,7 @@ fn pin_creates_only_ibd_skeleton_miss_is_lookup_stage_miss() { .unwrap(); let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - rec_tx(0x52, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(rec_tx(0x52, 1), vec![OutputRecord::unspent(1, vec![0x51])]), vec![InputRecord { prev_txid: parent_tx.txid, parent: rbitcoin_store::InputParent::Prevout { @@ -2015,8 +1985,8 @@ fn pin_creates_only_ibd_skeleton_miss_is_lookup_stage_miss() { #[test] fn pin_for_wire_incomplete_outs_is_invariant_error() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::ArchiveWritePlan; use rbitcoin_primitives::Fk; - use rbitcoin_query::ArchiveWritePlan; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; let (path, q) = tmp_query(); q.enter_direct_index_mode().unwrap(); @@ -2045,10 +2015,7 @@ fn pin_for_wire_incomplete_outs_is_invariant_error() { }]; let mut plan = ArchiveWritePlan { packed: vec![( - rbitcoin_query::CreatePinInner::records( - spend_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(spend_tx, vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins, )], planned_fks: vec![Fk(2)], @@ -2075,8 +2042,8 @@ fn pin_for_wire_incomplete_outs_is_invariant_error() { output_start_fk: Fk::NULL, output_count: 0, }; - let pin = rbitcoin_query::CreatePinInner::records(parent_tx, Vec::new()); - let mut inflight = rbitcoin_query::InFlight::new(); + let pin = crate::CreatePinInner::records(parent_tx, Vec::new()); + let mut inflight = crate::InFlight::new(); inflight.note_pins([(Fk(parent_id), &pin)], None); let mut parent_pin = ParentPinStamp::take_from_plan(&mut plan); @@ -2096,7 +2063,7 @@ fn pin_for_wire_incomplete_outs_is_invariant_error() { #[test] fn parent_pin_stamp_take_from_plan_moves_maps() { use super::ParentPinStamp; - use rbitcoin_query::{ArchiveWritePlan, ParentIdent, U64Map}; + use crate::{ArchiveWritePlan, ParentIdent, U64Map}; let mut idents = U64Map::default(); idents.insert( @@ -2139,8 +2106,8 @@ fn parent_pin_stamp_take_from_plan_moves_maps() { #[test] fn pin_takes_stamp_parent_vouts() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::ArchiveWritePlan; use rbitcoin_primitives::Fk; - use rbitcoin_query::ArchiveWritePlan; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; let (path, q) = tmp_query(); let parent_tx = TxRecord { @@ -2167,7 +2134,7 @@ fn pin_takes_stamp_parent_vouts() { let range = q.store().txs.body_range(pfk).unwrap(); let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( + crate::CreatePinInner::records( TxRecord { txid: [0x22u8; 32], version: 1, @@ -2194,7 +2161,7 @@ fn pin_takes_stamp_parent_vouts() { let spent = q.store().tx_spent_range(pfk).unwrap(); plan.external_parents.insert( parent_id, - rbitcoin_query::ParentIdent::with_loc(parent_tx.txid, range, spent, 1), + crate::ParentIdent::with_loc(parent_tx.txid, range, spent, 1), ); plan.external_parent_vouts.insert(parent_id, vec![0]); let mut stamp = ParentPinStamp::take_from_plan(&mut plan); @@ -2215,8 +2182,8 @@ fn pin_takes_stamp_parent_vouts() { #[test] fn pin_for_wire_create_pin_shares_script_bytes() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::{ArchiveWritePlan, CreatePin}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{ArchiveWritePlan, CreatePin}; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; use std::sync::Arc; @@ -2231,10 +2198,8 @@ fn pin_for_wire_create_pin_shares_script_bytes() { output_start_fk: Fk::NULL, output_count: 1, }; - let pin: CreatePin = rbitcoin_query::CreatePinInner::records( - parent_tx.clone(), - vec![OutputRecord::unspent(50, script)], - ); + let pin: CreatePin = + crate::CreatePinInner::records(parent_tx.clone(), vec![OutputRecord::unspent(50, script)]); let expect = pin.out_parts(0).expect("vout 0").1.as_ptr(); let child_tx = TxRecord { txid: [0x42u8; 32], @@ -2252,10 +2217,7 @@ fn pin_for_wire_create_pin_shares_script_bytes() { vec![InputRecord::coinbase(u32::MAX, vec![0x01], vec![])], ), ( - rbitcoin_query::CreatePinInner::records( - child_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(child_tx, vec![OutputRecord::unspent(1, vec![0x51])]), vec![InputRecord { prev_txid: parent_tx.txid, parent: rbitcoin_store::InputParent::Prevout { @@ -2300,9 +2262,9 @@ fn write_encodes_seqsigwit_from_wire() { confirm_write_phase, ScriptPreverified, }; use crate::accept_and_connect_block; - use crate::milestone::Milestone; - use crate::params::ChainParams; - use crate::regtest_pad::mine_empty_regtest; + use rbitcoin_consensus::mine_empty_regtest; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::Height; use std::sync::Arc; @@ -2345,8 +2307,8 @@ fn write_encodes_seqsigwit_from_wire() { #[test] fn pin_plan_edges_without_packed_ins() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::{ArchiveWritePlan, CreatePin, SpendEdge}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{ArchiveWritePlan, CreatePin, SpendEdge}; use rbitcoin_store::{OutputRecord, TxRecord}; use std::sync::Arc; let (path, q) = tmp_query(); @@ -2361,7 +2323,7 @@ fn pin_plan_edges_without_packed_ins() { output_start_fk: Fk::NULL, output_count: 1, }; - let pin: CreatePin = rbitcoin_query::CreatePinInner::records( + let pin: CreatePin = crate::CreatePinInner::records( parent_tx.clone(), vec![OutputRecord::unspent(50, vec![0x51])], ); @@ -2378,10 +2340,7 @@ fn pin_plan_edges_without_packed_ins() { plan.packed = vec![ (Arc::clone(&pin), vec![]), ( - rbitcoin_query::CreatePinInner::records( - child_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(child_tx, vec![OutputRecord::unspent(1, vec![0x51])]), vec![], ), ]; @@ -2413,13 +2372,13 @@ fn pin_plan_edges_without_packed_ins() { #[test] fn pin_plan_empty_edges_is_invariant() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::ArchiveWritePlan; use rbitcoin_primitives::Fk; - use rbitcoin_query::ArchiveWritePlan; use rbitcoin_store::{OutputRecord, TxRecord}; use std::sync::Arc; let (path, q) = tmp_query(); q.enter_direct_index_mode().unwrap(); - let pin = rbitcoin_query::CreatePinInner::records( + let pin = crate::CreatePinInner::records( TxRecord { txid: [0x11u8; 32], version: 1, @@ -2450,8 +2409,8 @@ fn pin_plan_empty_edges_is_invariant() { #[test] fn pin_sparse_need_high_vout_only() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::{ArchiveWritePlan, CreatePin}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{ArchiveWritePlan, CreatePin}; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; use std::sync::Arc; @@ -2501,10 +2460,8 @@ fn pin_sparse_need_high_vout_only() { script_sig: vec![], witness: vec![], }]; - let spend_pin: CreatePin = rbitcoin_query::CreatePinInner::records( - spend_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let spend_pin: CreatePin = + crate::CreatePinInner::records(spend_tx, vec![OutputRecord::unspent(1, vec![0x51])]); let mut plan = ArchiveWritePlan { packed: vec![(Arc::clone(&spend_pin), spend_ins)], planned_fks: vec![Fk(2)], @@ -2513,11 +2470,11 @@ fn pin_sparse_need_high_vout_only() { edges: Default::default(), batch_creates: vec![], external_parents: { - let mut m = rbitcoin_query::U64Map::default(); + let mut m = crate::U64Map::default(); let spent = q.store().tx_spent_range(pfk).unwrap(); m.insert( parent_id, - rbitcoin_query::ParentIdent::with_loc(parent_tx.txid, range, spent, 4), + crate::ParentIdent::with_loc(parent_tx.txid, range, spent, 4), ); m }, @@ -2549,8 +2506,8 @@ fn pin_sparse_need_high_vout_only() { #[test] fn pin_range_fill_does_not_count_as_cache_hit() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::ArchiveWritePlan; use rbitcoin_primitives::Fk; - use rbitcoin_query::ArchiveWritePlan; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; let (path, q) = tmp_query(); @@ -2605,10 +2562,7 @@ fn pin_range_fill_does_not_count_as_cache_hit() { .collect(); let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - spend_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(spend_tx, vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins, )]; plan.planned_fks = vec![Fk(100)]; @@ -2616,7 +2570,7 @@ fn pin_range_fill_does_not_count_as_cache_hit() { if let Some(id) = fks[i].get() { plan.external_parents.insert( id, - rbitcoin_query::ParentIdent::with_loc( + crate::ParentIdent::with_loc( items[i].0.txid, ranges[i], q.store().tx_spent_range(fks[i]).unwrap(), @@ -2644,8 +2598,8 @@ fn pin_range_fill_does_not_count_as_cache_hit() { #[test] fn pin_stamp_outs_is_cache_not_new() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::{ArchiveWritePlan, CreatePin}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{ArchiveWritePlan, CreatePin}; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; use std::sync::Arc; @@ -2664,7 +2618,7 @@ fn pin_stamp_outs_is_cache_not_new() { }; let parent_out = OutputRecord::unspent(50, vec![0x51, 0xaa]); let pin: CreatePin = - rbitcoin_query::CreatePinInner::records(parent_tx.clone(), vec![parent_out.clone()]); + crate::CreatePinInner::records(parent_tx.clone(), vec![parent_out.clone()]); let pfk = Fk(7); let spend_tx = TxRecord { @@ -2688,16 +2642,13 @@ fn pin_stamp_outs_is_cache_not_new() { }]; let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - spend_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(spend_tx, vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins, )]; plan.planned_fks = vec![Fk(100)]; plan.external_parents.insert( 7, - rbitcoin_query::ParentIdent { + crate::ParentIdent { txid: tid, body: Some((99, 1)), spent: None, @@ -2727,8 +2678,8 @@ fn pin_stamp_outs_is_cache_not_new() { #[test] fn pin_recent_identity_without_outs_still_range_fills() { use super::{pin_for_wire_batch, ParentPinStamp}; + use crate::ArchiveWritePlan; use rbitcoin_primitives::Fk; - use rbitcoin_query::ArchiveWritePlan; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; let (path, q) = tmp_query(); @@ -2777,22 +2728,14 @@ fn pin_recent_identity_without_outs_still_range_fills() { }]; let mut plan = ArchiveWritePlan::empty(); plan.packed = vec![( - rbitcoin_query::CreatePinInner::records( - spend_tx, - vec![OutputRecord::unspent(1, vec![0x51])], - ), + crate::CreatePinInner::records(spend_tx, vec![OutputRecord::unspent(1, vec![0x51])]), spend_ins, )]; plan.planned_fks = vec![Fk(100)]; if let Some(id) = fks[0].get() { plan.external_parents.insert( id, - rbitcoin_query::ParentIdent::with_loc( - tid, - range, - q.store().tx_spent_range(fks[0]).unwrap(), - 1, - ), + crate::ParentIdent::with_loc(tid, range, q.store().tx_spent_range(fks[0]).unwrap(), 1), ); } @@ -2817,8 +2760,6 @@ fn store_start_states_lookup_load_confirm() { confirm_scripts_phase, confirm_wire_load_from_plan, confirm_wire_lookup_stamp, confirm_write_phase, ScriptPreverified, }; - use crate::milestone::Milestone; - use crate::params::ChainParams; use crate::{accept_and_connect_block, prepare_block_for_archive}; use bitcoin::block::{Header, Version}; use bitcoin::blockdata::transaction::{ @@ -2828,6 +2769,8 @@ fn store_start_states_lookup_load_confirm() { use bitcoin::locktime::absolute::LockTime; use bitcoin::CompactTarget; use bitcoin::{Amount, Block, BlockHash, ScriptBuf, Sequence, TxMerkleNode, Witness}; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::Height; use std::sync::Arc; @@ -2838,7 +2781,7 @@ fn store_start_states_lookup_load_confirm() { let maturity = params.coinbase_maturity(); fn coinbase(height: u32) -> Transaction { - let mut ss = crate::bip34_height_script(height); + let mut ss = rbitcoin_consensus::bip34_height_script(height); ss.push(bitcoin::opcodes::all::OP_CHECKSIG.to_u8()); let script = ScriptBuf::from_bytes(ss); Transaction { @@ -3012,7 +2955,7 @@ fn store_start_states_lookup_load_confirm() { (Height(h_need), Arc::new(b_need.clone()), None), ]; match confirm_wire_lookup_stamp(&q, ¶ms, ms, &mixed_arcs, None) { - Err(crate::error::ConsensusError::Store(rbitcoin_store::StoreError::Corrupt(m))) => { + Err(crate::ConfirmError::Store(rbitcoin_store::StoreError::Corrupt(m))) => { assert_eq!(m, "invariant: confirm batch mixed archived"); } Ok(_) => panic!("mixed lookup stamp must fail closed"), @@ -3020,7 +2963,7 @@ fn store_start_states_lookup_load_confirm() { } let mixed_run = [(Height(h_have), b_have), (Height(h_need), b_need)]; match super::confirm_wire_load_phase(&q, ¶ms, ms, &mixed_run, &ScriptPreverified::new()) { - Err(crate::error::ConsensusError::Store(rbitcoin_store::StoreError::Corrupt(m))) => { + Err(crate::ConfirmError::Store(rbitcoin_store::StoreError::Corrupt(m))) => { assert_eq!(m, "invariant: confirm batch mixed archived"); } Ok(_) => panic!("mixed one-shot load must fail closed"), @@ -3033,16 +2976,17 @@ fn store_start_states_lookup_load_confirm() { #[test] fn wire_lookup_empty_and_noncontiguous() { use super::confirm_wire_lookup_stamp; - use crate::error::ConsensusError; - use crate::milestone::Milestone; - use crate::params::{genesis_block, ChainParams}; + use crate::ConfirmError; + use rbitcoin_consensus::ConsensusError; + use rbitcoin_consensus::Milestone; + use rbitcoin_consensus::{genesis_block, ChainParams}; use rbitcoin_primitives::Height; use std::sync::Arc; let (path, q) = tmp_query(); let params = ChainParams::regtest(); match confirm_wire_lookup_stamp(&q, ¶ms, Milestone::NONE, &[], None) { - Err(ConsensusError::BadBlock("empty confirm batch")) => {} + Err(ConfirmError::Rules(ConsensusError::BadBlock("empty confirm batch"))) => {} Ok(_) => panic!("empty lookup must refuse"), Err(e) => panic!("empty lookup must refuse, got {e}"), } @@ -3050,7 +2994,7 @@ fn wire_lookup_empty_and_noncontiguous() { let a = (Height(0), Arc::new(g.clone()), None); let b = (Height(2), Arc::new(g), None); match confirm_wire_lookup_stamp(&q, ¶ms, Milestone::NONE, &[a, b], None) { - Err(ConsensusError::BadBlock("confirm run not contiguous")) => {} + Err(ConfirmError::Rules(ConsensusError::BadBlock("confirm run not contiguous"))) => {} Ok(_) => panic!("gap lookup must refuse"), Err(e) => panic!("gap lookup must refuse, got {e}"), } @@ -3060,9 +3004,10 @@ fn wire_lookup_empty_and_noncontiguous() { /// Direct write skips SH FkMap; Class A idx holds the body range. #[test] fn direct_write_skips_create_pin_map_idx_without_recent() { - use crate::regtest_pad::mine_empty_regtest; - use crate::{accept_and_connect_block, ChainParams, Milestone}; + use crate::accept_and_connect_block; use bitcoin::hashes::Hash; + use rbitcoin_consensus::mine_empty_regtest; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; let (path, q) = tmp_query(); q.enter_direct_index_mode().unwrap(); @@ -3090,12 +3035,13 @@ fn one_shot_load_matches_stamp_then_load_from_plan() { confirm_wire_load_from_plan, confirm_wire_load_phase, confirm_wire_lookup_stamp, ScriptPreverified, }; - use crate::regtest_pad::mine_empty_regtest; - use crate::{accept_and_connect_block, ChainParams, Milestone}; + use crate::accept_and_connect_block; + use rbitcoin_consensus::mine_empty_regtest; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; use std::sync::Arc; - fn open_q(_tag: &str) -> (rbitcoin_query::testutil::TempDir, rbitcoin_query::Query) { + fn open_q(_tag: &str) -> (crate::testutil::TempDir, crate::Query) { tmp_query() } @@ -3185,8 +3131,8 @@ fn one_shot_load_matches_stamp_then_load_from_plan() { #[test] fn collect_spend_abs_after_fill_is_one_walk() { use super::{collect_spend_abs_after_fill, Prepared}; + use crate::BatchParents; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::BatchParents; use rbitcoin_store::OutputRecord; let mut bp = BatchParents::new(); @@ -3263,8 +3209,7 @@ fn collect_spend_abs_after_fill_is_one_walk() { fn structural_run_clears_reused_scratch_between_batches() { use super::phases::{structural_run, StructuralReuse}; use super::{collect_spend_abs_after_fill, Prepared}; - use crate::milestone::Milestone; - use crate::params::ChainParams; + use crate::BatchParents; use bitcoin::absolute::LockTime; use bitcoin::block::{Header, Version}; use bitcoin::hashes::Hash; @@ -3274,16 +3219,15 @@ fn structural_run_clears_reused_scratch_between_batches() { Amount, Block, BlockHash, CompactTarget, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness, }; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::{Fk, Height}; - use rbitcoin_query::BatchParents; use rbitcoin_store::{InputRecord, OutputRecord}; use std::sync::Arc; let (path, q) = tiny_query(); - let parent_pin = rbitcoin_query::CreatePinInner::records( - rec_tx(0x41, 1), - vec![OutputRecord::unspent(1, vec![0x51])], - ); + let parent_pin = + crate::CreatePinInner::records(rec_tx(0x41, 1), vec![OutputRecord::unspent(1, vec![0x51])]); let (fks, loc) = q .store() .put_tx_full_batch_from_pins( @@ -3388,7 +3332,7 @@ fn structural_run_clears_reused_scratch_between_batches() { &wire, &bp, &jobs, - &crate::block::ClassAWave::default(), + &crate::block_pipeline::ClassAWave::default(), reuse, ) .expect("reused scratch still connects") @@ -3454,10 +3398,10 @@ fn datadir_hdd_submits_head_after_annotate() { confirm_write_phase, finish_post_commit_hashes, ScriptPreverified, }; use crate::accept_and_connect_block; - use crate::milestone::Milestone; - use crate::params::ChainParams; - use crate::regtest_pad::mine_empty_regtest; use bitcoin::hashes::Hash; + use rbitcoin_consensus::mine_empty_regtest; + use rbitcoin_consensus::ChainParams; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::{Fk, Height}; use std::sync::Arc; diff --git a/crates/rbitcoin-query/src/connect_facade.rs b/crates/rbitcoin-query/src/connect_facade.rs new file mode 100644 index 0000000000..6d28e0b2d8 --- /dev/null +++ b/crates/rbitcoin-query/src/connect_facade.rs @@ -0,0 +1,345 @@ +//! Connect façade. Query loads and writes; consensus decides the block. + +use crate::ConfirmError; +use bitcoin::hashes::Hash; +use bitcoin::Block; +use rbitcoin_consensus::block::{validate_block_structure_hashed, ValidationContext}; +use rbitcoin_consensus::{ChainParams, Milestone}; +use rbitcoin_primitives::{Fk, Height}; +use rbitcoin_store::HeaderRecord; +use std::sync::Arc; + +use crate::confirm_run; +use crate::convert::{block_to_apply, block_to_apply_with_txids_prev, header_to_record}; +use crate::{Query, TxApply, TxPrecompute, WirePlanNeed}; + +/// Confirm a contiguous tip-extension run of wire blocks (sync all stages). +/// +/// See [`confirm_wire_run`]: lookup → load → scripts → write. IBD uses the split +/// phases for pipeline overlap. +pub use crate::confirm_run::{ + confirm_bq_resolve_wave_capped, confirm_scripts_phase, confirm_wire_load_from_plan, + confirm_wire_load_phase, confirm_wire_load_phase_pipelined, confirm_wire_lookup_stamp, + confirm_wire_run, confirm_wire_run_preverified, confirm_write_phase, + confirm_write_phase_or_return, drive_script_waves_with, finish_post_commit_hashes, + parent_missing_from_chain, replay_spend_annotations, take_wave_items_for_load, BqResolveWave, + ConfirmLoadOutcome, ConfirmScriptOutcome, LoadedBatch, PlanStampOutcome, ScriptOkBatch, + ScriptPreverified, WireBlockIn, WireLoadPipeline, BQ_RESOLVE_WAVE_MAX_BLOCKS, + BQ_RESOLVE_WAVE_MAX_INPUTS, +}; + +/// Wake the IBD scripts publisher (`ibd-confirm`) after `scriptq` send or close. +pub use rbitcoin_consensus::unpark_script_publisher; + +/// Accept + archive + confirm in one step (genesis / tip extension / tests). +/// +/// **Same path as IBD confirm:** structure + header checks, then Class A +/// archive, then [`confirm_wire_run`] (lookup → load pin denserels → scripts → +/// structural → Class C → abs spend annotate). No separate +/// `put_spend_batch_by_create`. +/// +/// Idempotent when `height` is already confirmed for this block hash. +/// Full script verify (no mempool skip) — use +/// [`accept_and_connect_block_preverified`] on tip follow with a live mempool. +pub fn accept_and_connect_block( + query: &Query, + params: &ChainParams, + height: Height, + block: &Block, + milestone: Milestone, +) -> Result { + accept_and_connect_block_preverified( + query, + params, + height, + Arc::new(block.clone()), + milestone, + &ScriptPreverified::new(), + None, + ) +} + +/// Like [`accept_and_connect_block`], skipping script verify for `preverified` +/// txids (tip follow: live mempool after accept). Reorg disconnect stays outside. +pub fn accept_and_connect_block_preverified( + query: &Query, + params: &ChainParams, + height: Height, + block: Arc, + milestone: Milestone, + preverified: &ScriptPreverified, + pres: Option>, +) -> Result { + let hash = block.block_hash().to_byte_array(); + if let Some(h) = query.height_of_hash(&hash).map_err(ConfirmError::from)? { + if h == height { + if let Some((fk, _)) = query + .get_header_by_hash(&hash) + .map_err(ConfirmError::from)? + { + confirm_run::finish_post_commit(query, height.0, &hash)?; + return Ok(fk); + } + } + } + + // Unified height-ordered path: wire → lookup → load (pin+assemble) → scripts → + // write (Class A + structural + Class C + annotate). No archive-then-reload. + let fks = confirm_wire_run_preverified( + query, + params, + milestone, + &[(height, block, pres)], + preverified, + )?; + if let Some(fk) = fks.into_iter().next() { + return Ok(fk); + } + // Write skipped heights ≤ tip (idempotent race). A body we just ran + // through lookup/load must have a header — missing is an invariant, not + // a soft NotFound (inflated confirmed[] used to hit this on tip+1). + query + .get_header_by_hash(&hash) + .map_err(ConfirmError::from)? + .map(|(fk, _)| fk) + .ok_or(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "invariant: confirm write skipped but header missing", + ))) +} + +fn class_a_header_and_txids( + query: &Query, + params: &ChainParams, + block: &Block, +) -> Result<(HeaderRecord, Vec<[u8; 32]>), ConfirmError> { + let ctx = ValidationContext::archive_structure(params); + let txids = validate_block_structure_hashed(block, &ctx)?; + let hash = block.header.block_hash().to_byte_array(); + rbitcoin_consensus::header::pow_hash_meets_target(hash, block.header.bits, params.pow_limit)?; + let prev = block.header.prev_blockhash; + let prev_fk = if prev.to_byte_array() == [0u8; 32] { + Fk::NULL + } else { + rbitcoin_consensus::block::require_linked_parent( + query + .get_header_by_hash(prev.as_byte_array()) + .map_err(ConfirmError::from)? + .map(|(fk, _)| fk), + )? + }; + Ok((header_to_record(prev_fk, &block.header, hash), txids)) +} + +/// Class A only (no tip / Class C). Crash and `plan=None` tests. +/// +/// Not a production IBD API — confirm write uses `archive_plan_batch_from_wire` +/// + commit (wire `seqsigwit` is encoded from the block and plan edges). +pub fn commit_class_a_block( + query: &Query, + params: &ChainParams, + height: Height, + block: &Block, + milestone: Milestone, +) -> Result<(), ConfirmError> { + let _ = (height, milestone); + let (header, txids) = class_a_header_and_txids(query, params, block)?; + let fk = query.ensure_header(&header).map_err(ConfirmError::from)?; + let arc = Arc::new(block.clone()); + query + .archive_class_a_from_wire(&[(fk, &arc, txids.as_slice())]) + .map_err(ConfirmError::from)?; + Ok(()) +} + +/// Class A for a contiguous run in one plan (same-batch parent stamp). +/// +/// Use this instead of N×[`commit_class_a_block`] when later blocks spend +/// earlier unconfirmed creates. +pub fn commit_class_a_run( + query: &Query, + params: &ChainParams, + blocks: &[(Height, Block)], + milestone: Milestone, +) -> Result<(), ConfirmError> { + let _ = milestone; + let mut owned = Vec::with_capacity(blocks.len()); + for (_, block) in blocks { + let (header, txids) = class_a_header_and_txids(query, params, block)?; + let fk = query.ensure_header(&header).map_err(ConfirmError::from)?; + owned.push((fk, Arc::new(block.clone()), txids)); + } + let refs: Vec> = owned + .iter() + .map(|(fk, b, ids)| (*fk, b, ids.as_slice())) + .collect(); + query + .archive_class_a_from_wire(&refs) + .map_err(ConfirmError::from)?; + Ok(()) +} + +/// CPU-side prep for Class A archive. +pub fn prepare_block_for_archive( + query: &Query, + params: &ChainParams, + block: &Block, +) -> Result<(HeaderRecord, Vec), ConfirmError> { + let hash = block.block_hash().to_byte_array(); + if query.is_block_archived(&hash).map_err(ConfirmError::from)? { + // Standalone archive helper (not confirm pipeline): one hash pass here. + return block_to_apply(query, &block.header, &block.txdata); + } + prepare_block_for_archive_new(query, params, block) +} + +fn prepare_block_for_archive_new( + query: &Query, + params: &ChainParams, + block: &Block, +) -> Result<(HeaderRecord, Vec), ConfirmError> { + let (header, txids) = class_a_header_and_txids(query, params, block)?; + let (_, txs) = + block_to_apply_with_txids_prev(header.prev_fk, &block.header, &block.txdata, &txids)?; + Ok((header, txs)) +} + +#[cfg(test)] +mod coverage_tests { + use super::*; + use crate::Query; + use bitcoin::absolute::LockTime; + use bitcoin::block::Version; + use bitcoin::hashes::Hash; + use bitcoin::script::ScriptBuf; + use bitcoin::transaction::Version as TxVersion; + use bitcoin::{Amount, OutPoint, Sequence, Target, Transaction, TxIn, TxOut, Witness}; + use rbitcoin_consensus::block::ScriptCheckJob; + use rbitcoin_consensus::{ + genesis_block, mine_empty_regtest, ChainParams, ConsensusError, Milestone, + }; + use rbitcoin_primitives::Height; + use std::path::PathBuf; + fn temp_store() -> (PathBuf, Query) { + let path = std::env::temp_dir().join(format!( + "rbitcoin-consensus-cov-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + )); + std::fs::create_dir_all(&path).unwrap(); + let q = Query::open_or_create_tiny(&path).expect("open store"); + (path, q) + } + + #[test] + fn verify_tx_scripts_detached_acs_job() { + let tx = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: bitcoin::Txid::from_byte_array([1; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + }; + let prevouts = vec![TxOut { + value: Amount::from_sat(10), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }]; + rbitcoin_consensus::verify_tx_scripts_detached(prevouts.clone(), tx.clone()).unwrap(); + let job = ScriptCheckJob::new( + prevouts, + tx, + rbitcoin_consensus::block::ScriptVerifyFlags::buried(true, true, true, true, true), + ); + rbitcoin_consensus::block::verify_one_script_job(&job).unwrap(); + } + + #[test] + fn verify_tx_scripts_detached_forks_op_true_and_op_return() { + let spend = |spk: Vec| { + let tx = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: bitcoin::Txid::from_byte_array([1; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + }; + let prevouts = vec![TxOut { + value: Amount::from_sat(10), + script_pubkey: ScriptBuf::from_bytes(spk), + }]; + rbitcoin_consensus::verify_tx_scripts_detached_forks( + prevouts, tx, true, true, true, true, true, + ) + }; + spend(vec![0x51]).unwrap(); + assert!(spend(vec![0x6a]).is_err()); + } + + #[test] + fn regtest_connect_archive_and_confirm_path() { + let (path, q) = temp_store(); + let params = ChainParams::regtest(); + let ms = Milestone::height(1_000_000); + let genesis = genesis_block(¶ms); + let gen_fk = accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, ms).unwrap(); + + let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); + // prepare helpers stay (CPU-side); confirm is sole Class A. + let (_hr, _txs) = prepare_block_for_archive(&q, ¶ms, &b1).unwrap(); + accept_and_connect_block(&q, ¶ms, Height(1), &b1, ms).unwrap(); + // already-have prepare after connect keeps the parent fk + let (hr, _) = prepare_block_for_archive(&q, ¶ms, &b1).unwrap(); + assert_eq!(hr.prev_fk, gen_fk); + + let _ = std::fs::remove_dir_all(&path); + } + + #[test] + fn assemble_second_block_rejects_stale_nversion() { + let (path, q) = temp_store(); + let params = ChainParams::regtest(); + let ms = Milestone::height(1_000_000); + let genesis = genesis_block(¶ms); + accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, ms).unwrap(); + + let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); + let mut b2 = mine_empty_regtest(b1.block_hash(), b1.header.time + 600, 2); + b2.header.version = Version::from_consensus(1); + let target = Target::from_compact(b2.header.bits); + for nonce in 0..u32::MAX { + b2.header.nonce = nonce; + if b2.header.validate_pow(target).is_ok() { + break; + } + } + let err = + confirm_wire_run(&q, ¶ms, ms, &[(Height(1), b1), (Height(2), b2)]).unwrap_err(); + assert!( + matches!(err, ConfirmError::Rules(ConsensusError::BadVersion(1))), + "{err:?}" + ); + let _ = std::fs::remove_dir_all(&path); + } +} diff --git a/crates/rbitcoin-consensus/src/convert.rs b/crates/rbitcoin-query/src/convert.rs similarity index 88% rename from crates/rbitcoin-consensus/src/convert.rs rename to crates/rbitcoin-query/src/convert.rs index dde49b3e7e..f19cf16e67 100644 --- a/crates/rbitcoin-consensus/src/convert.rs +++ b/crates/rbitcoin-query/src/convert.rs @@ -1,9 +1,9 @@ -use crate::error::ConsensusError; +use crate::ConfirmError; +use crate::{Query, TxApply}; use bitcoin::block::Header; use bitcoin::hashes::Hash; use bitcoin::Transaction; use rbitcoin_primitives::Fk; -use rbitcoin_query::{Query, TxApply}; use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, TxRecord}; /// Header row from wire fields. `hash` is the caller-computed `header.block_hash()`. @@ -25,7 +25,7 @@ pub fn block_to_apply( query: &Query, header: &Header, txs: &[Transaction], -) -> Result<(HeaderRecord, Vec), ConsensusError> { +) -> Result<(HeaderRecord, Vec), ConfirmError> { let txids: Vec<[u8; 32]> = txs .iter() .map(|t| t.compute_txid().to_byte_array()) @@ -42,17 +42,16 @@ pub fn block_to_apply_with_txids( header: &Header, txs: &[Transaction], txids: &[[u8; 32]], -) -> Result<(HeaderRecord, Vec), ConsensusError> { - if txs.len() != txids.len() { - return Err(ConsensusError::BadBlock("txid count mismatch")); - } +) -> Result<(HeaderRecord, Vec), ConfirmError> { + rbitcoin_consensus::block::reject_txid_count(txs.len(), txids.len())?; let prev_fk = if header.prev_blockhash.to_byte_array() == [0u8; 32] { Fk::NULL } else { - query - .get_header_by_hash(header.prev_blockhash.as_byte_array())? - .map(|(fk, _)| fk) - .ok_or(ConsensusError::BadPrev)? + rbitcoin_consensus::block::require_linked_parent( + query + .get_header_by_hash(header.prev_blockhash.as_byte_array())? + .map(|(fk, _)| fk), + )? }; block_to_apply_with_txids_prev(prev_fk, header, txs, txids) } @@ -64,10 +63,8 @@ pub fn block_to_apply_with_txids_prev( header: &Header, txs: &[Transaction], txids: &[[u8; 32]], -) -> Result<(HeaderRecord, Vec), ConsensusError> { - if txs.len() != txids.len() { - return Err(ConsensusError::BadBlock("txid count mismatch")); - } +) -> Result<(HeaderRecord, Vec), ConfirmError> { + rbitcoin_consensus::block::reject_txid_count(txs.len(), txids.len())?; let hash = header.block_hash().to_byte_array(); let header_rec = header_to_record(prev_fk, header, hash); let mut out = Vec::with_capacity(txs.len()); @@ -77,7 +74,7 @@ pub fn block_to_apply_with_txids_prev( Ok((header_rec, out)) } -fn tx_to_apply(tx: &Transaction, txid: [u8; 32]) -> Result { +fn tx_to_apply(tx: &Transaction, txid: [u8; 32]) -> Result { let inputs: Vec = tx .input .iter() @@ -130,6 +127,7 @@ mod tests { Amount, BlockHash, CompactTarget, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Witness, }; + use rbitcoin_consensus::ConsensusError; #[test] fn header_to_record_stores_caller_hash() { @@ -173,14 +171,14 @@ mod tests { assert_eq!(apply.tx.txid, want); assert_eq!(apply.inputs.len(), 1); assert_eq!(apply.outputs[0].value, 50_0000_0000); - let edges = [rbitcoin_query::SpendEdge { + let edges = [crate::SpendEdge { prev_txid: [0u8; 32], vout: u32::MAX, spend_fk: Fk(9), create_fk: Fk::NULL, vin: 0, }]; - let ins = rbitcoin_query::input_records_from_wire(&tx, Fk(9), &edges).unwrap(); + let ins = crate::input_records_from_wire(&tx, Fk(9), &edges).unwrap(); assert_eq!(ins, apply.inputs); } @@ -215,11 +213,11 @@ mod tests { // Mismatch paths. assert!(matches!( block_to_apply_with_txids_prev(Fk::NULL, &header, std::slice::from_ref(&tx), &[]), - Err(ConsensusError::BadBlock(_)) + Err(ConfirmError::Rules(ConsensusError::BadBlock(_))) )); assert!(matches!( block_to_apply_with_txids_prev(Fk::NULL, &header, &[], &[[0u8; 32]]), - Err(ConsensusError::BadBlock(_)) + Err(ConfirmError::Rules(ConsensusError::BadBlock(_))) )); let (rec, apps) = block_to_apply_with_txids_prev(Fk::NULL, &header, &[tx], &[txid]).unwrap(); @@ -268,19 +266,18 @@ mod tests { script_pubkey: ScriptBuf::from_bytes(vec![0x51]), }], }; - let edge = |prev_txid: [u8; 32], vout: u32| rbitcoin_query::SpendEdge { + let edge = |prev_txid: [u8; 32], vout: u32| crate::SpendEdge { prev_txid, vout, spend_fk: Fk(9), create_fk: Fk(5), vin: 0, }; - let ok = - rbitcoin_query::input_records_from_wire(&tx, Fk(9), &[edge([1u8; 32], 3)]).unwrap(); + let ok = crate::input_records_from_wire(&tx, Fk(9), &[edge([1u8; 32], 3)]).unwrap(); assert_eq!(ok[0].prev_txid, [1u8; 32]); assert_eq!(ok[0].prev_index(), 3); for bad in [edge([2u8; 32], 3), edge([1u8; 32], 4)] { - let err = rbitcoin_query::input_records_from_wire(&tx, Fk(9), &[bad]) + let err = crate::input_records_from_wire(&tx, Fk(9), &[bad]) .expect_err("mismatched edge must not encode"); assert!( format!("{err}").contains("edge/wire prevout"), diff --git a/crates/rbitcoin-query/src/header_read.rs b/crates/rbitcoin-query/src/header_read.rs new file mode 100644 index 0000000000..a81c381ba2 --- /dev/null +++ b/crates/rbitcoin-query/src/header_read.rs @@ -0,0 +1,192 @@ +//! Header reads. Decisions are [`rbitcoin_consensus::check_header`]. + +use crate::ConfirmError; +use bitcoin::block::Header; +use bitcoin::hashes::Hash; +use bitcoin::CompactTarget; +use rbitcoin_consensus::header::{check_header, next_work_bits, ParentHeader}; +use rbitcoin_consensus::ChainParams; +use rbitcoin_primitives::Height; + +use crate::Query; + +pub fn validate_header( + query: &Query, + params: &ChainParams, + height: Height, + header: &Header, +) -> Result<(), ConfirmError> { + validate_header_hashed( + query, + params, + height, + header, + header.block_hash().to_byte_array(), + ) +} + +/// [`validate_header`] using a caller-computed header hash (lookup already hashed). +pub fn validate_header_hashed( + query: &Query, + params: &ChainParams, + height: Height, + header: &Header, + hash: [u8; 32], +) -> Result<(), ConfirmError> { + let parent = if height.0 == 0 { + None + } else { + query + .header_at_height(Height(height.0 - 1))? + .map(|(_fk, prev)| ParentHeader { + hash: prev.hash, + time: prev.timestamp, + bits: CompactTarget::from_consensus(prev.bits), + }) + }; + let times = if parent.is_some() { + mtp_times_ending_at(query, Height(height.0 - 1))? + } else { + Vec::new() + }; + let period_first = if parent.is_some() { + period_first_time(query, params, height.0)? + } else { + None + }; + check_header( + params, + height, + header, + hash, + parent, + ×, + period_first, + |h| { + header_bits_at(query, Height(h)) + .ok() + .map(CompactTarget::from_consensus) + }, + ) + .map(|_| ()) + .map_err(ConfirmError::from) +} + +/// Contextual checks when the parent is a known header, not necessarily on the +/// best chain (`header_at_height` would miss). +pub fn median_time_past(query: &Query, height: Height) -> Result { + let times = mtp_times_ending_at(query, height)?; + rbitcoin_consensus::block::median_or_bad_header(×).map_err(ConfirmError::from) +} + +/// Timestamps ending at `height` for [`median_time_past`]. A missing plan above +/// the confirmed tip stays a retryable load fault. A gap at or below the tip +/// is [`ConsensusError::BadPrev`]. +pub fn mtp_times_ending_at(query: &Query, height: Height) -> Result, ConfirmError> { + if let Some((n, buf)) = query.store().mtp_times_at(height) { + return Ok(buf[..n as usize].to_vec()); + } + let mut times = Vec::with_capacity(11); + let start = height.0.saturating_sub(10); + let tip = query.tip_height().map(|h| h.0).unwrap_or(0); + for h in start..=height.0 { + if let Some((_fk, rec)) = query.header_at_height(Height(h))? { + times.push(rec.timestamp); + continue; + } + if let Some(plan) = query.confirm_parent_cache().get_header_plan(h) { + times.push(plan.header_rec.timestamp); + continue; + } + rbitcoin_consensus::block::header_gap_on_chain(h, tip)?; + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "confirm: load incomplete (parent header plan missing above tip)", + ))); + } + Ok(times) +} + +/// MTP from the confirmed chain only (write structural). Tip-ahead heights +/// must be carried on [`crate::confirm_run`] `Prepared::prev_mtp`. +pub fn median_time_past_store(query: &Query, height: Height) -> Result { + if let Some((n, buf)) = query.store().mtp_times_at(height) { + return rbitcoin_consensus::block::median_or_bad_header(&buf[..n as usize]) + .map_err(ConfirmError::from); + } + let mut times = Vec::with_capacity(11); + let start = height.0.saturating_sub(10); + for h in start..=height.0 { + if let Some((_fk, rec)) = query.header_at_height(Height(h))? { + times.push(rec.timestamp); + continue; + } + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( + "confirm: write MTP missing confirmed header (carry prev_mtp)", + ))); + } + rbitcoin_consensus::block::median_or_bad_header(×).map_err(ConfirmError::from) +} + +/// Expected `nBits` for a new header at `height`. +/// +/// `header_time` is the candidate block's timestamp (Core `pblock->GetBlockTime()`). +pub fn expected_next_bits( + query: &Query, + params: &ChainParams, + height: Height, + header_time: u32, +) -> Result { + if height.0 == 0 { + let g = rbitcoin_consensus::genesis_block(params); + return Ok(g.header.bits); + } + + let prev_height = Height(height.0 - 1); + let (_fk, prev_rec) = + rbitcoin_consensus::block::require_linked_parent(query.header_at_height(prev_height)?)?; + let prev_bits = CompactTarget::from_consensus(prev_rec.bits); + let period_first = period_first_time(query, params, height.0)?; + rbitcoin_consensus::block::require_next_work(next_work_bits( + params, + height.0, + prev_bits, + prev_rec.timestamp, + header_time, + period_first, + |h| { + header_bits_at(query, Height(h)) + .ok() + .map(CompactTarget::from_consensus) + }, + )) + .map_err(ConfirmError::from) +} + +/// Difficulty for the header at `height`, from the parent and the period start. +/// +/// `period_first_time` is the timestamp of the header at `height - interval` +/// when `height` is a retarget boundary. `bits_at` supplies earlier `nBits` +/// for the testnet min-difficulty walk. `None` means an ancestor is missing. +pub fn period_first_time( + query: &Query, + params: &ChainParams, + height: u32, +) -> Result, ConfirmError> { + let interval = params.difficulty_adjustment_interval(); + if interval == 0 || !height.is_multiple_of(interval) || params.no_pow_retargeting() { + return Ok(None); + } + Ok(query + .header_at_height(Height(height - interval))? + .map(|(_fk, first_rec)| first_rec.timestamp)) +} + +pub fn header_bits_at(query: &Query, height: Height) -> Result { + if let Some((_fk, rec)) = query.header_at_height(height)? { + return Ok(rec.bits); + } + if let Some(plan) = query.confirm_parent_cache().get_header_plan(height.0) { + return Ok(plan.header_rec.bits); + } + rbitcoin_consensus::block::require_linked_parent(None).map_err(ConfirmError::from) +} diff --git a/crates/rbitcoin-query/src/header_store_tests.rs b/crates/rbitcoin-query/src/header_store_tests.rs new file mode 100644 index 0000000000..67bd89d0d6 --- /dev/null +++ b/crates/rbitcoin-query/src/header_store_tests.rs @@ -0,0 +1,241 @@ +//! Store-backed header reads. Rule rejects stay in `rbitcoin_consensus::check_header`. +#![cfg(test)] + +use crate::testutil::FixtureChain; +use bitcoin::hashes::Hash; +use bitcoin::CompactTarget; +use rbitcoin_consensus::{genesis_block, ChainParams, ConsensusError}; +use rbitcoin_primitives::{median_time_past_times, Fk, Height}; +use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, TxRecord}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::{ + expected_next_bits, median_time_past, median_time_past_store, validate_header, ConfirmError, + Query, TxApply, +}; + +fn temp_q() -> (std::path::PathBuf, Query) { + let n = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("rbitcoin-hdr-mtp-{n}")); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let q = Query::open_or_create_tiny(dir.join("store")).unwrap(); + (dir, q) +} + +/// Write-gate-safe synthetic coinbase: non-null `prev_fk` must commit `parent_hash`. +fn coinbase(h: u32, prev: Fk, parent_hash: Option<[u8; 32]>) -> (HeaderRecord, TxApply) { + let version = 1; + let timestamp = 1_000 + h * 10; + let bits = 0x207fffff; + let nonce = h; + let mut merkle = [0u8; 32]; + merkle[0..4].copy_from_slice(&h.to_le_bytes()); + merkle[4] = 0xcd; + let hash = match parent_hash { + None => merkle, + Some(ph) => { + rbitcoin_store::block_header_hash(version, &ph, &merkle, timestamp, bits, nonce) + } + }; + let header = HeaderRecord { + prev_fk: prev, + version, + timestamp, + bits, + nonce, + merkle_root: merkle, + hash, + size: 0, + weight: 0, + }; + let mut txid = [0u8; 32]; + txid[0..4].copy_from_slice(&h.to_le_bytes()); + txid[31] = 0xcb; + let ta = TxApply { + tx: TxRecord { + txid, + version: 1, + locktime: 0, + input_start_fk: Fk::NULL, + input_count: 1, + output_start_fk: Fk::NULL, + output_count: 1, + }, + inputs: vec![InputRecord { + prev_txid: [0u8; 32], + parent: rbitcoin_store::InputParent::Coinbase, + sequence: u32::MAX, + script_sig: vec![h as u8], + witness: vec![], + }], + outputs: vec![OutputRecord::unspent(50, vec![0x51])], + }; + (header, ta) +} + +#[test] +fn mtp_from_confirmed_chain_and_missing_above_tip() { + let (dir, q) = temp_q(); + let mut prev = Fk::NULL; + let mut parent_hash: Option<[u8; 32]> = None; + for h in 0..3u32 { + let (hdr, ta) = coinbase(h, prev, parent_hash); + parent_hash = Some(hdr.hash); + prev = q.connect_block(Height(h), &hdr, &[ta]).unwrap(); + } + let mtp = median_time_past(&q, Height(2)).unwrap(); + // times: 1000, 1010, 1020 → middle 1010 + assert_eq!(mtp, 1010); + let (n, buf) = q + .store() + .mtp_times_at(Height(2)) + .expect("ring covers confirmed tip"); + assert_eq!(n, 3); + assert_eq!(median_time_past_times(&buf[..3]).unwrap(), 1010); + + // Height above tip with no plan → incomplete load error (not BadPrev). + let err = median_time_past(&q, Height(5)).unwrap_err(); + assert!( + format!("{err}").contains("load incomplete"), + "above tip must be the retryable load error, got {err:?}" + ); + + let (empty_dir, empty) = temp_q(); + let err = median_time_past(&empty, Height(0)).unwrap_err(); + assert!( + matches!(err, ConfirmError::Rules(ConsensusError::BadPrev)), + "a missing header at the tip is BadPrev, got {err:?}" + ); + let _ = std::fs::remove_dir_all(&empty_dir); + + // expected_next_bits: height 0 + regtest no-retarget. + let params = ChainParams::regtest(); + let gbits = expected_next_bits(&q, ¶ms, Height(0), 0).unwrap(); + assert_eq!(gbits, genesis_block(¶ms).header.bits); + let b1 = expected_next_bits(&q, ¶ms, Height(1), 0).unwrap(); + let (_fk, rec0) = q.header_at_height(Height(0)).unwrap().unwrap(); + assert_eq!(b1.to_consensus(), rec0.bits); + + // Bad prev header height. + assert!(expected_next_bits(&q, ¶ms, Height(99), 0).is_err()); + + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn sequential_tip_mtp_matches_ring_and_survives_pop() { + let (dir, q) = temp_q(); + let mut prev = Fk::NULL; + let mut parent_hash: Option<[u8; 32]> = None; + let mut times = Vec::new(); + for h in 0..12u32 { + let (hdr, ta) = coinbase(h, prev, parent_hash); + times.push(hdr.timestamp); + parent_hash = Some(hdr.hash); + prev = q.connect_block(Height(h), &hdr, &[ta]).unwrap(); + } + let want11 = median_time_past_times(×[1..]).unwrap(); + assert_eq!(median_time_past_store(&q, Height(11)).unwrap(), want11); + assert_eq!(median_time_past(&q, Height(11)).unwrap(), want11); + let (n, buf) = q.store().mtp_times_at(Height(11)).expect("ring at tip"); + assert_eq!(n, 11); + assert_eq!(median_time_past_times(&buf[..11]).unwrap(), want11); + assert!( + q.store().mtp_times_at(Height(5)).is_none(), + "historical MTP is not the tip ring" + ); + assert_eq!( + median_time_past_store(&q, Height(5)).unwrap(), + median_time_past_times(×[0..=5]).unwrap() + ); + + q.disconnect_tip().unwrap(); + let want10 = median_time_past_times(×[0..=10]).unwrap(); + assert_eq!(median_time_past_store(&q, Height(10)).unwrap(), want10); + let (n, buf) = q + .store() + .mtp_times_at(Height(10)) + .expect("ring rebuilt after pop"); + assert_eq!(median_time_past_times(&buf[..n as usize]).unwrap(), want10); + + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn validate_header_genesis_and_bad_prev() { + let (dir, q) = temp_q(); + let params = ChainParams::regtest(); + let g = genesis_block(¶ms); + // Wrong genesis hash at height 0. + let mut bad = g.header; + bad.nonce ^= 1; + let err = validate_header(&q, ¶ms, Height(0), &bad).unwrap_err(); + assert!( + matches!(err, ConfirmError::Rules(ConsensusError::BadHeader(_))), + "{err:?}" + ); + + // Synthetic tip for prev linkage (child hash commits to parent). + let (h0, ta0) = coinbase(0, Fk::NULL, None); + let prev = q.connect_block(Height(0), &h0, &[ta0]).unwrap(); + let (h1, ta1) = coinbase(1, prev, Some(h0.hash)); + q.connect_block(Height(1), &h1, &[ta1]).unwrap(); + + // Header with wrong prev hash at height 1. + let mut hdr = g.header; + hdr.prev_blockhash = bitcoin::BlockHash::from_byte_array([0xee; 32]); + hdr.time = 2_000; + hdr.bits = bitcoin::CompactTarget::from_consensus(0x207fffff); + let err = validate_header(&q, ¶ms, Height(1), &hdr).unwrap_err(); + assert!( + matches!( + err, + ConfirmError::Rules( + ConsensusError::BadPrev + | ConsensusError::BadHeader(_) + | ConsensusError::BadVersion(_) + ) + ), + "{err:?}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn testnet_min_difficulty_after_20_minute_gap() { + let (dir, q) = temp_q(); + let params = ChainParams::testnet(); + assert!(params.allow_min_difficulty_blocks()); + let (h0, ta0) = coinbase(0, Fk::NULL, None); + let prev_fk = q.connect_block(Height(0), &h0, &[ta0]).unwrap(); + let limit = params.pow_limit.to_compact_lossy(); + assert_ne!(CompactTarget::from_consensus(h0.bits), limit); + + let spacing = params.btc.pow_target_spacing as u32; + let gap = expected_next_bits(&q, ¶ms, Height(1), h0.timestamp + 2 * spacing + 1).unwrap(); + assert_eq!(gap, limit); + let eq_boundary = + expected_next_bits(&q, ¶ms, Height(1), h0.timestamp + 2 * spacing).unwrap(); + assert_eq!(eq_boundary.to_consensus(), h0.bits); + + let (mut h1, ta1) = coinbase(1, prev_fk, Some(h0.hash)); + h1.timestamp = h0.timestamp + 2 * spacing + 1; + h1.bits = limit.to_consensus(); + h1.hash = rbitcoin_store::block_header_hash( + h1.version, + &h0.hash, + &h1.merkle_root, + h1.timestamp, + h1.bits, + h1.nonce, + ); + q.connect_block(Height(1), &h1, &[ta1]).unwrap(); + let walked = expected_next_bits(&q, ¶ms, Height(2), h1.timestamp + 100).unwrap(); + assert_eq!(walked.to_consensus(), h0.bits); + + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/rbitcoin-query/src/in_flight.rs b/crates/rbitcoin-query/src/in_flight.rs index 0c016e353e..8c799826b3 100644 --- a/crates/rbitcoin-query/src/in_flight.rs +++ b/crates/rbitcoin-query/src/in_flight.rs @@ -15,7 +15,7 @@ //! [`InFlight::drop_from_height`] on pack height. use crate::archive::CreatePin; -use crate::id_map::TxidHasher; +use crate::TxidHasher; use crate::U64Map; use rbitcoin_primitives::Fk; use std::collections::{BTreeMap, HashMap}; diff --git a/crates/rbitcoin-consensus/src/index_rows.rs b/crates/rbitcoin-query/src/index_rows.rs similarity index 92% rename from crates/rbitcoin-consensus/src/index_rows.rs rename to crates/rbitcoin-query/src/index_rows.rs index 8d1c19c40d..8cc6f1edbe 100644 --- a/crates/rbitcoin-consensus/src/index_rows.rs +++ b/crates/rbitcoin-query/src/index_rows.rs @@ -8,12 +8,13 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; +use crate::Query; use bitcoin::bip158::BlockFilter; use rbitcoin_primitives::{Fk, Height}; -use rbitcoin_query::Query; -use crate::script_pool::start_for_each_owned_chunk; -use crate::ConsensusError; +use crate::ConfirmError; +use rbitcoin_consensus::script_pool::start_for_each_owned_chunk; +use rbitcoin_consensus::ConsensusError; /// Per-tx tweak (`None` = ineligible), block order. pub(crate) type HeightTweaks = Vec>; @@ -41,7 +42,7 @@ pub(crate) struct IndexCommit { pub(crate) fn finish_wave( jobs: Vec, f: fn(&T) -> Result<(), ConsensusError>, -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { if let Some(wave) = start_for_each_owned_chunk(jobs, f, 1)? { wave.finish()?; } @@ -68,7 +69,7 @@ pub(crate) fn rows_from_slots(slots: &[Arc>>]) -> I pub(crate) fn commit_index_rows( query: &Query, rows: IndexRows, -) -> Result { +) -> Result { let t0 = Instant::now(); let moved = commit_filters(query, rows.filters)? || commit_tweaks(query, rows.tweaks)?; Ok(IndexCommit { @@ -81,7 +82,7 @@ pub(crate) fn commit_index_rows( fn commit_filters( query: &Query, filters: Vec<(Height, BlockFilter, Fk)>, -) -> Result { +) -> Result { let Some(next) = query.filter_index_next() else { return Ok(false); }; @@ -106,7 +107,7 @@ fn commit_filters( fn commit_tweaks( query: &Query, tweaks: Vec<(Height, Fk, HeightTweaks)>, -) -> Result { +) -> Result { let Some(next) = query.tweak_index_next() else { return Ok(false); }; diff --git a/crates/rbitcoin-consensus/src/index_writebehind.rs b/crates/rbitcoin-query/src/index_writebehind.rs similarity index 95% rename from crates/rbitcoin-consensus/src/index_writebehind.rs rename to crates/rbitcoin-query/src/index_writebehind.rs index e9f1f057c9..642b0840a7 100644 --- a/crates/rbitcoin-consensus/src/index_writebehind.rs +++ b/crates/rbitcoin-query/src/index_writebehind.rs @@ -16,13 +16,14 @@ //! a one-height window. use crate::index_rows::{self, IndexHeightOut, IndexRows}; -use crate::silent_payments::tweak_records_from_window; -use crate::ConsensusError; +use crate::sp_read::tweak_records_from_window; +use crate::ConfirmError; +use crate::Query; +use rbitcoin_consensus::ConsensusError; use rbitcoin_log::progress; #[cfg(test)] use rbitcoin_primitives::Fk; use rbitcoin_primitives::Height; -use rbitcoin_query::Query; use rbitcoin_store::IndexWindow; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{sync_channel, Receiver}; @@ -143,14 +144,14 @@ pub fn index_tip_entry(query: &Query) -> IndexTipEntry { /// Before confirm: seal each enabled index whose gap through the tip fits /// [`INDEX_STARTUP_GAP_HEIGHTS`], then set [`Query::index_live`] when every /// enabled index is contiguous with the tip. -pub fn prepare_live_indexes(query: &Query) -> Result<(), ConsensusError> { +pub fn prepare_live_indexes(query: &Query) -> Result<(), ConfirmError> { prepare_live_indexes_limited(query, INDEX_STARTUP_GAP_HEIGHTS) } pub(crate) fn prepare_live_indexes_limited( query: &Query, max_heights: u32, -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { let tip_h = query.tip_height().map(|h| h.0); if let Some(tip) = tip_h { let repair_f = repair_from(query.filter_index_next(), tip, max_heights); @@ -193,7 +194,7 @@ fn seal_through( filter_from: Option, tweak_from: Option, tip: u32, -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { let mut start = match (filter_from, tweak_from) { (Some(f), Some(t)) => f.min(t), (Some(f), None) => f, @@ -215,7 +216,7 @@ fn seal_through( let window = Arc::new(read_window_from(query, start, end, tweak_from)?); let rows = assemble_window_from(&window, filter_from, tweak_from)?; if index_rows::commit_index_rows(query, rows)?.moved { - return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt( + return Err(ConfirmError::Store(rbitcoin_store::StoreError::Corrupt( "invariant: startup index repair", ))); } @@ -228,7 +229,7 @@ fn seal_through( Ok(()) } -fn plan_window(query: &Query, start: u32, target: u32) -> Result { +fn plan_window(query: &Query, start: u32, target: u32) -> Result { let mut end = start; let mut txs = 0u64; while end < target && end - start + 1 < WINDOW_HEIGHTS { @@ -249,7 +250,7 @@ fn plan_window(query: &Query, start: u32, target: u32) -> Result Result { +fn read_window(query: &Query, start: u32, end: u32) -> Result { read_window_from(query, start, end, query.tweak_index_next()) } @@ -258,7 +259,7 @@ fn read_window_from( start: u32, end: u32, tweaks_from: Option, -) -> Result { +) -> Result { let heights = query.index_heights(start, end, tweaks_from)?; let mut window = query.read_index_window(&heights)?; for block in &mut window.blocks { @@ -275,12 +276,16 @@ struct IndexHeightJob { out: Arc>>, } -fn assemble_index_height(job: &IndexHeightJob) -> Result<(), ConsensusError> { +fn assemble_index_height_pool(job: &IndexHeightJob) -> Result<(), ConsensusError> { + assemble_index_height(job).map_err(ConfirmError::into_pool) +} + +fn assemble_index_height(job: &IndexHeightJob) -> Result<(), ConfirmError> { let block = &job.window.blocks[job.index]; let filter = if job.want_filter { Some(( block.height, - rbitcoin_query::basic_filter_of(&block.hash, &job.window, job.index)?, + crate::basic_filter_of(&block.hash, &job.window, job.index)?, block.header_fk, )) } else { @@ -301,7 +306,7 @@ fn assemble_index_height(job: &IndexHeightJob) -> Result<(), ConsensusError> { /// One job per height that still needs a filter or tweaks. The wave joins /// before return, so the `Arc` is only shared for that call. -fn assemble_window(query: &Query, window: &Arc) -> Result { +fn assemble_window(query: &Query, window: &Arc) -> Result { assemble_window_from(window, query.filter_index_next(), query.tweak_index_next()) } @@ -309,7 +314,7 @@ fn assemble_window_from( window: &Arc, filter_from: Option, tweak_from: Option, -) -> Result { +) -> Result { let mut slots = Vec::with_capacity(window.blocks.len()); let mut jobs = Vec::new(); for (index, block) in window.blocks.iter().enumerate() { @@ -328,7 +333,7 @@ fn assemble_window_from( } slots.push(out); } - index_rows::finish_wave(jobs, assemble_index_height)?; + index_rows::finish_wave(jobs, assemble_index_height_pool)?; Ok(index_rows::rows_from_slots(&slots)) } @@ -420,7 +425,7 @@ fn commit_window( query: &Query, window: &Arc, stages: &IndexStageMs, -) -> Result { +) -> Result { if window.blocks.is_empty() { return Ok(CommitTimes { ok: true, @@ -435,7 +440,7 @@ fn commit_window( let commit_ns = committed.put_ns; stages.add_build(build_ns); stages.add_commit(commit_ns); - rbitcoin_query::note_confirm( + crate::note_confirm( &query.confirm_stats().blockfilter_ns, build_ns.saturating_add(commit_ns), ); @@ -447,7 +452,7 @@ fn commit_window( } /// Seal every released height on this thread (regtest `generate`, tests). -pub fn build_indexes_released(query: &Query) -> Result<(), ConsensusError> { +pub fn build_indexes_released(query: &Query) -> Result<(), ConfirmError> { let mut live = IndexStages::default(); live.observe(query, query.index_target(), true, true); while let (Some(start), Some(target)) = (next_needed(query), query.index_target()) { @@ -480,7 +485,7 @@ fn cpu_worker( rx: Receiver, resync: &AtomicBool, stages: &IndexStageMs, -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { for ready in rx { if resync.load(Ordering::Acquire) { continue; @@ -533,7 +538,7 @@ fn run_writebehind( query: &Query, stop: &AtomicBool, on_filters_caught_up: impl FnOnce(), -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { // Spawned after catch-up: the current tip was already announced. if let Some(tip) = query.tip_height() { query.release_index_writebehind(tip); @@ -575,7 +580,7 @@ fn io_loop( tx: &std::sync::mpsc::SyncSender, progress_every: Duration, on_filters_caught_up: impl FnOnce(), -) -> Result<(), ConsensusError> { +) -> Result<(), ConfirmError> { let mut on_filters_caught_up = Some(on_filters_caught_up); let mut cursor: Option = None; let mut pass: Option = None; @@ -779,7 +784,7 @@ mod tests { prev_fk: &mut Fk, prev_hash: &mut Option<[u8; 32]>, ) { - use rbitcoin_query::testutil::FixtureChain; + use crate::testutil::FixtureChain; use rbitcoin_store::{InputRecord, OutputRecord}; for height in from..=to { @@ -856,13 +861,13 @@ mod tests { /// the serial window walk. #[test] fn per_height_jobs_match_serial_window() { + use crate::testutil::FixtureChain; use bitcoin::hashes::{hash160, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; - use rbitcoin_query::testutil::FixtureChain; use rbitcoin_store::{InputRecord, OutputRecord}; - let _gate = crate::script_pool::steal_test_gate(); - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("idx-jobs"); + let _gate = rbitcoin_consensus::script_pool::steal_test_gate(); + let (dir, q) = crate::testutil::tiny_query_labeled("idx-jobs"); q.set_block_filter_index(true).unwrap(); q.set_sptweaks_enabled(true, Height(0)).unwrap(); @@ -938,7 +943,7 @@ mod tests { let serial_f = q.basic_filter_from_window(&window, i).unwrap(); assert_eq!(got.filters[i].1.content, serial_f.content, "filter {i}"); assert_eq!(got.filters[i].2, block.header_fk); - let serial_t = crate::silent_payments::tweak_records_from_window(&window, i).unwrap(); + let serial_t = crate::sp_read::tweak_records_from_window(&window, i).unwrap(); assert_eq!(got.tweaks[i].2, serial_t, "tweaks {i}"); } assert!(got.tweaks[0].2.iter().all(|t| t.is_none()), "coinbase"); @@ -982,10 +987,10 @@ mod tests { #[test] fn released_builder_crosses_a_full_window_and_success_does_not_resync() { - use rbitcoin_query::testutil::FixtureChain; + use crate::testutil::FixtureChain; use rbitcoin_store::{InputRecord, OutputRecord}; - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("idx-full-window"); + let (dir, q) = crate::testutil::tiny_query_labeled("idx-full-window"); q.set_block_filter_index(true).unwrap(); q.set_sptweaks_enabled(true, Height(0)).unwrap(); @@ -1090,10 +1095,10 @@ mod tests { #[test] fn filter_catchup_callback_fires_only_after_the_released_tip() { - use rbitcoin_query::testutil::FixtureChain; + use crate::testutil::FixtureChain; use rbitcoin_store::{InputRecord, OutputRecord}; - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("idx-filter-catchup"); + let (dir, q) = crate::testutil::tiny_query_labeled("idx-filter-catchup"); q.set_block_filter_index(true).unwrap(); let mut prev_fk = Fk::NULL; let mut prev_hash = None; @@ -1239,7 +1244,7 @@ mod tests { /// counts from its own start and ends at its own total. #[test] fn released_builder_reports_each_index_from_its_start() { - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("idx-progress-released"); + let (dir, q) = crate::testutil::tiny_query_labeled("idx-progress-released"); q.set_block_filter_index(true).unwrap(); let tip = WINDOW_HEIGHTS + 1; coinbase_chain(&q, tip); @@ -1269,7 +1274,7 @@ mod tests { #[test] fn startup_repair_reports_only_the_indexes_it_seals() { - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("idx-progress-repair"); + let (dir, q) = crate::testutil::tiny_query_labeled("idx-progress-repair"); q.set_block_filter_index(true).unwrap(); q.set_sptweaks_enabled(true, Height(0)).unwrap(); let tip = WINDOW_HEIGHTS + 1; @@ -1297,7 +1302,7 @@ mod tests { /// it began. #[test] fn writebehind_registers_a_stage_for_a_wide_gap() { - let (dir, q) = rbitcoin_query::testutil::tiny_query_labeled("idx-progress-wb"); + let (dir, q) = crate::testutil::tiny_query_labeled("idx-progress-wb"); q.set_block_filter_index(true).unwrap(); let tip = WINDOW_HEIGHTS + 1; coinbase_chain(&q, tip); @@ -1345,8 +1350,8 @@ mod tests { txid: [u8; 32], inputs: Vec, outputs: Vec, - ) -> rbitcoin_query::TxApply { - rbitcoin_query::TxApply { + ) -> crate::TxApply { + crate::TxApply { tx: rbitcoin_store::TxRecord { txid, version: 2, diff --git a/crates/rbitcoin-query/src/lib.rs b/crates/rbitcoin-query/src/lib.rs index eceb9412a5..77f78db86f 100644 --- a/crates/rbitcoin-query/src/lib.rs +++ b/crates/rbitcoin-query/src/lib.rs @@ -12,7 +12,6 @@ mod confirm_load; mod confirm_parent_cache; mod confirm_stats; mod connect; -mod id_map; mod in_flight; mod reconstruct; mod resolved_wire; @@ -24,22 +23,59 @@ mod sp_tweaks; mod spend_sync; mod stamp; pub mod testutil; -mod tx_precompute; mod wave_prevout; mod write_create_loc; +#[cfg(test)] +mod bip68_store_tests; +mod block_pipeline; +mod confirm_error; +mod confirm_run; +mod connect_facade; +mod convert; +mod header_read; +#[cfg(test)] +mod header_store_tests; +mod index_rows; +mod index_writebehind; +mod milestone_gate; +#[cfg(test)] +mod overlay_csv_tests; +mod regtest_pad; +mod sp_read; +#[cfg(test)] +mod sp_store_tests; +#[cfg(test)] +mod structure_rule_tests; +#[cfg(test)] +mod wire_agree_tests; + pub use combined_stage::{load_creates_once, CombinedCreate}; +pub use confirm_error::ConfirmError; +pub use connect_facade::*; +pub use convert::header_to_record; +pub use header_read::{ + expected_next_bits, median_time_past, median_time_past_store, validate_header, + validate_header_hashed, +}; +pub use index_writebehind::{ + build_indexes_released, index_tip_entry, prepare_live_indexes, spawn_index_writebehind, + IndexTipEntry, INDEX_STARTUP_GAP_HEIGHTS, +}; +pub use rbitcoin_consensus::{ + block_wire_len_from_pres, decode_block_precomputes, pres_for_tip, IdMap, OutPointHasher, + OutPointSet, TxPrecompute, TxidHasher, TxidMap, TxidSet, +}; pub use reconstruct::{BlockTxStatRows, StampedTxstatBlock}; +pub use regtest_pad::pad_empty_from; pub use resolved_wire::{BlockQueueWaveIntake, ResolvedWire}; pub use soft_densify::{ bq_assign_stop_bytes, soft_ahead_quarter_full, soft_assign_restricted, soft_confirm_window_covered, soft_confirm_window_n, soft_densify_band_hi, BQ_SOFT_FREE_BYTES, TIP_HOLE_MIN_AHEAD_BLOCKS, }; +pub use sp_read::{tweaks_at_height, tweaks_for_height, tweaks_from_thin_and_body}; pub use sp_tweaks::{ThinTweakRangeLimits, ThinTweakRow}; -pub use tx_precompute::{ - block_wire_len_from_pres, decode_block_precomputes, pres_for_tip, TxPrecompute, -}; use bitcoin::absolute::LockTime; use bitcoin::block::{Header as BlockHeader, Version as BlockVersion}; @@ -226,7 +262,6 @@ pub use catchup::IndexMode; pub use chain_view::{header_lookup_count, ChainView, ChainViewKind}; pub use confirm_load::SpendEdges; pub use connect::{spawn_sh_writebehind, ConfirmPrepared}; -pub use id_map::{IdMap, OutPointHasher, OutPointSet, TxidHasher, TxidMap, TxidSet}; pub use in_flight::InFlight; pub use scripthash::{ BlockTouch, FilteredHistory, HistoryFilter, HistoryOrder, ScriptHashBalance, @@ -374,6 +409,9 @@ pub struct Query { /// Cooperative cancel for in-flight confirm load. Set on IBD SIGINT /// teardown so the confirm load thread aborts before process exit. confirm_cancel: std::sync::atomic::AtomicBool, + /// One header read returns [`StoreError::Cancelled`]. Tests only. + #[cfg(feature = "test-hooks")] + header_read_cancel: std::sync::atomic::AtomicBool, /// Confirmed best-chain `hash → height` (kept for process life; ~60 MiB mainnet). /// /// Avoids O(tip) header walks on Esplora/P2P `height_of_hash`. @@ -536,6 +574,8 @@ impl Query { sptweaks_origin: AtomicU32::new(sptweaks_origin), index_mode_cell: std::sync::atomic::AtomicU8::new(IndexMode::Tip as u8), confirm_cancel: std::sync::atomic::AtomicBool::new(false), + #[cfg(feature = "test-hooks")] + header_read_cancel: std::sync::atomic::AtomicBool::new(false), height_by_hash: Mutex::new(HeightByHashIndex::default()), reconstruct_archived: AtomicU64::new(0), max_sh_creates: AtomicU32::new(DEFAULT_MAX_SH_CREATES), @@ -1240,6 +1280,14 @@ impl Query { .load(std::sync::atomic::Ordering::SeqCst) } + /// The next [`Self::header_at_height`] returns [`StoreError::Cancelled`]. + #[cfg(feature = "test-hooks")] + #[doc(hidden)] + pub fn fail_next_header_read_cancelled(&self) { + self.header_read_cancel + .store(true, std::sync::atomic::Ordering::SeqCst); + } + /// Last height with SH creates applied (after tip). `None` if empty chain. pub fn sh_indexed_through_height(&self) -> Option { let v = self.sh.indexed_through.load(AtomicOrdering::Acquire); diff --git a/crates/rbitcoin-query/src/milestone_gate.rs b/crates/rbitcoin-query/src/milestone_gate.rs new file mode 100644 index 0000000000..34fc6d289f --- /dev/null +++ b/crates/rbitcoin-query/src/milestone_gate.rs @@ -0,0 +1,20 @@ +//! Confirm-path milestone gate. The predicate is [`rbitcoin_consensus::Milestone::skips_scripts`]. + +use rbitcoin_consensus::Milestone; + +use crate::Query; + +/// Notes `milestone_gate_ns` (`ibd: perf_dbg milestone_us`). +pub fn skips_on_query(milestone: Milestone, query: &Query, height: u32, block: &[u8; 32]) -> bool { + let t = std::time::Instant::now(); + let skip = milestone.skips_scripts(height, block, |h| query.milestone_header_at(h)); + crate::note_confirm( + &query.confirm_stats().milestone_gate_ns, + t.elapsed().as_nanos() as u64, + ); + skip +} + +pub fn check_scripts(milestone: Milestone, query: &Query, height: u32, block: &[u8; 32]) -> bool { + !skips_on_query(milestone, query, height, block) +} diff --git a/crates/rbitcoin-query/src/overlay_csv_tests.rs b/crates/rbitcoin-query/src/overlay_csv_tests.rs new file mode 100644 index 0000000000..b1f9bacc65 --- /dev/null +++ b/crates/rbitcoin-query/src/overlay_csv_tests.rs @@ -0,0 +1,152 @@ +//! CSV activation overlay changes confirm, not only the height getter. +#![cfg(test)] + +use bitcoin::absolute::LockTime; +use bitcoin::blockdata::constants; +use bitcoin::script::ScriptBuf; +use bitcoin::transaction::Version as TxVersion; +use bitcoin::{Amount, Network, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; +use rbitcoin_consensus::{mine_regtest_paying, ChainParams, Milestone}; +use rbitcoin_primitives::Height; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::{accept_and_connect_block, pad_empty_from, Query}; + +/// Overlay must change **confirm** BIP68, not just the getter. +#[test] +fn overlay_csv_102_is_lax_at_101_strict_at_102() { + let n = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("rbtc-overlay-csv-{n}")); + let q = Query::open_or_create_tiny(&dir).unwrap(); + let mut params = ChainParams::regtest(); + params.apply_test_activation_height("csv", 102).unwrap(); + let genesis = constants::genesis_block(Network::Regtest); + accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); + let (tip, tip_time, cbs) = pad_empty_from( + &q, + ¶ms, + genesis.block_hash(), + genesis.header.time, + 1, + 100, + 1, + ); + let spk = ScriptBuf::from_bytes(vec![0x51]); + let parent = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: cbs[0], + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(50_0000_0000 - 1_000), + script_pubkey: spk.clone(), + }], + }; + let b101 = mine_regtest_paying(tip, tip_time + 600, 101, spk.clone(), vec![parent.clone()]); + accept_and_connect_block(&q, ¶ms, Height(101), &b101, Milestone::NONE) + .expect("parent spend at 101"); + + let child = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: parent.compute_txid(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(10), + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(50_0000_0000 - 2_000), + script_pubkey: spk.clone(), + }], + }; + let b102 = mine_regtest_paying( + b101.block_hash(), + b101.header.time + 600, + 102, + spk.clone(), + vec![child], + ); + let err = + accept_and_connect_block(&q, ¶ms, Height(102), &b102, Milestone::NONE).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("not final") || msg.contains("nonfinal") || msg.contains("BIP68"), + "csv@102 must reject BIP68-locked spend at height 102: {msg}" + ); + + let dir2 = std::env::temp_dir().join(format!("rbtc-overlay-csv-lax-{n}")); + let q2 = Query::open_or_create_tiny(&dir2).unwrap(); + let mut lax = ChainParams::regtest(); + lax.apply_test_activation_height("csv", 200).unwrap(); + accept_and_connect_block(&q2, &lax, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); + let (tip2, time2, cbs2) = pad_empty_from( + &q2, + &lax, + genesis.block_hash(), + genesis.header.time, + 1, + 100, + 1, + ); + let p_lax = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: cbs2[0], + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(50_0000_0000 - 1_000), + script_pubkey: spk.clone(), + }], + }; + let b101l = mine_regtest_paying(tip2, time2 + 600, 101, spk.clone(), vec![p_lax.clone()]); + accept_and_connect_block(&q2, &lax, Height(101), &b101l, Milestone::NONE).unwrap(); + let c_lax = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: p_lax.compute_txid(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(10), + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(50_0000_0000 - 2_000), + script_pubkey: spk, + }], + }; + let b102l = mine_regtest_paying( + b101l.block_hash(), + b101l.header.time + 600, + 102, + ScriptBuf::from_bytes(vec![0x51]), + vec![c_lax], + ); + accept_and_connect_block(&q2, &lax, Height(102), &b102l, Milestone::NONE) + .expect("csv@200: BIP68 not consensus at height 102"); + let _ = std::fs::remove_dir_all(&dir); + let _ = std::fs::remove_dir_all(&dir2); +} diff --git a/crates/rbitcoin-query/src/regtest_pad.rs b/crates/rbitcoin-query/src/regtest_pad.rs new file mode 100644 index 0000000000..2b2ceb87e8 --- /dev/null +++ b/crates/rbitcoin-query/src/regtest_pad.rs @@ -0,0 +1,34 @@ +//! Regtest helpers that open a [`Query`]. + +use rbitcoin_consensus::{mine_empty_regtest, ChainParams, Milestone}; + +use crate::accept_and_connect_block; +use rbitcoin_primitives::Height; + +use crate::Query; + +/// Connect empty blocks `from_h..=last`. Returns tip hash/time and coinbase txids +/// for heights `1..=collect_coinbases` (empty if `collect_coinbases == 0`). +pub fn pad_empty_from( + query: &Query, + params: &ChainParams, + mut tip: bitcoin::BlockHash, + mut tip_time: u32, + from_h: u32, + last: u32, + collect_coinbases: u32, +) -> (bitcoin::BlockHash, u32, Vec) { + let ms = Milestone::NONE; + let mut cbs = Vec::new(); + for h in from_h..=last { + let b = mine_empty_regtest(tip, tip_time + 600, h); + if h >= 1 && h <= collect_coinbases { + cbs.push(b.txdata[0].compute_txid()); + } + accept_and_connect_block(query, params, Height(h), &b, ms).unwrap(); + tip = b.block_hash(); + tip_time = b.header.time; + } + query.apply_sh_pending().unwrap(); + (tip, tip_time, cbs) +} diff --git a/crates/rbitcoin-query/src/sp_read.rs b/crates/rbitcoin-query/src/sp_read.rs new file mode 100644 index 0000000000..f1d95b7907 --- /dev/null +++ b/crates/rbitcoin-query/src/sp_read.rs @@ -0,0 +1,262 @@ +//! Store-backed BIP-352 tweak readers. Pure tweak math stays in consensus. +use bitcoin::hashes::Hash; +use bitcoin::{OutPoint, Transaction, TxOut, Witness}; +use rbitcoin_consensus::silent_payments::{is_p2tr, tweak_from_tx, TaprootOut, TxTweak}; +use rbitcoin_consensus::ChainParams; +use rbitcoin_primitives::{Fk, Height}; +use rbitcoin_store::{IndexWindow, InputRecord, LoadedTweakTx, OutputRecord, StoreError}; +use std::collections::{BTreeMap, HashMap}; + +use crate::{ConfirmError, Query}; + +/// Txid and outputs of a spent create. +type ParentLookup<'a> = dyn Fn(Fk) -> Option<([u8; 32], &'a [OutputRecord])> + 'a; + +/// Prefer the thin index (no parent peeks). Hole / no table → [`tweaks_for_height`]. +pub fn tweaks_at_height( + query: &Query, + params: &ChainParams, + height: Height, +) -> Result, ConfirmError> { + if !params.taproot_active_at(height.0) { + return Ok(BTreeMap::new()); + } + match query.load_thin_tweaks(height) { + Ok(Some(rows)) => { + let mut out = BTreeMap::new(); + for r in rows { + out.insert( + r.txid, + TxTweak { + tweak: r.tweak, + output_pubkeys: r + .p2tr + .into_iter() + .map(|(vout, xonly, value)| TaprootOut { vout, xonly, value }) + .collect(), + }, + ); + } + return Ok(out); + } + Ok(None) => { + rbitcoin_log::debug!( + "sp_tweaks: naive fallback h={} (hole or index off)", + height.0 + ); + } + Err(e) => return Err(e.into()), + } + tweaks_for_height(query, params, height) +} + +/// Confirmed height → eligible txid (internal order) → tweak. +/// +/// Pre-Taproot and missing heights return an empty map (no error). Does not +/// reconstruct a wire block. Walks packed bodies **and parent outs**. +pub fn tweaks_for_height( + query: &Query, + params: &ChainParams, + height: Height, +) -> Result, ConfirmError> { + query.require_sp_tweaks_unpruned()?; + if !params.taproot_active_at(height.0) { + return Ok(BTreeMap::new()); + } + let fks = match query.block_tx_fks(height) { + Ok(f) => f, + Err(StoreError::NotFound) => return Ok(BTreeMap::new()), + Err(e) => return Err(e.into()), + }; + if fks.is_empty() { + return Ok(BTreeMap::new()); + } + + let wave = rbitcoin_store::load_tweak_wave(&query.store().txs, &fks)?; + let mut by_fk: HashMap = HashMap::new(); + for t in &wave.txs { + by_fk.insert(t.fk, (t.rec.txid, &t.outs)); + } + for (fk, (txid, outs)) in &wave.parents { + by_fk.entry(Fk(*fk)).or_insert((*txid, outs)); + } + let txs: Vec<&LoadedTweakTx> = wave.txs.iter().collect(); + let tweaks = tweaks_for_txs(&txs, &|fk| by_fk.get(&fk).copied())?; + Ok(txs + .iter() + .zip(tweaks) + .filter_map(|(t, tw)| Some((t.rec.txid, tw?))) + .collect()) +} + +/// Tweak records of `window.blocks[i]` in block order (`None` = ineligible). +pub fn tweak_records_from_window( + window: &IndexWindow, + i: usize, +) -> Result>, ConfirmError> { + let txs: Vec<&LoadedTweakTx> = window.blocks[i].txs.iter().collect(); + let tweaks = tweaks_for_txs(&txs, &|fk| Some((window.txid(fk)?, window.outs(fk)?)))?; + Ok(tweaks.into_iter().map(|t| t.map(|t| t.tweak)).collect()) +} + +/// Per-tx tweak (`None` = ineligible) for `txs` in order. Only P2TR-output +/// txs with loaded inputs are candidates; EC math runs on the caller. +fn tweaks_for_txs<'a>( + txs: &[&'a LoadedTweakTx], + parent: &ParentLookup<'a>, +) -> Result>, ConfirmError> { + let mut out = vec![None; txs.len()]; + for (i, t) in txs.iter().enumerate() { + if !t.need_seqsigwit { + continue; + } + let Some(inputs) = t.inputs.as_ref() else { + continue; + }; + let (tx, prevouts) = build_tx_and_prevouts(inputs, &t.outs, parent)?; + out[i] = tweak_from_tx(&tx, &prevouts); + } + Ok(out) +} + +#[allow(clippy::type_complexity)] // packed row / pin / script-hash tuple is the on-disk shape +/// Tweaks-wire `TxTweak` from a stored 33-byte tweak + this tx’s packed outs. +/// +/// No parent IO. `Some(tweak)` with missing packed outs is corrupt. +pub fn tweaks_from_thin_and_body( + rows: &[([u8; 32], Option<[u8; 33]>, Option<&[OutputRecord]>)], +) -> Result, StoreError> { + let mut out = BTreeMap::new(); + for (txid, tweak, outs) in rows { + let Some(tweak) = tweak else { + continue; + }; + let Some(outs) = outs else { + return Err(StoreError::Corrupt( + "invariant: thin tweak missing packed body", + )); + }; + out.insert( + *txid, + TxTweak { + tweak: *tweak, + output_pubkeys: taproot_outs_from_records(outs), + }, + ); + } + Ok(out) +} + +fn taproot_outs_from_records(outputs: &[OutputRecord]) -> Vec { + let mut out = Vec::new(); + for (i, o) in outputs.iter().enumerate() { + if !is_p2tr(&o.script) { + continue; + } + let mut xonly = [0u8; 32]; + xonly.copy_from_slice(&o.script[2..34]); + let value = o.value; + out.push(TaprootOut { + vout: i as u32, + xonly, + value, + }); + } + out +} + +/// A spent parent missing from the lookup is an invariant break, not an +/// ineligible tx. +fn build_tx_and_prevouts( + inputs: &[InputRecord], + outputs: &[OutputRecord], + parent: &ParentLookup<'_>, +) -> Result<(Transaction, Vec), StoreError> { + let mut prevouts = Vec::with_capacity(inputs.len()); + let mut txins = Vec::with_capacity(inputs.len()); + for inp in inputs { + let (prev_txid, prev_script, prev_value) = if inp.is_coinbase() { + ([0u8; 32], Vec::new(), 0u64) + } else { + const MISSING: &str = "invariant: sp_tweaks spent parent missing"; + let (tid, outs) = parent(inp.create_fk()).ok_or(StoreError::Corrupt(MISSING))?; + let o = outs + .get(inp.prev_index() as usize) + .ok_or(StoreError::Corrupt(MISSING))?; + (tid, o.script.clone(), o.value) + }; + let wit_refs: Vec<&[u8]> = inp.witness.iter().map(|w| w.as_slice()).collect(); + txins.push(bitcoin::TxIn { + previous_output: OutPoint { + txid: bitcoin::Txid::from_byte_array(prev_txid), + vout: inp.prev_index(), + }, + script_sig: bitcoin::ScriptBuf::from_bytes(inp.script_sig.clone()), + sequence: bitcoin::Sequence::from_consensus(inp.sequence), + witness: Witness::from_slice(&wit_refs), + }); + let value = bitcoin::Amount::from_sat(prev_value); + prevouts.push(TxOut { + value, + script_pubkey: bitcoin::ScriptBuf::from_bytes(prev_script), + }); + } + let mut txouts = Vec::with_capacity(outputs.len()); + for o in outputs { + let value = bitcoin::Amount::from_sat(o.value); + txouts.push(TxOut { + value, + script_pubkey: bitcoin::ScriptBuf::from_bytes(o.script.clone()), + }); + } + Ok(( + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: txins, + output: txouts, + }, + prevouts, + )) +} + +#[cfg(test)] +mod classifier_record_tests { + use super::{build_tx_and_prevouts, taproot_outs_from_records}; + use rbitcoin_primitives::Fk; + use rbitcoin_store::{InputRecord, OutputRecord}; + + #[test] + fn taproot_record_keeps_a_zero_value_and_skips_other_scripts() { + let mut p2tr = vec![0x51, 0x20]; + p2tr.extend_from_slice(&[0xab; 32]); + let outs = vec![ + OutputRecord::unspent(0, p2tr), + OutputRecord::unspent(7, vec![0x51]), + ]; + let got = taproot_outs_from_records(&outs); + assert_eq!(got.len(), 1); + assert_eq!(got[0].vout, 0); + assert_eq!(got[0].value, 0); + assert_eq!(got[0].xonly, [0xab; 32]); + } + + #[test] + fn build_tx_uses_the_parent_output_value() { + let parent_txid = [0x11; 32]; + let parent_outs = [OutputRecord::unspent(7, vec![0x51])]; + let inp = InputRecord::prevout(parent_txid, Fk(3), 0, 0xffff_ffff, Vec::new(), Vec::new()); + let spend_out = OutputRecord::unspent(1, vec![0x51]); + let parent = |fk: Fk| { + if fk == Fk(3) { + Some((parent_txid, parent_outs.as_slice())) + } else { + None + } + }; + let (tx, prevouts) = build_tx_and_prevouts(&[inp], &[spend_out], &parent).unwrap(); + assert_eq!(prevouts[0].value.to_sat(), 7); + assert_eq!(tx.output[0].value.to_sat(), 1); + assert_eq!(tx.input[0].previous_output.vout, 0); + } +} diff --git a/crates/rbitcoin-query/src/sp_store_tests.rs b/crates/rbitcoin-query/src/sp_store_tests.rs new file mode 100644 index 0000000000..4d1c2ea67f --- /dev/null +++ b/crates/rbitcoin-query/src/sp_store_tests.rs @@ -0,0 +1,492 @@ +//! Store-backed silent-payment tweak reads. +#![cfg(test)] + +use crate::sp_read::tweak_records_from_window; +use crate::testutil::FixtureChain; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; +use bitcoin::{Amount, ScriptBuf, TxOut}; +use rbitcoin_consensus::{mine_empty_regtest, tweak_from_tx, ChainParams, Milestone}; +use rbitcoin_primitives::{Fk, Height}; +use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, StoreError, TxRecord}; + +use crate::{ + accept_and_connect_block, build_indexes_released, prepare_live_indexes, Query, TxApply, +}; + +fn tmp_store() -> (crate::testutil::TempDir, Query) { + crate::testutil::tiny_query_labeled("sp-tweaks") +} + +fn header(h: u32, prev_fk: Fk, prev_hash: Option<[u8; 32]>) -> HeaderRecord { + crate::testutil::header_with_merkle_marker(h, prev_fk, prev_hash, 0xec) +} + +#[test] +fn thin_compose_matches_engine_outs_without_parents() { + let (dir, q) = tmp_store(); + let params = ChainParams::regtest(); + // Reuse the P2WPKH→P2TR spend from the walk test via a tiny local chain. + use bitcoin::hashes::hash160; + use bitcoin::secp256k1::SecretKey; + let secp_ctx = Secp256k1::new(); + let sk = SecretKey::from_slice(&[2u8; 32]).unwrap(); + let pk = bitcoin::secp256k1::PublicKey::from_secret_key(&secp_ctx, &sk); + let ser = pk.serialize(); + let h160 = hash160::Hash::hash(&ser); + let mut p2wpkh = vec![0x00, 0x14]; + p2wpkh.extend_from_slice(h160.as_ref()); + let (xonly, _) = pk.x_only_public_key(); + let mut p2tr = vec![0x51, 0x20]; + p2tr.extend_from_slice(&xonly.serialize()); + let mut genesis_txid = [0u8; 32]; + genesis_txid[31] = 0xcb; + let h0 = header(0, Fk::NULL, None); + let fk0 = q + .connect_block( + Height(0), + &h0, + &[TxApply { + tx: TxRecord { + txid: genesis_txid, + version: 1, + locktime: 0, + input_start_fk: Fk::NULL, + input_count: 1, + output_start_fk: Fk::NULL, + output_count: 1, + }, + inputs: vec![InputRecord::coinbase(u32::MAX, vec![0x00], vec![])], + outputs: vec![OutputRecord::unspent(50_0000_0000, p2wpkh.clone())], + }], + ) + .unwrap(); + let create_fk = q.block_tx_fks(Height(0)).unwrap()[0]; + let mut spend_txid = [0u8; 32]; + spend_txid[0] = 0x11; + spend_txid[31] = 0xcd; + let h1 = header(1, fk0, Some(h0.hash)); + q.connect_block( + Height(1), + &h1, + &[TxApply { + tx: TxRecord { + txid: spend_txid, + version: 2, + locktime: 0, + input_start_fk: Fk::NULL, + input_count: 1, + output_start_fk: Fk::NULL, + output_count: 1, + }, + inputs: vec![InputRecord { + prev_txid: genesis_txid, + parent: rbitcoin_store::InputParent::Prevout { create_fk, vout: 0 }, + sequence: u32::MAX, + script_sig: vec![], + witness: vec![vec![0u8; 64], ser.to_vec()], + }], + outputs: vec![OutputRecord::unspent(49_0000_0000, p2tr.clone())], + }], + ) + .unwrap(); + + let naive = crate::tweaks_for_height(&q, ¶ms, Height(1)).unwrap(); + let t = naive.get(&spend_txid).unwrap(); + let spend_fk = q.block_tx_fks(Height(1)).unwrap()[0]; + let (_rec, _ins, outs) = q.store().get_tx_full(spend_fk).unwrap(); + let composed = + crate::tweaks_from_thin_and_body(&[(spend_txid, Some(t.tweak), Some(outs.as_slice()))]) + .unwrap(); + let c = composed.get(&spend_txid).unwrap(); + assert_eq!(c.tweak, t.tweak); + assert_eq!(c.output_pubkeys, t.output_pubkeys); + + let miss = crate::tweaks_from_thin_and_body(&[(spend_txid, Some(t.tweak), None)]); + assert!( + matches!(miss, Err(StoreError::Corrupt(m)) if m.contains("invariant")), + "got {miss:?}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn tweaks_for_height_unknown_is_empty() { + let (dir, q) = tmp_store(); + let params = ChainParams::regtest(); + assert!(crate::tweaks_for_height(&q, ¶ms, Height(0)) + .unwrap() + .is_empty()); + let main = ChainParams::mainnet(); + assert!(crate::tweaks_for_height(&q, &main, Height(100)) + .unwrap() + .is_empty()); + let _ = std::fs::remove_dir_all(&dir); +} + +/// A connect with tweaks on seals that height. Disconnect truncates, and +/// the replacement block is sealed on the next connect. +#[test] +fn live_connect_seals_tweaks_and_reorg_truncates() { + let (dir, q) = tmp_store(); + let params = ChainParams::regtest(); + q.set_sptweaks_enabled(true, Height(0)).unwrap(); + prepare_live_indexes(&q).unwrap(); + let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); + accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(1))); + let thin0 = q.load_thin_tweaks(Height(0)).unwrap().expect("indexed"); + assert!( + thin0.is_empty(), + "coinbase is not eligible — no Class A join" + ); + + let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); + accept_and_connect_block(&q, ¶ms, Height(1), &b1, Milestone::NONE).unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(2))); + + q.disconnect_tip().unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(1))); + assert!(q.load_thin_tweaks(Height(1)).unwrap().is_none()); + + let b1b = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 601, 1); + accept_and_connect_block(&q, ¶ms, Height(1), &b1b, Milestone::NONE).unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(2))); + assert!(q.load_thin_tweaks(Height(1)).unwrap().is_some()); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Contiguous connects seal tweaks. The materialize does not rewrite them. +#[test] +fn live_connect_seals_tweaks_builder_leaves_them() { + let (dir, q) = tmp_store(); + let params = ChainParams::regtest(); + q.set_sptweaks_enabled(true, Height(0)).unwrap(); + prepare_live_indexes(&q).unwrap(); + let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); + accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); + let b1 = mine_empty_regtest(genesis.block_hash(), genesis.header.time + 600, 1); + accept_and_connect_block(&q, ¶ms, Height(1), &b1, Milestone::NONE).unwrap(); + let b2 = mine_empty_regtest(b1.block_hash(), b1.header.time + 600, 2); + accept_and_connect_block(&q, ¶ms, Height(2), &b2, Milestone::NONE).unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(3))); + + q.release_index_writebehind(Height(2)); + build_indexes_released(&q).unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(3))); + for h in 0..=2 { + assert!( + q.load_thin_tweaks(Height(h)).unwrap().is_some(), + "height {h}" + ); + } + let _ = std::fs::remove_dir_all(&dir); +} + +struct SpKeys { + p2wpkh: Vec, + p2tr: Vec, + pubkey: Vec, +} + +fn sp_keys() -> SpKeys { + use bitcoin::hashes::hash160; + let sk = SecretKey::from_slice(&[2u8; 32]).unwrap(); + let pk = PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk); + let ser = pk.serialize(); + let mut p2wpkh = vec![0x00, 0x14]; + p2wpkh.extend_from_slice(hash160::Hash::hash(&ser).as_ref()); + let mut p2tr = vec![0x51, 0x20]; + p2tr.extend_from_slice(&pk.x_only_public_key().0.serialize()); + SpKeys { + p2wpkh, + p2tr, + pubkey: ser.to_vec(), + } +} + +fn sp_spend( + tag: u8, + prev: ([u8; 32], Fk, u32), + witness: Vec>, + outs: Vec, +) -> TxApply { + let mut txid = [0u8; 32]; + txid[0] = tag; + TxApply { + tx: TxRecord { + txid, + version: 2, + locktime: 0, + input_start_fk: Fk::NULL, + input_count: 1, + output_start_fk: Fk::NULL, + output_count: outs.len() as u32, + }, + inputs: vec![InputRecord { + prev_txid: prev.0, + parent: rbitcoin_store::InputParent::Prevout { + create_fk: prev.1, + vout: prev.2, + }, + sequence: u32::MAX, + script_sig: vec![], + witness, + }], + outputs: outs, + } +} + +fn seal_tweaks(q: &Query, through: u32) { + q.release_index_writebehind(Height(through)); + build_indexes_released(q).unwrap(); + assert_eq!(q.sptweaks_next_height(), Some(Height(through + 1))); +} + +/// Height 1 carries two eligible P2WPKH→P2TR spends around a spend with +/// fat seqsigwit and a fat output script. The naive walk, the index +/// window reader, the engine on the rebuilt wire tx, and the served +/// index all give the same tweaks, and the served read spans the +/// middle txout but not its seqsigwit. +fn pin_tweaks_agree_and_thin_serve_skips_fat_witness( + q: &Query, + params: &ChainParams, + keys: &SpKeys, + a: [u8; 32], + b: [u8; 32], +) { + let naive = crate::tweaks_for_height(q, params, Height(1)).unwrap(); + assert_eq!(naive.len(), 2, "the OP_TRUE spend is not eligible"); + let fks = q.block_tx_fks(Height(1)).unwrap(); + let wire_a = q.reconstruct_tx(fks[0]).unwrap(); + let engine_prev = [TxOut { + value: Amount::from_sat(20_0000_0000), + script_pubkey: ScriptBuf::from_bytes(keys.p2wpkh.clone()), + }]; + let engine = tweak_from_tx(&wire_a, &engine_prev).unwrap(); + assert_eq!(naive[&a].tweak, engine.tweak); + assert_eq!(naive[&a].output_pubkeys, engine.output_pubkeys); + + let want = vec![Some(naive[&a].tweak), None, Some(naive[&b].tweak)]; + for start in [0, 1] { + let heights = q.index_heights(start, 1, Some(0)).unwrap(); + let window = q.read_index_window(&heights).unwrap(); + let i = window.blocks.len() - 1; + assert_eq!( + tweak_records_from_window(&window, i).unwrap(), + want, + "window from {start}: the parent inside or outside the window" + ); + } + + let mid_seqsigwit = q.store().tx_seqsigwit_range(fks[1]).unwrap().1; + let mid_txout = q.store().txs.body_range(fks[1]).unwrap().1; + let _ = q.sample_reset_thin_tweak_body_bytes(); + let rows = q.load_thin_tweaks(Height(1)).unwrap().expect("indexed"); + let read = q.sample_reset_thin_tweak_body_bytes(); + assert_eq!( + rows.iter().map(|r| r.txid).collect::>(), + [a, b], + "ineligible txs are not joined from Class A" + ); + assert_eq!((rows[0].p2tr.len(), rows[1].p2tr.len()), (2, 1)); + assert!( + read >= mid_txout, + "the txout span covers the ineligible hole (read={read} txout={mid_txout})" + ); + assert!( + read < mid_seqsigwit, + "the fat seqsigwit stays out (read={read} seqsigwit={mid_seqsigwit})" + ); + let indexed = crate::tweaks_at_height(q, params, Height(1)).unwrap(); + for txid in [a, b] { + assert_eq!(indexed[&txid].tweak, naive[&txid].tweak); + assert_eq!(indexed[&txid].output_pubkeys, naive[&txid].output_pubkeys); + } +} + +fn pin_thin_range_limits_and_hole(q: &Query, a: [u8; 32], c: [u8; 32]) { + use crate::ThinTweakRangeLimits; + + let all = q + .load_thin_tweaks_range(Height(0), ThinTweakRangeLimits::default()) + .unwrap(); + assert_eq!( + all.iter() + .map(|(h, rows)| (h.0, rows.len())) + .collect::>(), + [(0, 0), (1, 2), (2, 1)] + ); + assert_eq!(all[2].1[0].txid, c); + for (h, rows) in &all { + let single = q.load_thin_tweaks(*h).unwrap().expect("indexed"); + assert_eq!(single.len(), rows.len()); + for (s, r) in single.iter().zip(rows) { + assert_eq!((s.txid, s.tweak, &s.p2tr), (r.txid, r.tweak, &r.p2tr)); + } + } + let limited = |max_heights, max_eligible| { + q.load_thin_tweaks_range( + Height(1), + ThinTweakRangeLimits { + max_heights, + max_eligible, + ..ThinTweakRangeLimits::default() + }, + ) + .unwrap() + }; + assert_eq!(limited(1, 8192).len(), 1); + assert_eq!( + limited(10, 1).len(), + 1, + "the eligible budget stops after the first height that spends it" + ); + assert!(q + .load_thin_tweaks_range(Height(3), ThinTweakRangeLimits::default()) + .unwrap() + .is_empty()); + let only2 = q + .load_thin_tweaks_range(Height(2), ThinTweakRangeLimits::default()) + .unwrap(); + assert_eq!(only2.len(), 1); + assert_eq!(only2[0].0, Height(2)); + + let cut = |cut_through| { + q.load_thin_tweaks_range( + Height(1), + ThinTweakRangeLimits { + cut_through, + ..ThinTweakRangeLimits::default() + }, + ) + .unwrap() + }; + let kept = cut(true); + let row_a = kept[0].1.iter().find(|r| r.txid == a).unwrap(); + assert_eq!( + row_a.p2tr.iter().map(|o| o.0).collect::>(), + [1], + "cut-through drops the P2TR output spent at height 2" + ); + let hist = cut(false); + assert_eq!( + hist[0].1.iter().find(|r| r.txid == a).unwrap().p2tr.len(), + 2 + ); +} + +/// One synthetic chain through the index builder: coinbase at 0, two +/// eligible spends around a fat ineligible one at 1, a spend of one of +/// those P2TR outputs plus a third eligible spend at 2 (sealed with 1 in +/// one window), and at 3 the spend that leaves the first tx all spent. +#[test] +fn sp_tweaks_confirm_life() { + let (dir, q) = tmp_store(); + let params = ChainParams::regtest(); + let keys = sp_keys(); + q.set_sptweaks_enabled(true, Height(0)).unwrap(); + let sig_wit = || vec![vec![0u8; 64], keys.pubkey.clone()]; + let p2tr = |sat| OutputRecord::unspent(sat, keys.p2tr.clone()); + + let mut cb_txid = [0u8; 32]; + cb_txid[31] = 0xcb; + let h0 = header(0, Fk::NULL, None); + let fk0 = q + .connect_block( + Height(0), + &h0, + &[TxApply { + tx: TxRecord { + txid: cb_txid, + version: 1, + locktime: 0, + input_start_fk: Fk::NULL, + input_count: 1, + output_start_fk: Fk::NULL, + output_count: 4, + }, + inputs: vec![InputRecord::coinbase(u32::MAX, vec![0x00], vec![])], + outputs: vec![ + OutputRecord::unspent(20_0000_0000, keys.p2wpkh.clone()), + OutputRecord::unspent(20_0000_0000, keys.p2wpkh.clone()), + OutputRecord::unspent(5_0000_0000, vec![0x51]), + OutputRecord::unspent(5_0000_0000, keys.p2wpkh.clone()), + ], + }], + ) + .unwrap(); + seal_tweaks(&q, 0); + assert!( + q.load_thin_tweaks(Height(0)).unwrap().unwrap().is_empty(), + "a coinbase is not eligible" + ); + let cb = q.block_tx_fks(Height(0)).unwrap()[0]; + + let spend_a = sp_spend( + 0xaa, + (cb_txid, cb, 0), + sig_wit(), + vec![p2tr(12_0000_0000), p2tr(7_0000_0000)], + ); + let fat = sp_spend( + 0xfe, + (cb_txid, cb, 2), + vec![vec![0u8; 16_384]], + vec![OutputRecord::unspent(4_0000_0000, vec![0x51; 4096])], + ); + let spend_b = sp_spend(0xbb, (cb_txid, cb, 1), sig_wit(), vec![p2tr(19_0000_0000)]); + let (a, b) = (spend_a.tx.txid, spend_b.tx.txid); + let h1 = header(1, fk0, Some(h0.hash)); + let fk1 = q + .connect_block(Height(1), &h1, &[spend_a, fat, spend_b]) + .unwrap(); + let a_fk = q.block_tx_fks(Height(1)).unwrap()[0]; + + let spend_c = sp_spend(0xcc, (cb_txid, cb, 3), sig_wit(), vec![p2tr(4_0000_0000)]); + let c = spend_c.tx.txid; + let spend_a0 = sp_spend( + 0xd0, + (a, a_fk, 0), + vec![vec![0u8; 64]], + vec![OutputRecord::unspent(11_0000_0000, keys.p2wpkh.clone())], + ); + let h2 = header(2, fk1, Some(h1.hash)); + let fk2 = q + .connect_block(Height(2), &h2, &[spend_c, spend_a0]) + .unwrap(); + assert_eq!( + q.sptweaks_next_height(), + Some(Height(1)), + "confirm writes none" + ); + seal_tweaks(&q, 2); + + pin_tweaks_agree_and_thin_serve_skips_fat_witness(&q, ¶ms, &keys, a, b); + pin_thin_range_limits_and_hole(&q, a, c); + + let spend_a1 = sp_spend( + 0xd1, + (a, a_fk, 1), + vec![vec![0u8; 64]], + vec![OutputRecord::unspent(6_0000_0000, vec![0x51])], + ); + let h3 = header(3, fk2, Some(h2.hash)); + q.connect_block(Height(3), &h3, &[spend_a1]).unwrap(); + seal_tweaks(&q, 3); + let gone = q + .load_thin_tweaks_range( + Height(1), + crate::ThinTweakRangeLimits { + cut_through: true, + ..crate::ThinTweakRangeLimits::default() + }, + ) + .unwrap(); + assert_eq!( + gone[0].1.iter().map(|r| r.txid).collect::>(), + [b], + "an all-spent eligible tx is omitted" + ); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/rbitcoin-query/src/stamp.rs b/crates/rbitcoin-query/src/stamp.rs index 8b9fbb11bf..bb7153a212 100644 --- a/crates/rbitcoin-query/src/stamp.rs +++ b/crates/rbitcoin-query/src/stamp.rs @@ -9,8 +9,8 @@ //! later-wave stamp reads pin loc. IBD (`skeleton = Some`) never loc-by-fk: //! spent is pin loc, skeleton TipOnly loc, or unset for write fill. -use crate::id_map::{IdMap, TxidHasher}; use crate::{CreatePin, InFlight, QueryError, U64Map}; +use crate::{IdMap, TxidHasher}; use rbitcoin_primitives::Fk; use rbitcoin_store::Store; use std::collections::HashMap; diff --git a/crates/rbitcoin-consensus/src/block/structure_rule_tests.rs b/crates/rbitcoin-query/src/structure_rule_tests.rs similarity index 90% rename from crates/rbitcoin-consensus/src/block/structure_rule_tests.rs rename to crates/rbitcoin-query/src/structure_rule_tests.rs index 496b3cb972..a00e67e51a 100644 --- a/crates/rbitcoin-consensus/src/block/structure_rule_tests.rs +++ b/crates/rbitcoin-query/src/structure_rule_tests.rs @@ -1,17 +1,10 @@ //! structure_rule_tests (peeled from block.rs). -use super::{ - apply_witness_commitment, bip16_active_from_prev_mtp, bip34_height_script, block_has_witness, - block_has_witness_from_pres, block_subsidy, check_tx_local, is_p2sh_script, is_p2wpkh_program, - is_p2wsh_program, last_script_push, merkle_root_bytes, script_sigop_count, - validate_block_structure, validate_block_structure_with_pres, witness_commitment_script, - ScriptCheckJob, ScriptVerifyFlags, TxPrecompute, ValidationContext, BIP16_EXCEPTION_MAINNET, - MAX_BLOCK_STRIPPED_SIZE, MAX_BLOCK_TX_COUNT, MAX_BLOCK_WEIGHT, MIN_TX_WEIGHT, - TAPROOT_EXCEPTION_MAINNET, +use crate::block_pipeline::{ + assemble_block_prevouts, resolve_prevout, structural_validate_spends, AsmPrevoutAcc, + ClassAWave, RunCreateHeight, StructuralScratch, }; -use crate::error::ConsensusError; -use crate::milestone::Milestone; -use crate::params::ChainParams; +use crate::ConfirmError; use bitcoin::absolute::LockTime; use bitcoin::block::{Header, Version}; use bitcoin::consensus::encode::VarInt; @@ -22,6 +15,16 @@ use bitcoin::{ Amount, Block, BlockHash, CompactTarget, OutPoint, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Witness, }; +use rbitcoin_consensus::block::{ + apply_witness_commitment, bip16_active_from_prev_mtp, bip34_height_script, block_has_witness, + block_has_witness_from_pres, block_subsidy, check_tx_local, is_p2sh_script, is_p2wpkh_program, + is_p2wsh_program, last_script_push, merkle_root_bytes, script_sigop_count, + validate_block_structure, validate_block_structure_with_pres, witness_commitment_script, + PendingCreates, ScriptCheckJob, ScriptVerifyFlags, ValidationContext, BIP16_EXCEPTION_MAINNET, + MAX_BLOCK_STRIPPED_SIZE, MAX_BLOCK_TX_COUNT, MAX_BLOCK_WEIGHT, MIN_TX_WEIGHT, + TAPROOT_EXCEPTION_MAINNET, +}; +use rbitcoin_consensus::{ChainParams, ConsensusError, Milestone, TxPrecompute}; use rbitcoin_primitives::Height; fn params() -> ChainParams { @@ -37,11 +40,11 @@ fn ctx_h(height: u32) -> ValidationContext<'static> { #[test] fn check_block_wire_junk_does_not_panic() { for data in [b"".as_slice(), &[0u8; 1], &[0xff; 80], b"not-a-block"] { - let _ = super::check_block_wire(data); + let _ = rbitcoin_consensus::block::check_block_wire(data); } let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); let raw = bitcoin::consensus::encode::serialize(&genesis); - let _ = super::check_block_wire(&raw); + let _ = rbitcoin_consensus::block::check_block_wire(&raw); } fn coinbase(height: u32) -> Transaction { @@ -112,12 +115,15 @@ fn block_with(txs: Vec) -> Block { /// `stamp_sub struct=` split: hash-encode vs extra walks (no algorithm change). #[test] fn structure_meters_split_txid_wtxid_walk() { - let stats = rbitcoin_query::ConfirmStats::default(); + let stats = crate::ConfirmStats::default(); let mut spend = non_coinbase_spend(1); spend.input[0].witness = Witness::from_slice(&[&[0x01]]); let mut b = block_with(vec![coinbase(1), spend]); apply_witness_commitment(&mut b); - validate_block_structure_with_pres(&b, &ctx_h(1), None, Some(&stats)) + let mut note = |txid_ns, wtxid_ns, walk_ns| { + stats.note_struct_parts(txid_ns, wtxid_ns, walk_ns); + }; + validate_block_structure_with_pres(&b, &ctx_h(1), None, Some(&mut note)) .expect("witness block structure"); let s = stats.take_window(); assert!( @@ -132,8 +138,8 @@ fn structure_meters_split_txid_wtxid_walk() { #[test] fn structure_with_pres_skips_from_tx() { - use rbitcoin_query::TxPrecompute; - let stats = rbitcoin_query::ConfirmStats::default(); + use TxPrecompute; + let stats = crate::ConfirmStats::default(); let mut spend = non_coinbase_spend(1); spend.input[0].witness = Witness::from_slice(&[&[0x01]]); let mut b = block_with(vec![coinbase(1), spend]); @@ -144,11 +150,14 @@ fn structure_with_pres_skips_from_tx() { .map(TxPrecompute::from_tx) .collect::>() .into(); + let mut note = |txid_ns, wtxid_ns, walk_ns| { + stats.note_struct_parts(txid_ns, wtxid_ns, walk_ns); + }; let out = validate_block_structure_with_pres( &b, &ctx_h(1), Some(std::sync::Arc::clone(&pres)), - Some(&stats), + Some(&mut note), ) .expect("stashed pres must still enforce merkle"); assert_eq!(out.len(), pres.len()); @@ -168,7 +177,7 @@ fn structure_with_pres_skips_from_tx() { /// Confirm assemble must Arc-share lookup/structure pres, not `Arc::new(p.clone())`. #[test] fn script_jobs_from_same_pres_slice_share_pre() { - use rbitcoin_query::TxPrecompute; + use TxPrecompute; let spend = non_coinbase_spend(1); let pres: std::sync::Arc<[TxPrecompute]> = std::sync::Arc::from([TxPrecompute::from_tx(&spend)]); @@ -176,14 +185,14 @@ fn script_jobs_from_same_pres_slice_share_pre() { pres[0].txid, vec![], spend.clone(), - crate::block::ScriptVerifyFlags::buried(true, true, true, true, true), + ScriptVerifyFlags::buried(true, true, true, true, true), ) .with_pre_slice(std::sync::Arc::clone(&pres), 0); let job_b = ScriptCheckJob::with_txid( pres[0].txid, vec![], spend, - crate::block::ScriptVerifyFlags::buried(true, true, true, true, true), + ScriptVerifyFlags::buried(true, true, true, true, true), ) .with_pre_slice(std::sync::Arc::clone(&pres), 0); assert!( @@ -196,6 +205,13 @@ fn script_jobs_from_same_pres_slice_share_pre() { ); } +fn rule_err(err: ConfirmError) -> ConsensusError { + match err { + ConfirmError::Rules(e) => e, + other => panic!("expected a consensus rule reject, got {other}"), + } +} + fn assert_bad_block(err: ConsensusError, needle: &str) { match err { ConsensusError::BadBlock(s) => { @@ -345,10 +361,9 @@ fn padded_spend(data_len: usize) -> Transaction { /// An unspent connected coinbase with the same txid is BIP30 on regtest (no /// 91842/91880 grandfather). Signet activates BIP34 at height 1, and Core's /// empty BIP34 hash still enforces BIP30 on every signet block. -fn rejects_unspent_overwrite(q: &rbitcoin_query::Query, first: &Transaction) { - use crate::block::{structural_validate_spends, RunCreateHeight}; +fn rejects_unspent_overwrite(q: &crate::Query, first: &Transaction) { + use crate::{BatchParents, OutPointSet, U32Map}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, U32Map}; let dup = block_with(vec![first.clone()]); let signet = Box::leak(Box::new(ChainParams::signet())); for (ctx, net) in [ @@ -374,8 +389,8 @@ fn rejects_unspent_overwrite(q: &rbitcoin_query::Query, first: &Transaction) { &parents, &mut U32Map::default(), &heights, - &crate::block::ClassAWave::default(), - &mut crate::block::StructuralScratch::default(), + &ClassAWave::default(), + &mut StructuralScratch::default(), &abs_jobs, ) .expect_err("an unspent overwrite must trip BIP30"); @@ -389,11 +404,7 @@ fn rejects_unspent_overwrite(q: &rbitcoin_query::Query, first: &Transaction) { fn plant_unspent_coinbase( label: &str, -) -> ( - rbitcoin_store::testutil::TempDir, - rbitcoin_query::Query, - Transaction, -) { +) -> (rbitcoin_store::testutil::TempDir, crate::Query, Transaction) { let (path, q, first, _fk) = plant_coinbase_with_outputs(label, coinbase(1).output); (path, q, first) } @@ -404,13 +415,13 @@ fn plant_coinbase_with_outputs( outputs: Vec, ) -> ( rbitcoin_store::testutil::TempDir, - rbitcoin_query::Query, + crate::Query, Transaction, rbitcoin_primitives::Fk, ) { use rbitcoin_primitives::Fk; use rbitcoin_store::{InputRecord, OutputRecord, TxRecord}; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled(label); + let (path, q) = crate::testutil::tiny_query_labeled(label); q.enter_direct_index_mode().unwrap(); let mut first = coinbase(1); first.output = outputs; @@ -450,7 +461,7 @@ fn plant_coinbase_with_outputs( (path, q, first, fk) } -fn plant_bip34_ancestor(q: &rbitcoin_query::Query, hash: [u8; 32]) { +fn plant_bip34_ancestor(q: &crate::Query, hash: [u8; 32]) { use rbitcoin_store::HeaderRecord; let main = ChainParams::mainnet(); let hfk = q @@ -467,12 +478,11 @@ fn plant_bip34_ancestor(q: &rbitcoin_query::Query, hash: [u8; 32]) { } fn bip30_message_at_mainnet_above_bip34( - q: &rbitcoin_query::Query, + q: &crate::Query, block: &Block, -) -> Result<(), ConsensusError> { - use crate::block::{structural_validate_spends, RunCreateHeight}; +) -> Result<(), ConfirmError> { + use crate::{BatchParents, OutPointSet, U32Map}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, U32Map}; let main = Box::leak(Box::new(ChainParams::mainnet())); let ctx = ValidationContext::at(main, Height(main.btc.bip34_height + 1), Milestone::NONE); let heights = RunCreateHeight::Spans(Vec::new()); @@ -491,8 +501,8 @@ fn bip30_message_at_mainnet_above_bip34( &parents, &mut U32Map::default(), &heights, - &crate::block::ClassAWave::default(), - &mut crate::block::StructuralScratch::default(), + &ClassAWave::default(), + &mut StructuralScratch::default(), &abs_jobs, ) .map(|_| ()) @@ -500,7 +510,7 @@ fn bip30_message_at_mainnet_above_bip34( /// Mainnet skips BIP30 only when the header at BIP34 height is the real hash. /// A wrong ancestor still rejects an unspent overwrite. -fn mainnet_ancestor_skip_then_miss(q: &rbitcoin_query::Query, first: &Transaction) { +fn mainnet_ancestor_skip_then_miss(q: &crate::Query, first: &Transaction) { let main = ChainParams::mainnet(); let real = main.bip34_hash.expect("mainnet BIP34 hash").to_byte_array(); plant_bip34_ancestor(q, real); @@ -682,7 +692,10 @@ fn no_coinbase_with_a_64_byte_tx_is_mutated() { ), "{err:?}" ); - assert_eq!(crate::error::block_reject_reason(&err), "bad-cb-missing"); + assert_eq!( + rbitcoin_consensus::block_reject_reason(&err), + "bad-cb-missing" + ); } let err = validate_block_structure(&block_with(vec![a, b]), &ctx_h(1)).unwrap_err(); assert!( @@ -909,7 +922,7 @@ fn rejects_coinbase_excess_value_fast() { let mut cb = coinbase(1); cb.output[0].value = Amount::from_sat(50_0000_0000 + 1); let block = block_with(vec![cb]); - let err = super::check_coinbase_subsidy(&block, &ctx_h(1), 0).unwrap_err(); + let err = rbitcoin_consensus::block::check_coinbase_subsidy(&block, &ctx_h(1), 0).unwrap_err(); assert_bad_block(err, "coinbase excess value"); } @@ -986,25 +999,25 @@ fn last_script_push_rejects_a_non_push_and_a_short_pushdata() { #[test] fn p3_default_milestone_heights() { - use crate::params::default_milestone_height; + use rbitcoin_consensus::default_milestone_height; use rbitcoin_primitives::Network; assert_eq!(default_milestone_height(Network::Regtest), 0); assert_eq!(default_milestone_height(Network::Mainnet), 840_000); assert_eq!(default_milestone_height(Network::Testnet), 2_500_000); assert_eq!(default_milestone_height(Network::Signet), 0); - let anchor = crate::default_milestone_anchor(Network::Mainnet).unwrap(); + let anchor = rbitcoin_consensus::default_milestone_anchor(Network::Mainnet).unwrap(); assert_eq!( anchor.hash.to_string(), - crate::params::MAINNET_MILESTONE_HASH + rbitcoin_consensus::MAINNET_MILESTONE_HASH ); assert_ne!(anchor.min_work_be, [0u8; 32]); - let testnet = crate::default_milestone_anchor(Network::Testnet).unwrap(); + let testnet = rbitcoin_consensus::default_milestone_anchor(Network::Testnet).unwrap(); assert_eq!( testnet.hash.to_string(), - crate::params::TESTNET_MILESTONE_HASH + rbitcoin_consensus::TESTNET_MILESTONE_HASH ); - assert!(crate::default_milestone_anchor(Network::Signet).is_none()); - assert!(crate::default_milestone_anchor(Network::Regtest).is_none()); + assert!(rbitcoin_consensus::default_milestone_anchor(Network::Signet).is_none()); + assert!(rbitcoin_consensus::default_milestone_anchor(Network::Regtest).is_none()); } #[test] @@ -1208,18 +1221,16 @@ fn bip34_wrong_push_encoding_rejected_after_activation() { #[test] fn assemble_rejects_empty_and_fk_mismatch() { - use super::assemble_block_prevouts; - use rbitcoin_query::{BatchParents, OutPointSet, SpendEdges}; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("assemble"); + use crate::{BatchParents, OutPointSet, SpendEdges}; + use assemble_block_prevouts; let ctx = ctx_h(1); let empty = block_with(vec![]); let parents = BatchParents::new(); let thin = SpendEdges::default(); let mut spent = OutPointSet::default(); - let mut creates = super::PendingCreates::default(); + let mut creates = PendingCreates::default(); let zero = [0u8; 32]; let err = assemble_block_prevouts( - &q, &empty, &ctx, None, @@ -1233,10 +1244,11 @@ fn assemble_rejects_empty_and_fk_mismatch() { false, None, None, + true, ) .err() .expect("empty"); - assert_bad_block(err, "empty"); + assert_bad_block(rule_err(err), "empty"); // Wrong archived fk count: coinbase alone needs 1 fk; pass empty slice. let b = block_with(vec![coinbase(1)]); spent.clear(); @@ -1248,7 +1260,6 @@ fn assemble_rejects_empty_and_fk_mismatch() { .collect(); let bh = b.header.block_hash().to_byte_array(); let err2 = assemble_block_prevouts( - &q, &b, &ctx, Some(&[]), @@ -1262,10 +1273,11 @@ fn assemble_rejects_empty_and_fk_mismatch() { false, None, None, + true, ) .err() .expect("fk mismatch"); - assert_bad_block(err2, "archived tx fk"); + assert_bad_block(rule_err(err2), "archived tx fk"); // first tx not coinbase let bad = block_with(vec![non_coinbase_spend(1)]); spent.clear(); @@ -1277,7 +1289,6 @@ fn assemble_rejects_empty_and_fk_mismatch() { .collect(); let bh3 = bad.header.block_hash().to_byte_array(); let err3 = assemble_block_prevouts( - &q, &bad, &ctx, None, @@ -1291,25 +1302,25 @@ fn assemble_rejects_empty_and_fk_mismatch() { false, None, None, + true, ) .err() .expect("not coinbase"); - assert_bad_block(err3, "coinbase"); - let _ = std::fs::remove_dir_all(&path); + assert_bad_block(rule_err(err3), "coinbase"); } /// Pack creates are `txid → fk` (one insert). Meters flush once (in_n = spends). #[test] fn assemble_pending_creates_is_txid_map_and_meters_flush() { - use super::assemble_block_prevouts; + use crate::{BatchParents, OutPointSet, SpendEdges}; + use assemble_block_prevouts; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, SpendEdges}; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("assemble-creates"); + let (path, q) = crate::testutil::tiny_query_labeled("assemble-creates"); let ctx = ctx_h(1); let parents = BatchParents::new(); let thin = SpendEdges::default(); let mut spent = OutPointSet::default(); - let mut creates = super::PendingCreates::default(); + let mut creates = PendingCreates::default(); let b = block_with(vec![coinbase(1)]); let tids: Vec<[u8; 32]> = b .txdata @@ -1319,8 +1330,7 @@ fn assemble_pending_creates_is_txid_map_and_meters_flush() { let bh = b.header.block_hash().to_byte_array(); let bip16 = bip16_active_from_prev_mtp(ctx.params, ctx.height.0, &bh, 0); let _ = q.confirm_stats().take_window(); - assemble_block_prevouts( - &q, + let (_, _, _, _, meters) = assemble_block_prevouts( &b, &ctx, Some(&[Fk(1)]), @@ -1334,8 +1344,10 @@ fn assemble_pending_creates_is_txid_map_and_meters_flush() { bip16, None, None, + true, ) .expect("coinbase-only assemble"); + meters.acc.flush(q.confirm_stats()); assert_eq!(creates.len(), 1, "one create fk per tx, not per vout"); assert_eq!(creates.get(&tids[0]), Some(&Fk(1))); let w = q.confirm_stats().take_window(); @@ -1348,12 +1360,11 @@ fn assemble_pending_creates_is_txid_map_and_meters_flush() { /// Confirm jobs keep prevout scripts in the wire block or the pin Arc. #[test] fn script_job_shares_prevout_script() { - use super::assemble_block_prevouts; + use crate::{BatchParents, OutPointSet, SpendEdge, SpendEdges}; + use assemble_block_prevouts; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, SpendEdge, SpendEdges}; use rbitcoin_store::{OutputRecord, TxRecord}; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("share-prevout-script"); let ctx = ctx_h(1); let parent_txid = [0x42u8; 32]; let hist_script = vec![0x51, 0x11, 0x22]; @@ -1436,9 +1447,8 @@ fn script_job_shares_prevout_script() { }], ); let mut spent = OutPointSet::default(); - let mut creates = super::PendingCreates::default(); - let (jobs, _, _, _) = assemble_block_prevouts( - &q, + let mut creates = PendingCreates::default(); + let (jobs, _, _, _, _) = assemble_block_prevouts( &wire, &ctx, Some(&[Fk(1), Fk(2), Fk(3)]), @@ -1452,6 +1462,7 @@ fn script_job_shares_prevout_script() { bip16, Some(&wire), None, + true, ) .expect("assemble shared prevouts"); assert_eq!(jobs.len(), 2); @@ -1469,17 +1480,17 @@ fn script_job_shares_prevout_script() { wire.txdata[1].output[0].script_pubkey.as_bytes().as_ptr(), "same-block prevout script is the wire output" ); - let _ = std::fs::remove_dir_all(&path); } /// Optimistic IBD: unstamped parent must not recover via `tx_fk_by_txid_tip`. #[test] fn optimistic_assemble_unstamped_parent_is_invariant() { - use super::assemble_block_prevouts; - use crate::{accept_and_connect_block, mine_empty_regtest}; + use crate::accept_and_connect_block; + use crate::{BatchParents, OutPointSet, SpendEdges}; + use assemble_block_prevouts; + use rbitcoin_consensus::mine_empty_regtest; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, SpendEdges}; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("assemble-unstamped"); + let (path, q) = crate::testutil::tiny_query_labeled("assemble-unstamped"); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); @@ -1520,7 +1531,7 @@ fn optimistic_assemble_unstamped_parent_is_invariant() { let parents = BatchParents::new(); let thin = SpendEdges::default(); let mut spent = OutPointSet::default(); - let mut creates = super::PendingCreates::default(); + let mut creates = PendingCreates::default(); let create_txids: Vec<[u8; 32]> = block .txdata .iter() @@ -1529,7 +1540,6 @@ fn optimistic_assemble_unstamped_parent_is_invariant() { let bh = block.header.block_hash().to_byte_array(); let spend_fks = [Fk(3), Fk(4)]; let err = assemble_block_prevouts( - &q, &block, &ctx, Some(&spend_fks), @@ -1543,6 +1553,7 @@ fn optimistic_assemble_unstamped_parent_is_invariant() { bip16_active_from_prev_mtp(ctx.params, ctx.height.0, &bh, b1.header.time), None, None, + true, ) .err() .expect("unstamped parent must not head-recover"); @@ -1559,10 +1570,9 @@ fn optimistic_assemble_unstamped_parent_is_invariant() { /// coinbase still immature 4 deep. #[test] fn accept_rejects_connect_spend_rules() { - use crate::{ - accept_and_connect_block, mine_empty_regtest, pad_empty_from, prepare_regtest_candidate, - }; - let (_dir, q) = rbitcoin_query::testutil::tiny_query_labeled("connect-spend-rules"); + use crate::{accept_and_connect_block, pad_empty_from}; + use rbitcoin_consensus::{mine_empty_regtest, prepare_regtest_candidate}; + let (_dir, q) = crate::testutil::tiny_query_labeled("connect-spend-rules"); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); @@ -1600,7 +1610,7 @@ fn accept_rejects_connect_spend_rules() { let err = accept_and_connect_block(&q, ¶ms, Height(1), &same_cb, Milestone::NONE) .expect_err("same-block coinbase spend must be immature"); match err { - ConsensusError::BadTx("coinbase immature") => {} + ConfirmError::Rules(ConsensusError::BadTx("coinbase immature")) => {} other => panic!("expected coinbase immature, got {other}"), } @@ -1618,7 +1628,7 @@ fn accept_rejects_connect_spend_rules() { let err = accept_and_connect_block(&q, ¶ms, Height(2), &dup, Milestone::NONE) .expect_err("same-block double spend"); assert!( - matches!(err, ConsensusError::BadTx(s) if s.contains("double spend")), + matches!(err, ConfirmError::Rules(ConsensusError::BadTx(s)) if s.contains("double spend")), "same-block double spend: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(1))); @@ -1638,7 +1648,7 @@ fn accept_rejects_connect_spend_rules() { let err = accept_and_connect_block(&q, ¶ms, Height(2), &bad_order, Milestone::NONE) .expect_err("child-before-parent"); assert!( - matches!(err, ConsensusError::MissingPrevout), + matches!(err, ConfirmError::Rules(ConsensusError::MissingPrevout)), "child-before-parent: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(1))); @@ -1649,7 +1659,7 @@ fn accept_rejects_connect_spend_rules() { let err = accept_and_connect_block(&q, ¶ms, Height(2), &over, Milestone::NONE) .expect_err("in < out"); assert!( - matches!(err, ConsensusError::BadTx(s) if s.contains("in < out")), + matches!(err, ConfirmError::Rules(ConsensusError::BadTx(s)) if s.contains("in < out")), "in < out: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(1))); @@ -1662,7 +1672,7 @@ fn accept_rejects_connect_spend_rules() { let err = accept_and_connect_block(&q, ¶ms, Height(5), &b5, Milestone::NONE) .expect_err("height-1 coinbase still immature 4 deep"); assert!( - matches!(err, ConsensusError::BadTx(s) if s.contains("immature")), + matches!(err, ConfirmError::Rules(ConsensusError::BadTx(s)) if s.contains("immature")), "4-deep immature: {err:?}" ); } @@ -1671,10 +1681,9 @@ fn accept_rejects_connect_spend_rules() { /// missing prevout at any depth, with or without script checks. #[test] fn genesis_coinbase_is_never_a_coin() { - use crate::{ - accept_and_connect_block, mine_empty_regtest, pad_empty_from, prepare_regtest_candidate, - }; - let (_dir, q) = rbitcoin_query::testutil::tiny_query_labeled("genesis-cb-spend"); + use crate::{accept_and_connect_block, pad_empty_from}; + use rbitcoin_consensus::{mine_empty_regtest, prepare_regtest_candidate}; + let (_dir, q) = crate::testutil::tiny_query_labeled("genesis-cb-spend"); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); @@ -1706,7 +1715,7 @@ fn genesis_coinbase_is_never_a_coin() { let err = accept_and_connect_block(&q, ¶ms, Height(1), &b1, Milestone::NONE) .expect_err("genesis coinbase spend at height 1"); assert!( - matches!(err, ConsensusError::MissingPrevout), + matches!(err, ConfirmError::Rules(ConsensusError::MissingPrevout)), "height 1: {err:?}" ); @@ -1726,7 +1735,7 @@ fn genesis_coinbase_is_never_a_coin() { let err = accept_and_connect_block(&q, ¶ms, Height(h), &block, ms) .expect_err("mature genesis coinbase spend"); assert!( - matches!(err, ConsensusError::MissingPrevout), + matches!(err, ConfirmError::Rules(ConsensusError::MissingPrevout)), "{ms:?}: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(maturity))); @@ -1739,11 +1748,10 @@ fn genesis_coinbase_is_never_a_coin() { /// rejects it. `OP_TRUE` and `OP_RETURN` are not this case. #[test] fn anchored_milestone_skips_a_failing_p2pkh_script() { - use crate::milestone::MilestoneAnchor; - use crate::{accept_and_connect_block, mine_regtest_paying, pad_empty_from}; - use bitcoin::hashes::Hash; + use crate::{accept_and_connect_block, pad_empty_from}; + use rbitcoin_consensus::{mine_regtest_paying, MilestoneAnchor}; - let (_dir, q) = rbitcoin_query::testutil::tiny_query_labeled("anchor-skip-p2pkh"); + let (_dir, q) = crate::testutil::tiny_query_labeled("anchor-skip-p2pkh"); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); @@ -1820,7 +1828,7 @@ fn anchored_milestone_skips_a_failing_p2pkh_script() { let err = accept_and_connect_block(&q, ¶ms, Height(h), &block, above) .expect_err("one height above the anchor still runs the failing script"); assert!( - matches!(err, ConsensusError::Script(_)), + matches!(err, ConfirmError::Rules(ConsensusError::Script(_))), "expected a script failure, got {err:?}" ); assert_eq!(q.tip_height(), Some(Height(maturity))); @@ -1831,7 +1839,10 @@ fn anchored_milestone_skips_a_failing_p2pkh_script() { note(h, wrong); let err = accept_and_connect_block(&q, ¶ms, Height(h), &block, anchored(h, block_hash)) .expect_err("a different hash at the anchor height does not skip"); - assert!(matches!(err, ConsensusError::Script(_)), "{err:?}"); + assert!( + matches!(err, ConfirmError::Rules(ConsensusError::Script(_))), + "{err:?}" + ); assert_eq!(q.tip_height(), Some(Height(maturity))); q.clear_milestone_path_above(0); @@ -1848,10 +1859,9 @@ fn anchored_milestone_skips_a_failing_p2pkh_script() { /// connects, and the archive keeps the script bytes. #[test] fn spending_an_unspendable_output_is_missing_prevout() { - use crate::{ - accept_and_connect_block, mine_empty_regtest, pad_empty_from, prepare_regtest_candidate, - }; - let (_dir, q) = rbitcoin_query::testutil::tiny_query_labeled("unspendable-not-a-coin"); + use crate::{accept_and_connect_block, pad_empty_from}; + use rbitcoin_consensus::{mine_empty_regtest, prepare_regtest_candidate}; + let (_dir, q) = crate::testutil::tiny_query_labeled("unspendable-not-a-coin"); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); @@ -1932,7 +1942,7 @@ fn spending_an_unspendable_output_is_missing_prevout() { ); assert_eq!(q.tip_height(), Some(Height(maturity))); - let reject = |q: &rbitcoin_query::Query, + let reject = |q: &crate::Query, tip: BlockHash, time: u32, height: u32, @@ -1944,12 +1954,12 @@ fn spending_an_unspendable_output_is_missing_prevout() { let err = accept_and_connect_block(q, ¶ms, Height(height), &block, ms) .expect_err("unspendable output is not a coin"); assert!( - matches!(err, ConsensusError::MissingPrevout), + matches!(err, ConfirmError::Rules(ConsensusError::MissingPrevout)), "{ms:?} h={height}: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(height - 1))); }; - let connect = |q: &rbitcoin_query::Query, + let connect = |q: &crate::Query, tip: BlockHash, time: u32, height: u32, @@ -2018,12 +2028,11 @@ fn spending_an_unspendable_output_is_missing_prevout() { #[test] fn assemble_milestone_pin_still_rejects_bad_blk_sigops() { - use super::assemble_block_prevouts; + use crate::{BatchParents, OutPointSet, SpendEdge, SpendEdges}; + use assemble_block_prevouts; use bitcoin::hashes::{sha256, Hash}; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, SpendEdge, SpendEdges}; use rbitcoin_store::{OutputRecord, TxRecord}; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("assemble-ms-sigops"); let ws = vec![0xacu8; 80_001]; let h = sha256::Hash::hash(&ws); let mut spk = vec![0x00, 0x20]; @@ -2083,7 +2092,7 @@ fn assemble_milestone_pin_still_rejects_bad_blk_sigops() { }], ); let mut spent = OutPointSet::default(); - let mut creates = super::PendingCreates::default(); + let mut creates = PendingCreates::default(); let tids: Vec<[u8; 32]> = b .txdata .iter() @@ -2091,7 +2100,6 @@ fn assemble_milestone_pin_still_rejects_bad_blk_sigops() { .collect(); let bh = b.header.block_hash().to_byte_array(); let err = assemble_block_prevouts( - &q, &b, &ctx, Some(&[Fk::NULL, spend_fk]), @@ -2105,21 +2113,21 @@ fn assemble_milestone_pin_still_rejects_bad_blk_sigops() { true, None, None, + true, ) .err() .expect("over-budget pin P2WSH must reject"); - assert_bad_block(err, "bad-blk-sigops"); - let _ = std::fs::remove_dir_all(&path); + assert_bad_block(rule_err(err), "bad-blk-sigops"); } /// Anchored skip uses the header path. A height match with a different hash /// still builds script jobs. -fn signet_low_work_fork_runs_scripts(q: &rbitcoin_query::Query) { - use super::assemble_block_prevouts; - use crate::milestone::MilestoneAnchor; +fn signet_low_work_fork_runs_scripts(q: &crate::Query) { + use crate::{BatchParents, OutPointSet, SpendEdge, SpendEdges}; + use assemble_block_prevouts; use bitcoin::hashes::Hash; + use rbitcoin_consensus::MilestoneAnchor; use rbitcoin_primitives::Fk; - use rbitcoin_query::{BatchParents, OutPointSet, SpendEdge, SpendEdges}; use rbitcoin_store::{OutputRecord, TxRecord}; let mut parent_txid = [0u8; 32]; parent_txid[0] = 0x42; @@ -2190,11 +2198,10 @@ fn signet_low_work_fork_runs_scripts(q: &rbitcoin_query::Query) { .map(|t| t.compute_txid().to_byte_array()) .collect(); let bh = b.header.block_hash().to_byte_array(); - let run = |q: &rbitcoin_query::Query| { + let run = |q: &crate::Query| { let mut spent = OutPointSet::default(); - let mut creates = super::PendingCreates::default(); + let mut creates = PendingCreates::default(); assemble_block_prevouts( - q, &b, &ctx, Some(&[Fk::NULL, spend_fk]), @@ -2208,12 +2215,13 @@ fn signet_low_work_fork_runs_scripts(q: &rbitcoin_query::Query) { true, None, None, + crate::milestone_gate::check_scripts(ms, q, 1, &bh), ) .expect("op_true spend assembles") .0 }; assert_eq!(run(q).len(), 1, "missing anchor path still checks scripts"); - assert!(crate::milestone::check_scripts(ms, q, 1, &bh)); + assert!(crate::milestone_gate::check_scripts(ms, q, 1, &bh)); let mut base = [0u8; 32]; base[1] = 2; let mut one = [0u8; 32]; @@ -2236,14 +2244,14 @@ fn signet_low_work_fork_runs_scripts(q: &rbitcoin_query::Query) { run(q).is_empty(), "header path through the anchor skips scripts" ); - assert!(!crate::milestone::check_scripts(ms, q, 1, &bh)); + assert!(!crate::milestone_gate::check_scripts(ms, q, 1, &bh)); q.clear_milestone_path_above(0); assert_eq!(run(q).len(), 1, "cleared path checks scripts again"); } /// Confirms a height-1 tx that spends `create_fk:0`. fn plant_confirmed_spend( - q: &rbitcoin_query::Query, + q: &crate::Query, create_txid: [u8; 32], create_fk: rbitcoin_primitives::Fk, ) { @@ -2285,13 +2293,41 @@ fn plant_confirmed_spend( .unwrap(); } +#[test] +fn bip30_and_immature_coinbase_reject_from_facts() { + use rbitcoin_consensus::block::{reject_bip30_unspent_prior, reject_one_create}; + assert!(reject_bip30_unspent_prior(2, 2, true).is_ok()); + let bip30 = reject_bip30_unspent_prior(5, 1, true).unwrap_err(); + assert!( + matches!(bip30, ConsensusError::BadTx(s) if s.contains("BIP30")), + "{bip30:?}" + ); + assert!(reject_bip30_unspent_prior(5, 1, false).is_ok()); + + let missing = reject_one_create(10, 100, None, false).unwrap_err(); + assert!( + matches!(missing, ConsensusError::BadTx(s) if s.contains("missingorspent")), + "{missing:?}" + ); + let forward = reject_one_create(10, 100, Some(11), false).unwrap_err(); + assert!( + matches!(forward, ConsensusError::MissingPrevout), + "{forward:?}" + ); + let immature = reject_one_create(10, 100, Some(1), true).unwrap_err(); + assert!( + matches!(immature, ConsensusError::BadTx(s) if s.contains("immature")), + "{immature:?}" + ); + assert!(reject_one_create(101, 100, Some(1), true).is_ok()); +} + /// Core never adds `IsUnspendable` outputs (leading `OP_RETURN`, or a script /// over 10_000 bytes) to the coin view, so they do not block a BIP30 /// overwrite. A spendable unspent output still does. #[test] fn bip30_ignores_unspendable_outputs() { - use crate::block::{structural_validate_spends, RunCreateHeight}; - use rbitcoin_query::{BatchParents, OutPointSet, U32Map}; + use crate::{BatchParents, OutPointSet, U32Map}; let outputs = vec![ TxOut { value: Amount::from_sat(50_0000_0000), @@ -2308,7 +2344,7 @@ fn bip30_ignores_unspendable_outputs() { ]; let (path, q, first, fk) = plant_coinbase_with_outputs("bip30-unspendable", outputs); let dup = block_with(vec![first.clone()]); - let check = |q: &rbitcoin_query::Query| { + let check = |q: &crate::Query| { let mut mtp_cache = U32Map::default(); mtp_cache.insert(9, 0); let parents = BatchParents::new(); @@ -2331,8 +2367,8 @@ fn bip30_ignores_unspendable_outputs() { &parents, &mut mtp_cache, &RunCreateHeight::Spans(Vec::new()), - &crate::block::ClassAWave::default(), - &mut crate::block::StructuralScratch::default(), + &ClassAWave::default(), + &mut StructuralScratch::default(), &abs_jobs, ) .err() @@ -2357,9 +2393,9 @@ fn bip30_ignores_unspendable_outputs() { /// with and without a script job. #[test] fn resolve_prevout_rejects_an_unspendable_script() { - use super::resolve_prevout; + use crate::block_pipeline::resolve_prevout; + use crate::BatchParents; use rbitcoin_primitives::Fk; - use rbitcoin_query::BatchParents; use rbitcoin_store::{OutputRecord, TxRecord}; let txid = [0x42u8; 32]; let fk = Fk(7); @@ -2383,7 +2419,7 @@ fn resolve_prevout_rejects_an_unspendable_script() { witness: Witness::new(), }; let block = block_with(vec![coinbase(1)]); - let txid_index = super::TxidMap::::default(); + let txid_index = rbitcoin_consensus::block::TxidMap::::default(); let try_spend = |script: Vec, need_script_buf: bool| { let mut parents = BatchParents::new(); parents.put_resolved( @@ -2405,23 +2441,29 @@ fn resolve_prevout_rejects_an_unspendable_script() { true, need_script_buf, None, - &mut super::AsmPrevoutAcc::default(), + &mut AsmPrevoutAcc::default(), ) }; - let show = |r: &Result<_, ConsensusError>| match r { + let show = |r: &Result<_, ConfirmError>| match r { Ok(_) => "resolved as a coin".to_string(), Err(e) => format!("{e:?}"), }; for need_script_buf in [false, true] { let oversized = try_spend(vec![0x51; 10_001], need_script_buf); assert!( - matches!(oversized, Err(ConsensusError::MissingPrevout)), + matches!( + oversized, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "need_script_buf={need_script_buf} oversized: {}", show(&oversized) ); let op_return = try_spend(vec![0x6a, 0x01, 0x42], need_script_buf); assert!( - matches!(op_return, Err(ConsensusError::MissingPrevout)), + matches!( + op_return, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "need_script_buf={need_script_buf} OP_RETURN: {}", show(&op_return) ); @@ -2450,13 +2492,13 @@ fn buried_rules_and_a_lying_header_path() { /// N1: Optimistic miss is lookup invariant; pin hit / identity / vout still classified. #[test] fn n1_assemble_cold_why_reasons() { - use super::resolve_prevout; use crate::accept_and_connect_block; - use crate::confirm_phase_stats; + use crate::block_pipeline::resolve_prevout; + use crate::block_pipeline::test_phase as confirm_phase_stats; + use crate::BatchParents; use rbitcoin_primitives::Fk; - use rbitcoin_query::BatchParents; use rbitcoin_store::OutputRecord; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("n1-cold-why"); + let (path, q) = crate::testutil::tiny_query_labeled("n1-cold-why"); let params = ChainParams::regtest(); let ms = Milestone::NONE; let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); @@ -2507,13 +2549,13 @@ fn n1_assemble_cold_why_reasons() { witness: Witness::new(), }; let empty_block = block_with(vec![coinbase(4)]); - let txid_index = super::TxidMap::::default(); + let txid_index = rbitcoin_consensus::block::TxidMap::::default(); // Thread-local N1 counters (process-global atomics race under parallel cargo test). let _ = confirm_phase_stats::sample_tl_assemble_cold_why_and_reset(); let _ = confirm_phase_stats::sample_tl_batch_cold_n_and_reset(); - fn assert_lookup_miss(err: ConsensusError) { + fn assert_lookup_miss(err: ConfirmError) { let msg = format!("{err}"); assert!( msg.contains("invariant") && msg.contains("lookup stage miss"), @@ -2536,7 +2578,7 @@ fn n1_assemble_cold_why_reasons() { false, true, None, - &mut super::AsmPrevoutAcc::default(), + &mut AsmPrevoutAcc::default(), ) .err() .expect("null_fk optimistic"); @@ -2558,7 +2600,7 @@ fn n1_assemble_cold_why_reasons() { false, true, None, - &mut super::AsmPrevoutAcc::default(), + &mut AsmPrevoutAcc::default(), ) .err() .expect("not_pin optimistic"); @@ -2590,7 +2632,7 @@ fn n1_assemble_cold_why_reasons() { false, true, None, - &mut super::AsmPrevoutAcc::default(), + &mut AsmPrevoutAcc::default(), ) .expect("batch hit"); let why = confirm_phase_stats::sample_tl_assemble_cold_why_and_reset(); @@ -2619,7 +2661,7 @@ fn n1_assemble_cold_why_reasons() { false, true, None, - &mut super::AsmPrevoutAcc::default(), + &mut AsmPrevoutAcc::default(), ) { Ok(_) => panic!("mismatch must hard-fail"), Err(e) => e, @@ -2656,7 +2698,7 @@ fn n1_assemble_cold_why_reasons() { false, true, None, - &mut super::AsmPrevoutAcc::default(), + &mut AsmPrevoutAcc::default(), ) { Ok(_) => panic!("vout_miss must hard-fail"), Err(e) => e, @@ -2692,7 +2734,7 @@ fn already_archived_schema13_pin_identity_tip_follow() { }; use std::sync::Arc; - let (path, q) = rbitcoin_query::testutil::tiny_query_labeled("plan-none-pin-id"); + let (path, q) = crate::testutil::tiny_query_labeled("plan-none-pin-id"); q.set_spend_index(true); let params = ChainParams::regtest(); let ms = Milestone::NONE; @@ -2863,7 +2905,7 @@ fn already_archived_schema13_pin_identity_tip_follow() { let err = accept_and_connect_block(&q, ¶ms, Height(h_n2 + 1), &b_ds, ms) .expect_err("double-spend of C1 must fail"); assert!( - matches!(err, ConsensusError::PrevoutSpent) + matches!(err, ConfirmError::Rules(ConsensusError::PrevoutSpent)) || format!("{err}").contains("spent") || format!("{err}").contains("prevout"), "got {err}" @@ -2988,89 +3030,6 @@ fn script_flags_follow_core_exception_table() { /// The exception hashes change execution, not only the flag bits. /// -/// A taproot key-path signature with hashtype byte `0x00` is invalid when -/// taproot is on and ignored when the mainnet exception turns taproot off. -/// A P2SH redeem of `OP_0` matches the template hash either way. On the BIP16 -/// exception the redeem is not executed. On the next hash it is, and fails. -#[test] -fn exception_hashes_change_which_scripts_run() { - let main = Box::leak(Box::new(ChainParams::mainnet())); - - let mut program = vec![0x51, 0x20]; - program.extend_from_slice(&[0xff; 32]); - let mut sig = vec![0x11u8; 65]; - sig[64] = 0x00; - let tap_tx = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::null(), - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::from_slice(&[sig.as_slice()]), - }], - output: vec![TxOut { - value: Amount::from_sat(1), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }], - }; - let tap_prev = vec![TxOut { - value: Amount::from_sat(2), - script_pubkey: ScriptBuf::from_bytes(program), - }]; - let tap_flags = |hash: &[u8; 32]| ScriptVerifyFlags::for_block(main, 692_261, hash, 0); - let accept = ScriptCheckJob::new( - tap_prev.clone(), - tap_tx.clone(), - tap_flags(&TAPROOT_EXCEPTION_MAINNET), - ); - crate::script::verify_job_all_inputs(&accept) - .expect("taproot exception does not run the key path"); - let reject = ScriptCheckJob::new(tap_prev, tap_tx, tap_flags(&[1u8; 32])); - let err = crate::script::verify_job_all_inputs(&reject).expect_err("neighbor enforces taproot"); - assert!( - format!("{err}").contains("sighash"), - "expected hashtype 0x00 reject, got {err}" - ); - - let redeem = [0x00u8]; - let redeem_hash = bitcoin::hashes::hash160::Hash::hash(&redeem); - let mut p2sh = vec![0xa9, 0x14]; - p2sh.extend_from_slice(redeem_hash.as_byte_array()); - p2sh.push(0x87); - let p2sh_tx = Transaction { - version: TxVersion::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::null(), - script_sig: ScriptBuf::from_bytes(vec![0x01, 0x00]), - sequence: Sequence::MAX, - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(1), - script_pubkey: ScriptBuf::from_bytes(vec![0x51]), - }], - }; - let p2sh_prev = vec![TxOut { - value: Amount::from_sat(2), - script_pubkey: ScriptBuf::from_bytes(p2sh), - }]; - let p2sh_flags = |hash: &[u8; 32]| ScriptVerifyFlags::for_block(main, 170_060, hash, 0); - let bare = ScriptCheckJob::new( - p2sh_prev.clone(), - p2sh_tx.clone(), - p2sh_flags(&BIP16_EXCEPTION_MAINNET), - ); - crate::script::verify_job_all_inputs(&bare) - .expect("BIP16 exception checks the hash and does not run the redeem"); - let enforced = ScriptCheckJob::new(p2sh_prev, p2sh_tx, p2sh_flags(&[2u8; 32])); - assert!( - crate::script::verify_job_all_inputs(&enforced).is_err(), - "neighbor executes the OP_0 redeem" - ); -} - /// Confirm jobs share wire Arc — same Transaction address, no deep clone. #[test] fn script_job_shared_tx_is_wire_pointer() { @@ -3096,12 +3055,12 @@ fn script_job_shared_tx_is_wire_pointer() { let tid = block.txdata[1].compute_txid().to_byte_array(); let job = ScriptCheckJob::from_parts( tid, - super::JobPrevouts::owned(vec![TxOut { + rbitcoin_consensus::block::JobPrevouts::owned(vec![TxOut { value: Amount::from_sat(50_0000_0000), script_pubkey: ScriptBuf::from_bytes(vec![0x51]), }]), - super::JobTx::shared(Arc::clone(&block), 1), - crate::block::ScriptVerifyFlags::buried(true, true, true, true, true), + rbitcoin_consensus::block::JobTx::shared(Arc::clone(&block), 1), + ScriptVerifyFlags::buried(true, true, true, true, true), ); assert!(std::ptr::eq( &*job.tx as *const Transaction, @@ -3234,7 +3193,7 @@ fn pres_has_witness_matches_block_walk() { #[test] fn exceeds_sigops_limit_matches_the_block_cap() { - use super::exceeds_sigops_limit; + use rbitcoin_consensus::block::exceeds_sigops_limit; assert!(!exceeds_sigops_limit(0)); assert!(!exceeds_sigops_limit(79_999)); assert!(!exceeds_sigops_limit(80_000)); @@ -3244,7 +3203,7 @@ fn exceeds_sigops_limit_matches_the_block_cap() { #[test] fn should_use_pres_requires_an_index_inside_the_slice() { - use super::should_use_pres; + use rbitcoin_consensus::block::should_use_pres; assert!(should_use_pres(0, 1)); assert!(!should_use_pres(1, 1)); assert!(!should_use_pres(0, 0)); @@ -3254,7 +3213,7 @@ fn should_use_pres_requires_an_index_inside_the_slice() { #[test] fn lock_time_cutoff_uses_block_time_at_height_zero_even_when_csv_is_active() { - use super::assemble_lock_time_cutoff; + use crate::block_pipeline::assemble_lock_time_cutoff; let mut block = block_with(vec![coinbase(0)]); block.header.time = 100; let mtp = 200; @@ -3281,7 +3240,7 @@ fn lock_time_cutoff_uses_block_time_at_height_zero_even_when_csv_is_active() { #[test] fn max_money_fits_in_i64_so_the_assemble_cast_is_in_range() { - use super::{exceeds_max_money, money_range_out_sum}; + use rbitcoin_consensus::block::{exceeds_max_money, money_range_out_sum}; let max_money = Amount::MAX_MONEY.to_sat(); assert!(!exceeds_max_money(0)); assert!(!exceeds_max_money(max_money)); @@ -3310,14 +3269,17 @@ fn assemble_without_pres_sums_zero_and_rejects_over_max() { }], } } - assert_eq!(super::assemble_tx_value_out(&one(0), 0, None).unwrap(), 0); + assert_eq!( + rbitcoin_consensus::block::assemble_tx_value_out(&one(0), 0, None).unwrap(), + 0 + ); let max_money = Amount::MAX_MONEY.to_sat(); assert_eq!( - super::assemble_tx_value_out(&one(max_money), 0, None).unwrap(), + rbitcoin_consensus::block::assemble_tx_value_out(&one(max_money), 0, None).unwrap(), max_money as i64 ); assert!(matches!( - super::assemble_tx_value_out(&one(max_money + 1), 0, None), + rbitcoin_consensus::block::assemble_tx_value_out(&one(max_money + 1), 0, None), Err(ConsensusError::BadBlock("bad-txns-vout-toolarge")) )); } @@ -3333,7 +3295,7 @@ fn witness_commitment_leaves_a_legacy_block_unchanged() { #[test] fn resolve_prevout_rejects_a_same_index_parent() { - use super::{resolve_prevout, AsmPrevoutAcc, TxidMap}; + use rbitcoin_consensus::block::TxidMap; let parent = coinbase(1); let txid = parent.compute_txid().to_byte_array(); let spend = Transaction { @@ -3363,7 +3325,7 @@ fn resolve_prevout_rejects_a_same_index_parent() { None, &index, 1, - &rbitcoin_query::BatchParents::new(), + &crate::BatchParents::new(), true, true, false, @@ -3377,9 +3339,9 @@ fn resolve_prevout_rejects_a_same_index_parent() { #[test] fn same_height_confirmed_spender_is_not_below_its_create() { - use super::structural_apply_one_meta; + use crate::block_pipeline::structural_apply_one_meta; use rbitcoin_primitives::Fk; - let (_dir, query) = rbitcoin_query::testutil::tiny_query_labeled("spend-same-h"); + let (_dir, query) = crate::testutil::tiny_query_labeled("spend-same-h"); let store = query.store(); store .confirmed @@ -3388,7 +3350,7 @@ fn same_height_confirmed_spender_is_not_below_its_create() { store.header_txs.put_range(Fk(1), Fk(1), 1).unwrap(); store.strong_tx.set_strong(Fk(1), Fk(1)).unwrap(); store.rebuild_height_fence().unwrap(); - let mut scratch = super::StructuralScratch::default(); + let mut scratch = crate::block_pipeline::StructuralScratch::default(); scratch.height_by_id.insert(9, 0); scratch.field_h_by_id.insert(1, 0); structural_apply_one_meta( diff --git a/crates/rbitcoin-query/src/wire_agree_tests.rs b/crates/rbitcoin-query/src/wire_agree_tests.rs new file mode 100644 index 0000000000..9c209889f4 --- /dev/null +++ b/crates/rbitcoin-query/src/wire_agree_tests.rs @@ -0,0 +1,85 @@ +//! Store wire walk and consensus decode agree on every truncation. +#![cfg(test)] + +use bitcoin::absolute::LockTime; +use bitcoin::hashes::Hash; +use bitcoin::transaction::Version; +use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; + +fn legacy_1in() -> Transaction { + Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: bitcoin::Txid::from_byte_array([0x11; 32]), + vout: 0, + }, + script_sig: ScriptBuf::from_bytes(vec![0x51, 0x51]), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(50_000), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + } +} + +fn p2wpkh_like() -> Transaction { + let mut tx = legacy_1in(); + tx.input[0].script_sig = ScriptBuf::new(); + tx.input[0].witness = Witness::from_slice(&[vec![0x30; 71], vec![0x02; 33]]); + tx +} + +const FLAG_ZERO_TX: [u8; 10] = [2, 0, 0, 0, 0x00, 0x00, 7, 0, 0, 0]; + +/// Peer intake walks the wire and lookup decodes it. A payload one side +/// accepts and the other refuses either strands a queued body or drops an +/// honest one, so every truncation and single-byte rewrite must agree. +#[test] +fn block_wire_walk_agrees_with_decode() { + use bitcoin::consensus::encode::serialize; + let zero_in = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + }; + // Segwit with one empty and one non-empty witness decodes; rewriting + // the non-empty stack count to 0 hits the all-empty refusal. + let mut mixed = p2wpkh_like(); + mixed.input.insert(0, legacy_1in().input[0].clone()); + mixed.input[1].witness = Witness::from_slice(&[[0x01]]); + let mut raw = vec![0u8; 80]; + raw.push(5); + for tx in [legacy_1in(), p2wpkh_like(), zero_in, mixed] { + raw.extend_from_slice(&serialize(&tx)); + } + raw.extend_from_slice(&FLAG_ZERO_TX); + let agree = |p: &[u8]| { + let decoded = rbitcoin_consensus::decode_block_precomputes(p, false) + .map(|(b, _, _)| b.txdata.iter().map(|t| t.input.len() as u32).sum()); + assert_eq!( + rbitcoin_store::block_wire_input_count(p), + decoded, + "payload {p:02x?}" + ); + decoded + }; + assert_eq!(agree(&raw), Some(4)); + for n in 0..raw.len() { + agree(&raw[..n]); + } + for i in 80..raw.len() { + for v in [0x00, 0x01, 0x02, 0xfd, 0xfe, 0xff] { + let mut p = raw.clone(); + p[i] = v; + agree(&p); + } + } +} diff --git a/crates/rbitcoin-rpc/src/blockstats.rs b/crates/rbitcoin-rpc/src/blockstats.rs index 74c90ad1c5..76649b6786 100644 --- a/crates/rbitcoin-rpc/src/blockstats.rs +++ b/crates/rbitcoin-rpc/src/blockstats.rs @@ -571,7 +571,7 @@ fn stats_for_stamped( height: Height, stamped: &StampedTxstatBlock, ) -> Result { - let mediantime = rbitcoin_consensus::median_time_past(ctx.query.as_ref(), height) + let mediantime = rbitcoin_query::median_time_past(ctx.query.as_ref(), height) .unwrap_or(stamped.rec.timestamp); let params = match ctx.chain.as_ref() { Some(c) => c.params.clone(), @@ -621,8 +621,8 @@ fn stats_for_connected( block: &Block, parents: &PrevoutMap, ) -> Result { - let mediantime = rbitcoin_consensus::median_time_past(ctx.query.as_ref(), height) - .unwrap_or(block.header.time); + let mediantime = + rbitcoin_query::median_time_past(ctx.query.as_ref(), height).unwrap_or(block.header.time); let params = match ctx.chain.as_ref() { Some(c) => c.params.clone(), None => rbitcoin_consensus::ChainParams::for_network(ctx.network), diff --git a/crates/rbitcoin-rpc/src/methods/chain.rs b/crates/rbitcoin-rpc/src/methods/chain.rs index b9535aea3f..b4c0c5505a 100644 --- a/crates/rbitcoin-rpc/src/methods/chain.rs +++ b/crates/rbitcoin-rpc/src/methods/chain.rs @@ -74,8 +74,8 @@ pub(crate) fn getblockchaininfo(ctx: &RpcContext) -> Result { }; let (time, mediantime) = if let Some(h) = ctx.query.tip_height() { if let Ok(Some((_, rec))) = ctx.query.header_at_height(h) { - let mtp = rbitcoin_consensus::median_time_past(ctx.query.as_ref(), h) - .unwrap_or(rec.timestamp); + let mtp = + rbitcoin_query::median_time_past(ctx.query.as_ref(), h).unwrap_or(rec.timestamp); (rec.timestamp, mtp) } else { (0u32, 0u32) @@ -285,7 +285,7 @@ fn active_block_header(ctx: &RpcContext, height: Height, verbose: bool) -> Resul "versionHex": format!("{:08x}", rec.version), "merkleroot": hash_hex_display(&rec.merkle_root), "time": rec.timestamp, - "mediantime": rbitcoin_consensus::median_time_past(ctx.query.as_ref(), height) + "mediantime": rbitcoin_query::median_time_past(ctx.query.as_ref(), height) .unwrap_or(rec.timestamp), "nonce": rec.nonce, "bits": format!("{:08x}", rec.bits), @@ -715,7 +715,7 @@ pub(crate) fn getblock(ctx: &RpcContext, params: &RpcParams) -> Result Result Result { .unwrap_or(0) }); let curtime = tip_time.saturating_add(1).max(now); - let bits = rbitcoin_consensus::expected_next_bits( - ctx.query.as_ref(), - ¶ms, - Height(next_h), - curtime, - ) - .map(|c| c.to_consensus()) - .unwrap_or(tip_bits); + let bits = + rbitcoin_query::expected_next_bits(ctx.query.as_ref(), ¶ms, Height(next_h), curtime) + .map(|c| c.to_consensus()) + .unwrap_or(tip_bits); let mintime = if tip_h == 0 { tip_time } else { - rbitcoin_consensus::median_time_past(ctx.query.as_ref(), Height(tip_h)).unwrap_or(tip_time) + rbitcoin_query::median_time_past(ctx.query.as_ref(), Height(tip_h)).unwrap_or(tip_time) }; let selected = mempool_block_txs(ctx); let mut fees = 0u64; @@ -1182,14 +1178,12 @@ fn submit_checkblock_reason( params: &rbitcoin_consensus::ChainParams, milestone: rbitcoin_consensus::Milestone, ) -> Result, rbitcoin_store::StoreError> { - use rbitcoin_consensus::ConsensusError; let vctx = match spend_height { Some(h) => rbitcoin_consensus::ValidationContext::at(params, Height(h), milestone), None => rbitcoin_consensus::ValidationContext::archive_structure(params), }; match rbitcoin_consensus::validate_block_structure(block, &vctx) { Ok(()) => Ok(None), - Err(ConsensusError::Store(e)) => Err(e), Err(e) => Ok(Some(rbitcoin_consensus::block_reject_reason(&e))), } } diff --git a/crates/rbitcoin-rpc/src/methods_tests.rs b/crates/rbitcoin-rpc/src/methods_tests.rs index f6921a4308..c25fa6c6b7 100644 --- a/crates/rbitcoin-rpc/src/methods_tests.rs +++ b/crates/rbitcoin-rpc/src/methods_tests.rs @@ -60,7 +60,7 @@ fn empty_102_pad() -> &'static Empty102Pad { let (ctx, dir) = ctx_empty(); let params = rbitcoin_consensus::ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - rbitcoin_consensus::accept_and_connect_block( + rbitcoin_query::accept_and_connect_block( &ctx.query, ¶ms, Height::GENESIS, @@ -68,7 +68,7 @@ fn empty_102_pad() -> &'static Empty102Pad { rbitcoin_consensus::Milestone::NONE, ) .unwrap(); - let (_tip, _time, coinbases) = rbitcoin_consensus::pad_empty_from( + let (_tip, _time, coinbases) = rbitcoin_query::pad_empty_from( &ctx.query, ¶ms, genesis.block_hash(), @@ -98,9 +98,8 @@ fn ctx_from_empty_102() -> (RpcContext, TempDir, Vec) { #[test] fn empty_102_pad_copy_keeps_the_other_tip() { - use rbitcoin_consensus::{ - accept_and_connect_block, mine_empty_regtest, ChainParams, Milestone, - }; + use rbitcoin_consensus::{mine_empty_regtest, ChainParams, Milestone}; + use rbitcoin_query::accept_and_connect_block; let (a, dir_a, cbs_a) = ctx_from_empty_102(); let (b, dir_b, cbs_b) = ctx_from_empty_102(); @@ -795,8 +794,9 @@ fn mempool_txid_lookups_do_not_list_live() { use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; let (ctx, dir) = ctx_empty(); let params = ChainParams::regtest(); @@ -810,7 +810,7 @@ fn mempool_txid_lookups_do_not_list_live() { ) .unwrap(); const N_SPENDS: u32 = 3; - let (_tip, _tip_time, coinbase_txids) = rbitcoin_consensus::pad_empty_from( + let (_tip, _tip_time, coinbase_txids) = rbitcoin_query::pad_empty_from( &ctx.query, ¶ms, genesis.block_hash(), @@ -2529,7 +2529,8 @@ fn generate_selects_chained_mempool_parent_first() { use bitcoin::consensus::encode::serialize; use bitcoin::transaction::Version as TxVersion; use bitcoin::{OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{pad_empty_from, ChainParams}; + use rbitcoin_consensus::ChainParams; + use rbitcoin_query::pad_empty_from; let (ctx, dir, _hub) = ctx_regtest_hub(); let params = ChainParams::regtest(); @@ -2646,7 +2647,7 @@ fn getblockstats_coinbase_only_and_op_return_match_helper() { let want = crate::blockstats::compute_block_stats( 1, &block, - rbitcoin_consensus::median_time_past(hub.query.as_ref(), Height(1)) + rbitcoin_query::median_time_past(hub.query.as_ref(), Height(1)) .unwrap_or(block.header.time), rbitcoin_consensus::block_subsidy(1, &hub.params), |op| { @@ -2729,7 +2730,7 @@ fn getblockstats_coinbase_only_and_op_return_match_helper() { let want = crate::blockstats::compute_block_stats( h, &block, - rbitcoin_consensus::median_time_past(hub.query.as_ref(), Height(h)) + rbitcoin_query::median_time_past(hub.query.as_ref(), Height(h)) .unwrap_or(block.header.time), rbitcoin_consensus::block_subsidy(h, &hub.params), |op| { @@ -3538,8 +3539,9 @@ fn testmempoolaccept_rbf_does_not_evict_conflict() { use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; let (ctx, dir) = ctx_empty(); let params = ChainParams::regtest(); @@ -3552,7 +3554,7 @@ fn testmempoolaccept_rbf_does_not_evict_conflict() { Milestone::NONE, ) .unwrap(); - let (_tip, _tip_time, coinbase_txids) = rbitcoin_consensus::pad_empty_from( + let (_tip, _tip_time, coinbase_txids) = rbitcoin_query::pad_empty_from( &ctx.query, ¶ms, genesis.block_hash(), diff --git a/crates/rbitcoin-store/src/integrity.rs b/crates/rbitcoin-store/src/integrity.rs index 929ad4ac91..a1f16f68b9 100644 --- a/crates/rbitcoin-store/src/integrity.rs +++ b/crates/rbitcoin-store/src/integrity.rs @@ -11,8 +11,9 @@ use crate::error::StoreError; use crate::header_table::block_header_hash; use crate::store::Store; -use bitcoin_hashes::{sha256, Hash, HashEngine}; -use rbitcoin_primitives::{schema_file_openable, Fk, Height, SCHEMA_VERSION, STORE_MAGIC}; +use rbitcoin_primitives::{ + merkle_root_from_txids, schema_file_openable, Fk, Height, SCHEMA_VERSION, STORE_MAGIC, +}; use std::fs::OpenOptions; use std::io::{Read, Write}; use std::path::Path; @@ -171,78 +172,6 @@ impl TipRevalidateReport { } } -/// Bitcoin block merkle root from leaf txids (internal byte order). -pub fn merkle_root_from_txids(leaves: &[[u8; 32]]) -> [u8; 32] { - merkle_root_mutated(leaves).0 -} - -/// [`merkle_root_from_txids`] plus Core's `ComputeMerkleRoot` mutation flag -/// (CVE-2012-2459): some level held two equal hashes in one pair before odd -/// padding, so a shorter tx list has the same root. -pub fn merkle_root_mutated(leaves: &[[u8; 32]]) -> ([u8; 32], bool) { - if leaves.is_empty() { - return ([0u8; 32], false); - } - let mut mutated = false; - let mut level: Vec<[u8; 32]> = leaves.to_vec(); - while level.len() > 1 { - mutated |= level.chunks_exact(2).any(|pair| pair[0] == pair[1]); - level = merkle_parent_level(level); - } - (level[0], mutated) -} - -/// Sibling hashes from `leaves[index]` up to the root, deepest first. Empty for -/// one leaf. Folding the leaf with them (left when the index bit is 0) -/// reproduces [`merkle_root_from_txids`]. The leaf's own value feeds no entry. -pub fn merkle_branch(leaves: &[[u8; 32]], mut index: usize) -> Vec<[u8; 32]> { - let mut branch = Vec::new(); - let mut level: Vec<[u8; 32]> = leaves.to_vec(); - while level.len() > 1 { - let sibling = (index ^ 1).min(level.len() - 1); - branch.push(level[sibling]); - level = merkle_parent_level(level); - index /= 2; - } - branch -} - -/// Root reached by folding `leaf` at `index` with its [`merkle_branch`]. The -/// leaf is the left child when the index bit at that depth is 0. -pub fn merkle_root_from_branch(leaf: [u8; 32], branch: &[[u8; 32]], index: usize) -> [u8; 32] { - branch - .iter() - .enumerate() - .fold(leaf, |acc, (depth, sibling)| { - if (index >> depth) & 1 == 0 { - hash256_concat(&acc, sibling) - } else { - hash256_concat(sibling, &acc) - } - }) -} - -// Bitcoin pairs an odd last node with itself. -fn merkle_parent_level(mut level: Vec<[u8; 32]>) -> Vec<[u8; 32]> { - if level.len() % 2 == 1 { - level.push(level[level.len() - 1]); - } - level - .chunks_exact(2) - .map(|pair| hash256_concat(&pair[0], &pair[1])) - .collect() -} - -fn hash256_concat(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] { - let mut eng = sha256::HashEngine::default(); - eng.input(a); - eng.input(b); - let mid = sha256::Hash::from_engine(eng); - let mut eng2 = sha256::HashEngine::default(); - eng2.input(mid.as_byte_array()); - sha256::Hash::from_engine(eng2).to_byte_array() -} - impl Store { /// Persist tip seal after a complete Class C tip barrier (connect or disconnect). /// @@ -663,7 +592,7 @@ mod tests { use super::*; use crate::header_table::HeaderRecord; use crate::tx_table::{InputRecord, OutputRecord, TxRecord}; - use rbitcoin_primitives::Fk; + use rbitcoin_primitives::{merkle_branch, merkle_root_from_branch, merkle_root_mutated, Fk}; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; @@ -729,7 +658,7 @@ mod tests { assert_eq!(merkle_root_from_txids(&[a]), a); let b = [2u8; 32]; let root = merkle_root_from_txids(&[a, b]); - assert_eq!(root, hash256_concat(&a, &b)); + assert_eq!(root, merkle_root_from_branch(a, &[b], 0)); } /// CVE-2012-2459: a repeated tail at any level keeps the root and is @@ -760,26 +689,17 @@ mod tests { assert_eq!(merkle_branch(&[a, b], 0), vec![b]); assert_eq!( merkle_branch(&[a, b, c], 0), - vec![b, hash256_concat(&c, &c)] + vec![b, merkle_root_from_txids(&[c, c])] ); assert_eq!( merkle_branch(&[a, b, c], 2), - vec![c, hash256_concat(&a, &b)] + vec![c, merkle_root_from_txids(&[a, b])] ); for n in 1..=9u8 { let leaves: Vec<[u8; 32]> = (0..n).map(|i| [i + 10; 32]).collect(); let root = merkle_root_from_txids(&leaves); for (index, leaf) in leaves.iter().enumerate() { let branch = merkle_branch(&leaves, index); - let mut h = *leaf; - for (depth, sib) in branch.iter().enumerate() { - h = if (index >> depth) & 1 == 0 { - hash256_concat(&h, sib) - } else { - hash256_concat(sib, &h) - }; - } - assert_eq!(h, root, "n={n} index={index}"); assert_eq!( merkle_root_from_branch(*leaf, &branch, index), root, diff --git a/crates/rbitcoin-store/src/lib.rs b/crates/rbitcoin-store/src/lib.rs index 792666fb7d..aedcb31f2c 100644 --- a/crates/rbitcoin-store/src/lib.rs +++ b/crates/rbitcoin-store/src/lib.rs @@ -82,12 +82,12 @@ pub use idx_body_pipeline::{BodyMode as IdxBodyMode, IdxBodyJob, SpendSlotRun}; pub use index_build_uring::{read_index_window, IndexBlock, IndexHeight, IndexWindow}; pub use input::InputEdge; pub use int_map::{FkMap, FkSet, U32Map, U64IdentityHasher, U64Map, U64Set}; -pub use integrity::{ - merkle_branch, merkle_root_from_branch, merkle_root_from_txids, merkle_root_mutated, - VERIFY_TIP_BLOCKS, -}; +pub use integrity::VERIFY_TIP_BLOCKS; pub use io_backend::{ReadIoBackend, WriteIoBackend}; pub use point_table::PointRecord; +pub use rbitcoin_primitives::{ + merkle_branch, merkle_root_from_branch, merkle_root_from_txids, merkle_root_mutated, +}; pub use scripthash::{ script_hash, sh_heads_insert_capped, ColdProgress, ScriptHashRecord, INCLUDE_HWM_NAME, SH_HEADS_CAP, diff --git a/crates/rbitcoin-sv2/Cargo.toml b/crates/rbitcoin-sv2/Cargo.toml index fb0c3c53e6..3127751c00 100644 --- a/crates/rbitcoin-sv2/Cargo.toml +++ b/crates/rbitcoin-sv2/Cargo.toml @@ -10,6 +10,7 @@ description = "Stratum v2 Template Distribution Protocol server (Noise over TCP) [dependencies] rbitcoin-log = { workspace = true } rbitcoin-consensus = { workspace = true } +rbitcoin-query = { workspace = true } rbitcoin-net = { workspace = true } rbitcoin-primitives = { workspace = true } rbitcoin-store = { workspace = true } diff --git a/crates/rbitcoin-sv2/src/template.rs b/crates/rbitcoin-sv2/src/template.rs index 80f5fa8bd5..e2a1ecf3f9 100644 --- a/crates/rbitcoin-sv2/src/template.rs +++ b/crates/rbitcoin-sv2/src/template.rs @@ -6,11 +6,11 @@ use bitcoin::consensus::encode::serialize; use bitcoin::hashes::Hash; use bitcoin::{Amount, ScriptBuf, Target, Transaction, TxOut}; use rbitcoin_consensus::{ - bip34_height_script, block_subsidy, expected_next_bits, median_time_past, - witness_commitment_script, MAX_BLOCK_WEIGHT, + bip34_height_script, block_subsidy, witness_commitment_script, MAX_BLOCK_WEIGHT, }; use rbitcoin_net::{ChainHub, SelectBudget}; use rbitcoin_primitives::Height; +use rbitcoin_query::{expected_next_bits, median_time_past}; use std::io; use std::sync::Arc; use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; diff --git a/crates/rbitcoin-sv2/src/template_tests.rs b/crates/rbitcoin-sv2/src/template_tests.rs index 01ca38ffc4..58374d8eec 100644 --- a/crates/rbitcoin-sv2/src/template_tests.rs +++ b/crates/rbitcoin-sv2/src/template_tests.rs @@ -12,11 +12,11 @@ use bitcoin::{ }; use bitcoin::{CompactTarget, Target}; use rbitcoin_consensus::{ - bip34_height_script, block_subsidy, confirm_scripts_phase, expected_next_bits, - median_time_past, mine_empty_regtest, mine_regtest_paying, witness_commitment_script, - ChainParams, + bip34_height_script, block_subsidy, mine_empty_regtest, mine_regtest_paying, + witness_commitment_script, ChainParams, }; use rbitcoin_primitives::Height; +use rbitcoin_query::{confirm_scripts_phase, expected_next_bits, median_time_past}; use rbitcoin_store::merkle_root_from_txids; use std::sync::Arc; use std::time::Duration; diff --git a/crates/rbitcoin-sv2/src/test_chain.rs b/crates/rbitcoin-sv2/src/test_chain.rs index 352323311b..898ec31ed3 100644 --- a/crates/rbitcoin-sv2/src/test_chain.rs +++ b/crates/rbitcoin-sv2/src/test_chain.rs @@ -1,8 +1,9 @@ use bitcoin::{Network, Txid}; -use rbitcoin_consensus::{accept_and_connect_block, pad_empty_from, ChainParams, Milestone}; +use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_net::{ChainHub, MempoolHub}; use rbitcoin_primitives::Height; use rbitcoin_query::testutil::{tiny_query_labeled, TempDir}; +use rbitcoin_query::{accept_and_connect_block, pad_empty_from}; use std::path::Path; use std::sync::Arc; diff --git a/crates/rbitcoin-test/src/chain_fixture.rs b/crates/rbitcoin-test/src/chain_fixture.rs index 4e5271bf87..7ece3abf1a 100644 --- a/crates/rbitcoin-test/src/chain_fixture.rs +++ b/crates/rbitcoin-test/src/chain_fixture.rs @@ -5,11 +5,10 @@ use std::sync::OnceLock; use bitcoin::hashes::Hash; use bitcoin::{Amount, Block, BlockHash, Txid}; -use rbitcoin_consensus::{ - accept_and_connect_block, pad_empty_from as consensus_pad, ChainParams, Milestone, -}; +use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; use rbitcoin_query::Query; +use rbitcoin_query::{accept_and_connect_block, pad_empty_from as consensus_pad}; use crate::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_spend}; @@ -158,7 +157,7 @@ fn mine_mature_regtest_with_spend(query: &Query, params: &ChainParams) -> Mature } } -/// Fast empty pad via [`rbitcoin_consensus::pad_empty_from`]. +/// Fast empty pad via [`rbitcoin_query::pad_empty_from`]. /// /// Mines heights `from_h..=last` on top of `tip` / `tip_time`. Prefer this for /// coinbase-maturity padding instead of looping `confirm_wire_run`. diff --git a/crates/rbitcoin-test/tests/consensus_rules.rs b/crates/rbitcoin-test/tests/consensus_rules.rs index 4ba7180ae2..02aa46d84b 100644 --- a/crates/rbitcoin-test/tests/consensus_rules.rs +++ b/crates/rbitcoin-test/tests/consensus_rules.rs @@ -4,12 +4,12 @@ use bitcoin::hashes::Hash; use bitcoin::{Amount, BlockHash, CompactTarget}; -use rbitcoin_consensus::{ - accept_and_connect_block, confirm_wire_run, expected_next_bits, genesis_block, - median_time_past, validate_header, ChainParams, ConsensusError, Milestone, -}; +use rbitcoin_consensus::{genesis_block, ChainParams, ConsensusError, Milestone}; use rbitcoin_primitives::Height; -use rbitcoin_query::Query; +use rbitcoin_query::{ + accept_and_connect_block, confirm_wire_run, expected_next_bits, median_time_past, + validate_header, ConfirmError, Query, +}; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_spend}; use rbitcoin_test::{pad_empty_from, TestDatadir}; @@ -157,7 +157,7 @@ fn pin_h6_pow_limit(q: &Query, params: &ChainParams, genesis: &bitcoin::Block) { tight.pow_limit = ChainParams::mainnet().pow_limit; let err = validate_header(q, &tight, Height(1), &near.header).unwrap_err(); assert!( - matches!(err, ConsensusError::BadHeader(s) if s.contains("pow limit")), + matches!(err, ConfirmError::Rules(ConsensusError::BadHeader(s)) if s.contains("pow limit")), "h6: {err:?}" ); } @@ -191,7 +191,7 @@ fn pin_bip68_time_lock( let early = mine_regtest_block(tip, time + 600, early_h, vec![csv_time.clone()]); let err = accept_and_connect_block(q, params, Height(early_h), &early, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx(s)) if s.contains("nonfinal")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("nonfinal")), "bip68 time just short: {err:?}" ); @@ -236,7 +236,7 @@ fn h8_rejects_timestamp_too_far_in_future() { } let err = validate_header(&q, ¶ms, Height(1), &bad.header).unwrap_err(); assert!( - matches!(err, ConsensusError::BadHeader(s) if s.contains("future")), + matches!(err, ConfirmError::Rules(ConsensusError::BadHeader(s)) if s.contains("future")), "{err:?}" ); } @@ -265,7 +265,7 @@ fn pin_subsidy_interval_two_overlay() { let old_floor = mine_regtest_block(h1.block_hash(), h1.header.time + 599, 2, vec![]); let err = accept_and_connect_block(&q, ¶ms, Height(2), &old_floor, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadBlock(s)) if s.contains("coinbase excess")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadBlock(s))) if s.contains("coinbase excess")), "50 BTC at interval must exceed the new 25 BTC floor: {err:?}" ); @@ -275,7 +275,7 @@ fn pin_subsidy_interval_two_overlay() { grind_pow(&mut excess); let err = accept_and_connect_block(&q, ¶ms, Height(2), &excess, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadBlock(s)) if s.contains("coinbase excess")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadBlock(s))) if s.contains("coinbase excess")), "subsidy+1 at new floor: {err:?}" ); @@ -312,7 +312,7 @@ fn h7_rejects_header_hash_above_target() { } let err = validate_header(&q, ¶ms, Height(1), &h).unwrap_err(); assert!( - matches!(err, ConsensusError::InvalidPow), + matches!(err, ConfirmError::Rules(ConsensusError::InvalidPow)), "expected InvalidPow, got {err:?}" ); } @@ -357,7 +357,7 @@ fn coinbase_maturity_holds_inside_one_confirm_batch() { assert_eq!(q.tip_height(), Some(Height(spend_h))); } else { assert!( - matches!(res, Err(ConsensusError::BadTx(s)) if s.contains("immature")), + matches!(res, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("immature")), "coinbase spent at created+99 in the same batch: {res:?}" ); assert_eq!(q.tip_height(), Some(Height::GENESIS)); @@ -402,7 +402,10 @@ fn same_batch_spend_of_a_later_block_is_missing() { ], ); assert!( - matches!(err, Err(ConsensusError::MissingPrevout)), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "spend of an output created one block later in the batch: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -410,7 +413,10 @@ fn same_batch_spend_of_a_later_block_is_missing() { // Both bodies are archived now, and the parent is not connected. let err = confirm_wire_run(&q, ¶ms, Milestone::NONE, &[(Height(101), child_first)]); assert!( - matches!(err, Err(ConsensusError::MissingPrevout)), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "the spending block retried alone: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -431,7 +437,7 @@ fn same_batch_spend_of_a_later_block_is_missing() { let b103 = mine_regtest_block(b102_hash, time + 1800, 103, vec![again]); let err = confirm_wire_run(&q, ¶ms, Milestone::NONE, &[(Height(103), b103)]); assert!( - matches!(err, Err(ConsensusError::PrevoutSpent)), + matches!(err, Err(ConfirmError::Rules(ConsensusError::PrevoutSpent))), "second spend of the parent output: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(102))); @@ -451,7 +457,7 @@ fn header_and_spending_boundaries() { bad_g.header.nonce = g.header.nonce.wrapping_add(1); let err = validate_header(&q, ¶ms, Height::GENESIS, &bad_g.header).unwrap_err(); assert!( - matches!(err, ConsensusError::BadHeader(s) if s.contains("genesis")), + matches!(err, ConfirmError::Rules(ConsensusError::BadHeader(s)) if s.contains("genesis")), "h1: {err:?}" ); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &g, Milestone::NONE).unwrap(); @@ -472,7 +478,9 @@ fn header_and_spending_boundaries() { assert!( matches!( err, - Err(ConsensusError::BadBlock("unexpected witness before segwit")) + Err(ConfirmError::Rules(ConsensusError::BadBlock( + "unexpected witness before segwit" + ))) ), "pre-activation witness block must hit the SegWit gate: {err:?}" ); @@ -481,14 +489,17 @@ fn header_and_spending_boundaries() { bad_prev.header.prev_blockhash = BlockHash::from_byte_array([0xee; 32]); grind_pow(&mut bad_prev); let err = validate_header(&q, ¶ms, Height(1), &bad_prev.header).unwrap_err(); - assert!(matches!(err, ConsensusError::BadPrev), "h2: {err:?}"); + assert!( + matches!(err, ConfirmError::Rules(ConsensusError::BadPrev)), + "h2: {err:?}" + ); let mut bad_bits = mine_regtest_block(g.block_hash(), g.header.time + 600, 1, vec![]); bad_bits.header.bits = CompactTarget::from_consensus(0x207f_fffe); grind_pow(&mut bad_bits); let err = validate_header(&q, ¶ms, Height(1), &bad_bits.header).unwrap_err(); assert!( - matches!(err, ConsensusError::BadHeader(s) if s.contains("bits") || s.contains("proof")), + matches!(err, ConfirmError::Rules(ConsensusError::BadHeader(s)) if s.contains("bits") || s.contains("proof")), "h5: {err:?}" ); @@ -513,7 +524,7 @@ fn header_and_spending_boundaries() { grind_pow(&mut eq); let err = validate_header(&q11, ¶ms, Height(12), &eq.header).unwrap_err(); assert!( - matches!(err, ConsensusError::BadHeader(s) if s.contains("median-time")), + matches!(err, ConfirmError::Rules(ConsensusError::BadHeader(s)) if s.contains("median-time")), "time==mtp: {err:?}" ); @@ -534,7 +545,7 @@ fn header_and_spending_boundaries() { ); let err = accept_and_connect_block(&q99, ¶ms, Height(100), &bad100, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx(s)) if s.contains("immature")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("immature")), "immature at created+99: {err:?}" ); assert_eq!(q99.tip_height(), Some(Height(99))); @@ -558,7 +569,10 @@ fn header_and_spending_boundaries() { let miss_block = mine_regtest_block(tip, time, 101, vec![missing]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &miss_block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::MissingPrevout)), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "{case} must reject as MissingPrevout: {err:?}" ); } @@ -571,7 +585,10 @@ fn header_and_spending_boundaries() { let oob_block = mine_regtest_block(tip, time, 101, vec![oob]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &oob_block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::MissingPrevout)), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "confirmed txid vout {vout} past n_out must reject as MissingPrevout: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -589,7 +606,10 @@ fn header_and_spending_boundaries() { &[(Height(101), b101), (Height(102), b102)], ); assert!( - matches!(err, Err(ConsensusError::MissingPrevout)), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "same-batch parent vout past n_out must reject as MissingPrevout: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -600,7 +620,7 @@ fn header_and_spending_boundaries() { let nf_block = mine_regtest_block(tip, time, 101, vec![nonfinal]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &nf_block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx(s)) if s.contains("nonfinal") || s.contains("not final")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("nonfinal") || s.contains("not final")), "locktime==height: {err:?}" ); @@ -610,7 +630,12 @@ fn header_and_spending_boundaries() { let csv_block = mine_regtest_block(tip, time, 101, vec![csv_early]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &csv_block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx("bad-txns-nonfinal"))), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::BadTx( + "bad-txns-nonfinal" + ))) + ), "relative lock 200 at height 101 must reject, got {err:?}" ); @@ -618,7 +643,7 @@ fn header_and_spending_boundaries() { let over_block = mine_regtest_block(tip, time, 101, vec![over]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &over_block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx(s)) if s.contains("in < out")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("in < out")), "in < out: {err:?}" ); @@ -628,7 +653,7 @@ fn header_and_spending_boundaries() { grind_pow(&mut excess); let err = accept_and_connect_block(&q, ¶ms, Height(101), &excess, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadBlock(s)) if s.contains("coinbase excess")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadBlock(s))) if s.contains("coinbase excess")), "subsidy+1: {err:?}" ); @@ -637,7 +662,7 @@ fn header_and_spending_boundaries() { let dup = mine_regtest_block(tip, time, 101, vec![s1, s2]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &dup, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx(s)) if s.contains("double spend")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("double spend")), "same-block double spend: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -647,7 +672,10 @@ fn header_and_spending_boundaries() { let bad_order = mine_regtest_block(tip, time, 101, vec![child, parent]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &bad_order, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::MissingPrevout)), + matches!( + err, + Err(ConfirmError::Rules(ConsensusError::MissingPrevout)) + ), "child-before-parent must not become tip: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -657,7 +685,7 @@ fn header_and_spending_boundaries() { let empty_block = mine_regtest_block(tip, time, 101, vec![empty_out]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &empty_block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::BadTx(s)) if s.contains("no outputs")), + matches!(err, Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("no outputs")), "non-coinbase empty vout: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); @@ -678,10 +706,10 @@ fn header_and_spending_boundaries() { let again = mine_regtest_block(good.block_hash(), time + 600, 102, vec![spent_again]); let err = accept_and_connect_block(&q, ¶ms, Height(102), &again, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::PrevoutSpent)) + matches!(err, Err(ConfirmError::Rules(ConsensusError::PrevoutSpent))) || matches!( err, - Err(ConsensusError::BadTx(s)) if s.contains("double spend") || s.contains("spent") + Err(ConfirmError::Rules(ConsensusError::BadTx(s))) if s.contains("double spend") || s.contains("spent") ), "already spent: {err:?}" ); @@ -740,7 +768,7 @@ fn witness_program_rules_bind_below_segwit_height() { let block = mine_regtest_block(tip, time + 600, 101, vec![fund, spend]); let err = accept_and_connect_block(&q, ¶ms, Height(101), &block, Milestone::NONE); assert!( - matches!(err, Err(ConsensusError::Script(_))), + matches!(err, Err(ConfirmError::Rules(ConsensusError::Script(_)))), "{case} below segwit height: {err:?}" ); assert_eq!(q.tip_height(), Some(Height(100))); diff --git a/crates/rbitcoin-test/tests/core_analogs.rs b/crates/rbitcoin-test/tests/core_analogs.rs index 9bdf93f7ff..81b0aa9120 100644 --- a/crates/rbitcoin-test/tests/core_analogs.rs +++ b/crates/rbitcoin-test/tests/core_analogs.rs @@ -16,12 +16,11 @@ use bitcoin::hashes::Hash; use bitcoin::{ Amount, BlockHash, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }; -use rbitcoin_consensus::{ - accept_and_connect_block, grind_regtest_pow, ChainParams, ConsensusError, Milestone, -}; +use rbitcoin_consensus::{grind_regtest_pow, ChainParams, ConsensusError, Milestone}; use rbitcoin_net::MempoolHub; use rbitcoin_primitives::Height; -use rbitcoin_query::Query; +use rbitcoin_query::accept_and_connect_block; +use rbitcoin_query::{ConfirmError, Query}; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_spend}; use rbitcoin_test::{ assert_reconstruct_eq, open_mature_regtest_with_spend, pad_empty_from, MatureRegtestChain, @@ -157,8 +156,10 @@ fn analog_milestone_and_mempool_persist() { assert!( matches!( err, - ConsensusError::MissingPrevout - | ConsensusError::BadTx("bad-txns-inputs-missingorspent") + ConfirmError::Rules( + ConsensusError::MissingPrevout + | ConsensusError::BadTx("bad-txns-inputs-missingorspent"), + ) ), "expected missing-prevout class under milestone, got: {err}" ); @@ -740,7 +741,7 @@ fn analog_block_filters_from_class_a() { let q = Arc::new(q); let run_appender = |until: &dyn Fn(Option) -> bool| { let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let bf = rbitcoin_consensus::spawn_index_writebehind( + let bf = rbitcoin_query::spawn_index_writebehind( Arc::clone(&q), Arc::clone(&stop), || {}, @@ -811,7 +812,7 @@ fn analog_block_filters_from_class_a() { "open drops the slot whose block left the best chain" ); q.release_index_writebehind(Height(last)); - rbitcoin_consensus::build_indexes_released(&q).unwrap(); + rbitcoin_query::build_indexes_released(&q).unwrap(); let (bytes, _) = q.basic_filter_at(last).unwrap().unwrap(); assert_eq!(bytes, reference(&alt).content); } diff --git a/crates/rbitcoin-test/tests/cross_surface.rs b/crates/rbitcoin-test/tests/cross_surface.rs index 97c9e73d21..d5f3660eda 100644 --- a/crates/rbitcoin-test/tests/cross_surface.rs +++ b/crates/rbitcoin-test/tests/cross_surface.rs @@ -6,11 +6,12 @@ use bitcoin::hashes::Hash; use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; -use rbitcoin_consensus::{accept_and_connect_block, pad_empty_from, ChainParams, Milestone}; +use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_electrum::electrum_scripthash_hex; use rbitcoin_node::{run_p2p, NodeConfig, Sv2AuthoritySecret}; use rbitcoin_primitives::{Height, Network}; use rbitcoin_query::Query; +use rbitcoin_query::{accept_and_connect_block, pad_empty_from}; use rbitcoin_test::{open_mature_regtest_with_spend, TestDatadir}; use serde_json::{json, Value}; use std::collections::HashMap; diff --git a/crates/rbitcoin-test/tests/electrum_protocol.rs b/crates/rbitcoin-test/tests/electrum_protocol.rs index 83fa14d752..90847abc3f 100644 --- a/crates/rbitcoin-test/tests/electrum_protocol.rs +++ b/crates/rbitcoin-test/tests/electrum_protocol.rs @@ -45,7 +45,7 @@ fn electrum_maturity() -> &'static ElectrumMaturity { let q = Query::open_or_create_tiny(&store).unwrap(); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - rbitcoin_consensus::accept_and_connect_block( + rbitcoin_query::accept_and_connect_block( &q, ¶ms, rbitcoin_primitives::Height::GENESIS, @@ -53,7 +53,7 @@ fn electrum_maturity() -> &'static ElectrumMaturity { Milestone::NONE, ) .unwrap(); - let (tip, tip_time, coinbases) = rbitcoin_consensus::pad_empty_from( + let (tip, tip_time, coinbases) = rbitcoin_query::pad_empty_from( &q, ¶ms, genesis.block_hash(), @@ -687,7 +687,7 @@ async fn electrum_server_version_history_balance() { next_h, vec![], ); - rbitcoin_consensus::accept_and_connect_block( + rbitcoin_query::accept_and_connect_block( q.as_ref(), ¶ms, rbitcoin_primitives::Height(next_h), @@ -739,7 +739,7 @@ async fn electrum_server_version_history_balance() { bitcoin::script::ScriptBuf::from_bytes(vec![0x00]), vec![], ); - rbitcoin_consensus::accept_and_connect_block( + rbitcoin_query::accept_and_connect_block( q.as_ref(), ¶ms, rbitcoin_primitives::Height(miss_h), @@ -1026,8 +1026,8 @@ fn block_touch_sees_spend_only_heights() { use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::accept_and_connect_block; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_store::script_hash; let dir = TempDir::new().unwrap(); @@ -1382,9 +1382,10 @@ async fn electrum_leftover_mempool_does_not_double_count() { use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{accept_and_connect_block, Milestone}; + use rbitcoin_consensus::Milestone; use rbitcoin_net::MempoolHub; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; let dir = TempDir::new().unwrap(); let params = ChainParams::regtest(); @@ -1670,8 +1671,9 @@ async fn electrum_and_esplora_asof_hides_later_spend() { use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{accept_and_connect_block, Milestone}; + use rbitcoin_consensus::Milestone; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; const ASOF: &str = "1.4.2-asof"; let dir = TempDir::new().unwrap(); @@ -2230,8 +2232,9 @@ async fn electrum_tweaks_subscribe_streams_then_done() { use bitcoin::hashes::{hash160, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{accept_and_connect_block, tweak_from_tx, Milestone}; + use rbitcoin_consensus::{tweak_from_tx, Milestone}; use rbitcoin_primitives::{Fk, Height}; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_query::testutil::FixtureChain; use rbitcoin_query::TxApply; use rbitcoin_store::{HeaderRecord, InputRecord, OutputRecord, TxRecord}; @@ -2587,8 +2590,9 @@ fn pin_tip_follow_after_materialize( tip: bitcoin::BlockHash, time: u32, ) { - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_test::mine::mine_regtest_block; let params = ChainParams::regtest(); @@ -2628,8 +2632,9 @@ fn pin_tip_follow_after_materialize( #[test] fn direct_indexes_then_sh_bulk_at_tip() { use bitcoin::hashes::Hash; - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_primitives::Height; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis}; let dir = TempDir::new().unwrap(); diff --git a/crates/rbitcoin-test/tests/integration_multinode.rs b/crates/rbitcoin-test/tests/integration_multinode.rs index a2e8575ce3..90f9fafe61 100644 --- a/crates/rbitcoin-test/tests/integration_multinode.rs +++ b/crates/rbitcoin-test/tests/integration_multinode.rs @@ -66,7 +66,7 @@ fn padded_query_pad() -> &'static PaddedQueryPad { use std::sync::OnceLock; static PAD: OnceLock = OnceLock::new(); PAD.get_or_init(|| { - use rbitcoin_consensus::accept_and_connect_block; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_test::pad_empty_from; let keep = TempDir::labeled("padded-query").expect("padded query pad"); @@ -877,7 +877,7 @@ fn mine_on(node: &P2PNode, height: u32) -> BlockHash { #[test] fn padded_query_copy_keeps_the_other_tip_and_filter_hwm() { - use rbitcoin_consensus::accept_and_connect_block; + use rbitcoin_query::accept_and_connect_block; let a = TempDir::new().unwrap(); let b = TempDir::new().unwrap(); @@ -1920,7 +1920,7 @@ async fn tip_follow_after_ibd() { peer.query .set_sptweaks_enabled(true, Height(ChainParams::regtest().taproot_height())) .unwrap(); - rbitcoin_consensus::prepare_live_indexes(&peer.query).unwrap(); + rbitcoin_query::prepare_live_indexes(&peer.query).unwrap(); sync_ibd(&peer, seed.local_addr).await; peer.wait_height(5, Duration::from_secs(10)) .await @@ -1941,7 +1941,7 @@ async fn tip_follow_after_ibd() { let spawn_builder = || { let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); let (q, at) = (Arc::clone(&pq), Arc::clone(&caught_up_at)); - let h = rbitcoin_consensus::spawn_index_writebehind( + let h = rbitcoin_query::spawn_index_writebehind( Arc::clone(&pq), Arc::clone(&stop), || {}, @@ -2103,7 +2103,7 @@ async fn reorg_to_longer_branch() { hub.accept_block(b).unwrap(); } assert_eq!(hub.tip_height(), Some(4)); - rbitcoin_consensus::build_indexes_released(&hub.query).unwrap(); + rbitcoin_query::build_indexes_released(&hub.query).unwrap(); assert_eq!(hub.query.basic_filter_hwm().unwrap(), Some(4)); let old_3 = hub.query.basic_filter_at(3).unwrap().unwrap().0; @@ -2136,7 +2136,7 @@ async fn reorg_to_longer_branch() { assert!(matches!(outcome, AcceptOutcome::Accepted { height: 6 })); assert_eq!(hub.tip_height(), Some(6)); assert_eq!(hub.tip_hash().unwrap(), branch.last().unwrap().block_hash()); - rbitcoin_consensus::build_indexes_released(&hub.query).unwrap(); + rbitcoin_query::build_indexes_released(&hub.query).unwrap(); assert_eq!(hub.query.basic_filter_hwm().unwrap(), Some(6)); for (h, b) in (3..).zip(&branch) { assert_eq!( @@ -2664,8 +2664,9 @@ fn pin_held_sixteen_vs_seventeen( /// longer side branch (tip-mode accept path, not IBD body-queue). #[test] fn reorg_same_height_then_multi_block_branch() { - use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; + use rbitcoin_consensus::{ChainParams, Milestone}; use rbitcoin_net::{AcceptOutcome, ChainHub}; + use rbitcoin_query::accept_and_connect_block; use rbitcoin_test::pad_empty_from; let dir = TempDir::new().unwrap(); diff --git a/crates/rbitcoin-test/tests/scenarios.rs b/crates/rbitcoin-test/tests/scenarios.rs index a225f53a19..2fe03c61d8 100644 --- a/crates/rbitcoin-test/tests/scenarios.rs +++ b/crates/rbitcoin-test/tests/scenarios.rs @@ -7,9 +7,10 @@ use bitcoin::consensus::encode::deserialize; use bitcoin::hashes::Hash; use bitcoin::{Amount, Block, BlockHash}; use rbitcoin_cli::cli_main as cli_cli_main; -use rbitcoin_consensus::{accept_and_connect_block, genesis_block, ChainParams, Milestone}; +use rbitcoin_consensus::{genesis_block, ChainParams, Milestone}; use rbitcoin_node::{cli_main as node_cli_main, run_node, NodeConfig}; use rbitcoin_primitives::{Fk, Height, Network, VERSION}; +use rbitcoin_query::accept_and_connect_block; use rbitcoin_query::testutil::FixtureChain; use rbitcoin_query::{stamp_external_parents, InFlight, Query}; use rbitcoin_store::{HeaderRecord, Store, StoreError, TxRecord}; @@ -1165,9 +1166,8 @@ fn chain_connect_reorg_and_growth() { /// false-positive PrevoutSpent (tip is the Class C commit point). #[test] fn confirm_survives_partial_class_c_without_tip_advance() { - use rbitcoin_consensus::{ - accept_and_connect_block, commit_class_a_block, confirm_wire_run, ChainParams, Milestone, - }; + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::{accept_and_connect_block, commit_class_a_block, confirm_wire_run}; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_spend}; let td = TestDatadir::new().unwrap(); @@ -1317,7 +1317,7 @@ fn pin_head_parent_via_waves_and_leftover_stamp( spend_h: u32, ) { use bitcoin::consensus::encode::serialize; - use rbitcoin_consensus::{ + use rbitcoin_query::{ confirm_bq_resolve_wave_capped, confirm_wire_lookup_stamp, take_wave_items_for_load, WireLoadPipeline, BQ_RESOLVE_WAVE_MAX_BLOCKS, BQ_RESOLVE_WAVE_MAX_INPUTS, }; @@ -1422,7 +1422,7 @@ fn pin_plan_none_stamp_of_archived_spend( b_spend: &Block, spend_h: u32, ) { - use rbitcoin_consensus::{confirm_wire_lookup_stamp, ConsensusError}; + use rbitcoin_query::confirm_wire_lookup_stamp; let hash = b_spend.block_hash().to_byte_array(); let (hfk, _) = q.get_header_by_hash(&hash).unwrap().unwrap(); @@ -1449,7 +1449,7 @@ fn pin_plan_none_stamp_of_archived_spend( let n = fks.len() as u32; q.store().header_txs.put_range(hfk, fks[0], n + 1).unwrap(); match confirm_wire_lookup_stamp(q, params, ms, &items, None) { - Err(ConsensusError::Store(StoreError::Corrupt(m))) => { + Err(rbitcoin_query::ConfirmError::Store(StoreError::Corrupt(m))) => { assert_eq!(m, "invariant: archived stamp tx_fks/txids length"); } Err(e) => panic!("list/wire length mismatch must be Corrupt, got {e}"), @@ -1461,9 +1461,8 @@ fn pin_plan_none_stamp_of_archived_spend( /// Resume: spend archived with create_fk (archive sticky/head); confirm spends. #[test] fn resume_tx_head_resolves_external_prev() { - use rbitcoin_consensus::{ - accept_and_connect_block, commit_class_a_block, confirm_wire_run, ChainParams, Milestone, - }; + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::{accept_and_connect_block, commit_class_a_block, confirm_wire_run}; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_spend}; let td = TestDatadir::new().unwrap(); @@ -1562,8 +1561,8 @@ fn resume_tx_head_resolves_external_prev() { #[test] fn consensus_mature_chain_spend_reconstruct_and_scripthash() { use bitcoin::p2p::ServiceFlags; - use rbitcoin_consensus::{commit_class_a_block, confirm_wire_run}; use rbitcoin_net::local_service_flags; + use rbitcoin_query::{commit_class_a_block, confirm_wire_run}; use rbitcoin_store::script_hash; let td = TestDatadir::new().unwrap(); @@ -1846,10 +1845,9 @@ fn consensus_mature_chain_spend_reconstruct_and_scripthash() { assert!(flags.has(ServiceFlags::WITNESS)); // Consensus header helpers on the same pad (no second open). - let mtp = rbitcoin_consensus::median_time_past(&q, Height(sample_tip)).unwrap(); + let mtp = rbitcoin_query::median_time_past(&q, Height(sample_tip)).unwrap(); assert!(mtp > 0, "mtp={mtp}"); - let bits = - rbitcoin_consensus::expected_next_bits(&q, ¶ms, Height(sample_tip + 1), 0).unwrap(); + let bits = rbitcoin_query::expected_next_bits(&q, ¶ms, Height(sample_tip + 1), 0).unwrap(); let tip_bits = q .header_at_height(Height(sample_tip)) .unwrap() @@ -1879,7 +1877,7 @@ fn pin_abandoned_fork_is_not_a_parent( chain: &rbitcoin_test::MatureRegtestChain, ) { use bitcoin::consensus::encode::serialize; - use rbitcoin_consensus::{ + use rbitcoin_query::{ confirm_bq_resolve_wave_capped, take_wave_items_for_load, BQ_RESOLVE_WAVE_MAX_BLOCKS, BQ_RESOLVE_WAVE_MAX_INPUTS, }; @@ -1953,7 +1951,7 @@ fn pin_archived_sibling_bodies( ) { use bitcoin::consensus::encode::serialize; use bitcoin::{ScriptBuf, TxOut}; - use rbitcoin_consensus::commit_class_a_block; + use rbitcoin_query::commit_class_a_block; let tip_h = chain.tip_height(); let bad_hash = double_spend.block_hash().to_byte_array(); @@ -2010,7 +2008,7 @@ fn pin_reconnect_archived_run_extends_height_index( blocks: &[Block], tip_h: u32, ) { - use rbitcoin_consensus::confirm_wire_run; + use rbitcoin_query::confirm_wire_run; let from_h = q.tip_height().unwrap().0; assert_eq!( @@ -2213,7 +2211,7 @@ fn pin_scripthash_views_on_pad( #[allow(clippy::cognitive_complexity)] // one pad, many block/tx surface arms fn pin_block_and_tx_surface_on_pad(q: &Query, chain: &rbitcoin_test::MatureRegtestChain) { use bitcoin::consensus::encode::serialize; - use rbitcoin_consensus::header_to_record; + use rbitcoin_query::header_to_record; use rbitcoin_query::ConfirmPrepared; let spend_h = Height(chain.spend_height); @@ -2347,7 +2345,7 @@ fn pin_same_run_create_then_spend( parent_cb: bitcoin::Txid, create_h: u32, ) -> (BlockHash, u32) { - use rbitcoin_consensus::{commit_class_a_run, confirm_wire_run}; + use rbitcoin_query::{commit_class_a_run, confirm_wire_run}; let mk_parent = spend_anyone_can_spend(parent_cb, 0, Amount::from_sat(49_0000_0000)); let b_create = mine_regtest_block(tip, tip_time + 600, create_h, vec![mk_parent]); @@ -2387,7 +2385,7 @@ fn pin_both_vouts_of_one_input_parent( use bitcoin::absolute::LockTime; use bitcoin::transaction::Version as TxVersion; use bitcoin::{OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; - use rbitcoin_consensus::{commit_class_a_block, commit_class_a_run, confirm_wire_run}; + use rbitcoin_query::{commit_class_a_block, commit_class_a_run, confirm_wire_run}; use rbitcoin_test::mine::{spend_many_anyone_can_spend, split_anyone_can_spend}; let split = split_anyone_can_spend( @@ -2577,9 +2575,10 @@ fn pin_already_at_height_retry_annotates_spend( /// Also exercises parent pin stats + tip advance, and load ready timeout/cancel. #[test] fn three_stage_confirm_and_parent_pin_surface() { - use rbitcoin_consensus::{ + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::{ accept_and_connect_block, commit_class_a_run, confirm_scripts_phase, - confirm_wire_load_phase, confirm_write_phase, ChainParams, Milestone, ScriptPreverified, + confirm_wire_load_phase, confirm_write_phase, ScriptPreverified, }; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_spend}; @@ -2711,10 +2710,11 @@ fn three_stage_confirm_and_parent_pin_surface() { /// from the prior load batch. Mixed store(≤tip)+plan(>tip) must succeed. #[test] fn confirm_load_ahead_of_write_does_not_badprev() { - use rbitcoin_consensus::{ + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::{ accept_and_connect_block, commit_class_a_block, confirm_scripts_phase, confirm_wire_load_phase, confirm_wire_load_phase_pipelined, confirm_write_phase, - ChainParams, Milestone, ScriptPreverified, WireLoadPipeline, + ScriptPreverified, WireLoadPipeline, }; use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis}; @@ -2928,9 +2928,10 @@ fn block_cache_and_mempool_hub_surface() { /// Multi-height raw wire → tip via split load/scripts/commit (no pre-archive reload). #[test] fn unified_wire_pipeline_multi_block_to_tip() { - use rbitcoin_consensus::{ + use rbitcoin_consensus::{ChainParams, Milestone}; + use rbitcoin_query::{ commit_class_a_block, confirm_scripts_phase, confirm_wire_load_phase, confirm_wire_run, - confirm_write_phase, ChainParams, Milestone, ScriptPreverified, + confirm_write_phase, ScriptPreverified, }; let td = TestDatadir::new().unwrap(); @@ -3049,15 +3050,17 @@ fn pin_confirm_refuses_without_advancing_tip( ms: Milestone, b1: &Block, ) { - use rbitcoin_consensus::{confirm_wire_load_phase, header_to_record, ScriptPreverified}; use rbitcoin_query::ConfirmPrepared; + use rbitcoin_query::{confirm_wire_load_phase, header_to_record, ScriptPreverified}; let none = ScriptPreverified::new(); let g = regtest_genesis(); let non_contiguous = [(Height(1), g.clone()), (Height(3), g)]; for batch in [&[][..], &non_contiguous[..]] { match confirm_wire_load_phase(q, params, ms, batch, &none) { - Err(rbitcoin_consensus::ConsensusError::BadBlock(_)) => {} + Err(rbitcoin_query::ConfirmError::Rules( + rbitcoin_consensus::ConsensusError::BadBlock(_), + )) => {} Err(e) => panic!("expected BadBlock, got {e}"), Ok(_) => panic!("load of {} blocks must refuse", batch.len()), } @@ -3102,7 +3105,7 @@ fn pin_bq_wave_then_stamp_confirms_empty_block( tip_time: u32, ) { use bitcoin::consensus::encode::serialize; - use rbitcoin_consensus::{ + use rbitcoin_query::{ confirm_bq_resolve_wave_capped, confirm_scripts_phase, confirm_wire_load_from_plan, confirm_wire_lookup_stamp, confirm_write_phase, take_wave_items_for_load, ScriptPreverified, BQ_RESOLVE_WAVE_MAX_BLOCKS, BQ_RESOLVE_WAVE_MAX_INPUTS, @@ -3147,7 +3150,7 @@ fn pin_wire_prep_ahead_cross_batch( cb: bitcoin::Txid, ha: u32, ) -> (BlockHash, u32) { - use rbitcoin_consensus::{ + use rbitcoin_query::{ confirm_scripts_phase, confirm_wire_load_phase_pipelined, confirm_write_phase, ScriptPreverified, WireLoadPipeline, }; @@ -3294,7 +3297,7 @@ fn pin_wire_prep_already_archived( cb: bitcoin::Txid, ha: u32, ) -> (BlockHash, u32) { - use rbitcoin_consensus::{ + use rbitcoin_query::{ commit_class_a_run, confirm_scripts_phase, confirm_wire_load_phase, confirm_write_phase, ScriptPreverified, }; @@ -3346,7 +3349,7 @@ fn pin_wire_prep_cold_class_a_denserels( cb: bitcoin::Txid, h_split: u32, ) { - use rbitcoin_consensus::{ + use rbitcoin_query::{ confirm_scripts_phase, confirm_wire_load_phase, confirm_write_phase, ScriptPreverified, }; use rbitcoin_test::mine::split_anyone_can_spend; @@ -3361,8 +3364,7 @@ fn pin_wire_prep_cold_class_a_denserels( ); let b_split = mine_regtest_block(tip, tip_time + 600, h_split, vec![split]); let parent_txid = b_split.txdata[1].compute_txid(); - rbitcoin_consensus::confirm_wire_run(q, params, ms, &[(Height(h_split), b_split.clone())]) - .unwrap(); + rbitcoin_query::confirm_wire_run(q, params, ms, &[(Height(h_split), b_split.clone())]).unwrap(); assert!( q.store() .get_fk_by_txid(parent_txid.as_byte_array()) diff --git a/docs/CRATES.md b/docs/CRATES.md index 7c5e592cfc..e7652c68da 100644 --- a/docs/CRATES.md +++ b/docs/CRATES.md @@ -9,8 +9,8 @@ shared libraries through storage and runtime crates to composition and tools; | `rbitcoin-primitives` | Shared Bitcoin types and newtypes used across the workspace | | `rbitcoin-log` | Leveled stderr logging; live progress of long stages (`progress`) | | `rbitcoin-store` | Relational archive and Class A/B/C on-disk tables | -| `rbitcoin-query` | Archive, confirm, reconstruction, and query APIs over the store | -| `rbitcoin-consensus` | Header, block, and script validation | +| `rbitcoin-query` | Archive, confirm pipeline, reconstruction, and query APIs over the store. Loads rows, writes Class A / Class C / spend annotations, and calls `rbitcoin-consensus` for rule decisions | +| `rbitcoin-consensus` | Header, block, and script rules from bytes and caller facts. Does not open a store | | `rbitcoin-mempool` | Live transaction graph and admission policy | | `rbitcoin-net` | P2P, IBD, tip follow, and transaction relay | | `rbitcoin-rpc` | Core-class JSON-RPC subset | diff --git a/docs/invariants.md b/docs/invariants.md index 4af30cb22a..935d17a425 100644 --- a/docs/invariants.md +++ b/docs/invariants.md @@ -153,8 +153,8 @@ packs at/above the leaving **pack** height **before** the next bind. ## Related code -- Confirm write annotate / ensure: `rbitcoin-consensus` `confirm_run::{post_commit,collect_spend_abs_after_fill,ensure_spend_abs_layouts,pin_for_wire_batch}` -- Structural: `rbitcoin-consensus` `block::structural_validate_spends` +- Confirm write annotate / ensure: `rbitcoin-query` `confirm_run::{post_commit,collect_spend_abs_after_fill,ensure_spend_abs_layouts,pin_for_wire_batch}` +- Structural: `rbitcoin-query` `block_pipeline::structural_validate_spends` (rule decisions it calls live in `rbitcoin-consensus` `block::facts`) - Pin / denserels: `rbitcoin-query` `confirm_load`, `BatchParents`, `pin_for_wire_batch` (cold range / adopt) - Abs annotate: `rbitcoin-store` `put_spend_batch_by_abs_meta` diff --git a/fuzz/src/chain_review.rs b/fuzz/src/chain_review.rs index b6bc919cb7..7780dc1f3f 100644 --- a/fuzz/src/chain_review.rs +++ b/fuzz/src/chain_review.rs @@ -14,11 +14,12 @@ use bitcoin::{ TxOut, Witness, }; use rbitcoin_consensus::{ - block_subsidy, confirm_scripts_phase, genesis_block, grind_regtest_pow, mine_empty_regtest, - mine_regtest_paying, ChainParams, Milestone, REGTEST_BLOCK_SPACING, REGTEST_POW_BITS, + block_subsidy, genesis_block, grind_regtest_pow, mine_empty_regtest, mine_regtest_paying, + ChainParams, Milestone, REGTEST_BLOCK_SPACING, REGTEST_POW_BITS, }; use rbitcoin_net::{AcceptOutcome, ChainHub, NetError}; use rbitcoin_primitives::Height; +use rbitcoin_query::confirm_scripts_phase; use crate::block_diff::{BlockOracle, BlockStanding, OracleReply};