Skip to content

Commit 1a77475

Browse files
committed
Merge #2027: feat(chain): add new list_ordered_canonical_txs method
12c1076 feat(chain): introduce new `list_ordered_canonical_txs` (Leonardo Lima) f34117b test(chain): add `test_list_ordered_canonical_txs` (Leonardo Lima) a201be0 chore(test): rename `txid_to_name` to proper `tx_name_to_txid` (Leonardo Lima) 6cf720e chore(docs): fix typo in docs (Leonardo Lima) Pull request description: ### Description It introduces a new method for topological ordering canonical transactions, `list_ordered_canonical_txs`. It now ensures the dependency-based transaction processing, guaranteeing that parent transactions always appears before their children transaction. The existing `list_canonical_txs` and `try_list_canonical_txs` methods have been deprecated in favor of the new ordered version. ### Notes to the reviewers "[...] For those reviewing, and wondering why we don't have a fallible try version of this method, it's because we don't have a fallible ChainOracle implementation - we will get rid of ChainOracle trait soon anyway." This PR is intended for a point release so that bdk_wallet 2.x users can get a topologically sorted list of transactions (we need a point release on bdk_wallet 2.x as well). It might be useful to take a look at the new test scenarios that I've added, it shows some specific scenarios where the current implementation and output of `canonical_txs` didn't output the transactions in topological order. Let me know if you think the TopologicalIter algorithm and/or API could be improved. ### Changelog notice ``` #### Added - New `list_ordered_canonical_txs` method to `TxGraph` that returns canonical transactions in topological order, ensuring parent transactions always appear before their children #### Deprecated - `list_canonical_txs` method - use `list_ordered_canonical_txs` instead for guaranteed topological ordering - `try_list_canonical_txs` method - use `list_ordered_canonical_txs` instead for guaranteed topological ordering ``` ### Checklists #### All Submissions: * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: ValuedMammal: ACK 12c1076 Tree-SHA512: 73c8cc40e6b77844af52ddc1a788cf1190357e08f487f443ab492881e50ed4343598e2607f542e7d9b69615cd102f29ec3bda5942753186860d919433b1d62dd
2 parents 822ce5e + 12c1076 commit 1a77475

File tree

10 files changed

+750
-14
lines changed

10 files changed

+750
-14
lines changed

crates/chain/benches/canonicalization.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_tx
9999
assert_eq!(txs.count(), exp_txs);
100100
}
101101

102+
fn run_list_ordered_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
103+
let txs = tx_graph.graph().list_ordered_canonical_txs(
104+
chain,
105+
chain.tip().block_id(),
106+
CanonicalizationParams::default(),
107+
);
108+
assert_eq!(txs.count(), exp_txs);
109+
}
110+
102111
fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
103112
let utxos = tx_graph.graph().filter_chain_txouts(
104113
chain,
@@ -147,6 +156,13 @@ pub fn many_conflicting_unconfirmed(c: &mut Criterion) {
147156
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
148157
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2))
149158
});
159+
c.bench_function(
160+
"many_conflicting_unconfirmed::list_ordered_canonical_txs",
161+
{
162+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
163+
move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, 2))
164+
},
165+
);
150166
c.bench_function("many_conflicting_unconfirmed::filter_chain_txouts", {
151167
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
152168
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 2))
@@ -185,6 +201,10 @@ pub fn many_chained_unconfirmed(c: &mut Criterion) {
185201
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
186202
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2101))
187203
});
204+
c.bench_function("many_chained_unconfirmed::list_ordered_canonical_txs", {
205+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
206+
move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, 2101))
207+
});
188208
c.bench_function("many_chained_unconfirmed::filter_chain_txouts", {
189209
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
190210
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 1))
@@ -234,6 +254,13 @@ pub fn nested_conflicts(c: &mut Criterion) {
234254
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
235255
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH))
236256
});
257+
c.bench_function(
258+
"nested_conflicts_unconfirmed::list_ordered_canonical_txs",
259+
{
260+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
261+
move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH))
262+
},
263+
);
237264
c.bench_function("nested_conflicts_unconfirmed::filter_chain_txouts", {
238265
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
239266
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, GRAPH_DEPTH))

crates/chain/src/canonical_iter.rs

