From 944e3cc8e99b88ce0a3e7b1197112b824ff9d872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 23 Jan 2025 12:41:04 +1100 Subject: [PATCH 1/5] feat(chain): Signed txs should displace unsigned txs in `TxGraph`. --- crates/chain/src/tx_graph.rs | 19 +++++--- crates/chain/tests/test_tx_graph.rs | 70 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 2d512cfea..83310a697 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -590,11 +590,20 @@ impl TxGraph { let tx_node = self.txs.entry(txid).or_default(); match tx_node { TxNodeInternal::Whole(existing_tx) => { - debug_assert_eq!( - existing_tx.as_ref(), - tx.as_ref(), - "tx of same txid should never change" - ); + // We want to be able to replace an unsigned tx with a signed tx. + // The tx with more weight has precedence (and tiebreak with the actual tx data). + // We can also check whether the witness is valid and also prioritize signatures + // with less weight, but that is more work and this solution is good enough. + if existing_tx.as_ref() != tx.as_ref() { + let (_, tx_with_precedence) = Ord::max( + (existing_tx.weight(), existing_tx.as_ref()), + (tx.weight(), tx.as_ref()), + ); + if tx_with_precedence == tx.as_ref() { + *existing_tx = tx.clone(); + changeset.txs.insert(tx); + } + } } partial_tx => { for txin in &tx.input { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index ef57ac15b..b1904a6d9 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -10,6 +10,8 @@ use bdk_chain::{ Anchor, ChainOracle, ChainPosition, Merge, }; use bdk_testenv::{block_id, hash, utils::new_tx}; +use bitcoin::hex::FromHex; +use bitcoin::Witness; use bitcoin::{ absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid, @@ -282,6 +284,74 @@ fn insert_tx_displaces_txouts() { assert_eq!(tx_graph.get_txout(outpoint), Some(txout)); } +#[test] +fn insert_signed_tx_displaces_unsigned() { + let previous_output = OutPoint::new(hash!("prev"), 2); + let unsigned_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output, + script_sig: ScriptBuf::default(), + sequence: transaction::Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + }], + output: vec![TxOut { + value: Amount::from_sat(24_000), + script_pubkey: ScriptBuf::default(), + }], + }; + let signed_tx = Transaction { + input: vec![TxIn { + previous_output, + script_sig: ScriptBuf::default(), + sequence: transaction::Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::from_slice(&[ + // Random witness from mempool.space + Vec::from_hex("d59118058bf9e8604cec5c0b4a13430b07286482784da313594e932faad074dc4bd27db7cbfff9ad32450db097342d0148ec21c3033b0c27888fd2fd0de2e9b5") + .unwrap(), + ]), + }], + ..unsigned_tx.clone() + }; + + // Signed tx must displace unsigned. + { + let mut tx_graph = TxGraph::::default(); + let changeset_insert_unsigned = tx_graph.insert_tx(unsigned_tx.clone()); + let changeset_insert_signed = tx_graph.insert_tx(signed_tx.clone()); + assert_eq!( + changeset_insert_unsigned, + ChangeSet { + txs: [Arc::new(unsigned_tx.clone())].into(), + ..Default::default() + } + ); + assert_eq!( + changeset_insert_signed, + ChangeSet { + txs: [Arc::new(signed_tx.clone())].into(), + ..Default::default() + } + ); + } + + // Unsigned tx must not displace signed. + { + let mut tx_graph = TxGraph::::default(); + let changeset_insert_signed = tx_graph.insert_tx(signed_tx.clone()); + let changeset_insert_unsigned = tx_graph.insert_tx(unsigned_tx.clone()); + assert_eq!( + changeset_insert_signed, + ChangeSet { + txs: [Arc::new(signed_tx)].into(), + ..Default::default() + } + ); + assert!(changeset_insert_unsigned.is_empty()); + } +} + #[test] fn insert_txout_does_not_displace_tx() { let mut tx_graph = TxGraph::::default(); From 251bd7e5c87057d3aca4e80bbe2c6eade8933c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 23 Jan 2025 13:33:43 +1100 Subject: [PATCH 2/5] refactor(chain): Make private fields in `CanonicalIter` concise. --- crates/chain/src/canonical_iter.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/chain/src/canonical_iter.rs b/crates/chain/src/canonical_iter.rs index 99550ab7f..04225d5cf 100644 --- a/crates/chain/src/canonical_iter.rs +++ b/crates/chain/src/canonical_iter.rs @@ -13,10 +13,10 @@ pub struct CanonicalIter<'g, A, C> { chain: &'g C, chain_tip: BlockId, - unprocessed_txs_with_anchors: + unprocessed_anchored_txs: Box, &'g BTreeSet)> + 'g>, - unprocessed_txs_with_last_seens: Box, u64)> + 'g>, - unprocessed_txs_left_over: VecDeque<(Txid, Arc, u32)>, + unprocessed_seen_txs: Box, u64)> + 'g>, + unprocessed_leftover_txs: VecDeque<(Txid, Arc, u32)>, canonical: HashMap, CanonicalReason)>, not_canonical: HashSet, @@ -28,12 +28,12 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { /// Constructs [`CanonicalIter`]. pub fn new(tx_graph: &'g TxGraph, chain: &'g C, chain_tip: BlockId) -> Self { let anchors = tx_graph.all_anchors(); - let pending_anchored = Box::new( + let unprocessed_anchored_txs = Box::new( tx_graph .txids_by_descending_anchor_height() .filter_map(|(_, txid)| Some((txid, tx_graph.get_tx(txid)?, anchors.get(&txid)?))), ); - let pending_last_seen = Box::new( + let unprocessed_seen_txs = Box::new( tx_graph .txids_by_descending_last_seen() .filter_map(|(last_seen, txid)| Some((txid, tx_graph.get_tx(txid)?, last_seen))), @@ -42,9 +42,9 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { tx_graph, chain, chain_tip, - unprocessed_txs_with_anchors: pending_anchored, - unprocessed_txs_with_last_seens: pending_last_seen, - unprocessed_txs_left_over: VecDeque::new(), + unprocessed_anchored_txs, + unprocessed_seen_txs, + unprocessed_leftover_txs: VecDeque::new(), canonical: HashMap::new(), not_canonical: HashSet::new(), queue: VecDeque::new(), @@ -73,7 +73,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { } } // cannot determine - self.unprocessed_txs_left_over.push_back(( + self.unprocessed_leftover_txs.push_back(( txid, tx, anchors @@ -147,7 +147,7 @@ impl Iterator for CanonicalIter<'_, A, C> { return Some(Ok((txid, tx, reason))); } - if let Some((txid, tx, anchors)) = self.unprocessed_txs_with_anchors.next() { + if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() { if !self.is_canonicalized(txid) { if let Err(err) = self.scan_anchors(txid, tx, anchors) { return Some(Err(err)); @@ -156,7 +156,7 @@ impl Iterator for CanonicalIter<'_, A, C> { continue; } - if let Some((txid, tx, last_seen)) = self.unprocessed_txs_with_last_seens.next() { + if let Some((txid, tx, last_seen)) = self.unprocessed_seen_txs.next() { if !self.is_canonicalized(txid) { let observed_in = ObservedIn::Mempool(last_seen); self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in)); @@ -164,7 +164,7 @@ impl Iterator for CanonicalIter<'_, A, C> { continue; } - if let Some((txid, tx, height)) = self.unprocessed_txs_left_over.pop_front() { + if let Some((txid, tx, height)) = self.unprocessed_leftover_txs.pop_front() { if !self.is_canonicalized(txid) { let observed_in = ObservedIn::Block(height); self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in)); From 9624b001124a1a2d712d04206a53034f882937bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 23 Jan 2025 15:16:46 +1100 Subject: [PATCH 3/5] feat(chain,wallet)!: Add ability to modify canonicalization algorithm Introduce `CanonicalizationMods` which is passed in to `CanonicalIter::new`. `CanonicalizationMods::assume_canonical` is the only field right now. This contains a list of txids that we assume to be canonical, superceding any other canonicalization rules. --- crates/bitcoind_rpc/tests/test_emitter.rs | 12 +- crates/chain/benches/canonicalization.rs | 5 +- crates/chain/src/canonical_iter.rs | 55 ++++++++- crates/chain/src/tx_graph.rs | 113 +++++++++++------- crates/chain/tests/test_indexed_tx_graph.rs | 7 +- crates/chain/tests/test_tx_graph.rs | 12 +- crates/chain/tests/test_tx_graph_conflicts.rs | 7 +- crates/electrum/tests/test_electrum.rs | 12 +- crates/wallet/src/wallet/mod.rs | 21 +++- crates/wallet/tests/wallet.rs | 8 +- .../example_bitcoind_rpc_polling/src/main.rs | 4 +- example-crates/example_cli/Cargo.toml | 2 +- example-crates/example_cli/src/lib.rs | 16 ++- example-crates/example_electrum/src/main.rs | 9 +- example-crates/example_esplora/src/main.rs | 9 +- 15 files changed, 215 insertions(+), 77 deletions(-) diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 14b0c9212..0b7a4b626 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -5,7 +5,7 @@ use bdk_chain::{ bitcoin::{Address, Amount, Txid}, local_chain::{CheckPoint, LocalChain}, spk_txout::SpkTxOutIndex, - Balance, BlockId, IndexedTxGraph, Merge, + Balance, BlockId, CanonicalizationMods, IndexedTxGraph, Merge, }; use bdk_testenv::{anyhow, TestEnv}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; @@ -306,9 +306,13 @@ fn get_balance( ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); - let balance = recv_graph - .graph() - .balance(recv_chain, chain_tip, outpoints, |_, _| true); + let balance = recv_graph.graph().balance( + recv_chain, + chain_tip, + CanonicalizationMods::NONE, + outpoints, + |_, _| true, + ); Ok(balance) } diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index 3002a7ca3..2f7b27e08 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -1,3 +1,4 @@ +use bdk_chain::CanonicalizationMods; use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph}; use bdk_core::{BlockId, CheckPoint}; use bdk_core::{ConfirmationBlockTime, TxUpdate}; @@ -92,7 +93,7 @@ fn setup(f: F) -> (KeychainTxGraph, Lo fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) { let txs = tx_graph .graph() - .list_canonical_txs(chain, chain.tip().block_id()); + .list_canonical_txs(chain, chain.tip().block_id(), CanonicalizationMods::NONE); assert_eq!(txs.count(), exp_txs); } @@ -100,6 +101,7 @@ fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_t let utxos = tx_graph.graph().filter_chain_txouts( chain, chain.tip().block_id(), + CanonicalizationMods::NONE, tx_graph.index.outpoints().clone(), ); assert_eq!(utxos.count(), exp_txos); @@ -109,6 +111,7 @@ fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp let utxos = tx_graph.graph().filter_chain_unspents( chain, chain.tip().block_id(), + CanonicalizationMods::NONE, tx_graph.index.outpoints().clone(), ); assert_eq!(utxos.count(), exp_utxos); diff --git a/crates/chain/src/canonical_iter.rs b/crates/chain/src/canonical_iter.rs index 04225d5cf..8c972abae 100644 --- a/crates/chain/src/canonical_iter.rs +++ b/crates/chain/src/canonical_iter.rs @@ -4,15 +4,34 @@ use crate::{Anchor, ChainOracle, TxGraph}; use alloc::boxed::Box; use alloc::collections::BTreeSet; use alloc::sync::Arc; +use alloc::vec::Vec; use bdk_core::BlockId; use bitcoin::{Transaction, Txid}; +/// Modifies the canonicalization algorithm. +#[derive(Debug, Default, Clone)] +pub struct CanonicalizationMods { + /// Transactions that will supercede all other transactions. + /// + /// In case of conflicting transactions within `assume_canonical`, transactions that appear + /// later in the list (have higher index) have precedence. + pub assume_canonical: Vec, +} + +impl CanonicalizationMods { + /// No mods. + pub const NONE: Self = Self { + assume_canonical: Vec::new(), + }; +} + /// Iterates over canonical txs. pub struct CanonicalIter<'g, A, C> { tx_graph: &'g TxGraph, chain: &'g C, chain_tip: BlockId, + unprocessed_assumed_txs: Box)> + 'g>, unprocessed_anchored_txs: Box, &'g BTreeSet)> + 'g>, unprocessed_seen_txs: Box, u64)> + 'g>, @@ -26,8 +45,19 @@ pub struct CanonicalIter<'g, A, C> { impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { /// Constructs [`CanonicalIter`]. - pub fn new(tx_graph: &'g TxGraph, chain: &'g C, chain_tip: BlockId) -> Self { + pub fn new( + tx_graph: &'g TxGraph, + chain: &'g C, + chain_tip: BlockId, + mods: CanonicalizationMods, + ) -> Self { let anchors = tx_graph.all_anchors(); + let unprocessed_assumed_txs = Box::new( + mods.assume_canonical + .into_iter() + .rev() + .filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))), + ); let unprocessed_anchored_txs = Box::new( tx_graph .txids_by_descending_anchor_height() @@ -42,6 +72,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { tx_graph, chain, chain_tip, + unprocessed_assumed_txs, unprocessed_anchored_txs, unprocessed_seen_txs, unprocessed_leftover_txs: VecDeque::new(), @@ -147,6 +178,12 @@ impl Iterator for CanonicalIter<'_, A, C> { return Some(Ok((txid, tx, reason))); } + if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() { + if !self.is_canonicalized(txid) { + self.mark_canonical(txid, tx, CanonicalReason::assumed()); + } + } + if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() { if !self.is_canonicalized(txid) { if let Err(err) = self.scan_anchors(txid, tx, anchors) { @@ -189,6 +226,12 @@ pub enum ObservedIn { /// The reason why a transaction is canonical. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CanonicalReason { + /// This transaction is explicitly assumed to be canonical by the caller, superceding all other + /// canonicalization rules. + Assumed { + /// Whether it is a descendant that is assumed to be canonical. + descendant: Option, + }, /// This transaction is anchored in the best chain by `A`, and therefore canonical. Anchor { /// The anchor that anchored the transaction in the chain. @@ -207,6 +250,12 @@ pub enum CanonicalReason { } impl CanonicalReason { + /// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other + /// transactions. + pub fn assumed() -> Self { + Self::Assumed { descendant: None } + } + /// Constructs a [`CanonicalReason`] from an `anchor`. pub fn from_anchor(anchor: A) -> Self { Self::Anchor { @@ -229,6 +278,9 @@ impl CanonicalReason { /// descendant, but is transitively relevant. pub fn to_transitive(&self, descendant: Txid) -> Self { match self { + CanonicalReason::Assumed { .. } => Self::Assumed { + descendant: Some(descendant), + }, CanonicalReason::Anchor { anchor, .. } => Self::Anchor { anchor: anchor.clone(), descendant: Some(descendant), @@ -244,6 +296,7 @@ impl CanonicalReason { /// descendant. pub fn descendant(&self) -> &Option { match self { + CanonicalReason::Assumed { descendant, .. } => descendant, CanonicalReason::Anchor { descendant, .. } => descendant, CanonicalReason::ObservedIn { descendant, .. } => descendant, } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 83310a697..5766afe4a 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -94,6 +94,7 @@ use crate::collections::*; use crate::BlockId; use crate::CanonicalIter; use crate::CanonicalReason; +use crate::CanonicalizationMods; use crate::ObservedIn; use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge}; use alloc::collections::vec_deque::VecDeque; @@ -829,25 +830,46 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, ) -> impl Iterator, A>, C::Error>> { - self.canonical_iter(chain, chain_tip).flat_map(move |res| { - res.map(|(txid, _, canonical_reason)| { - let tx_node = self.get_tx_node(txid).expect("must contain tx"); - let chain_position = match canonical_reason { - CanonicalReason::Anchor { anchor, descendant } => match descendant { - Some(_) => { - let direct_anchor = tx_node - .anchors - .iter() - .find_map(|a| -> Option> { - match chain.is_block_in_chain(a.anchor_block(), chain_tip) { - Ok(Some(true)) => Some(Ok(a.clone())), - Ok(Some(false)) | Ok(None) => None, - Err(err) => Some(Err(err)), - } - }) - .transpose()?; - match direct_anchor { + fn find_direct_anchor( + tx_node: &TxNode<'_, Arc, A>, + chain: &C, + chain_tip: BlockId, + ) -> Result, C::Error> { + tx_node + .anchors + .iter() + .find_map(|a| -> Option> { + match chain.is_block_in_chain(a.anchor_block(), chain_tip) { + Ok(Some(true)) => Some(Ok(a.clone())), + Ok(Some(false)) | Ok(None) => None, + Err(err) => Some(Err(err)), + } + }) + .transpose() + } + self.canonical_iter(chain, chain_tip, mods) + .flat_map(move |res| { + res.map(|(txid, _, canonical_reason)| { + let tx_node = self.get_tx_node(txid).expect("must contain tx"); + let chain_position = match canonical_reason { + CanonicalReason::Assumed { descendant } => match descendant { + Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? { + Some(anchor) => ChainPosition::Confirmed { + anchor, + transitively: None, + }, + None => ChainPosition::Unconfirmed { + last_seen: tx_node.last_seen_unconfirmed, + }, + }, + None => ChainPosition::Unconfirmed { + last_seen: tx_node.last_seen_unconfirmed, + }, + }, + CanonicalReason::Anchor { anchor, descendant } => match descendant { + Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? { Some(anchor) => ChainPosition::Confirmed { anchor, transitively: None, @@ -856,26 +878,25 @@ impl TxGraph { anchor, transitively: descendant, }, - } - } - None => ChainPosition::Confirmed { - anchor, - transitively: None, + }, + None => ChainPosition::Confirmed { + anchor, + transitively: None, + }, }, - }, - CanonicalReason::ObservedIn { observed_in, .. } => match observed_in { - ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed { - last_seen: Some(last_seen), + CanonicalReason::ObservedIn { observed_in, .. } => match observed_in { + ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed { + last_seen: Some(last_seen), + }, + ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None }, }, - ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None }, - }, - }; - Ok(CanonicalTx { - chain_position, - tx_node, + }; + Ok(CanonicalTx { + chain_position, + tx_node, + }) }) }) - }) } /// List graph transactions that are in `chain` with `chain_tip`. @@ -887,8 +908,9 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, ) -> impl Iterator, A>> { - self.try_list_canonical_txs(chain, chain_tip) + self.try_list_canonical_txs(chain, chain_tip, mods) .map(|res| res.expect("infallible")) } @@ -915,11 +937,12 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, outpoints: impl IntoIterator + 'a, ) -> Result)> + 'a, C::Error> { let mut canon_txs = HashMap::, A>>::new(); let mut canon_spends = HashMap::::new(); - for r in self.try_list_canonical_txs(chain, chain_tip) { + for r in self.try_list_canonical_txs(chain, chain_tip, mods) { let canonical_tx = r?; let txid = canonical_tx.tx_node.txid; @@ -988,8 +1011,9 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, ) -> CanonicalIter<'a, A, C> { - CanonicalIter::new(self, chain, chain_tip) + CanonicalIter::new(self, chain, chain_tip, mods) } /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with @@ -1002,9 +1026,10 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, outpoints: impl IntoIterator + 'a, ) -> impl Iterator)> + 'a { - self.try_filter_chain_txouts(chain, chain_tip, outpoints) + self.try_filter_chain_txouts(chain, chain_tip, mods, outpoints) .expect("oracle is infallible") } @@ -1030,10 +1055,11 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, outpoints: impl IntoIterator + 'a, ) -> Result)> + 'a, C::Error> { Ok(self - .try_filter_chain_txouts(chain, chain_tip, outpoints)? + .try_filter_chain_txouts(chain, chain_tip, mods, outpoints)? .filter(|(_, full_txo)| full_txo.spent_by.is_none())) } @@ -1047,9 +1073,10 @@ impl TxGraph { &'a self, chain: &'a C, chain_tip: BlockId, + mods: CanonicalizationMods, txouts: impl IntoIterator + 'a, ) -> impl Iterator)> + 'a { - self.try_filter_chain_unspents(chain, chain_tip, txouts) + self.try_filter_chain_unspents(chain, chain_tip, mods, txouts) .expect("oracle is infallible") } @@ -1069,6 +1096,7 @@ impl TxGraph { &self, chain: &C, chain_tip: BlockId, + mods: CanonicalizationMods, outpoints: impl IntoIterator, mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool, ) -> Result { @@ -1077,7 +1105,7 @@ impl TxGraph { let mut untrusted_pending = Amount::ZERO; let mut confirmed = Amount::ZERO; - for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, outpoints)? { + for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, mods, outpoints)? { match &txout.chain_position { ChainPosition::Confirmed { .. } => { if txout.is_confirmed_and_spendable(chain_tip.height) { @@ -1113,10 +1141,11 @@ impl TxGraph { &self, chain: &C, chain_tip: BlockId, + mods: CanonicalizationMods, outpoints: impl IntoIterator, trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool, ) -> Balance { - self.try_balance(chain, chain_tip, outpoints, trust_predicate) + self.try_balance(chain, chain_tip, mods, outpoints, trust_predicate) .expect("oracle is infallible") } } diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 1e28eb6a2..378350eb9 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -9,7 +9,7 @@ use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, - tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, + tx_graph, Balance, CanonicalizationMods, ChainPosition, ConfirmationBlockTime, DescriptorExt, }; use bdk_testenv::{ block_id, hash, @@ -271,6 +271,7 @@ fn test_list_owned_txouts() { .filter_chain_txouts( &local_chain, chain_tip, + CanonicalizationMods::NONE, graph.index.outpoints().iter().cloned(), ) .collect::>(); @@ -280,6 +281,7 @@ fn test_list_owned_txouts() { .filter_chain_unspents( &local_chain, chain_tip, + CanonicalizationMods::NONE, graph.index.outpoints().iter().cloned(), ) .collect::>(); @@ -287,6 +289,7 @@ fn test_list_owned_txouts() { let balance = graph.graph().balance( &local_chain, chain_tip, + CanonicalizationMods::NONE, graph.index.outpoints().iter().cloned(), |_, spk: ScriptBuf| trusted_spks.contains(&spk), ); @@ -589,7 +592,7 @@ fn test_get_chain_position() { // check chain position let chain_pos = graph .graph() - .list_canonical_txs(chain, chain.tip().block_id()) + .list_canonical_txs(chain, chain.tip().block_id(), CanonicalizationMods::NONE) .find_map(|canon_tx| { if canon_tx.tx_node.txid == txid { Some(canon_tx.chain_position) diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index b1904a6d9..6bdb632ad 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,7 +2,7 @@ #[macro_use] mod common; -use bdk_chain::{collections::*, BlockId, ConfirmationBlockTime}; +use bdk_chain::{collections::*, BlockId, CanonicalizationMods, ConfirmationBlockTime}; use bdk_chain::{ local_chain::LocalChain, tx_graph::{self, CalculateFeeError}, @@ -953,6 +953,7 @@ fn test_chain_spends() { .filter_chain_txouts( chain, tip.block_id(), + CanonicalizationMods::NONE, tx_graph.all_txouts().map(|(op, _)| ((), op)), ) .filter_map(|(_, full_txo)| Some((full_txo.outpoint, full_txo.spent_by?))) @@ -962,7 +963,7 @@ fn test_chain_spends() { tx_graph: &TxGraph| -> HashMap> { tx_graph - .list_canonical_txs(chain, tip.block_id()) + .list_canonical_txs(chain, tip.block_id(), CanonicalizationMods::NONE) .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position)) .collect() }; @@ -1131,13 +1132,14 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch .collect(); let chain = LocalChain::from_blocks(blocks).unwrap(); let canonical_txs: Vec<_> = graph - .list_canonical_txs(&chain, chain.tip().block_id()) + .list_canonical_txs(&chain, chain.tip().block_id(), CanonicalizationMods::NONE) .collect(); assert!(canonical_txs.is_empty()); // tx0 with seen_at should be returned by canonical txs let _ = graph.insert_seen_at(txids[0], 2); - let mut canonical_txs = graph.list_canonical_txs(&chain, chain.tip().block_id()); + let mut canonical_txs = + graph.list_canonical_txs(&chain, chain.tip().block_id(), CanonicalizationMods::NONE); assert_eq!( canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(), txids[0] @@ -1147,7 +1149,7 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch // tx1 with anchor is also canonical let _ = graph.insert_anchor(txids[1], block_id!(2, "B")); let canonical_txids: Vec<_> = graph - .list_canonical_txs(&chain, chain.tip().block_id()) + .list_canonical_txs(&chain, chain.tip().block_id(), CanonicalizationMods::NONE) .map(|tx| tx.tx_node.txid) .collect(); assert!(canonical_txids.contains(&txids[1])); diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index ff4c8b1f9..20b8732b7 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -3,7 +3,7 @@ #[macro_use] mod common; -use bdk_chain::{Balance, BlockId}; +use bdk_chain::{Balance, BlockId, CanonicalizationMods}; use bdk_testenv::{block_id, hash, local_chain}; use bitcoin::{Amount, OutPoint, ScriptBuf}; use common::*; @@ -693,7 +693,7 @@ fn test_tx_conflict_handling() { let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter()); let txs = tx_graph - .list_canonical_txs(&local_chain, chain_tip) + .list_canonical_txs(&local_chain, chain_tip, CanonicalizationMods::NONE) .map(|tx| tx.tx_node.txid) .collect::>(); let exp_txs = scenario @@ -711,6 +711,7 @@ fn test_tx_conflict_handling() { .filter_chain_txouts( &local_chain, chain_tip, + CanonicalizationMods::NONE, spk_index.outpoints().iter().cloned(), ) .map(|(_, full_txout)| full_txout.outpoint) @@ -733,6 +734,7 @@ fn test_tx_conflict_handling() { .filter_chain_unspents( &local_chain, chain_tip, + CanonicalizationMods::NONE, spk_index.outpoints().iter().cloned(), ) .map(|(_, full_txout)| full_txout.outpoint) @@ -754,6 +756,7 @@ fn test_tx_conflict_handling() { let balance = tx_graph.balance( &local_chain, chain_tip, + CanonicalizationMods::NONE, spk_index.outpoints().iter().cloned(), |_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(), ); diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 7794589a6..ed9295e30 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -3,7 +3,7 @@ use bdk_chain::{ local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResponse}, spk_txout::SpkTxOutIndex, - Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, + Balance, CanonicalizationMods, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -20,9 +20,13 @@ fn get_balance( ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); - let balance = recv_graph - .graph() - .balance(recv_chain, chain_tip, outpoints, |_, _| true); + let balance = recv_graph.graph().balance( + recv_chain, + chain_tip, + CanonicalizationMods::NONE, + outpoints, + |_, _| true, + ); Ok(balance) } diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 70d26c080..1d6ffed0b 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -19,6 +19,7 @@ use alloc::{ sync::Arc, vec::Vec, }; +use chain::CanonicalizationMods; use core::{cmp::Ordering, fmt, mem, ops::Deref}; use bdk_chain::{ @@ -816,6 +817,7 @@ impl Wallet { .filter_chain_unspents( &self.chain, self.chain.tip().block_id(), + CanonicalizationMods::NONE, self.indexed_graph.index.outpoints().iter().cloned(), ) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) @@ -830,6 +832,7 @@ impl Wallet { .filter_chain_txouts( &self.chain, self.chain.tip().block_id(), + CanonicalizationMods::NONE, self.indexed_graph.index.outpoints().iter().cloned(), ) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) @@ -883,6 +886,7 @@ impl Wallet { .filter_chain_unspents( &self.chain, self.chain.tip().block_id(), + CanonicalizationMods::NONE, core::iter::once(((), op)), ) .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) @@ -1058,7 +1062,11 @@ impl Wallet { pub fn get_tx(&self, txid: Txid) -> Option { let graph = self.indexed_graph.graph(); graph - .list_canonical_txs(&self.chain, self.chain.tip().block_id()) + .list_canonical_txs( + &self.chain, + self.chain.tip().block_id(), + CanonicalizationMods::NONE, + ) .find(|tx| tx.tx_node.txid == txid) } @@ -1077,7 +1085,11 @@ impl Wallet { let tx_graph = self.indexed_graph.graph(); let tx_index = &self.indexed_graph.index; tx_graph - .list_canonical_txs(&self.chain, self.chain.tip().block_id()) + .list_canonical_txs( + &self.chain, + self.chain.tip().block_id(), + CanonicalizationMods::NONE, + ) .filter(|c_tx| tx_index.is_tx_relevant(&c_tx.tx_node.tx)) } @@ -1112,6 +1124,7 @@ impl Wallet { self.indexed_graph.graph().balance( &self.chain, self.chain.tip().block_id(), + CanonicalizationMods::NONE, self.indexed_graph.index.outpoints().iter().cloned(), |&(k, _), _| k == KeychainKind::Internal, ) @@ -1582,7 +1595,7 @@ impl Wallet { let txout_index = &self.indexed_graph.index; let chain_tip = self.chain.tip().block_id(); let chain_positions = graph - .list_canonical_txs(&self.chain, chain_tip) + .list_canonical_txs(&self.chain, chain_tip, CanonicalizationMods::NONE) .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position)) .collect::>(); @@ -1839,7 +1852,7 @@ impl Wallet { let confirmation_heights = self .indexed_graph .graph() - .list_canonical_txs(&self.chain, chain_tip) + .list_canonical_txs(&self.chain, chain_tip, CanonicalizationMods::NONE) .filter(|canon_tx| prev_txids.contains(&canon_tx.tx_node.txid)) // This is for a small performance gain. Although `.filter` filters out excess txs, it // will still consume the internal `CanonicalIter` entirely. Having a `.take` here diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 174a628d2..a7ca189cd 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::Context; use assert_matches::assert_matches; -use bdk_chain::{BlockId, ChainPosition, ConfirmationBlockTime}; +use bdk_chain::{BlockId, CanonicalizationMods, ChainPosition, ConfirmationBlockTime}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; use bdk_wallet::error::CreateTxError; @@ -4264,7 +4264,7 @@ fn test_wallet_transactions_relevant() { let chain_tip = test_wallet.local_chain().tip().block_id(); let canonical_tx_count_before = test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip) + .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) .count(); // add not relevant transaction to test wallet @@ -4281,7 +4281,7 @@ fn test_wallet_transactions_relevant() { let full_tx_count_after = test_wallet.tx_graph().full_txs().count(); let canonical_tx_count_after = test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip) + .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) .count(); assert_eq!(relevant_tx_count_before, relevant_tx_count_after); @@ -4290,7 +4290,7 @@ fn test_wallet_transactions_relevant() { .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); assert!(test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip) + .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); assert!(full_tx_count_before < full_tx_count_after); assert!(canonical_tx_count_before < canonical_tx_count_after); diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 95c547967..923043337 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -13,7 +13,7 @@ use bdk_bitcoind_rpc::{ }; use bdk_chain::{ bitcoin::{Block, Transaction}, - local_chain, Merge, + local_chain, CanonicalizationMods, Merge, }; use example_cli::{ anyhow, @@ -186,6 +186,7 @@ fn main() -> anyhow::Result<()> { graph.graph().balance( &*chain, synced_to.block_id(), + CanonicalizationMods::NONE, graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, ) @@ -323,6 +324,7 @@ fn main() -> anyhow::Result<()> { graph.graph().balance( &*chain, synced_to.block_id(), + CanonicalizationMods::NONE, graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, ) diff --git a/example-crates/example_cli/Cargo.toml b/example-crates/example_cli/Cargo.toml index 290908a6a..c38447fbd 100644 --- a/example-crates/example_cli/Cargo.toml +++ b/example-crates/example_cli/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]} +bdk_chain = { path = "../../crates/chain", version = "0.21.1", features = ["serde", "miniscript"] } bdk_coin_select = "0.3.0" bdk_file_store = { path = "../../crates/file_store" } bitcoin = { version = "0.32.0", features = ["base64"], default-features = false } diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 3a700db3a..a80d5a4fc 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -19,6 +19,7 @@ use bdk_chain::miniscript::{ psbt::PsbtExt, Descriptor, DescriptorPublicKey, }; +use bdk_chain::CanonicalizationMods; use bdk_chain::ConfirmationBlockTime; use bdk_chain::{ indexed_tx_graph, @@ -421,7 +422,12 @@ pub fn planned_utxos( let outpoints = graph.index.outpoints(); graph .graph() - .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())? + .try_filter_chain_unspents( + chain, + chain_tip, + CanonicalizationMods::NONE, + outpoints.iter().cloned(), + )? .filter_map(|((k, i), full_txo)| -> Option> { let desc = graph .index @@ -515,6 +521,7 @@ pub fn handle_commands( let balance = graph.graph().try_balance( chain, chain.get_chain_tip()?, + CanonicalizationMods::NONE, graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, )?; @@ -556,7 +563,12 @@ pub fn handle_commands( } => { let txouts = graph .graph() - .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())? + .try_filter_chain_txouts( + chain, + chain_tip, + CanonicalizationMods::NONE, + outpoints.iter().cloned(), + )? .filter(|(_, full_txo)| match (spent, unspent) { (true, false) => full_txo.spent_by.is_some(), (false, true) => full_txo.spent_by.is_none(), diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 9c705a3df..d27a91e0e 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -5,7 +5,7 @@ use bdk_chain::{ collections::BTreeSet, indexed_tx_graph, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationBlockTime, Merge, + CanonicalizationMods, ConfirmationBlockTime, Merge, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -229,6 +229,7 @@ fn main() -> anyhow::Result<()> { .filter_chain_unspents( &*chain, chain_tip.block_id(), + CanonicalizationMods::NONE, init_outpoints.iter().cloned(), ) .map(|(_, utxo)| utxo.outpoint), @@ -238,7 +239,11 @@ fn main() -> anyhow::Result<()> { request = request.txids( graph .graph() - .list_canonical_txs(&*chain, chain_tip.block_id()) + .list_canonical_txs( + &*chain, + chain_tip.block_id(), + CanonicalizationMods::NONE, + ) .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) .map(|canonical_tx| canonical_tx.tx_node.txid), ); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index cba86b862..2d0a76577 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -8,7 +8,7 @@ use bdk_chain::{ bitcoin::Network, keychain_txout::FullScanRequestBuilderExt, spk_client::{FullScanRequest, SyncRequest}, - Merge, + CanonicalizationMods, Merge, }; use bdk_esplora::{esplora_client, EsploraExt}; use example_cli::{ @@ -243,6 +243,7 @@ fn main() -> anyhow::Result<()> { .filter_chain_unspents( &*chain, local_tip.block_id(), + CanonicalizationMods::NONE, init_outpoints.iter().cloned(), ) .map(|(_, utxo)| utxo.outpoint), @@ -255,7 +256,11 @@ fn main() -> anyhow::Result<()> { request = request.txids( graph .graph() - .list_canonical_txs(&*chain, local_tip.block_id()) + .list_canonical_txs( + &*chain, + local_tip.block_id(), + CanonicalizationMods::NONE, + ) .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) .map(|canonical_tx| canonical_tx.tx_node.txid), ); From 0f00cdb4750d06dd1fa52bf9878933e5f6f3dd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 23 Jan 2025 17:03:27 +1100 Subject: [PATCH 4/5] text(chain,wallet): Test `assume_canonical` mod Also change `TxTemplate` API to allow for testing with `assume_canonical`. --- crates/chain/benches/canonicalization.rs | 8 +- crates/chain/tests/common/tx_template.rs | 47 +++-- crates/chain/tests/test_tx_graph.rs | 3 +- crates/chain/tests/test_tx_graph_conflicts.rs | 191 ++++++++++++++++-- crates/wallet/tests/wallet.rs | 18 +- 5 files changed, 228 insertions(+), 39 deletions(-) diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index 2f7b27e08..addbca2c3 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -91,9 +91,11 @@ fn setup(f: F) -> (KeychainTxGraph, Lo } fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) { - let txs = tx_graph - .graph() - .list_canonical_txs(chain, chain.tip().block_id(), CanonicalizationMods::NONE); + let txs = tx_graph.graph().list_canonical_txs( + chain, + chain.tip().block_id(), + CanonicalizationMods::NONE, + ); assert_eq!(txs.count(), exp_txs); } diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 0b0e2fd9e..f5885f8d5 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -4,7 +4,7 @@ use bdk_testenv::utils::DESCRIPTORS; use rand::distributions::{Alphanumeric, DistString}; use std::collections::HashMap; -use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor}; +use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, CanonicalizationMods}; use bitcoin::{ locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, @@ -24,6 +24,7 @@ pub struct TxTemplate<'a, A> { pub outputs: &'a [TxOutTemplate], pub anchors: &'a [A], pub last_seen: Option, + pub assume_canonical: bool, } #[allow(dead_code)] @@ -51,16 +52,24 @@ impl TxOutTemplate { } } +#[allow(dead_code)] +pub struct TxTemplateEnv<'a, A> { + pub tx_graph: TxGraph, + pub indexer: SpkTxOutIndex, + pub txid_to_name: HashMap<&'a str, Txid>, + pub canonicalization_mods: CanonicalizationMods, +} + #[allow(dead_code)] pub fn init_graph<'a, A: Anchor + Clone + 'a>( tx_templates: impl IntoIterator>, -) -> (TxGraph, SpkTxOutIndex, HashMap<&'a str, Txid>) { +) -> TxTemplateEnv<'a, A> { let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[2]).unwrap(); - let mut graph = TxGraph::::default(); - let mut spk_index = SpkTxOutIndex::default(); + let mut tx_graph = TxGraph::::default(); + let mut indexer = SpkTxOutIndex::default(); (0..10).for_each(|index| { - spk_index.insert_spk( + indexer.insert_spk( index, descriptor .at_derivation_index(index) @@ -68,8 +77,9 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( .script_pubkey(), ); }); - let mut tx_ids = HashMap::<&'a str, Txid>::new(); + let mut txid_to_name = HashMap::<&'a str, Txid>::new(); + let mut canonicalization_mods = CanonicalizationMods::default(); for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { let tx = Transaction { version: transaction::Version::non_standard(0), @@ -98,7 +108,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( witness: Witness::new(), }, TxInTemplate::PrevTx(prev_name, prev_vout) => { - let prev_txid = tx_ids.get(prev_name).expect( + let prev_txid = txid_to_name.get(prev_name).expect( "txin template must spend from tx of template that comes before", ); TxIn { @@ -120,21 +130,30 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( }, Some(index) => TxOut { value: Amount::from_sat(output.value), - script_pubkey: spk_index.spk_at_index(index).unwrap(), + script_pubkey: indexer.spk_at_index(index).unwrap(), }, }) .collect(), }; - tx_ids.insert(tx_tmp.tx_name, tx.compute_txid()); - spk_index.scan(&tx); - let _ = graph.insert_tx(tx.clone()); + let txid = tx.compute_txid(); + if tx_tmp.assume_canonical { + canonicalization_mods.assume_canonical.push(txid); + } + txid_to_name.insert(tx_tmp.tx_name, txid); + indexer.scan(&tx); + let _ = tx_graph.insert_tx(tx.clone()); for anchor in tx_tmp.anchors.iter() { - let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone()); + let _ = tx_graph.insert_anchor(txid, anchor.clone()); } if let Some(last_seen) = tx_tmp.last_seen { - let _ = graph.insert_seen_at(tx.compute_txid(), last_seen); + let _ = tx_graph.insert_seen_at(txid, last_seen); } } - (graph, spk_index, tx_ids) + TxTemplateEnv { + tx_graph, + indexer, + txid_to_name, + canonicalization_mods, + } } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 6bdb632ad..f2124fc1d 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1211,6 +1211,7 @@ fn call_map_anchors_with_non_deterministic_anchor() { outputs: &[TxOutTemplate::new(10000, Some(1))], anchors: &[block_id!(1, "A")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx2", @@ -1227,7 +1228,7 @@ fn call_map_anchors_with_non_deterministic_anchor() { ..Default::default() }, ]; - let (graph, _, _) = init_graph(&template); + let graph = init_graph(&template).tx_graph; let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor { anchor_block: a, // A non-deterministic value diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 20b8732b7..10025a1b3 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -3,7 +3,7 @@ #[macro_use] mod common; -use bdk_chain::{Balance, BlockId, CanonicalizationMods}; +use bdk_chain::{Balance, BlockId}; use bdk_testenv::{block_id, hash, local_chain}; use bitcoin::{Amount, OutPoint, ScriptBuf}; use common::*; @@ -59,6 +59,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(1))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "unconfirmed_conflict", @@ -130,6 +131,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -165,6 +167,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -207,6 +210,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -221,6 +225,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(30000, Some(2))], anchors: &[block_id!(4, "Orphaned Block")], last_seen: Some(300), + ..Default::default() }, ], exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]), @@ -242,6 +247,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -256,6 +262,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(30000, Some(2))], anchors: &[block_id!(4, "Orphaned Block")], last_seen: Some(100), + ..Default::default() }, ], exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]), @@ -277,6 +284,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -371,6 +379,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -459,6 +468,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -504,6 +514,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -549,6 +560,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -686,20 +698,161 @@ fn test_tx_conflict_handling() { exp_chain_txouts: HashSet::from([("tx", 0)]), exp_unspents: HashSet::from([("tx", 0)]), exp_balance: Balance { trusted_pending: Amount::from_sat(9000), ..Default::default() } - } + }, + Scenario { + name: "assume-canonical-tx displaces unconfirmed chain", + tx_templates: &[ + TxTemplate { + tx_name: "root", + inputs: &[TxInTemplate::Bogus], + outputs: &[ + TxOutTemplate::new(21_000, Some(0)), + TxOutTemplate::new(21_000, Some(1)), + ], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "unconfirmed", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(20_000, Some(1))], + last_seen: Some(2), + ..Default::default() + }, + TxTemplate { + tx_name: "unconfirmed_descendant", + inputs: &[ + TxInTemplate::PrevTx("unconfirmed", 0), + TxInTemplate::PrevTx("root", 1), + ], + outputs: &[TxOutTemplate::new(28_000, Some(2))], + last_seen: Some(2), + ..Default::default() + }, + TxTemplate { + tx_name: "assume_canonical", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(19_000, Some(3))], + assume_canonical: true, + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["root", "assume_canonical"]), + exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]), + exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::from_sat(19_000), + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_sat(21_000), + }, + }, + Scenario { + name: "assume-canonical-tx displaces confirmed chain", + tx_templates: &[ + TxTemplate { + tx_name: "root", + inputs: &[TxInTemplate::Bogus], + outputs: &[ + TxOutTemplate::new(21_000, Some(0)), + TxOutTemplate::new(21_000, Some(1)), + ], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "confirmed", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(20_000, Some(1))], + anchors: &[block_id!(2, "C")], + ..Default::default() + }, + TxTemplate { + tx_name: "confirmed_descendant", + inputs: &[ + TxInTemplate::PrevTx("confirmed", 0), + TxInTemplate::PrevTx("root", 1), + ], + outputs: &[TxOutTemplate::new(28_000, Some(2))], + anchors: &[block_id!(3, "D")], + ..Default::default() + }, + TxTemplate { + tx_name: "assume_canonical", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(19_000, Some(3))], + assume_canonical: true, + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["root", "assume_canonical"]), + exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]), + exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::from_sat(19_000), + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_sat(21_000), + }, + }, + Scenario { + name: "assume-canonical txs respects order", + tx_templates: &[ + TxTemplate { + tx_name: "root", + inputs: &[TxInTemplate::Bogus], + outputs: &[ + TxOutTemplate::new(21_000, Some(0)), + ], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "assume_a", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(20_000, Some(1))], + assume_canonical: true, + ..Default::default() + }, + TxTemplate { + tx_name: "assume_b", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(19_000, Some(1))], + assume_canonical: true, + ..Default::default() + }, + TxTemplate { + tx_name: "assume_c", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(18_000, Some(1))], + assume_canonical: true, + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["root", "assume_c"]), + exp_chain_txouts: HashSet::from([("root", 0), ("assume_c", 0)]), + exp_unspents: HashSet::from([("assume_c", 0)]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::from_sat(18_000), + untrusted_pending: Amount::ZERO, + confirmed: Amount::ZERO, + }, + }, ]; for scenario in scenarios { - let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter()); + let env = init_graph(scenario.tx_templates.iter()); - let txs = tx_graph - .list_canonical_txs(&local_chain, chain_tip, CanonicalizationMods::NONE) + let txs = env + .tx_graph + .list_canonical_txs(&local_chain, chain_tip, env.canonicalization_mods.clone()) .map(|tx| tx.tx_node.txid) .collect::>(); let exp_txs = scenario .exp_chain_txs .iter() - .map(|txid| *exp_tx_ids.get(txid).expect("txid must exist")) + .map(|txid| *env.txid_to_name.get(txid).expect("txid must exist")) .collect::>(); assert_eq!( txs, exp_txs, @@ -707,12 +860,13 @@ fn test_tx_conflict_handling() { scenario.name ); - let txouts = tx_graph + let txouts = env + .tx_graph .filter_chain_txouts( &local_chain, chain_tip, - CanonicalizationMods::NONE, - spk_index.outpoints().iter().cloned(), + env.canonicalization_mods.clone(), + env.indexer.outpoints().iter().cloned(), ) .map(|(_, full_txout)| full_txout.outpoint) .collect::>(); @@ -720,7 +874,7 @@ fn test_tx_conflict_handling() { .exp_chain_txouts .iter() .map(|(txid, vout)| OutPoint { - txid: *exp_tx_ids.get(txid).expect("txid must exist"), + txid: *env.txid_to_name.get(txid).expect("txid must exist"), vout: *vout, }) .collect::>(); @@ -730,12 +884,13 @@ fn test_tx_conflict_handling() { scenario.name ); - let utxos = tx_graph + let utxos = env + .tx_graph .filter_chain_unspents( &local_chain, chain_tip, - CanonicalizationMods::NONE, - spk_index.outpoints().iter().cloned(), + env.canonicalization_mods.clone(), + env.indexer.outpoints().iter().cloned(), ) .map(|(_, full_txout)| full_txout.outpoint) .collect::>(); @@ -743,7 +898,7 @@ fn test_tx_conflict_handling() { .exp_unspents .iter() .map(|(txid, vout)| OutPoint { - txid: *exp_tx_ids.get(txid).expect("txid must exist"), + txid: *env.txid_to_name.get(txid).expect("txid must exist"), vout: *vout, }) .collect::>(); @@ -753,12 +908,12 @@ fn test_tx_conflict_handling() { scenario.name ); - let balance = tx_graph.balance( + let balance = env.tx_graph.balance( &local_chain, chain_tip, - CanonicalizationMods::NONE, - spk_index.outpoints().iter().cloned(), - |_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(), + env.canonicalization_mods.clone(), + env.indexer.outpoints().iter().cloned(), + |_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(), ); assert_eq!( balance, scenario.exp_balance, diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index a7ca189cd..b38efeb45 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -4264,7 +4264,11 @@ fn test_wallet_transactions_relevant() { let chain_tip = test_wallet.local_chain().tip().block_id(); let canonical_tx_count_before = test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) + .list_canonical_txs( + test_wallet.local_chain(), + chain_tip, + CanonicalizationMods::NONE, + ) .count(); // add not relevant transaction to test wallet @@ -4281,7 +4285,11 @@ fn test_wallet_transactions_relevant() { let full_tx_count_after = test_wallet.tx_graph().full_txs().count(); let canonical_tx_count_after = test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) + .list_canonical_txs( + test_wallet.local_chain(), + chain_tip, + CanonicalizationMods::NONE, + ) .count(); assert_eq!(relevant_tx_count_before, relevant_tx_count_after); @@ -4290,7 +4298,11 @@ fn test_wallet_transactions_relevant() { .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); assert!(test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) + .list_canonical_txs( + test_wallet.local_chain(), + chain_tip, + CanonicalizationMods::NONE + ) .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); assert!(full_tx_count_before < full_tx_count_after); assert!(canonical_tx_count_before < canonical_tx_count_after); From b7072202d49ab06985a2fdf38144e54bed3acb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 23 Jan 2025 21:57:31 +1100 Subject: [PATCH 5/5] feat(chain)!: Add `CanonicalizationMods::assume_not_canonical` This is useful for crafting RBF transactions where you do not want to pick inputs from txs you are replacing or descendants of txs you are replacing. --- crates/chain/src/canonical_iter.rs | 43 ++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/crates/chain/src/canonical_iter.rs b/crates/chain/src/canonical_iter.rs index 8c972abae..ea1778bd1 100644 --- a/crates/chain/src/canonical_iter.rs +++ b/crates/chain/src/canonical_iter.rs @@ -9,19 +9,32 @@ use bdk_core::BlockId; use bitcoin::{Transaction, Txid}; /// Modifies the canonicalization algorithm. +#[non_exhaustive] #[derive(Debug, Default, Clone)] pub struct CanonicalizationMods { /// Transactions that will supercede all other transactions. /// /// In case of conflicting transactions within `assume_canonical`, transactions that appear /// later in the list (have higher index) have precedence. + /// + /// If the same transaction exists in both `assume_canonical` and `assume_not_canonical`, + /// `assume_not_canonical` will take precedence. pub assume_canonical: Vec, + + /// Transactions that will never be considered canonical. + /// + /// Descendants of these transactions will also be evicted. + /// + /// If the same transaction exists in both `assume_canonical` and `assume_not_canonical`, + /// `assume_not_canonical` will take precedence. + pub assume_not_canonical: Vec, } impl CanonicalizationMods { /// No mods. pub const NONE: Self = Self { assume_canonical: Vec::new(), + assume_not_canonical: Vec::new(), }; } @@ -51,6 +64,10 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { chain_tip: BlockId, mods: CanonicalizationMods, ) -> Self { + let mut not_canonical = HashSet::new(); + for txid in mods.assume_not_canonical { + Self::_mark_not_canonical(tx_graph, &mut not_canonical, txid); + } let anchors = tx_graph.all_anchors(); let unprocessed_assumed_txs = Box::new( mods.assume_canonical @@ -77,7 +94,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { unprocessed_seen_txs, unprocessed_leftover_txs: VecDeque::new(), canonical: HashMap::new(), - not_canonical: HashSet::new(), + not_canonical, queue: VecDeque::new(), } } @@ -142,18 +159,11 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { // Any conflicts with a canonical tx can be added to `not_canonical`. Descendants // of `not_canonical` txs can also be added to `not_canonical`. for (_, conflict_txid) in self.tx_graph.direct_conflicts(&tx) { - TxDescendants::new_include_root( + Self::_mark_not_canonical( self.tx_graph, + &mut self.not_canonical, conflict_txid, - |_: usize, txid: Txid| -> Option<()> { - if self.not_canonical.insert(txid) { - Some(()) - } else { - None - } - }, - ) - .run_until_finished() + ); } canonical_entry.insert((tx, this_reason)); self.queue.push_back(this_txid); @@ -162,6 +172,17 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { ) .run_until_finished() } + + fn _mark_not_canonical(tx_graph: &TxGraph, not_canonical: &mut HashSet, txid: Txid) { + TxDescendants::new_include_root(tx_graph, txid, |_: usize, txid: Txid| -> Option<()> { + if not_canonical.insert(txid) { + Some(()) + } else { + None + } + }) + .run_until_finished(); + } } impl Iterator for CanonicalIter<'_, A, C> {