[bdk_chain_redesign] Revert some API changes

Methods of old structures that return transaction(s) no longer return
`TxNode`, but `Transaction` as done previously.

`TxInGraph` is renamed to `TxNode`, while the internal `TxNode` is
renamed to `TxNodeInternal`.
This commit is contained in:
志宇 2023-03-30 18:33:53 +08:00
parent 8c906170c9
commit a1172def7d
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
7 changed files with 87 additions and 116 deletions

View File

@ -23,9 +23,7 @@ pub use bdk_chain::keychain::Balance;
use bdk_chain::{
chain_graph,
keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker},
sparse_chain,
tx_graph::TxInGraph,
BlockId, ConfirmationTime,
sparse_chain, BlockId, ConfirmationTime,
};
use bitcoin::consensus::encode::serialize;
use bitcoin::secp256k1::Secp256k1;
@ -455,11 +453,7 @@ impl<D> Wallet<D> {
let fee = inputs.map(|inputs| inputs.saturating_sub(outputs));
Some(TransactionDetails {
transaction: if include_raw {
Some(tx.tx.clone())
} else {
None
},
transaction: if include_raw { Some(tx.clone()) } else { None },
txid,
received,
sent,
@ -524,8 +518,7 @@ impl<D> Wallet<D> {
/// unconfirmed transactions last.
pub fn transactions(
&self,
) -> impl DoubleEndedIterator<Item = (ConfirmationTime, TxInGraph<'_, Transaction, ()>)> + '_
{
) -> impl DoubleEndedIterator<Item = (ConfirmationTime, &Transaction)> + '_ {
self.keychain_tracker
.chain_graph()
.transactions_in_chain()
@ -1034,7 +1027,7 @@ impl<D> Wallet<D> {
Some((ConfirmationTime::Confirmed { .. }, _tx)) => {
return Err(Error::TransactionConfirmed)
}
Some((_, tx)) => tx.tx.clone(),
Some((_, tx)) => tx.clone(),
};
if !tx
@ -1092,7 +1085,7 @@ impl<D> Wallet<D> {
outpoint: txin.previous_output,
psbt_input: Box::new(psbt::Input {
witness_utxo: Some(txout.clone()),
non_witness_utxo: Some(prev_tx.tx.clone()),
non_witness_utxo: Some(prev_tx.clone()),
..Default::default()
}),
},
@ -1620,7 +1613,7 @@ impl<D> Wallet<D> {
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
}
if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
psbt_input.non_witness_utxo = Some(prev_tx.tx.clone());
psbt_input.non_witness_utxo = Some(prev_tx.clone());
}
}
Ok(psbt_input)

View File

@ -2,7 +2,7 @@
use crate::{
collections::HashSet,
sparse_chain::{self, ChainPosition, SparseChain},
tx_graph::{self, TxGraph, TxInGraph},
tx_graph::{self, TxGraph},
BlockId, ForEachTxOut, FullTxOut, TxHeight,
};
use alloc::{string::ToString, vec::Vec};
@ -84,11 +84,9 @@ where
pub fn new(chain: SparseChain<P>, graph: TxGraph) -> Result<Self, NewError<P>> {
let mut missing = HashSet::default();
for (pos, txid) in chain.txids() {
if let Some(graphed_tx) = graph.get_tx(*txid) {
if let Some(tx) = graph.get_tx(*txid) {
let conflict = graph
.walk_conflicts(graphed_tx.tx, |_, txid| {
Some((chain.tx_position(txid)?.clone(), txid))
})
.walk_conflicts(tx, |_, txid| Some((chain.tx_position(txid)?.clone(), txid)))
.next();
if let Some((conflict_pos, conflict)) = conflict {
return Err(NewError::Conflict {
@ -145,7 +143,7 @@ where
match self.chain.tx_position(*txid) {
Some(original_pos) => {
if original_pos != pos {
let graphed_tx = self
let tx = self
.graph
.get_tx(*txid)
.expect("tx must exist as it is referenced in sparsechain")
@ -153,7 +151,7 @@ where
let _ = inflated_chain
.insert_tx(*txid, pos.clone())
.expect("must insert since this was already in update");
let _ = inflated_graph.insert_tx(graphed_tx.tx.clone());
let _ = inflated_graph.insert_tx(tx.clone());
}
}
None => {
@ -212,10 +210,10 @@ where
///
/// This does not necessarily mean that it is *confirmed* in the blockchain; it might just be in
/// the unconfirmed transaction list within the [`SparseChain`].
pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, TxInGraph<'_, Transaction, ()>)> {
pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &Transaction)> {
let position = self.chain.tx_position(txid)?;
let graphed_tx = self.graph.get_tx(txid).expect("must exist");
Some((position, graphed_tx))
let tx = self.graph.get_tx(txid).expect("must exist");
Some((position, tx))
}
/// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and
@ -348,7 +346,7 @@ where
None => continue,
};
let mut full_tx = self.graph.get_tx(txid).map(|tx| tx.tx);
let mut full_tx = self.graph.get_tx(txid);
if full_tx.is_none() {
full_tx = changeset.graph.tx.iter().find(|tx| tx.txid() == txid)
@ -428,9 +426,7 @@ where
/// Iterate over the full transactions and their position in the chain ordered by their position
/// in ascending order.
pub fn transactions_in_chain(
&self,
) -> impl DoubleEndedIterator<Item = (&P, TxInGraph<'_, Transaction, ()>)> {
pub fn transactions_in_chain(&self) -> impl DoubleEndedIterator<Item = (&P, &Transaction)> {
self.chain
.txids()
.map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist")))

View File

@ -5,7 +5,7 @@ use bitcoin::{OutPoint, Transaction, TxOut};
use crate::{
keychain::Balance,
sparse_chain::ChainPosition,
tx_graph::{Additions, TxGraph, TxInGraph},
tx_graph::{Additions, TxGraph, TxNode},
BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, TxIndexAdditions,
};
@ -15,7 +15,7 @@ pub struct TxInChain<'a, T, A> {
/// Where the transaction is observed (in a block or in mempool).
pub observed_in: ObservedIn<&'a A>,
/// The transaction with anchors and last seen timestamp.
pub tx: TxInGraph<'a, T, A>,
pub tx: TxNode<'a, T, A>,
}
/// An outwards-facing view of a relevant txout that is part of the *best chain*'s history.

View File

@ -71,7 +71,7 @@ use core::{
#[derive(Clone, Debug, PartialEq)]
pub struct TxGraph<A = ()> {
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)`
txs: HashMap<Txid, (TxNode, BTreeSet<A>, u64)>,
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, u64)>,
spends: BTreeMap<OutPoint, HashSet<Txid>>,
anchors: BTreeSet<(A, Txid)>,
@ -94,9 +94,9 @@ impl<A> Default for TxGraph<A> {
// pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>);
// pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut<ObservedIn<&'a A>>);
/// An outward-facing view of a transaction that resides in a [`TxGraph`].
/// An outward-facing view of a transaction node that resides in a [`TxGraph`].
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TxInGraph<'a, T, A> {
pub struct TxNode<'a, T, A> {
/// Txid of the transaction.
pub txid: Txid,
/// A partial or full representation of the transaction.
@ -107,7 +107,7 @@ pub struct TxInGraph<'a, T, A> {
pub last_seen: u64,
}
impl<'a, T, A> Deref for TxInGraph<'a, T, A> {
impl<'a, T, A> Deref for TxNode<'a, T, A> {
type Target = T;
fn deref(&self) -> &Self::Target {
@ -115,7 +115,7 @@ impl<'a, T, A> Deref for TxInGraph<'a, T, A> {
}
}
impl<'a, A> TxInGraph<'a, Transaction, A> {
impl<'a, A> TxNode<'a, Transaction, A> {
pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet<A>) -> Self {
Self {
txid: tx.txid(),
@ -131,12 +131,12 @@ impl<'a, A> TxInGraph<'a, Transaction, A> {
/// This can either be a whole transaction, or a partial transaction (where we only have select
/// outputs).
#[derive(Clone, Debug, PartialEq)]
enum TxNode {
enum TxNodeInternal {
Whole(Transaction),
Partial(BTreeMap<u32, TxOut>),
}
impl Default for TxNode {
impl Default for TxNodeInternal {
fn default() -> Self {
Self::Partial(BTreeMap::new())
}
@ -146,13 +146,13 @@ impl<A> TxGraph<A> {
/// Iterate over all tx outputs known by [`TxGraph`].
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
TxNode::Whole(tx) => tx
TxNodeInternal::Whole(tx) => tx
.output
.iter()
.enumerate()
.map(|(vout, txout)| (OutPoint::new(*txid, vout as _), txout))
.collect::<Vec<_>>(),
TxNode::Partial(txouts) => txouts
TxNodeInternal::Partial(txouts) => txouts
.iter()
.map(|(vout, txout)| (OutPoint::new(*txid, *vout as _), txout))
.collect::<Vec<_>>(),
@ -160,17 +160,17 @@ impl<A> TxGraph<A> {
}
/// Iterate over all full transactions in the graph.
pub fn full_transactions(&self) -> impl Iterator<Item = TxInGraph<'_, Transaction, A>> {
pub fn full_transactions(&self) -> impl Iterator<Item = TxNode<'_, Transaction, A>> {
self.txs
.iter()
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
TxNode::Whole(tx) => Some(TxInGraph {
TxNodeInternal::Whole(tx) => Some(TxNode {
txid,
tx,
anchors,
last_seen: *last_seen,
}),
TxNode::Partial(_) => None,
TxNodeInternal::Partial(_) => None,
})
}
@ -179,9 +179,14 @@ impl<A> TxGraph<A> {
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
///
/// [`get_txout`]: Self::get_txout
pub fn get_tx(&self, txid: Txid) -> Option<TxInGraph<'_, Transaction, A>> {
pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> {
self.get_tx_node(txid).map(|n| n.tx)
}
/// Get a transaction node by txid. This only returns `Some` for full transactions.
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Transaction, A>> {
match &self.txs.get(&txid)? {
(TxNode::Whole(tx), anchors, last_seen) => Some(TxInGraph {
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
txid,
tx,
anchors,
@ -194,21 +199,21 @@ impl<A> TxGraph<A> {
/// Obtains a single tx output (if any) at the specified outpoint.
pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> {
match &self.txs.get(&outpoint.txid)?.0 {
TxNode::Whole(tx) => tx.output.get(outpoint.vout as usize),
TxNode::Partial(txouts) => txouts.get(&outpoint.vout),
TxNodeInternal::Whole(tx) => tx.output.get(outpoint.vout as usize),
TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout),
}
}
/// Returns a [`BTreeMap`] of vout to output of the provided `txid`.
pub fn txouts(&self, txid: Txid) -> Option<BTreeMap<u32, &TxOut>> {
Some(match &self.txs.get(&txid)?.0 {
TxNode::Whole(tx) => tx
TxNodeInternal::Whole(tx) => tx
.output
.iter()
.enumerate()
.map(|(vout, txout)| (vout as u32, txout))
.collect::<BTreeMap<_, _>>(),
TxNode::Partial(txouts) => txouts
TxNodeInternal::Partial(txouts) => txouts
.iter()
.map(|(vout, txout)| (*vout, txout))
.collect::<BTreeMap<_, _>>(),
@ -276,12 +281,12 @@ impl<A> TxGraph<A> {
/// Iterate over all partial transactions (outputs only) in the graph.
pub fn partial_transactions(
&self,
) -> impl Iterator<Item = TxInGraph<'_, BTreeMap<u32, TxOut>, A>> {
) -> impl Iterator<Item = TxNode<'_, BTreeMap<u32, TxOut>, A>> {
self.txs
.iter()
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
TxNode::Whole(_) => None,
TxNode::Partial(partial) => Some(TxInGraph {
TxNodeInternal::Whole(_) => None,
TxNodeInternal::Partial(partial) => Some(TxNode {
txid,
tx: partial,
anchors,
@ -368,7 +373,7 @@ impl<A: Clone + Ord> TxGraph<A> {
update.txs.insert(
outpoint.txid,
(
TxNode::Partial([(outpoint.vout, txout)].into()),
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
BTreeSet::new(),
0,
),
@ -394,7 +399,7 @@ impl<A: Clone + Ord> TxGraph<A> {
let mut update = Self::default();
update
.txs
.insert(tx.txid(), (TxNode::Whole(tx), BTreeSet::new(), 0));
.insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
self.determine_additions(&update)
}
@ -478,10 +483,10 @@ impl<A: Clone + Ord> TxGraph<A> {
});
match self.txs.get_mut(&txid) {
Some((tx_node @ TxNode::Partial(_), _, _)) => {
*tx_node = TxNode::Whole(tx);
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
*tx_node = TxNodeInternal::Whole(tx);
}
Some((TxNode::Whole(tx), _, _)) => {
Some((TxNodeInternal::Whole(tx), _, _)) => {
debug_assert_eq!(
tx.txid(),
txid,
@ -490,7 +495,7 @@ impl<A: Clone + Ord> TxGraph<A> {
}
None => {
self.txs
.insert(txid, (TxNode::Whole(tx), BTreeSet::new(), 0));
.insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
}
}
}
@ -502,8 +507,9 @@ impl<A: Clone + Ord> TxGraph<A> {
.or_insert_with(Default::default);
match tx_entry {
(TxNode::Whole(_), _, _) => { /* do nothing since we already have full tx */ }
(TxNode::Partial(txouts), _, _) => {
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
}
(TxNodeInternal::Partial(txouts), _, _) => {
txouts.insert(outpoint.vout, txout);
}
}
@ -533,11 +539,11 @@ impl<A: Clone + Ord> TxGraph<A> {
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
(None, TxNode::Whole(update_tx)) => {
(None, TxNodeInternal::Whole(update_tx)) => {
additions.tx.insert(update_tx.clone());
0
}
(None, TxNode::Partial(update_txos)) => {
(None, TxNodeInternal::Partial(update_txos)) => {
additions.txout.extend(
update_txos
.iter()
@ -545,12 +551,18 @@ impl<A: Clone + Ord> TxGraph<A> {
);
0
}
(Some((TxNode::Whole(_), _, last_seen)), _) => *last_seen,
(Some((TxNode::Partial(_), _, last_seen)), TxNode::Whole(update_tx)) => {
(Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen,
(
Some((TxNodeInternal::Partial(_), _, last_seen)),
TxNodeInternal::Whole(update_tx),
) => {
additions.tx.insert(update_tx.clone());
*last_seen
}
(Some((TxNode::Partial(txos), _, last_seen)), TxNode::Partial(update_txos)) => {
(
Some((TxNodeInternal::Partial(txos), _, last_seen)),
TxNodeInternal::Partial(update_txos),
) => {
additions.txout.extend(
update_txos
.iter()
@ -608,8 +620,8 @@ impl<A: BlockAnchor> TxGraph<A> {
// 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!
let tx = match tx_node {
TxNode::Whole(tx) => tx,
TxNode::Partial(_) => {
TxNodeInternal::Whole(tx) => tx,
TxNodeInternal::Partial(_) => {
// [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now!
// [TODO] So we just assume the partial tx does not exist in the best chain :/
return Ok(None);
@ -618,7 +630,7 @@ impl<A: BlockAnchor> TxGraph<A> {
// [TODO] Is this logic correct? I do not think so, but it should be good enough for now!
let mut latest_last_seen = 0_u64;
for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx(txid)) {
for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) {
for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) {
if chain.is_block_in_best_chain(block_id)? {
// conflicting tx is in best chain, so the current tx cannot be in best chain!

View File

@ -1,13 +1,11 @@
#[macro_use]
mod common;
use std::collections::BTreeSet;
use bdk_chain::{
chain_graph::*,
collections::HashSet,
sparse_chain,
tx_graph::{self, TxGraph, TxInGraph},
tx_graph::{self, TxGraph},
BlockId, TxHeight,
};
use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness};
@ -367,15 +365,7 @@ fn test_get_tx_in_chain() {
let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap();
assert_eq!(
cg.get_tx_in_chain(tx.txid()),
Some((
&TxHeight::Unconfirmed,
TxInGraph {
txid: tx.txid(),
tx: &tx,
anchors: &BTreeSet::new(),
last_seen: 0
}
))
Some((&TxHeight::Unconfirmed, &tx,))
);
}
@ -407,18 +397,9 @@ fn test_iterate_transactions() {
assert_eq!(
cg.transactions_in_chain().collect::<Vec<_>>(),
vec![
(
&TxHeight::Confirmed(0),
TxInGraph::from_tx(&txs[2], &BTreeSet::new())
),
(
&TxHeight::Confirmed(1),
TxInGraph::from_tx(&txs[0], &BTreeSet::new())
),
(
&TxHeight::Unconfirmed,
TxInGraph::from_tx(&txs[1], &BTreeSet::new())
),
(&TxHeight::Confirmed(0), &txs[2],),
(&TxHeight::Confirmed(1), &txs[0],),
(&TxHeight::Unconfirmed, &txs[1],),
]
);
}

View File

@ -1,7 +1,6 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use std::collections::BTreeSet;
use bdk_chain::{
keychain::{Balance, KeychainTracker},
@ -9,7 +8,6 @@ use bdk_chain::{
bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut},
Descriptor,
},
tx_graph::TxInGraph,
BlockId, ConfirmationTime, TxHeight,
};
use bitcoin::TxIn;
@ -43,10 +41,7 @@ fn test_insert_tx() {
.chain_graph()
.transactions_in_chain()
.collect::<Vec<_>>(),
vec![(
&ConfirmationTime::Unconfirmed,
TxInGraph::from_tx(&tx, &BTreeSet::new())
)]
vec![(&ConfirmationTime::Unconfirmed, &tx,)]
);
assert_eq!(

View File

@ -2,12 +2,9 @@
mod common;
use bdk_chain::{
collections::*,
tx_graph::{Additions, TxGraph, TxInGraph},
BlockId,
};
use bitcoin::{
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
tx_graph::{Additions, TxGraph},
};
use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid};
use core::iter;
#[test]
@ -38,7 +35,7 @@ fn insert_txouts() {
)];
let mut graph = {
let mut graph = TxGraph::<(u32, BlockHash)>::default();
let mut graph = TxGraph::<()>::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
@ -94,7 +91,7 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
output: vec![],
};
let mut graph = TxGraph::<(u32, BlockHash)>::default();
let mut graph = TxGraph::<()>::default();
let _ = graph.insert_tx(tx);
assert!(graph.outspends(OutPoint::null()).is_empty());
assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none());
@ -124,8 +121,8 @@ fn insert_tx_graph_keeps_track_of_spend() {
output: vec![],
};
let mut graph1 = TxGraph::<(u32, BlockHash)>::default();
let mut graph2 = TxGraph::<(u32, BlockHash)>::default();
let mut graph1 = TxGraph::<()>::default();
let mut graph2 = TxGraph::<()>::default();
// insert in different order
let _ = graph1.insert_tx(tx1.clone());
@ -153,17 +150,14 @@ fn insert_tx_can_retrieve_full_tx_from_graph() {
output: vec![TxOut::default()],
};
let mut graph = TxGraph::<BlockId>::default();
let mut graph = TxGraph::<()>::default();
let _ = graph.insert_tx(tx.clone());
assert_eq!(
graph.get_tx(tx.txid()),
Some(TxInGraph::from_tx(&tx, &BTreeSet::new()))
);
assert_eq!(graph.get_tx(tx.txid()), Some(&tx));
}
#[test]
fn insert_tx_displaces_txouts() {
let mut tx_graph = TxGraph::<(u32, BlockHash)>::default();
let mut tx_graph = TxGraph::<()>::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
@ -219,7 +213,7 @@ fn insert_tx_displaces_txouts() {
#[test]
fn insert_txout_does_not_displace_tx() {
let mut tx_graph = TxGraph::<(u32, BlockHash)>::default();
let mut tx_graph = TxGraph::<()>::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
@ -275,7 +269,7 @@ fn insert_txout_does_not_displace_tx() {
#[test]
fn test_calculate_fee() {
let mut graph = TxGraph::<(u32, BlockHash)>::default();
let mut graph = TxGraph::<()>::default();
let intx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
@ -369,7 +363,7 @@ fn test_calculate_fee_on_coinbase() {
output: vec![TxOut::default()],
};
let graph = TxGraph::<(u32, BlockHash)>::default();
let graph = TxGraph::<()>::default();
assert_eq!(graph.calculate_fee(&tx), Some(0));
}
@ -411,7 +405,7 @@ fn test_conflicting_descendants() {
let txid_a = tx_a.txid();
let txid_b = tx_b.txid();
let mut graph = TxGraph::<(u32, BlockHash)>::default();
let mut graph = TxGraph::<()>::default();
let _ = graph.insert_tx(tx_a);
let _ = graph.insert_tx(tx_b);
@ -487,7 +481,7 @@ fn test_descendants_no_repeat() {
})
.collect::<Vec<_>>();
let mut graph = TxGraph::<(u32, BlockHash)>::default();
let mut graph = TxGraph::<()>::default();
let mut expected_txids = BTreeSet::new();
// these are NOT descendants of `tx_a`