Skip to content

Commit 4ee11aa

Browse files
committed
Merge #1064: Better tests for transaction conflict handling
6d601a7 test(chain): Add test for conflicting transactions (Daniela Brozzoni) 48ca95b test(chain): Add test for walk_ancestors (Daniela Brozzoni) 59a2403 test(chain): Introduce TxTemplate (Daniela Brozzoni) 6e51147 test(chain): add block_id! utility macro (Daniela Brozzoni) 62de55f fix(chain): Consider conflicting ancestors in... ...try_get_chain_pos (Daniela Brozzoni) a3e8480 doc(chain): Clarify direct_conflicts_of_tx's docs (Daniela Brozzoni) 4742d88 feat(chain): Introduce TxAncestors, walk_ancestors (Daniela Brozzoni) 2f26eca fix(chain): TxDescendants performs a BFS (Daniela Brozzoni) 486e0e1 doc(chain): Fix typos (Daniela Brozzoni) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### Description Fixes #1063. This PR introduces a new `TxTemplate` struct to test different transaction conflict scenarios in `TxGraph`. The following transaction conflict scenarios are tested: - 2 unconfirmed txs with different last_seens conflict. The most recent tx should be the only tx that appears in the list methods. - 3 unconfirmed txs with different last_seens conflict. The most recent tx should be the only tx that appears in the list methods. - An unconfirmed tx U conflicts with a tx anchored in orphaned block O. O has higher last_seen. O should be the only tx that appears in the list methods. - An unconfirmed tx U conflicts with a tx anchored in orphaned block O. U has higher last_seen. U should be the only tx that appears in the list methods. - Multiple unconfirmed txs conflict with a confirmed tx. None of the unconfirmed txs should appear in the list methods. - B and B' conflict. C spends B. B' is anchored in best chain. B and C should not appear in the list methods. - B and B' conflict. C spends B. B is anchored in best chain. B' should not appear in the list methods. - B and B' conflict. C spends both B and B'. C is impossible. - B and B' conflict. C spends both B and B'. C is impossible. B' is confirmed. - B and B' conflict. C spends both B and B'. C is impossible. D spends C. These tests revealed that `TxGraph::walk_conflicts` was not checking ancestors of the root tx for conflicts. `TxGraph::walk_conflicts` has been refactored to check for conflicting ancestor transactions by using a new `TxAncestors` iterator in `TxGraph`. ### Changelog notice - Introduced `tx_template` module - Introduced `TxGraph::TxAncestors` iterator - Refactored `TxGraph::walk_conflicts` to use `TxGraph::TxAncestors` - Added `walk_ancestors` to `TxGraph` ### Checklists All Submissions: - [x] I've signed all my commits - [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) - [x] I ran cargo fmt and cargo clippy before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: evanlinjin: ACK 6d601a7 Tree-SHA512: ea151392874c4312233e4e10299579f4eee4a7100ae344b4d7f19994284b49c1e43f37338bed931d16e77326021166ea0b94d6de3ccf50a8fabb25139a8e69b4
2 parents a7dbc22 + 6d601a7 commit 4ee11aa

File tree

5 files changed

+1212
-37
lines changed

5 files changed

+1212
-37
lines changed

crates/chain/src/tx_graph.rs

