From 486e0e143741a8c1312e7cb5258b3d43256dc9ec Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 15:48:42 +0200 Subject: [PATCH 1/9] doc(chain): Fix typos Co-authored-by: Wei Chen --- crates/chain/src/tx_graph.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index cfd2de9d..95797f28 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -324,10 +324,10 @@ impl TxGraph { /// The supplied closure takes in two inputs `(depth, descendant_txid)`: /// /// * `depth` is the distance between the starting `txid` and the `descendant_txid`. I.e., if the - /// descendant is spending an output of the starting `txid`; the `depth` will be 1. + /// descendant is spending an output of the starting `txid` then `depth` will be 1. /// * `descendant_txid` is the descendant's txid which we are considering to walk. /// - /// The supplied closure returns an `Option`, allowing the caller to map each node it vists + /// The supplied closure returns an `Option`, allowing the caller to map each node it visits /// and decide whether to visit descendants. pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants where @@ -1173,7 +1173,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { descendants } - /// Creates a `TxDescendants` from multiple starting transactions that include the starting + /// Creates a `TxDescendants` from multiple starting transactions that includes the starting /// `txid`s when iterating. pub(crate) fn from_multiple_include_root( graph: &'g TxGraph, From 2f26eca607dc5de83a9bb12c33fa5336026ab3dd Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 18:42:49 +0200 Subject: [PATCH 2/9] fix(chain): TxDescendants performs a BFS This commit also changes test_descendants_no_repeat to check the order of the transactions returned --- crates/chain/src/tx_graph.rs | 24 ++++++++++++------------ crates/chain/tests/test_tx_graph.rs | 11 +++-------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 95797f28..73d602df 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1145,7 +1145,7 @@ impl AsRef> for TxGraph { pub struct TxDescendants<'g, A, F> { graph: &'g TxGraph, visited: HashSet, - stack: Vec<(usize, Txid)>, + queue: VecDeque<(usize, Txid)>, filter_map: F, } @@ -1156,7 +1156,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { Self { graph, visited: Default::default(), - stack: [(0, txid)].into(), + queue: [(0, txid)].into(), filter_map, } } @@ -1166,10 +1166,10 @@ impl<'g, A, F> TxDescendants<'g, A, F> { let mut descendants = Self { graph, visited: Default::default(), - stack: Default::default(), + queue: Default::default(), filter_map, }; - descendants.populate_stack(1, txid); + descendants.populate_queue(1, txid); descendants } @@ -1186,7 +1186,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> { Self { graph, visited: Default::default(), - stack: txids.into_iter().map(|txid| (0, txid)).collect(), + queue: txids.into_iter().map(|txid| (0, txid)).collect(), filter_map, } } @@ -1205,25 +1205,25 @@ impl<'g, A, F> TxDescendants<'g, A, F> { let mut descendants = Self { graph, visited: Default::default(), - stack: Default::default(), + queue: Default::default(), filter_map, }; for txid in txids { - descendants.populate_stack(1, txid); + descendants.populate_queue(1, txid); } descendants } } impl<'g, A, F> TxDescendants<'g, A, F> { - fn populate_stack(&mut self, depth: usize, txid: Txid) { + fn populate_queue(&mut self, depth: usize, txid: Txid) { let spend_paths = self .graph .spends .range(tx_outpoint_range(txid)) .flat_map(|(_, spends)| spends) .map(|&txid| (depth, txid)); - self.stack.extend(spend_paths); + self.queue.extend(spend_paths); } } @@ -1235,8 +1235,8 @@ where fn next(&mut self) -> Option { let (op_spends, txid, item) = loop { - // we have exhausted all paths when stack is empty - let (op_spends, txid) = self.stack.pop()?; + // we have exhausted all paths when queue is empty + let (op_spends, txid) = self.queue.pop_front()?; // we do not want to visit the same transaction twice if self.visited.insert(txid) { // ignore paths when user filters them out @@ -1246,7 +1246,7 @@ where } }; - self.populate_stack(op_spends + 1, txid); + self.populate_queue(op_spends + 1, txid); Some(item) } } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 4c68f510..36a27a58 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -610,7 +610,7 @@ fn test_descendants_no_repeat() { .collect::>(); let mut graph = TxGraph::<()>::default(); - let mut expected_txids = BTreeSet::new(); + let mut expected_txids = Vec::new(); // these are NOT descendants of `tx_a` for tx in txs_not_connected { @@ -625,19 +625,14 @@ fn test_descendants_no_repeat() { .chain(core::iter::once(&tx_e)) { let _ = graph.insert_tx(tx.clone()); - assert!(expected_txids.insert(tx.txid())); + expected_txids.push(tx.txid()); } let descendants = graph .walk_descendants(tx_a.txid(), |_, txid| Some(txid)) .collect::>(); - assert_eq!(descendants.len(), expected_txids.len()); - - for txid in descendants { - assert!(expected_txids.remove(&txid)); - } - assert!(expected_txids.is_empty()); + assert_eq!(descendants, expected_txids); } #[test] From 4742d88ea322e43120fa96f6421a149294d26e3b Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 15:47:43 +0200 Subject: [PATCH 3/9] feat(chain): Introduce TxAncestors, walk_ancestors Co-authored-by: Wei Chen --- crates/chain/src/tx_graph.rs | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 73d602df..4c0cdb3d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,6 +55,7 @@ use crate::{ ChainOracle, ChainPosition, FullTxOut, }; use alloc::vec::Vec; +use alloc::collections::vec_deque::VecDeque; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use core::{ convert::Infallible, @@ -319,6 +320,30 @@ impl TxGraph { .map(|(outpoint, spends)| (outpoint.vout, spends)) } + /// Creates an iterator that filters and maps ancestor transactions. + /// + /// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx` + /// are transactions spent by `tx`). The supplied transaction is excluded from the iterator. + /// + /// The supplied closure takes in two inputs `(depth, ancestor_tx)`: + /// + /// * `depth` is the distance between the starting `Transaction` and the `ancestor_tx`. I.e., if + /// the `Transaction` is spending an output of the `ancestor_tx` then `depth` will be 1. + /// * `ancestor_tx` is the `Transaction`'s ancestor which we are considering to walk. + /// + /// The supplied closure returns an `Option`, allowing the caller to map each `Transaction` + /// it visits and decide whether to visit ancestors. + pub fn walk_ancestors<'g, F, O>( + &'g self, + tx: &'g Transaction, + walk_map: F, + ) -> TxAncestors<'g, A, F> + where + F: FnMut(usize, &'g Transaction) -> Option + 'g, + { + TxAncestors::new_exclude_root(self, tx, walk_map) + } + /// Creates an iterator that filters and maps descendants from the starting `txid`. /// /// The supplied closure takes in two inputs `(depth, descendant_txid)`: @@ -1137,6 +1162,126 @@ impl AsRef> for TxGraph { } } +/// An iterator that traverses ancestors of a given root transaction. +/// +/// The iterator excludes partial transactions. +/// +/// This `struct` is created by the [`walk_ancestors`] method of [`TxGraph`]. +/// +/// [`walk_ancestors`]: TxGraph::walk_ancestors +pub struct TxAncestors<'g, A, F> { + graph: &'g TxGraph, + visited: HashSet, + queue: VecDeque<(usize, &'g Transaction)>, + filter_map: F, +} + +impl<'g, A, F> TxAncestors<'g, A, F> { + /// Creates a `TxAncestors` that includes the starting `Transaction` when iterating. + pub(crate) fn new_include_root( + graph: &'g TxGraph, + tx: &'g Transaction, + filter_map: F, + ) -> Self { + Self { + graph, + visited: Default::default(), + queue: [(0, tx)].into(), + filter_map, + } + } + + /// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating. + pub(crate) fn new_exclude_root( + graph: &'g TxGraph, + tx: &'g Transaction, + filter_map: F, + ) -> Self { + let mut ancestors = Self { + graph, + visited: Default::default(), + queue: Default::default(), + filter_map, + }; + ancestors.populate_queue(1, tx); + ancestors + } + + /// Creates a `TxAncestors` from multiple starting `Transaction`s that includes the starting + /// `Transaction`s when iterating. + #[allow(unused)] + pub(crate) fn from_multiple_include_root( + graph: &'g TxGraph, + txs: I, + filter_map: F, + ) -> Self + where + I: IntoIterator, + { + Self { + graph, + visited: Default::default(), + queue: txs.into_iter().map(|tx| (0, tx)).collect(), + filter_map, + } + } + + /// Creates a `TxAncestors` from multiple starting `Transaction`s that excludes the starting + /// `Transaction`s when iterating. + #[allow(unused)] + pub(crate) fn from_multiple_exclude_root( + graph: &'g TxGraph, + txs: I, + filter_map: F, + ) -> Self + where + I: IntoIterator, + { + let mut ancestors = Self { + graph, + visited: Default::default(), + queue: Default::default(), + filter_map, + }; + for tx in txs { + ancestors.populate_queue(1, tx); + } + ancestors + } + + fn populate_queue(&mut self, depth: usize, tx: &'g Transaction) { + let ancestors = tx + .input + .iter() + .map(|txin| txin.previous_output.txid) + .filter(|&prev_txid| self.visited.insert(prev_txid)) + .filter_map(|prev_txid| self.graph.get_tx(prev_txid)) + .map(|tx| (depth, tx)); + self.queue.extend(ancestors); + } +} + +impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F> +where + F: FnMut(usize, &'g Transaction) -> Option, +{ + type Item = O; + + fn next(&mut self) -> Option { + loop { + // we have exhausted all paths when queue is empty + let (ancestor_depth, tx) = self.queue.pop_front()?; + // ignore paths when user filters them out + let item = match (self.filter_map)(ancestor_depth, tx) { + Some(item) => item, + None => continue, + }; + self.populate_queue(ancestor_depth + 1, tx); + return Some(item); + } + } +} + /// An iterator that traverses transaction descendants. /// /// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`]. From a3e8480ad9d41190da8de732dc8d00d636a0c911 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 16:51:15 +0200 Subject: [PATCH 4/9] doc(chain): Clarify direct_conflicts_of_tx's docs Co-authored-by: Wei Chen --- crates/chain/src/tx_graph.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 4c0cdb3d..036f116d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -381,8 +381,9 @@ impl TxGraph { /// transaction's inputs (spends). The conflicting txids are returned with the given /// transaction's vin (in which it conflicts). /// - /// Note that this only returns directly conflicting txids and does not include descendants of - /// those txids (which are technically also conflicting). + /// Note that this only returns directly conflicting txids and won't include: + /// - descendants of conflicting transactions (which are technically also conflicting) + /// - transactions conflicting with the given transaction's ancestors pub fn direct_conflicts_of_tx<'g>( &'g self, tx: &'g Transaction, From 62de55f12d910ac35180cbb3f0e4c4b353c44d11 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 16:51:50 +0200 Subject: [PATCH 5/9] fix(chain): Consider conflicting ancestors in... ...try_get_chain_pos In try_get_chain_pos, when we notice that a transaction is not included in the best chain, we check the transactions in mempool to find conflicting ones, and decide based on that if our transaction is still in mempool or has been dropped. This commit adds a check for transactions conflicting with the unconfirmed ancestors of our tx. Co-authored-by: Wei Chen --- crates/chain/src/tx_graph.rs | 78 ++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 036f116d..edc1e496 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -54,8 +54,8 @@ use crate::{ collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId, ChainOracle, ChainPosition, FullTxOut, }; -use alloc::vec::Vec; use alloc::collections::vec_deque::VecDeque; +use alloc::vec::Vec; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use core::{ convert::Infallible, @@ -697,8 +697,9 @@ impl TxGraph { } } - // The tx is not anchored to a block which is in the best chain, let's check whether we can - // ignore it by checking conflicts! + // The tx is not anchored to a block which is in the best chain, which means that it + // might be in mempool, or it might have been dropped already. + // Let's check conflicts to find out! let tx = match tx_node { TxNodeInternal::Whole(tx) => tx, TxNodeInternal::Partial(_) => { @@ -707,18 +708,71 @@ impl TxGraph { } }; - // If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then - // this tx cannot exist in the best chain - for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { - for block in conflicting_tx.anchors.iter().map(A::anchor_block) { - if chain.is_block_in_chain(block, chain_tip)? == Some(true) { - // conflicting tx is in best chain, so the current tx cannot be in best chain! + // We want to retrieve all the transactions that conflict with us, plus all the + // transactions that conflict with our unconfirmed ancestors, since they conflict with us + // as well. + // We only traverse unconfirmed ancestors since conflicts of confirmed transactions + // cannot be in the best chain. + + // First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the + // resulting array will also include `tx` + let unconfirmed_ancestor_txs = + TxAncestors::new_include_root(self, tx, |_, ancestor_tx: &Transaction| { + let tx_node = self.get_tx_node(ancestor_tx.txid())?; + // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in + // the best chain) + for block in tx_node.anchors { + match chain.is_block_in_chain(block.anchor_block(), chain_tip) { + Ok(Some(true)) => return None, + Err(e) => return Some(Err(e)), + _ => continue, + } + } + Some(Ok(tx_node)) + }) + .collect::, C::Error>>()?; + + // We determine our tx's last seen, which is the max between our last seen, + // and our unconf descendants' last seen. + let unconfirmed_descendants_txs = + TxDescendants::new_include_root(self, tx.txid(), |_, descendant_txid: Txid| { + let tx_node = self.get_tx_node(descendant_txid)?; + // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in + // the best chain) + for block in tx_node.anchors { + match chain.is_block_in_chain(block.anchor_block(), chain_tip) { + Ok(Some(true)) => return None, + Err(e) => return Some(Err(e)), + _ => continue, + } + } + Some(Ok(tx_node)) + }) + .collect::, C::Error>>()?; + + let tx_last_seen = unconfirmed_descendants_txs + .iter() + .max_by_key(|tx| tx.last_seen_unconfirmed) + .map(|tx| tx.last_seen_unconfirmed) + .expect("descendants always includes at least one transaction (the root tx"); + + // Now we traverse our ancestors and consider all their conflicts + for tx_node in unconfirmed_ancestor_txs { + // We retrieve all the transactions conflicting with this specific ancestor + let conflicting_txs = self.walk_conflicts(tx_node.tx, |_, txid| self.get_tx_node(txid)); + + // If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then + // this tx cannot exist in the best chain + for conflicting_tx in conflicting_txs { + for block in conflicting_tx.anchors { + if chain.is_block_in_chain(block.anchor_block(), chain_tip)? == Some(true) { + return Ok(None); + } + } + if conflicting_tx.last_seen_unconfirmed > tx_last_seen { return Ok(None); } } - if conflicting_tx.last_seen_unconfirmed > *last_seen { - return Ok(None); - } } Ok(Some(ChainPosition::Unconfirmed(*last_seen))) From 6e511473a5e5730d5cad237c651c83dc4c5d8756 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 16:02:48 +0200 Subject: [PATCH 6/9] test(chain): add block_id! utility macro Co-authored-by: Wei Chen --- crates/chain/tests/common/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index 2573fd96..694d90dc 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -1,3 +1,13 @@ +#[allow(unused_macros)] +macro_rules! block_id { + ($height:expr, $hash:literal) => {{ + bdk_chain::BlockId { + height: $height, + hash: bitcoin::hashes::Hash::hash($hash.as_bytes()), + } + }}; +} + #[allow(unused_macros)] macro_rules! h { ($index:literal) => {{ From 59a2403e2802d27c51f2d30b5f8e1ed9f813dfcc Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 15:43:48 +0200 Subject: [PATCH 7/9] test(chain): Introduce TxTemplate Co-authored-by: Wei Chen --- crates/chain/tests/common/mod.rs | 3 + crates/chain/tests/common/tx_template.rs | 136 +++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 crates/chain/tests/common/tx_template.rs diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index 694d90dc..d3db8191 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -1,3 +1,6 @@ +mod tx_template; +pub use tx_template::*; + #[allow(unused_macros)] macro_rules! block_id { ($height:expr, $hash:literal) => {{ diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs new file mode 100644 index 00000000..605a0ba7 --- /dev/null +++ b/crates/chain/tests/common/tx_template.rs @@ -0,0 +1,136 @@ +use rand::distributions::{Alphanumeric, DistString}; +use std::collections::HashMap; + +use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex}; +use bitcoin::{ + locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction, + TxIn, TxOut, Txid, Witness, +}; +use miniscript::Descriptor; + +/// Template for creating a transaction in `TxGraph`. +/// +/// The incentive for transaction templates is to create a transaction history in a simple manner to +/// avoid having to explicitly hash previous transactions to form previous outpoints of later +/// transactions. +#[derive(Clone, Copy, Default)] +pub struct TxTemplate<'a, A> { + /// Uniquely identifies the transaction, before it can have a txid. + pub tx_name: &'a str, + pub inputs: &'a [TxInTemplate<'a>], + pub outputs: &'a [TxOutTemplate], + pub anchors: &'a [A], + pub last_seen: Option, +} + +#[allow(dead_code)] +pub enum TxInTemplate<'a> { + /// This will give a random txid and vout. + Bogus, + + /// This is used for coinbase transactions because they do not have previous outputs. + Coinbase, + + /// Contains the `tx_name` and `vout` that we are spending. The rule is that we must only spend + /// from tx of a previous `TxTemplate`. + PrevTx(&'a str, usize), +} + +pub struct TxOutTemplate { + pub value: u64, + pub spk_index: Option, // some = get spk from SpkTxOutIndex, none = random spk +} + +#[allow(unused)] +impl TxOutTemplate { + pub fn new(value: u64, spk_index: Option) -> Self { + TxOutTemplate { value, spk_index } + } +} + +#[allow(dead_code)] +pub fn init_graph<'a>( + tx_templates: impl IntoIterator>, +) -> (TxGraph, SpkTxOutIndex, HashMap<&'a str, Txid>) { + let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap(); + let mut graph = TxGraph::::default(); + let mut spk_index = SpkTxOutIndex::default(); + (0..10).for_each(|index| { + spk_index.insert_spk( + index, + descriptor + .at_derivation_index(index) + .unwrap() + .script_pubkey(), + ); + }); + let mut tx_ids = HashMap::<&'a str, Txid>::new(); + + for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { + let tx = Transaction { + version: 0, + lock_time: LockTime::ZERO, + input: tx_tmp + .inputs + .iter() + .map(|input| match input { + TxInTemplate::Bogus => TxIn { + previous_output: OutPoint::new( + bitcoin::hashes::Hash::hash( + Alphanumeric + .sample_string(&mut rand::thread_rng(), 20) + .as_bytes(), + ), + bogus_txin_vout as u32, + ), + script_sig: ScriptBuf::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }, + TxInTemplate::Coinbase => TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }, + TxInTemplate::PrevTx(prev_name, prev_vout) => { + let prev_txid = tx_ids.get(prev_name).expect( + "txin template must spend from tx of template that comes before", + ); + TxIn { + previous_output: OutPoint::new(*prev_txid, *prev_vout as _), + script_sig: ScriptBuf::new(), + sequence: Sequence::default(), + witness: Witness::new(), + } + } + }) + .collect(), + output: tx_tmp + .outputs + .iter() + .map(|output| match &output.spk_index { + None => TxOut { + value: output.value, + script_pubkey: ScriptBuf::new(), + }, + Some(index) => TxOut { + value: output.value, + script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(), + }, + }) + .collect(), + }; + + tx_ids.insert(tx_tmp.tx_name, tx.txid()); + spk_index.scan(&tx); + let _ = graph.insert_tx(tx.clone()); + for anchor in tx_tmp.anchors.iter() { + let _ = graph.insert_anchor(tx.txid(), *anchor); + } + if let Some(seen_at) = tx_tmp.last_seen { + let _ = graph.insert_seen_at(tx.txid(), seen_at); + } + } + (graph, spk_index, tx_ids) +} From 48ca95b5412fd3719b749d33c85572941817e967 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 15:54:38 +0200 Subject: [PATCH 8/9] test(chain): Add test for walk_ancestors Co-authored-by: Wei Chen --- crates/chain/tests/test_tx_graph.rs | 204 +++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 36a27a58..a0efd100 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -5,7 +5,7 @@ use bdk_chain::{ collections::*, local_chain::LocalChain, tx_graph::{ChangeSet, TxGraph}, - Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor, + Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor, }; use bitcoin::{ absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, @@ -496,6 +496,208 @@ fn test_calculate_fee_on_coinbase() { assert_eq!(graph.calculate_fee(&tx), Ok(0)); } +// `test_walk_ancestors` uses the following transaction structure: +// +// a0 +// / \ +// b0 b1 b2 +// / \ \ / +// c0 c1 c2 c3 +// / \ / +// d0 d1 +// \ +// e0 +// +// where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc. +#[test] +fn test_walk_ancestors() { + let local_chain: LocalChain = (0..=20) + .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) + .collect::>() + .into(); + let tip = local_chain.tip().expect("must have tip"); + + let tx_a0 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(h!("op0"), 0), + ..TxIn::default() + }], + output: vec![TxOut::default(), TxOut::default()], + ..common::new_tx(0) + }; + + // tx_b0 spends tx_a0 + let tx_b0 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a0.txid(), 0), + ..TxIn::default() + }], + output: vec![TxOut::default(), TxOut::default()], + ..common::new_tx(0) + }; + + // tx_b1 spends tx_a0 + let tx_b1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a0.txid(), 1), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + let tx_b2 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(h!("op1"), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_c0 spends tx_b0 + let tx_c0 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_b0.txid(), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_c1 spends tx_b0 + let tx_c1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_b0.txid(), 1), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_c2 spends tx_b1 and tx_b2 + let tx_c2 = Transaction { + input: vec![ + TxIn { + previous_output: OutPoint::new(tx_b1.txid(), 0), + ..TxIn::default() + }, + TxIn { + previous_output: OutPoint::new(tx_b2.txid(), 0), + ..TxIn::default() + }, + ], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + let tx_c3 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(h!("op2"), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_d0 spends tx_c1 + let tx_d0 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_c1.txid(), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_d1 spends tx_c2 and tx_c3 + let tx_d1 = Transaction { + input: vec![ + TxIn { + previous_output: OutPoint::new(tx_c2.txid(), 0), + ..TxIn::default() + }, + TxIn { + previous_output: OutPoint::new(tx_c3.txid(), 0), + ..TxIn::default() + }, + ], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + // tx_e0 spends tx_d1 + let tx_e0 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_d1.txid(), 0), + ..TxIn::default() + }], + output: vec![TxOut::default()], + ..common::new_tx(0) + }; + + let mut graph = TxGraph::::new(vec![ + tx_a0.clone(), + tx_b0.clone(), + tx_b1.clone(), + tx_b2.clone(), + tx_c0.clone(), + tx_c1.clone(), + tx_c2.clone(), + tx_c3.clone(), + tx_d0.clone(), + tx_d1.clone(), + tx_e0.clone(), + ]); + + [&tx_a0, &tx_b1].iter().for_each(|&tx| { + let _ = graph.insert_anchor(tx.txid(), tip.block_id()); + }); + + let ancestors = [ + graph + .walk_ancestors(&tx_c0, |depth, tx| Some((depth, tx))) + .collect::>(), + graph + .walk_ancestors(&tx_d0, |depth, tx| Some((depth, tx))) + .collect::>(), + graph + .walk_ancestors(&tx_e0, |depth, tx| Some((depth, tx))) + .collect::>(), + // Only traverse unconfirmed ancestors of tx_e0 this time + graph + .walk_ancestors(&tx_e0, |depth, tx| { + let tx_node = graph.get_tx_node(tx.txid())?; + for block in tx_node.anchors { + match local_chain.is_block_in_chain(block.anchor_block(), tip.block_id()) { + Ok(Some(true)) => return None, + _ => continue, + } + } + Some((depth, tx_node.tx)) + }) + .collect::>(), + ]; + + let expected_ancestors = [ + vec![(1, &tx_b0), (2, &tx_a0)], + vec![(1, &tx_c1), (2, &tx_b0), (3, &tx_a0)], + vec![ + (1, &tx_d1), + (2, &tx_c2), + (2, &tx_c3), + (3, &tx_b1), + (3, &tx_b2), + (4, &tx_a0), + ], + vec![(1, &tx_d1), (2, &tx_c2), (2, &tx_c3), (3, &tx_b2)], + ]; + + for (txids, expected_txids) in ancestors.iter().zip(expected_ancestors.iter()) { + assert_eq!(txids, expected_txids); + } +} + #[test] fn test_conflicting_descendants() { let previous_output = OutPoint::new(h!("op"), 2); From 6d601a7e885bfc627594b794b1a460be47799eea Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Fri, 29 Sep 2023 15:43:58 +0200 Subject: [PATCH 9/9] test(chain): Add test for conflicting transactions Co-authored-by: Wei Chen --- crates/chain/tests/test_tx_graph_conflicts.rs | 629 ++++++++++++++++++ 1 file changed, 629 insertions(+) create mode 100644 crates/chain/tests/test_tx_graph_conflicts.rs diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs new file mode 100644 index 00000000..1794d845 --- /dev/null +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -0,0 +1,629 @@ +#[macro_use] +mod common; + +use std::collections::{BTreeSet, HashSet}; + +use bdk_chain::{keychain::Balance, BlockId}; +use bitcoin::{OutPoint, Script}; +use common::*; + +#[allow(dead_code)] +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_chain_txs` + exp_chain_txs: HashSet<&'a str>, + /// Outpoints that must exist in the output of `filter_chain_txouts` + exp_chain_txouts: HashSet<(&'a str, u32)>, + /// Outpoints of UTXOs that must exist in the output of `filter_chain_unspents` + exp_unspents: HashSet<(&'a str, u32)>, + /// Expected balances + exp_balance: Balance, +} + +/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when +/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure. +/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`], +/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data. +#[test] +fn test_tx_conflict_handling() { + // Create Local chains + let local_chain = local_chain!( + (0, h!("A")), + (1, h!("B")), + (2, h!("C")), + (3, h!("D")), + (4, h!("E")), + (5, h!("F")), + (6, h!("G")) + ); + let chain_tip = local_chain + .tip() + .map(|cp| cp.block_id()) + .unwrap_or_default(); + + let scenarios = [ + Scenario { + name: "2 unconfirmed txs with same last_seens conflict", + tx_templates: &[ + TxTemplate { + tx_name: "tx1", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(40000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "tx_conflict_1", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(20000, Some(2))], + last_seen: Some(300), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_conflict_2", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(30000, Some(3))], + last_seen: Some(300), + ..Default::default() + }, + ], + // correct output if filtered by fee rate: tx1, tx_conflict_1 + exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1", "tx_conflict_2"]), + exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0), ("tx_conflict_2", 0)]), + // correct output if filtered by fee rate: tx_conflict_1 + exp_unspents: HashSet::from([("tx_conflict_1", 0), ("tx_conflict_2", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 50000, // correct output if filtered by fee rate: 20000 + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "2 unconfirmed txs with different last_seens conflict", + tx_templates: &[ + TxTemplate { + tx_name: "tx1", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "tx_conflict_1", + inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(2))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_conflict_2", + inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::PrevTx("tx1", 1)], + outputs: &[TxOutTemplate::new(30000, Some(3))], + last_seen: Some(300), + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]), + exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]), + exp_unspents: HashSet::from([("tx_conflict_2", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 30000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "3 unconfirmed txs with different last_seens conflict", + tx_templates: &[ + TxTemplate { + tx_name: "tx1", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "tx_conflict_1", + inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_conflict_2", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(30000, Some(2))], + last_seen: Some(300), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_conflict_3", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(40000, Some(3))], + last_seen: Some(400), + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["tx1", "tx_conflict_3"]), + exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]), + exp_unspents: HashSet::from([("tx_conflict_3", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 40000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "unconfirmed tx conflicts with tx in orphaned block, orphaned higher last_seen", + tx_templates: &[ + TxTemplate { + tx_name: "tx1", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "tx_conflict_1", + inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_orphaned_conflict", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(30000, Some(2))], + anchors: &[block_id!(4, "Orphaned Block")], + last_seen: Some(300), + }, + ], + exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]), + exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]), + exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 30000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "unconfirmed tx conflicts with tx in orphaned block, orphaned lower last_seen", + tx_templates: &[ + TxTemplate { + tx_name: "tx1", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "tx_conflict_1", + inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_orphaned_conflict", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(30000, Some(2))], + anchors: &[block_id!(4, "Orphaned Block")], + last_seen: Some(100), + }, + ], + exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]), + exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]), + exp_unspents: HashSet::from([("tx_conflict_1", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 20000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "multiple unconfirmed txs conflict with a confirmed tx", + tx_templates: &[ + TxTemplate { + tx_name: "tx1", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "tx_conflict_1", + inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_conflict_2", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(30000, Some(2))], + last_seen: Some(300), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_conflict_3", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(40000, Some(3))], + last_seen: Some(400), + ..Default::default() + }, + TxTemplate { + tx_name: "tx_confirmed_conflict", + inputs: &[TxInTemplate::PrevTx("tx1", 0)], + outputs: &[TxOutTemplate::new(50000, Some(4))], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["tx1", "tx_confirmed_conflict"]), + exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]), + exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 50000, + }, + }, + Scenario { + name: "B and B' spend A and conflict, C spends B, all the transactions are unconfirmed, B' has higher last_seen than B", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + last_seen: Some(22), + ..Default::default() + }, + TxTemplate { + tx_name: "B", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(23), + ..Default::default() + }, + TxTemplate { + tx_name: "B'", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(20000, Some(2))], + last_seen: Some(24), + ..Default::default() + }, + TxTemplate { + tx_name: "C", + inputs: &[TxInTemplate::PrevTx("B", 0)], + outputs: &[TxOutTemplate::new(30000, Some(3))], + last_seen: Some(25), + ..Default::default() + }, + ], + // A, B, C will appear in the list methods + // This is because B' has a higher last seen than B, but C has a higher + // last seen than B', so B and C are considered canonical + exp_chain_txs: HashSet::from(["A", "B", "C"]), + exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]), + exp_unspents: HashSet::from([("C", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 30000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "B and B' spend A and conflict, C spends B, A and B' are in best chain", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "B", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(20000, Some(1))], + ..Default::default() + }, + TxTemplate { + tx_name: "B'", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(20000, Some(2))], + anchors: &[block_id!(4, "E")], + ..Default::default() + }, + TxTemplate { + tx_name: "C", + inputs: &[TxInTemplate::PrevTx("B", 0)], + outputs: &[TxOutTemplate::new(30000, Some(3))], + ..Default::default() + }, + ], + // B and C should not appear in the list methods + exp_chain_txs: HashSet::from(["A", "B'"]), + exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]), + exp_unspents: HashSet::from([("B'", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 20000, + }, + }, + Scenario { + name: "B and B' spend A and conflict, C spends B', A and B' are in best chain", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "B", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(20000, Some(1))], + ..Default::default() + }, + TxTemplate { + tx_name: "B'", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(20000, Some(2))], + anchors: &[block_id!(4, "E")], + ..Default::default() + }, + TxTemplate { + tx_name: "C", + inputs: &[TxInTemplate::PrevTx("B'", 0)], + outputs: &[TxOutTemplate::new(30000, Some(3))], + ..Default::default() + }, + ], + // B should not appear in the list methods + exp_chain_txs: HashSet::from(["A", "B'", "C"]), + exp_chain_txouts: HashSet::from([ + ("A", 0), + ("B'", 0), + ("C", 0), + ]), + exp_unspents: HashSet::from([("C", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 30000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "B and B' spend A and conflict, C spends both B and B', A is in best chain", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "B", + inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "B'", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(30000, Some(2))], + last_seen: Some(300), + ..Default::default() + }, + TxTemplate { + tx_name: "C", + inputs: &[ + TxInTemplate::PrevTx("B", 0), + TxInTemplate::PrevTx("B'", 0), + ], + outputs: &[TxOutTemplate::new(20000, Some(3))], + ..Default::default() + }, + ], + // C should not appear in the list methods + exp_chain_txs: HashSet::from(["A", "B'"]), + exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]), + exp_unspents: HashSet::from([("B'", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 30000, + untrusted_pending: 0, + confirmed: 0, + }, + }, + Scenario { + name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', A is in best chain", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "B", + inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "B'", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(50000, Some(4))], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "C", + inputs: &[ + TxInTemplate::PrevTx("B", 0), + TxInTemplate::PrevTx("B'", 0), + ], + outputs: &[TxOutTemplate::new(20000, Some(5))], + ..Default::default() + }, + ], + // C should not appear in the list methods + exp_chain_txs: HashSet::from(["A", "B'"]), + exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]), + exp_unspents: HashSet::from([("B'", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 50000, + }, + }, + Scenario { + name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', D spends C, A is in best chain", + tx_templates: &[ + TxTemplate { + tx_name: "A", + inputs: &[TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(10000, Some(0))], + anchors: &[block_id!(1, "B")], + last_seen: None, + }, + TxTemplate { + tx_name: "B", + inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus], + outputs: &[TxOutTemplate::new(20000, Some(1))], + last_seen: Some(200), + ..Default::default() + }, + TxTemplate { + tx_name: "B'", + inputs: &[TxInTemplate::PrevTx("A", 0)], + outputs: &[TxOutTemplate::new(50000, Some(4))], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "C", + inputs: &[ + TxInTemplate::PrevTx("B", 0), + TxInTemplate::PrevTx("B'", 0), + ], + outputs: &[TxOutTemplate::new(20000, Some(5))], + ..Default::default() + }, + TxTemplate { + tx_name: "D", + inputs: &[TxInTemplate::PrevTx("C", 0)], + outputs: &[TxOutTemplate::new(20000, Some(6))], + ..Default::default() + }, + ], + // D should not appear in the list methods + exp_chain_txs: HashSet::from(["A", "B'"]), + exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]), + exp_unspents: HashSet::from([("B'", 0)]), + exp_balance: Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 50000, + }, + }, + ]; + + for scenario in scenarios { + let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter()); + + let txs = tx_graph + .list_chain_txs(&local_chain, chain_tip) + .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")) + .collect::>(); + assert_eq!( + txs, exp_txs, + "\n[{}] 'list_chain_txs' failed", + scenario.name + ); + + let txouts = tx_graph + .filter_chain_txouts( + &local_chain, + chain_tip, + spk_index.outpoints().iter().cloned(), + ) + .map(|(_, full_txout)| full_txout.outpoint) + .collect::>(); + let exp_txouts = scenario + .exp_chain_txouts + .iter() + .map(|(txid, vout)| OutPoint { + txid: *exp_tx_ids.get(txid).expect("txid must exist"), + vout: *vout, + }) + .collect::>(); + assert_eq!( + txouts, exp_txouts, + "\n[{}] 'filter_chain_txouts' failed", + scenario.name + ); + + let utxos = tx_graph + .filter_chain_unspents( + &local_chain, + chain_tip, + spk_index.outpoints().iter().cloned(), + ) + .map(|(_, full_txout)| full_txout.outpoint) + .collect::>(); + let exp_utxos = scenario + .exp_unspents + .iter() + .map(|(txid, vout)| OutPoint { + txid: *exp_tx_ids.get(txid).expect("txid must exist"), + vout: *vout, + }) + .collect::>(); + assert_eq!( + utxos, exp_utxos, + "\n[{}] 'filter_chain_unspents' failed", + scenario.name + ); + + let balance = tx_graph.balance( + &local_chain, + chain_tip, + spk_index.outpoints().iter().cloned(), + |_, spk: &Script| spk_index.index_of_spk(spk).is_some(), + ); + assert_eq!( + balance, scenario.exp_balance, + "\n[{}] 'balance' failed", + scenario.name + ); + } +}