Lines changed: 160 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::collections::{HashMap, HashSet, VecDeque};
2-
use crate::tx_graph::{TxAncestors, TxDescendants};
2+
use crate::tx_graph::{CanonicalTx, TxAncestors, TxDescendants};
33
use crate::{Anchor, ChainOracle, TxGraph};
44
use alloc::boxed::Box;
55
use alloc::collections::BTreeSet;
@@ -14,7 +14,7 @@ type NotCanonicalSet = HashSet<Txid>;
1414
/// Modifies the canonicalization algorithm.
1515
#[derive(Debug, Default, Clone)]
1616
pub struct CanonicalizationParams {
17-
/// Transactions that will supercede all other transactions.
17+
/// Transactions that will supersede all other transactions.
1818
///
1919
/// In case of conflicting transactions within `assume_canonical`, transactions that appear
2020
/// later in the list (have higher index) have precedence.
@@ -108,7 +108,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
108108
.iter()
109109
.last()
110110
.expect(
111-
"tx taken from `unprocessed_txs_with_anchors` so it must atleast have an anchor",
111+
"tx taken from `unprocessed_txs_with_anchors` so it must at least have an anchor",
112112
)
113113
.confirmation_height_upper_bound(),
114114
));
@@ -266,7 +266,7 @@ pub enum ObservedIn {
266266
/// The reason why a transaction is canonical.
267267
#[derive(Debug, Clone, PartialEq, Eq)]
268268
pub enum CanonicalReason<A> {
269-
/// This transaction is explicitly assumed to be canonical by the caller, superceding all other
269+
/// This transaction is explicitly assumed to be canonical by the caller, superseding all other
270270
/// canonicalization rules.
271271
Assumed {
272272
/// Whether it is a descendant that is assumed to be canonical.
@@ -290,7 +290,7 @@ pub enum CanonicalReason<A> {
290290
}
291291

292292
impl<A: Clone> CanonicalReason<A> {
293-
/// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other
293+
/// Constructs a [`CanonicalReason`] for a transaction that is assumed to supersede all other
294294
/// transactions.
295295
pub fn assumed() -> Self {
296296
Self::Assumed { descendant: None }
@@ -312,7 +312,7 @@ impl<A: Clone> CanonicalReason<A> {
312312
}
313313
}
314314

315-
/// Contruct a new [`CanonicalReason`] from the original which is transitive to `descendant`.
315+
/// Construct a new [`CanonicalReason`] from the original which is transitive to `descendant`.
316316
///
317317
/// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's
318318
/// descendant, but is transitively relevant.
@@ -342,3 +342,157 @@ impl<A: Clone> CanonicalReason<A> {
342342
}
343343
}
344344
}
345+
346+
/// Iterator based on the Kahn's Algorithm, that yields transactions in topological spending order
347+
/// in depth, and properly sorted with level.
348+
///
349+
/// NOTE: Please refer to the Kahn's Algorithm reference: https://dl.acm.org/doi/pdf/10.1145/368996.369025
350+
pub(crate) struct TopologicalIterator<'a, A> {
351+
/// Map of txid to its canonical transaction
352+
canonical_txs: HashMap<Txid, CanonicalTx<'a, Arc<Transaction>, A>>,
353+
354+
/// Current level of transactions to process
355+
current_level: Vec<Txid>,
356+
/// Next level of transactions to process
357+
next_level: Vec<Txid>,
358+
359+
/// Adjacency list: parent txid -> list of children txids
360+
children_map: HashMap<Txid, Vec<Txid>>,
361+
/// Number of unprocessed parents for each transaction
362+
parent_count: HashMap<Txid, usize>,
363+
364+
/// Current index in the current level
365+
current_index: usize,
366+
}
367+
368+
impl<'a, A: Clone + Anchor> TopologicalIterator<'a, A> {
369+
/// Constructs [`TopologicalIterator`] from a list of `canonical_txs` (e.g [`CanonicalIter`]),
370+
/// in order to handle all the graph building internally.
371+
pub(crate) fn new(
372+
canonical_txs: impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>>,
373+
) -> Self {
374+
// Build a map from txid to canonical tx for quick lookup
375+
let mut tx_map: HashMap<Txid, CanonicalTx<'a, Arc<Transaction>, A>> = HashMap::new();
376+
let mut canonical_set: HashSet<Txid> = HashSet::new();
377+
378+
for canonical_tx in canonical_txs {
379+
let txid = canonical_tx.tx_node.txid;
380+
canonical_set.insert(txid);
381+
tx_map.insert(txid, canonical_tx);
382+
}
383+
384+
// Build the dependency graph (txid -> parents it depends on)
385+
let mut dependencies: HashMap<Txid, Vec<Txid>> = HashMap::new();
386+
let mut has_parents: HashSet<Txid> = HashSet::new();
387+
388+
for &txid in canonical_set.iter() {
389+
let canonical_tx = tx_map.get(&txid).expect("txid must exist in map");
390+
let tx = &canonical_tx.tx_node.tx;
391+
392+
// Find all parents (transactions this one depends on)
393+
let mut parents = Vec::new();
394+
if !tx.is_coinbase() {
395+
for txin in &tx.input {
396+
let parent_txid = txin.previous_output.txid;
397+
// Only include if the parent is also canonical
398+
if canonical_set.contains(&parent_txid) {
399+
parents.push(parent_txid);
400+
has_parents.insert(txid);
401+
}
402+
}
403+
}
404+
405+
if !parents.is_empty() {
406+
dependencies.insert(txid, parents);
407+
}
408+
}
409+
410+
// Build adjacency list and parent counts for traversal
411+
let mut parent_count = HashMap::new();
412+
let mut children_map: HashMap<Txid, Vec<Txid>> = HashMap::new();
413+
414+
for (txid, parents) in &dependencies {
415+
for parent_txid in parents {
416+
children_map.entry(*parent_txid).or_default().push(*txid);
417+
*parent_count.entry(*txid).or_insert(0) += 1;
418+
}
419+
}
420+
421+
// Find root transactions (those with no parents in the canonical set)
422+
let roots: Vec<Txid> = canonical_set
423+
.iter()
424+
.filter(|&&txid| !has_parents.contains(&txid))
425+
.copied()
426+
.collect();
427+
428+
// Sort the initial level
429+
let mut current_level = roots;
430+
Self::sort_level_by_chain_position(&mut current_level, &tx_map);
431+
432+
Self {
433+
canonical_txs: tx_map,
434+
current_level,
435+
next_level: Vec::new(),
436+
children_map,
437+
parent_count,
438+
current_index: 0,
439+
}
440+
}
441+
442+
/// Sort transactions within a level by their chain position
443+
/// Confirmed transactions come first (sorted by height), then unconfirmed (sorted by last_seen)
444+
fn sort_level_by_chain_position(
445+
level: &mut [Txid],
446+
canonical_txs: &HashMap<Txid, CanonicalTx<'a, Arc<Transaction>, A>>,
447+
) {
448+
level.sort_by(|&a_txid, &b_txid| {
449+
let a_tx = canonical_txs.get(&a_txid).expect("txid must exist");
450+
let b_tx = canonical_txs.get(&b_txid).expect("txid must exist");
451+
452+
a_tx.cmp(b_tx)
453+
});
454+
}
455+
456+
fn advance_to_next_level(&mut self) {
457+
self.current_level = core::mem::take(&mut self.next_level);
458+
Self::sort_level_by_chain_position(&mut self.current_level, &self.canonical_txs);
459+
self.current_index = 0;
460+
}
461+
}
462+
463+
impl<'a, A: Clone + Anchor> Iterator for TopologicalIterator<'a, A> {
464+
type Item = CanonicalTx<'a, Arc<Transaction>, A>;
465+
466+
fn next(&mut self) -> Option<Self::Item> {
467+
// If we've exhausted the current level, move to next
468+
if self.current_index >= self.current_level.len() {
469+
if self.next_level.is_empty() {
470+
return None;
471+
}
472+
self.advance_to_next_level();
473+
}
474+
475+
let current_txid = self.current_level[self.current_index];
476+
self.current_index += 1;
477+
478+
// If this is the last item in current level, prepare dependents for next level
479+
if self.current_index == self.current_level.len() {
480+
// Process all dependents of all transactions in current level
481+
for &tx in &self.current_level {
482+
if let Some(children) = self.children_map.get(&tx) {
483+
for &child in children {
484+
if let Some(count) = self.parent_count.get_mut(&child) {
485+
*count -= 1;
486+
if *count == 0 {
487+
self.next_level.push(child);
488+
}
489+
}
490+
}
491+
}
492+
}
493+
}
494+
495+
// Return the CanonicalTx for the current txid
496+
self.canonical_txs.get(&current_txid).cloned()
497+
}
498+
}

crates/chain/src/tx_graph.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,9 @@ impl<A: Anchor> TxGraph<A> {
988988
/// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
989989
/// observed in-chain, and the [`TxNode`].
990990
///
991+
/// NOTE: It does not guarantee the topological order of yielded transactions, the
992+
/// [`list_ordered_canonical_txs`] can be used instead.
993+
///
991994
/// # Error
992995
///
993996
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
@@ -996,6 +999,7 @@ impl<A: Anchor> TxGraph<A> {
996999
/// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
9971000
///
9981001
/// [`list_canonical_txs`]: Self::list_canonical_txs
1002+
/// [`list_ordered_canonical_txs`]: Self::list_ordered_canonical_txs
9991003
pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
10001004
&'a self,
10011005
chain: &'a C,
@@ -1079,7 +1083,11 @@ impl<A: Anchor> TxGraph<A> {
10791083
///
10801084
/// This is the infallible version of [`try_list_canonical_txs`].
10811085
///
1086+
/// NOTE: It does not guarantee the topological order of yielded transactions, the
1087+
/// [`list_ordered_canonical_txs`] can be used instead.
1088+
///
10821089
/// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
1090+
/// [`list_ordered_canonical_txs`]: Self::list_ordered_canonical_txs
10831091
pub fn list_canonical_txs<'a, C: ChainOracle<Error = Infallible> + 'a>(
10841092
&'a self,
10851093
chain: &'a C,
@@ -1090,6 +1098,28 @@ impl<A: Anchor> TxGraph<A> {
10901098
.map(|res| res.expect("infallible"))
10911099
}
10921100

1101+
/// List graph transactions that are in `chain` with `chain_tip` in topological order.
1102+
///
1103+
/// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
1104+
/// observed in-chain, and the [`TxNode`].
1105+
///
1106+
/// Transactions are returned in topological spending order, meaning that if transaction B
1107+
/// spends from transaction A, then A will always appear before B in the resulting list.
1108+
///
1109+
/// This is the infallible version which uses [`list_canonical_txs`] internally and then
1110+
/// reorders the transactions based on their spending relationships.
1111+
///
1112+
/// [`list_canonical_txs`]: Self::list_canonical_txs
1113+
pub fn list_ordered_canonical_txs<'a, C: ChainOracle<Error = Infallible>>(
1114+
&'a self,
1115+
chain: &'a C,
1116+
chain_tip: BlockId,
1117+
params: CanonicalizationParams,
1118+
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
1119+
use crate::canonical_iter::TopologicalIterator;
1120+
TopologicalIterator::new(self.list_canonical_txs(chain, chain_tip, params))
1121+
}
1122+
10931123
/// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
10941124
/// `chain_tip`.
10951125
///
@@ -1118,6 +1148,7 @@ impl<A: Anchor> TxGraph<A> {
11181148
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
11191149
let mut canon_txs = HashMap::<Txid, CanonicalTx<Arc<Transaction>, A>>::new();
11201150
let mut canon_spends = HashMap::<OutPoint, Txid>::new();
1151+
11211152
for r in self.try_list_canonical_txs(chain, chain_tip, params) {
11221153
let canonical_tx = r?;
11231154
let txid = canonical_tx.tx_node.txid;
@@ -1418,6 +1449,7 @@ impl<A: Anchor> TxGraph<A> {
14181449
I: fmt::Debug + Clone + Ord + 'a,
14191450
{
14201451
let indexer = indexer.as_ref();
1452+
14211453
self.try_list_canonical_txs(chain, chain_tip, CanonicalizationParams::default())
14221454
.flat_map(move |res| -> Vec<Result<(ScriptBuf, Txid), C::Error>> {
14231455
let range = &spk_index_range;

crates/chain/tests/common/tx_template.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl TxOutTemplate {
5656
pub struct TxTemplateEnv<'a, A> {
5757
pub tx_graph: TxGraph<A>,
5858
pub indexer: SpkTxOutIndex<u32>,
59-
pub txid_to_name: HashMap<&'a str, Txid>,
59+
pub tx_name_to_txid: HashMap<&'a str, Txid>,
6060
pub canonicalization_params: CanonicalizationParams,
6161
}
6262

@@ -77,7 +77,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
7777
.script_pubkey(),
7878
);
7979
});
80-
let mut txid_to_name = HashMap::<&'a str, Txid>::new();
80+
let mut tx_name_to_txid = HashMap::<&'a str, Txid>::new();
8181

8282
let mut canonicalization_params = CanonicalizationParams::default();
8383
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>(
108108
witness: Witness::new(),
109109
},
110110
TxInTemplate::PrevTx(prev_name, prev_vout) => {
111-
let prev_txid = txid_to_name.get(prev_name).expect(
111+
let prev_txid = tx_name_to_txid.get(prev_name).expect(
112112
"txin template must spend from tx of template that comes before",
113113
);
114114
TxIn {
@@ -140,7 +140,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
140140
if tx_tmp.assume_canonical {
141141
canonicalization_params.assume_canonical.push(txid);
142142
}
143-
txid_to_name.insert(tx_tmp.tx_name, txid);
143+
tx_name_to_txid.insert(tx_tmp.tx_name, txid);
144144
indexer.scan(&tx);
145145
let _ = tx_graph.insert_tx(tx.clone());
146146
for anchor in tx_tmp.anchors.iter() {
@@ -153,7 +153,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
153153
TxTemplateEnv {
154154
tx_graph,
155155
indexer,
156-
txid_to_name,
156+
tx_name_to_txid,
157157
canonicalization_params,
158158
}
159159
}

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ fn test_get_chain_position() {
782782
}
783783

784784
// check chain position
785+
785786
let chain_pos = graph
786787
.graph()
787788
.list_canonical_txs(

0 commit comments

Comments
 (0)