Lines changed: 228 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ use crate::{
5454
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
5555
ChainOracle, ChainPosition, FullTxOut,
5656
};
57+
use alloc::collections::vec_deque::VecDeque;
5758
use alloc::vec::Vec;
5859
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
5960
use core::{
@@ -319,15 +320,39 @@ impl<A> TxGraph<A> {
319320
.map(|(outpoint, spends)| (outpoint.vout, spends))
320321
}
321322

323+
/// Creates an iterator that filters and maps ancestor transactions.
324+
///
325+
/// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx`
326+
/// are transactions spent by `tx`). The supplied transaction is excluded from the iterator.
327+
///
328+
/// The supplied closure takes in two inputs `(depth, ancestor_tx)`:
329+
///
330+
/// * `depth` is the distance between the starting `Transaction` and the `ancestor_tx`. I.e., if
331+
/// the `Transaction` is spending an output of the `ancestor_tx` then `depth` will be 1.
332+
/// * `ancestor_tx` is the `Transaction`'s ancestor which we are considering to walk.
333+
///
334+
/// The supplied closure returns an `Option<T>`, allowing the caller to map each `Transaction`
335+
/// it visits and decide whether to visit ancestors.
336+
pub fn walk_ancestors<'g, F, O>(
337+
&'g self,
338+
tx: &'g Transaction,
339+
walk_map: F,
340+
) -> TxAncestors<'g, A, F>
341+
where
342+
F: FnMut(usize, &'g Transaction) -> Option<O> + 'g,
343+
{
344+
TxAncestors::new_exclude_root(self, tx, walk_map)
345+
}
346+
322347
/// Creates an iterator that filters and maps descendants from the starting `txid`.
323348
///
324349
/// The supplied closure takes in two inputs `(depth, descendant_txid)`:
325350
///
326351
/// * `depth` is the distance between the starting `txid` and the `descendant_txid`. I.e., if the
327-
/// descendant is spending an output of the starting `txid`; the `depth` will be 1.
352+
/// descendant is spending an output of the starting `txid` then `depth` will be 1.
328353
/// * `descendant_txid` is the descendant's txid which we are considering to walk.
329354
///
330-
/// The supplied closure returns an `Option<T>`, allowing the caller to map each node it vists
355+
/// The supplied closure returns an `Option<T>`, allowing the caller to map each node it visits
331356
/// and decide whether to visit descendants.
332357
pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants<A, F>
333358
where
@@ -356,8 +381,9 @@ impl<A> TxGraph<A> {
356381
/// transaction's inputs (spends). The conflicting txids are returned with the given
357382
/// transaction's vin (in which it conflicts).
358383
///
359-
/// Note that this only returns directly conflicting txids and does not include descendants of
360-
/// those txids (which are technically also conflicting).
384+
/// Note that this only returns directly conflicting txids and won't include:
385+
/// - descendants of conflicting transactions (which are technically also conflicting)
386+
/// - transactions conflicting with the given transaction's ancestors
361387
pub fn direct_conflicts_of_tx<'g>(
362388
&'g self,
363389
tx: &'g Transaction,
@@ -671,8 +697,9 @@ impl<A: Anchor> TxGraph<A> {
671697
}
672698
}
673699

674-
// The tx is not anchored to a block which is in the best chain, let's check whether we can
675-
// ignore it by checking conflicts!
700+
// The tx is not anchored to a block which is in the best chain, which means that it
701+
// might be in mempool, or it might have been dropped already.
702+
// Let's check conflicts to find out!
676703
let tx = match tx_node {
677704
TxNodeInternal::Whole(tx) => tx,
678705
TxNodeInternal::Partial(_) => {
@@ -681,18 +708,71 @@ impl<A: Anchor> TxGraph<A> {
681708
}
682709
};
683710

684-
// If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then
685-
// this tx cannot exist in the best chain
686-
for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) {
687-
for block in conflicting_tx.anchors.iter().map(A::anchor_block) {
688-
if chain.is_block_in_chain(block, chain_tip)? == Some(true) {
689-
// conflicting tx is in best chain, so the current tx cannot be in best chain!
711+
// We want to retrieve all the transactions that conflict with us, plus all the
712+
// transactions that conflict with our unconfirmed ancestors, since they conflict with us
713+
// as well.
714+
// We only traverse unconfirmed ancestors since conflicts of confirmed transactions
715+
// cannot be in the best chain.
716+
717+
// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
718+
// resulting array will also include `tx`
719+
let unconfirmed_ancestor_txs =
720+
TxAncestors::new_include_root(self, tx, |_, ancestor_tx: &Transaction| {
721+
let tx_node = self.get_tx_node(ancestor_tx.txid())?;
722+
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
723+
// the best chain)
724+
for block in tx_node.anchors {
725+
match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
726+
Ok(Some(true)) => return None,
727+
Err(e) => return Some(Err(e)),
728+
_ => continue,
729+
}
730+
}
731+
Some(Ok(tx_node))
732+
})
733+
.collect::<Result<Vec<_>, C::Error>>()?;
734+
735+
// We determine our tx's last seen, which is the max between our last seen,
736+
// and our unconf descendants' last seen.
737+
let unconfirmed_descendants_txs =
738+
TxDescendants::new_include_root(self, tx.txid(), |_, descendant_txid: Txid| {
739+
let tx_node = self.get_tx_node(descendant_txid)?;
740+
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
741+
// the best chain)
742+
for block in tx_node.anchors {
743+
match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
744+
Ok(Some(true)) => return None,
745+
Err(e) => return Some(Err(e)),
746+
_ => continue,
747+
}
748+
}
749+
Some(Ok(tx_node))
750+
})
751+
.collect::<Result<Vec<_>, C::Error>>()?;
752+
753+
let tx_last_seen = unconfirmed_descendants_txs
754+
.iter()
755+
.max_by_key(|tx| tx.last_seen_unconfirmed)
756+
.map(|tx| tx.last_seen_unconfirmed)
757+
.expect("descendants always includes at least one transaction (the root tx");
758+
759+
// Now we traverse our ancestors and consider all their conflicts
760+
for tx_node in unconfirmed_ancestor_txs {
761+
// We retrieve all the transactions conflicting with this specific ancestor
762+
let conflicting_txs = self.walk_conflicts(tx_node.tx, |_, txid| self.get_tx_node(txid));
763+
764+
// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
765+
// this tx cannot exist in the best chain
766+
for conflicting_tx in conflicting_txs {
767+
for block in conflicting_tx.anchors {
768+
if chain.is_block_in_chain(block.anchor_block(), chain_tip)? == Some(true) {
769+
return Ok(None);
770+
}
771+
}
772+
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
690773
return Ok(None);
691774
}
692775
}
693-
if conflicting_tx.last_seen_unconfirmed > *last_seen {
694-
return Ok(None);
695-
}
696776
}
697777

698778
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
@@ -1137,6 +1217,126 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
11371217
}
11381218
}
11391219

