diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index df9c08b01..cacc26ec1 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -99,6 +99,15 @@ fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_tx assert_eq!(txs.count(), exp_txs); } +fn run_list_ordered_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) { + let txs = tx_graph.graph().list_ordered_canonical_txs( + chain, + chain.tip().block_id(), + CanonicalizationParams::default(), + ); + assert_eq!(txs.count(), exp_txs); +} + fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) { let utxos = tx_graph.graph().filter_chain_txouts( chain, @@ -147,6 +156,13 @@ pub fn many_conflicting_unconfirmed(c: &mut Criterion) { let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2)) }); + c.bench_function( + "many_conflicting_unconfirmed::list_ordered_canonical_txs", + { + let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); + move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, 2)) + }, + ); c.bench_function("many_conflicting_unconfirmed::filter_chain_txouts", { let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 2)) @@ -185,6 +201,10 @@ pub fn many_chained_unconfirmed(c: &mut Criterion) { let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2101)) }); + c.bench_function("many_chained_unconfirmed::list_ordered_canonical_txs", { + let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); + move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, 2101)) + }); c.bench_function("many_chained_unconfirmed::filter_chain_txouts", { let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 1)) @@ -234,6 +254,13 @@ pub fn nested_conflicts(c: &mut Criterion) { let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH)) }); + c.bench_function( + "nested_conflicts_unconfirmed::list_ordered_canonical_txs", + { + let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); + move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH)) + }, + ); c.bench_function("nested_conflicts_unconfirmed::filter_chain_txouts", { let (tx_graph, chain) = (tx_graph.clone(), chain.clone()); move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, GRAPH_DEPTH)) diff --git a/crates/chain/src/canonical_iter.rs b/crates/chain/src/canonical_iter.rs index 204ead451..e314ffc57 100644 --- a/crates/chain/src/canonical_iter.rs +++ b/crates/chain/src/canonical_iter.rs @@ -1,5 +1,5 @@ use crate::collections::{HashMap, HashSet, VecDeque}; -use crate::tx_graph::{TxAncestors, TxDescendants}; +use crate::tx_graph::{CanonicalTx, TxAncestors, TxDescendants}; use crate::{Anchor, ChainOracle, TxGraph}; use alloc::boxed::Box; use alloc::collections::BTreeSet; @@ -14,7 +14,7 @@ type NotCanonicalSet = HashSet; /// Modifies the canonicalization algorithm. #[derive(Debug, Default, Clone)] pub struct CanonicalizationParams { - /// Transactions that will supercede all other transactions. + /// Transactions that will supersede all other transactions. /// /// In case of conflicting transactions within `assume_canonical`, transactions that appear /// later in the list (have higher index) have precedence. @@ -108,7 +108,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { .iter() .last() .expect( - "tx taken from `unprocessed_txs_with_anchors` so it must atleast have an anchor", + "tx taken from `unprocessed_txs_with_anchors` so it must at least have an anchor", ) .confirmation_height_upper_bound(), )); @@ -266,7 +266,7 @@ 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 + /// This transaction is explicitly assumed to be canonical by the caller, superseding all other /// canonicalization rules. Assumed { /// Whether it is a descendant that is assumed to be canonical. @@ -290,7 +290,7 @@ pub enum CanonicalReason { } impl CanonicalReason { - /// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other + /// Constructs a [`CanonicalReason`] for a transaction that is assumed to supersede all other /// transactions. pub fn assumed() -> Self { Self::Assumed { descendant: None } @@ -312,7 +312,7 @@ impl CanonicalReason { } } - /// Contruct a new [`CanonicalReason`] from the original which is transitive to `descendant`. + /// Construct a new [`CanonicalReason`] from the original which is transitive to `descendant`. /// /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's /// descendant, but is transitively relevant. @@ -342,3 +342,153 @@ impl CanonicalReason { } } } + +/// Iterator based on the Kahn's Algorithm, that yields transactions in topological order with +/// proper sorting within levels. +pub(crate) struct TopologicalIterator<'a, A> { + /// Map of txid to its canonical transaction + canonical_txs: HashMap, A>>, + + /// Current level of transactions to process + current_level: Vec, + /// Next level of transactions to process + next_level: Vec, + + /// Adjacency list: parent txid -> list of children txids + children_map: HashMap>, + /// Number of unprocessed parents for each transaction + parent_count: HashMap, + + /// Current index in the current level + current_index: usize, +} + +impl<'a, A: Clone + Anchor> TopologicalIterator<'a, A> { + pub(crate) fn new( + canonical_txs: impl Iterator, A>>, + ) -> Self { + // Build a map from txid to canonical tx for quick lookup + let mut tx_map: HashMap, A>> = HashMap::new(); + let mut canonical_set: HashSet = HashSet::new(); + + for canonical_tx in canonical_txs { + let txid = canonical_tx.tx_node.txid; + canonical_set.insert(txid); + tx_map.insert(txid, canonical_tx); + } + + // Build the dependency graph (txid -> parents it depends on) + let mut dependencies: HashMap> = HashMap::new(); + let mut has_parents: HashSet = HashSet::new(); + + for &txid in canonical_set.iter() { + let canonical_tx = tx_map.get(&txid).expect("txid must exist in map"); + let tx = &canonical_tx.tx_node.tx; + + // Find all parents (transactions this one depends on) + let mut parents = Vec::new(); + if !tx.is_coinbase() { + for txin in &tx.input { + let parent_txid = txin.previous_output.txid; + // Only include if the parent is also canonical + if canonical_set.contains(&parent_txid) { + parents.push(parent_txid); + has_parents.insert(txid); + } + } + } + + if !parents.is_empty() { + dependencies.insert(txid, parents); + } + } + + // Build adjacency list and parent counts for traversal + let mut parent_count = HashMap::new(); + let mut children_map: HashMap> = HashMap::new(); + + for (txid, parents) in &dependencies { + for parent_txid in parents { + children_map.entry(*parent_txid).or_default().push(*txid); + *parent_count.entry(*txid).or_insert(0) += 1; + } + } + + // Find root transactions (those with no parents in the canonical set) + let roots: Vec = canonical_set + .iter() + .filter(|&&txid| !has_parents.contains(&txid)) + .copied() + .collect(); + + // Sort the initial level + let mut current_level = roots; + Self::sort_level_by_chain_position(&mut current_level, &tx_map); + + Self { + canonical_txs: tx_map, + current_level, + next_level: Vec::new(), + children_map, + parent_count, + current_index: 0, + } + } + + /// Sort transactions within a level by their chain position + /// Confirmed transactions come first (sorted by height), then unconfirmed (sorted by last_seen) + fn sort_level_by_chain_position( + level: &mut [Txid], + canonical_txs: &HashMap, A>>, + ) { + level.sort_by(|&a_txid, &b_txid| { + let a_tx = canonical_txs.get(&a_txid).expect("txid must exist"); + let b_tx = canonical_txs.get(&b_txid).expect("txid must exist"); + + a_tx.cmp(b_tx) + }); + } + + fn advance_to_next_level(&mut self) { + self.current_level = core::mem::take(&mut self.next_level); + Self::sort_level_by_chain_position(&mut self.current_level, &self.canonical_txs); + self.current_index = 0; + } +} + +impl<'a, A: Clone + Anchor> Iterator for TopologicalIterator<'a, A> { + type Item = CanonicalTx<'a, Arc, A>; + + fn next(&mut self) -> Option { + // If we've exhausted the current level, move to next + if self.current_index >= self.current_level.len() { + if self.next_level.is_empty() { + return None; + } + self.advance_to_next_level(); + } + + let current_txid = self.current_level[self.current_index]; + self.current_index += 1; + + // If this is the last item in current level, prepare dependents for next level + if self.current_index == self.current_level.len() { + // Process all dependents of all transactions in current level + for &tx in &self.current_level { + if let Some(children) = self.children_map.get(&tx) { + for &child in children { + if let Some(count) = self.parent_count.get_mut(&child) { + *count -= 1; + if *count == 0 { + self.next_level.push(child); + } + } + } + } + } + } + + // Return the CanonicalTx for the current txid + self.canonical_txs.get(¤t_txid).cloned() + } +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 7bbdef63f..ffbb87e8a 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1090,6 +1090,31 @@ impl TxGraph { .map(|res| res.expect("infallible")) } + /// List graph transactions that are in `chain` with `chain_tip` in topological order. + /// + /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is + /// observed in-chain, and the [`TxNode`]. + /// + /// Transactions are returned in topological spending order, meaning that if transaction B + /// spends from transaction A, then A will always appear before B in the resulting list. + /// + /// This is the infallible version which uses [`list_canonical_txs`] internally and then + /// reorders the transactions based on their spending relationships. + /// + /// [`list_canonical_txs`]: Self::list_canonical_txs + pub fn list_ordered_canonical_txs<'a, C: ChainOracle>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + params: CanonicalizationParams, + ) -> impl Iterator, A>> { + use crate::canonical_iter::TopologicalIterator; + // Use the topological iterator to get the correct ordering + // The iterator handles all the graph building internally + #[allow(deprecated)] + TopologicalIterator::new(self.list_canonical_txs(chain, chain_tip, params)) + } + /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with /// `chain_tip`. /// @@ -1118,6 +1143,7 @@ impl TxGraph { ) -> 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, params) { let canonical_tx = r?; let txid = canonical_tx.tx_node.txid; @@ -1418,6 +1444,7 @@ impl TxGraph { I: fmt::Debug + Clone + Ord + 'a, { let indexer = indexer.as_ref(); + self.try_list_canonical_txs(chain, chain_tip, CanonicalizationParams::default()) .flat_map(move |res| -> Vec> { let range = &spk_index_range; diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 29f36169a..52f8e8508 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -56,7 +56,7 @@ impl TxOutTemplate { pub struct TxTemplateEnv<'a, A> { pub tx_graph: TxGraph, pub indexer: SpkTxOutIndex, - pub txid_to_name: HashMap<&'a str, Txid>, + pub tx_name_to_txid: HashMap<&'a str, Txid>, pub canonicalization_params: CanonicalizationParams, } @@ -77,7 +77,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( .script_pubkey(), ); }); - let mut txid_to_name = HashMap::<&'a str, Txid>::new(); + let mut tx_name_to_txid = HashMap::<&'a str, Txid>::new(); let mut canonicalization_params = CanonicalizationParams::default(); for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { @@ -108,7 +108,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( witness: Witness::new(), }, TxInTemplate::PrevTx(prev_name, prev_vout) => { - let prev_txid = txid_to_name.get(prev_name).expect( + let prev_txid = tx_name_to_txid.get(prev_name).expect( "txin template must spend from tx of template that comes before", ); TxIn { @@ -140,7 +140,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( if tx_tmp.assume_canonical { canonicalization_params.assume_canonical.push(txid); } - txid_to_name.insert(tx_tmp.tx_name, txid); + tx_name_to_txid.insert(tx_tmp.tx_name, txid); indexer.scan(&tx); let _ = tx_graph.insert_tx(tx.clone()); for anchor in tx_tmp.anchors.iter() { @@ -153,7 +153,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( TxTemplateEnv { tx_graph, indexer, - txid_to_name, + tx_name_to_txid, canonicalization_params, } } diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 1e87f009a..d6efef2ee 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -782,6 +782,7 @@ fn test_get_chain_position() { } // check chain position + let chain_pos = graph .graph() .list_canonical_txs( diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 685b62c6e..8c76c31b0 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -9,6 +9,7 @@ use bdk_chain::{ tx_graph::{ChangeSet, TxGraph}, Anchor, ChainOracle, ChainPosition, Merge, }; +use bdk_testenv::local_chain; use bdk_testenv::{block_id, hash, utils::new_tx}; use bitcoin::hex::FromHex; use bitcoin::Witness; @@ -1200,6 +1201,7 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch .into_iter() .collect(); let chain = LocalChain::from_blocks(blocks).unwrap(); + let canonical_txs: Vec<_> = graph .list_canonical_txs( &chain, @@ -1211,6 +1213,7 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch // 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(), @@ -1224,6 +1227,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, @@ -1540,3 +1544,514 @@ fn test_get_first_seen_of_a_tx() { let first_seen = graph.get_tx_node(txid).unwrap().first_seen; assert_eq!(first_seen, Some(seen_at)); } + +struct Scenario<'a> { + /// Name of the test scenario + name: &'a str, + /// Transaction templates + tx_templates: &'a [TxTemplate<'a, BlockId>], + /// Names of txs that must exist in the output of `list_canonical_txs` + exp_chain_txs: Vec<&'a str>, +} + +#[test] +fn test_list_canonical_txs_topologically() { + // chain + let local_chain: LocalChain = local_chain!( + (0, hash!("A")), + (1, hash!("B")), + (2, hash!("C")), + (3, hash!("D")), + (4, hash!("E")), + (5, hash!("F")), + (6, hash!("G")) + ); + let chain_tip = local_chain.tip().block_id(); + + let scenarios = [ + // a0 b0 c0 + Scenario { + name: "a0, b0 and c0 are roots, does not spend from any other transaction, and are in the best chain", + tx_templates: &[ + TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b0", + inputs: &[], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[], + outputs: &[TxOutTemplate::new(2500, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + ], + exp_chain_txs: Vec::from(["a0", "b0", "c0"]), + }, + // a0 b0 c0 + Scenario { + name: "a0, b0 and c0 are roots, does not spend from any other transaction, and have no anchor or last_seen", + tx_templates: &[ + TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b0", + inputs: &[], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[], + outputs: &[TxOutTemplate::new(2500, Some(0))], + anchors: &[], + last_seen: None, + assume_canonical: false, + }, + ], + exp_chain_txs: Vec::from([]), + }, + // a0 b0 c0 + Scenario { + name: "A, B and C are roots, does not spend from any other transaction, and are all have the same `last_seen`", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[], + last_seen: Some(1000), + assume_canonical: false, + }, + TxTemplate { + tx_name: "B", + inputs: &[], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[], + last_seen: Some(1000), + assume_canonical: false, + }, + TxTemplate { + tx_name: "C", + inputs: &[], + outputs: &[TxOutTemplate::new(2500, Some(0))], + anchors: &[], + last_seen: Some(1000), + assume_canonical: false, + }, + ], + exp_chain_txs: Vec::from(["A", "B", "C"]), + }, + // a0 + // \ + // b0 + // \ + // \ c0 + // \ / + // d0 + Scenario { + name: "b0 spends a0, d0 spends both b0 and c0, and are in the best chain", + tx_templates: &[ + TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "A")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b0", + inputs: &[TxInTemplate::PrevTx("a0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(2, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(3, "C")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "d0", + inputs: &[TxInTemplate::PrevTx("b0", 0), TxInTemplate::PrevTx("c0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(3, "C")], + last_seen: None, + assume_canonical: false, + }, + ], + exp_chain_txs: Vec::from(["a0", "b0", "c0", "d0"]), + }, + // a0 c0 + // \ + // b0 + // \ + // d0 + Scenario { + name: "b0 spends a0, d0 spends b0, and a0, b0 and c0 are in the best chain", + tx_templates: &[ + TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "A")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b0", + inputs: &[TxInTemplate::PrevTx("a0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(2, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(3, "C")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "d0", + inputs: &[TxInTemplate::PrevTx("b0", 0)], + outputs: &[TxOutTemplate::new(2500, Some(0))], + anchors: &[], + last_seen: Some(1000), + assume_canonical: false, + }, + ], + exp_chain_txs: Vec::from(["a0", "b0", "c0", "d0"]), + }, + // a0 + // \ + // b0 + // \ + // c0 + Scenario { + name: "c0 spend a0, b0 spend a0, and a0, b0 are in the best chain", + tx_templates: &[ + TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b0", + inputs: &[TxInTemplate::PrevTx("a0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[TxInTemplate::PrevTx("b0", 0)], + outputs: &[TxOutTemplate::new(2500, Some(0))], + anchors: &[], + last_seen: Some(1000), + assume_canonical: false, + }, + ], + exp_chain_txs: Vec::from(["a0", "b0", "c0"]), + }, + // a0 + // / \ + // b0 b1 + // / \ \ + // c0 \ c1 + // \ / + // d0 + Scenario { + name: "c0 spend b0, b0 spend a0, d0 spends both b0 and c1, c1 spend b1, b1 spend a0, and are all in the best chain", + tx_templates: &[TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, TxTemplate { + tx_name: "b0", + inputs: &[TxInTemplate::PrevTx("a0", 0)], + outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[TxInTemplate::PrevTx("b0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b1", + inputs: &[TxInTemplate::PrevTx("a0", 1)], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c1", + inputs: &[TxInTemplate::PrevTx("b1", 0)], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "d0", + inputs: &[TxInTemplate::PrevTx("b0", 1), TxInTemplate::PrevTx("c1", 0),], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }], + exp_chain_txs: Vec::from(["a0", "b0", "c0", "b1", "c1", "d0"]), + }, + // a0 d0 e0 + // / / \ + // b0 f0 f1 + // / \ / + // c0 g0 + Scenario { + name: "c0 spend b0, b0 spend a0, d0 does not spend any nor is spent by, g0 spends f0, f1, and f0 and f1 spends e0, and a0, d0, and e0 are in the best chain", + tx_templates: &[TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, TxTemplate { + tx_name: "b0", + inputs: &[TxInTemplate::PrevTx("a0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0)), TxOutTemplate::new(10000, Some(1))], + anchors: &[block_id!(2, "C")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[TxInTemplate::PrevTx("b0", 0)], + outputs: &[TxOutTemplate::new(2500, Some(0))], + anchors: &[block_id!(3, "D")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "d0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(3, "D")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "e0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], + anchors: &[block_id!(4, "E")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "f0", + inputs: &[TxInTemplate::PrevTx("e0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(5, "F")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "f1", + inputs: &[TxInTemplate::PrevTx("e0", 1)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(5, "F")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "g0", + inputs: &[TxInTemplate::PrevTx("f0", 0), TxInTemplate::PrevTx("f1", 0)], + outputs: &[TxOutTemplate::new(1000, Some(0))], + anchors: &[], + last_seen: Some(1000), + assume_canonical: false, + } + ], + exp_chain_txs: Vec::from(["a0", "b0", "c0", "d0", "e0", "f0", "f1", "g0"]), + }, + // a0 + // / \ \ + // e0 / b1 + // / / \ + // f0 / \ + // \/ \ + // b0 \ + // / \ / + // c0 \ c1 + // \ / + // d0 + Scenario { + name: "c0 spend b0, b0 spends both f0 and a0, f0 spend e0, e0 spend a0, d0 spends both b0 and c1, c1 spend b1, b1 spend a0, and are all in the best chain", + tx_templates: &[TxTemplate { + tx_name: "a0", + inputs: &[], + outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1)), TxOutTemplate::new(10000, Some(2))], + // outputs: &[TxOutTemplate::new(10000, Some(1)), TxOutTemplate::new(10000, Some(2))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "e0", + inputs: &[TxInTemplate::PrevTx("a0", 0)], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "f0", + inputs: &[TxInTemplate::PrevTx("e0", 0)], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b0", + inputs: &[TxInTemplate::PrevTx("f0", 0), TxInTemplate::PrevTx("a0", 1)], + outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c0", + inputs: &[TxInTemplate::PrevTx("b0", 0)], + outputs: &[TxOutTemplate::new(5000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "b1", + inputs: &[TxInTemplate::PrevTx("a0", 2)], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "c1", + inputs: &[TxInTemplate::PrevTx("b1", 0)], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }, + TxTemplate { + tx_name: "d0", + inputs: &[TxInTemplate::PrevTx("b0", 1), TxInTemplate::PrevTx("c1", 0), ], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + assume_canonical: false, + }], + exp_chain_txs: Vec::from(["a0", "e0", "f0", "b0", "c0", "b1", "c1", "d0"]), + }]; + + for scenario in scenarios { + let env = init_graph(scenario.tx_templates.iter()); + + let canonical_txids = env + .tx_graph + .list_ordered_canonical_txs( + &local_chain, + chain_tip, + env.canonicalization_params.clone(), + ) + .map(|tx| tx.tx_node.txid) + .collect::>(); + + let exp_txids = scenario + .exp_chain_txs + .iter() + .map(|txid| *env.tx_name_to_txid.get(txid).expect("txid must exist")) + .collect::>(); + + assert_eq!( + HashSet::::from_iter(canonical_txids.clone()), + HashSet::::from_iter(exp_txids.clone()), + "\n[{}] 'list_canonical_txs' failed", + scenario.name + ); + + assert!( + is_txs_in_topological_order(canonical_txids, env.tx_graph), + "\n[{}] 'list_canonical_txs' failed to output the txs in topological order", + scenario.name + ); + } +} + +fn is_txs_in_topological_order(txs: Vec, tx_graph: TxGraph) -> bool { + let mut seen: HashSet = HashSet::new(); + + for txid in txs { + let tx = tx_graph.get_tx(txid).expect("should exist"); + let inputs: Vec = tx + .input + .iter() + .map(|txin| txin.previous_output.txid) + .collect(); + + // assert that all the txin's have been seen already + for input_txid in inputs { + if !seen.contains(&input_txid) { + return false; + } + } + + // Add current transaction to seen set + seen.insert(txid); + } + + true +} diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 71944e404..113a985b4 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -978,7 +978,7 @@ fn test_tx_conflict_handling() { let exp_txs = scenario .exp_chain_txs .iter() - .map(|txid| *env.txid_to_name.get(txid).expect("txid must exist")) + .map(|txid| *env.tx_name_to_txid.get(txid).expect("txid must exist")) .collect::>(); assert_eq!( txs, exp_txs, @@ -1000,7 +1000,7 @@ fn test_tx_conflict_handling() { .exp_chain_txouts .iter() .map(|(txid, vout)| OutPoint { - txid: *env.txid_to_name.get(txid).expect("txid must exist"), + txid: *env.tx_name_to_txid.get(txid).expect("txid must exist"), vout: *vout, }) .collect::>(); @@ -1024,7 +1024,7 @@ fn test_tx_conflict_handling() { .exp_unspents .iter() .map(|(txid, vout)| OutPoint { - txid: *env.txid_to_name.get(txid).expect("txid must exist"), + txid: *env.tx_name_to_txid.get(txid).expect("txid must exist"), vout: *vout, }) .collect::>(); diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index ec226ab61..a47e6c61e 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -71,7 +71,7 @@ where if let Some(chain_update) = update.chain_update.clone() { let _ = chain .apply_update(chain_update) - .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; + .map_err(|err| anyhow::anyhow!("LocalChain update error: {err:?}"))?; } let _ = graph.apply_update(update.tx_update.clone()); diff --git a/examples/example_bitcoind_rpc_polling/src/main.rs b/examples/example_bitcoind_rpc_polling/src/main.rs index cb710151a..ff2426add 100644 --- a/examples/example_bitcoind_rpc_polling/src/main.rs +++ b/examples/example_bitcoind_rpc_polling/src/main.rs @@ -137,6 +137,7 @@ fn main() -> anyhow::Result<()> { } = rpc_args; let rpc_client = rpc_args.new_client()?; + let mut emitter = { let chain = chain.lock().unwrap(); let graph = graph.lock().unwrap(); @@ -237,6 +238,7 @@ fn main() -> anyhow::Result<()> { let sigterm_flag = start_ctrlc_handler(); let rpc_client = Arc::new(rpc_args.new_client()?); + let mut emitter = { let chain = chain.lock().unwrap(); let graph = graph.lock().unwrap(); diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs index db5f31d0f..07811c45a 100644 --- a/examples/example_cli/src/lib.rs +++ b/examples/example_cli/src/lib.rs @@ -229,7 +229,7 @@ impl FromStr for CoinSelectionAlgo { "oldest-first" => OldestFirst, "newest-first" => NewestFirst, "bnb" => BranchAndBound, - unknown => bail!("unknown coin selection algorithm '{}'", unknown), + unknown => bail!("unknown coin selection algorithm '{unknown}'"), }) } } @@ -715,8 +715,8 @@ pub fn handle_commands( _ => unimplemented!("multi xkey signer"), }; - let _ = sign_res - .map_err(|errors| anyhow::anyhow!("failed to sign PSBT {:?}", errors))?; + let _ = + sign_res.map_err(|errors| anyhow::anyhow!("failed to sign PSBT {errors:?}"))?; let mut obj = serde_json::Map::new(); obj.insert("psbt".to_string(), json!(psbt.to_string())); diff --git a/examples/example_electrum/src/main.rs b/examples/example_electrum/src/main.rs index bc76776a6..ede41315b 100644 --- a/examples/example_electrum/src/main.rs +++ b/examples/example_electrum/src/main.rs @@ -238,6 +238,7 @@ fn main() -> anyhow::Result<()> { .map(|(_, utxo)| utxo.outpoint), ); }; + if unconfirmed { request = request.txids( graph diff --git a/examples/example_esplora/src/main.rs b/examples/example_esplora/src/main.rs index f41d2536e..c08f23348 100644 --- a/examples/example_esplora/src/main.rs +++ b/examples/example_esplora/src/main.rs @@ -253,6 +253,7 @@ fn main() -> anyhow::Result<()> { .map(|(_, utxo)| utxo.outpoint), ); }; + if unconfirmed { // We want to search for whether the unconfirmed transaction is now confirmed. // We provide the unconfirmed txids to