1220+
/// An iterator that traverses ancestors of a given root transaction.
1221+
///
1222+
/// The iterator excludes partial transactions.
1223+
///
1224+
/// This `struct` is created by the [`walk_ancestors`] method of [`TxGraph`].
1225+
///
1226+
/// [`walk_ancestors`]: TxGraph::walk_ancestors
1227+
pub struct TxAncestors<'g, A, F> {
1228+
graph: &'g TxGraph<A>,
1229+
visited: HashSet<Txid>,
1230+
queue: VecDeque<(usize, &'g Transaction)>,
1231+
filter_map: F,
1232+
}
1233+
1234+
impl<'g, A, F> TxAncestors<'g, A, F> {
1235+
/// Creates a `TxAncestors` that includes the starting `Transaction` when iterating.
1236+
pub(crate) fn new_include_root(
1237+
graph: &'g TxGraph<A>,
1238+
tx: &'g Transaction,
1239+
filter_map: F,
1240+
) -> Self {
1241+
Self {
1242+
graph,
1243+
visited: Default::default(),
1244+
queue: [(0, tx)].into(),
1245+
filter_map,
1246+
}
1247+
}
1248+
1249+
/// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating.
1250+
pub(crate) fn new_exclude_root(
1251+
graph: &'g TxGraph<A>,
1252+
tx: &'g Transaction,
1253+
filter_map: F,
1254+
) -> Self {
1255+
let mut ancestors = Self {
1256+
graph,
1257+
visited: Default::default(),
1258+
queue: Default::default(),
1259+
filter_map,
1260+
};
1261+
ancestors.populate_queue(1, tx);
1262+
ancestors
1263+
}
1264+
1265+
/// Creates a `TxAncestors` from multiple starting `Transaction`s that includes the starting
1266+
/// `Transaction`s when iterating.
1267+
#[allow(unused)]
1268+
pub(crate) fn from_multiple_include_root<I>(
1269+
graph: &'g TxGraph<A>,
1270+
txs: I,
1271+
filter_map: F,
1272+
) -> Self
1273+
where
1274+
I: IntoIterator<Item = &'g Transaction>,
1275+
{
1276+
Self {
1277+
graph,
1278+
visited: Default::default(),
1279+
queue: txs.into_iter().map(|tx| (0, tx)).collect(),
1280+
filter_map,
1281+
}
1282+
}
1283+
1284+
/// Creates a `TxAncestors` from multiple starting `Transaction`s that excludes the starting
1285+
/// `Transaction`s when iterating.
1286+
#[allow(unused)]
1287+
pub(crate) fn from_multiple_exclude_root<I>(
1288+
graph: &'g TxGraph<A>,
1289+
txs: I,
1290+
filter_map: F,
1291+
) -> Self
1292+
where
1293+
I: IntoIterator<Item = &'g Transaction>,
1294+
{
1295+
let mut ancestors = Self {
1296+
graph,
1297+
visited: Default::default(),
1298+
queue: Default::default(),
1299+
filter_map,
1300+
};
1301+
for tx in txs {
1302+
ancestors.populate_queue(1, tx);
1303+
}
1304+
ancestors
1305+
}
1306+
1307+
fn populate_queue(&mut self, depth: usize, tx: &'g Transaction) {
1308+
let ancestors = tx
1309+
.input
1310+
.iter()
1311+
.map(|txin| txin.previous_output.txid)
1312+
.filter(|&prev_txid| self.visited.insert(prev_txid))
1313+
.filter_map(|prev_txid| self.graph.get_tx(prev_txid))
1314+
.map(|tx| (depth, tx));
1315+
self.queue.extend(ancestors);
1316+
}
1317+
}
1318+
1319+
impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F>
1320+
where
1321+
F: FnMut(usize, &'g Transaction) -> Option<O>,
1322+
{
1323+
type Item = O;
1324+
1325+
fn next(&mut self) -> Option<Self::Item> {
1326+
loop {
1327+
// we have exhausted all paths when queue is empty
1328+
let (ancestor_depth, tx) = self.queue.pop_front()?;
1329+
// ignore paths when user filters them out
1330+
let item = match (self.filter_map)(ancestor_depth, tx) {
1331+
Some(item) => item,
1332+
None => continue,
1333+
};
1334+
self.populate_queue(ancestor_depth + 1, tx);
1335+
return Some(item);
1336+
}
1337+
}
1338+
}
1339+
11401340
/// An iterator that traverses transaction descendants.
11411341
///
11421342
/// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`].
@@ -1145,7 +1345,7 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
11451345
pub struct TxDescendants<'g, A, F> {
11461346
graph: &'g TxGraph<A>,
11471347
visited: HashSet<Txid>,
1148-
stack: Vec<(usize, Txid)>,
1348+
queue: VecDeque<(usize, Txid)>,
11491349
filter_map: F,
11501350
}
11511351

@@ -1156,7 +1356,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
11561356
Self {
11571357
graph,
11581358
visited: Default::default(),
1159-
stack: [(0, txid)].into(),
1359+
queue: [(0, txid)].into(),
11601360
filter_map,
11611361
}
11621362
}
@@ -1166,14 +1366,14 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
11661366
let mut descendants = Self {
11671367
graph,
11681368
visited: Default::default(),
1169-
stack: Default::default(),
1369+
queue: Default::default(),
11701370
filter_map,
11711371
};
1172-
descendants.populate_stack(1, txid);
1372+
descendants.populate_queue(1, txid);
11731373
descendants
11741374
}
11751375

1176-
/// Creates a `TxDescendants` from multiple starting transactions that include the starting
1376+
/// Creates a `TxDescendants` from multiple starting transactions that includes the starting
11771377
/// `txid`s when iterating.
11781378
pub(crate) fn from_multiple_include_root<I>(
11791379
graph: &'g TxGraph<A>,
@@ -1186,7 +1386,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
11861386
Self {
11871387
graph,
11881388
visited: Default::default(),
1189-
stack: txids.into_iter().map(|txid| (0, txid)).collect(),
1389+
queue: txids.into_iter().map(|txid| (0, txid)).collect(),
11901390
filter_map,
11911391
}
11921392
}
@@ -1205,25 +1405,25 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
12051405
let mut descendants = Self {
12061406
graph,
12071407
visited: Default::default(),
1208-
stack: Default::default(),
1408+
queue: Default::default(),
12091409
filter_map,
12101410
};
12111411
for txid in txids {
1212-
descendants.populate_stack(1, txid);
1412+
descendants.populate_queue(1, txid);
12131413
}
12141414
descendants
12151415
}
12161416
}
12171417

12181418
impl<'g, A, F> TxDescendants<'g, A, F> {
1219-
fn populate_stack(&mut self, depth: usize, txid: Txid) {
1419+
fn populate_queue(&mut self, depth: usize, txid: Txid) {
12201420
let spend_paths = self
12211421
.graph
12221422
.spends
12231423
.range(tx_outpoint_range(txid))
12241424
.flat_map(|(_, spends)| spends)
12251425
.map(|&txid| (depth, txid));
1226-
self.stack.extend(spend_paths);
1426+
self.queue.extend(spend_paths);
12271427
}
12281428
}
12291429

@@ -1235,8 +1435,8 @@ where
12351435

12361436
fn next(&mut self) -> Option<Self::Item> {
12371437
let (op_spends, txid, item) = loop {
1238-
// we have exhausted all paths when stack is empty
1239-
let (op_spends, txid) = self.stack.pop()?;
1438+
// we have exhausted all paths when queue is empty
1439+
let (op_spends, txid) = self.queue.pop_front()?;
12401440
// we do not want to visit the same transaction twice
12411441
if self.visited.insert(txid) {
12421442
// ignore paths when user filters them out
@@ -1246,7 +1446,7 @@ where
12461446
}
12471447
};
12481448

1249-
self.populate_stack(op_spends + 1, txid);
1449+
self.populate_queue(op_spends + 1, txid);
12501450
Some(item)
12511451
}
12521452
}

crates/chain/tests/common/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
mod tx_template;
2+
pub use tx_template::*;
3+
4+
#[allow(unused_macros)]
5+
macro_rules! block_id {
6+
($height:expr, $hash:literal) => {{
7+
bdk_chain::BlockId {
8+
height: $height,
9+
hash: bitcoin::hashes::Hash::hash($hash.as_bytes()),
10+
}
11+
}};
12+
}
13+
114
#[allow(unused_macros)]
215
macro_rules! h {
316
($index:literal) => {{

0 commit comments

Comments
 (0)