]> Untitled Git - bdk/commitdiff
test(chain): add `test_list_ordered_canonical_txs`
authorLeonardo Lima <oleonardolima@users.noreply.github.com>
Fri, 12 Sep 2025 04:39:49 +0000 (14:39 +1000)
committerLeonardo Lima <oleonardolima@users.noreply.github.com>
Sat, 22 Nov 2025 04:39:23 +0000 (01:39 -0300)
- Add a new `test_list_ordered_canonical_txs`, to assert the topological
  spending order when yielding the list of canonical txs, it also adds a
  new helper `Scenario` structure, and `is_txs_in_topological_order`.

crates/chain/tests/test_tx_graph.rs

index 685b62c6e79c6d3c7b1da6fa84b252139a744d37..205ef58b20d41ca5f8c62691d0388d3c1ca55b11 100644 (file)
@@ -9,6 +9,7 @@ use bdk_chain::{
     tx_graph::{ChangeSet, TxGraph},
     Anchor, ChainOracle, ChainPosition, Merge,
 };
+use bdk_testenv::local_chain;
 use bdk_testenv::{block_id, hash, utils::new_tx};
 use bitcoin::hex::FromHex;
 use bitcoin::Witness;
@@ -1540,3 +1541,514 @@ fn test_get_first_seen_of_a_tx() {
     let first_seen = graph.get_tx_node(txid).unwrap().first_seen;
     assert_eq!(first_seen, Some(seen_at));
 }
+
+/// A helper structure to constructs multiple [`TxGraph`] scenarios, used in
+/// `test_list_ordered_canonical_txs`.
+struct Scenario<'a> {
+    /// Name of the test scenario
+    name: &'a str,
+    /// Transaction templates
+    tx_templates: &'a [TxTemplate<'a, BlockId>],
+    /// Names of txs that must exist in the output of `list_canonical_txs`
+    exp_chain_txs: Vec<&'a str>,
+}
+
+/// A helper method to assert the expected topological order for a given [`Vec<Txid>`].
+fn is_txs_in_topological_order(txs: Vec<Txid>, tx_graph: TxGraph<BlockId>) -> bool {
+    let mut seen: HashSet<Txid> = HashSet::new();
+
+    for txid in txs {
+        let tx = tx_graph.get_tx(txid).expect("should exist");
+        let inputs: Vec<Txid> = tx
+            .input
+            .iter()
+            .map(|txin| txin.previous_output.txid)
+            .collect();
+
+        // assert that all the txin's have been seen already
+        for input_txid in inputs {
+            if !seen.contains(&input_txid) {
+                return false;
+            }
+        }
+
+        // Add current transaction to seen set
+        seen.insert(txid);
+    }
+
+    true
+}
+
+#[test]
+fn test_list_ordered_canonical_txs() {
+    // chain
+    let local_chain: LocalChain = local_chain!(
+        (0, hash!("A")),
+        (1, hash!("B")),
+        (2, hash!("C")),
+        (3, hash!("D")),
+        (4, hash!("E")),
+        (5, hash!("F")),
+        (6, hash!("G"))
+    );
+    let chain_tip = local_chain.tip().block_id();
+
+    let scenarios = [
+    // a0  b0  c0
+    Scenario {
+        name: "a0, b0 and c0 are roots, does not spend from any other transaction, and are in the best chain",
+        tx_templates: &[
+            TxTemplate {
+                tx_name: "a0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(10000, Some(0))],
+                anchors: &[block_id!(1, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "b0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(1, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "c0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(2500, Some(0))],
+                anchors: &[block_id!(1, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+        ],
+        exp_chain_txs: Vec::from(["a0", "b0", "c0"]),
+    },
+    // a0  b0  c0
+    Scenario {
+        name: "a0, b0 and c0 are roots, does not spend from any other transaction, and have no anchor or last_seen",
+        tx_templates: &[
+            TxTemplate {
+                tx_name: "a0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(10000, Some(0))],
+                anchors: &[],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "b0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "c0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(2500, Some(0))],
+                anchors: &[],
+                last_seen: None,
+                assume_canonical: false,
+            },
+        ],
+        exp_chain_txs: Vec::from([]),
+    },
+    // a0  b0  c0
+    Scenario {
+        name: "A, B and C are roots, does not spend from any other transaction, and are all have the same `last_seen`",
+        tx_templates: &[
+            TxTemplate {
+                tx_name: "A",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(10000, Some(0))],
+                anchors: &[],
+                last_seen: Some(1000),
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "B",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[],
+                last_seen: Some(1000),
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "C",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(2500, Some(0))],
+                anchors: &[],
+                last_seen: Some(1000),
+                assume_canonical: false,
+            },
+        ],
+        exp_chain_txs: Vec::from(["A", "B", "C"]),
+    },
+    // a0
+    //  \
+    //  b0
+    //    \
+    //     \   c0
+    //      \  /
+    //       d0
+    Scenario {
+        name: "b0 spends a0, d0 spends both b0 and c0, and are in the best chain",
+        tx_templates: &[
+            TxTemplate {
+                tx_name: "a0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(10000, Some(0))],
+                anchors: &[block_id!(1, "A")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "b0",
+                inputs: &[TxInTemplate::PrevTx("a0", 0)],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(2, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "c0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(3, "C")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "d0",
+                inputs: &[TxInTemplate::PrevTx("b0", 0), TxInTemplate::PrevTx("c0", 0)],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(3, "C")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+        ],
+        exp_chain_txs: Vec::from(["a0", "b0", "c0", "d0"]),
+    },
+    // a0   c0
+    //  \
+    //  b0
+    //    \
+    //     d0
+    Scenario {
+        name: "b0 spends a0, d0 spends b0, and a0, b0 and c0 are in the best chain",
+        tx_templates: &[
+            TxTemplate {
+                tx_name: "a0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(10000, Some(0))],
+                anchors: &[block_id!(1, "A")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "b0",
+                inputs: &[TxInTemplate::PrevTx("a0", 0)],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(2, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "c0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(3, "C")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "d0",
+                inputs: &[TxInTemplate::PrevTx("b0", 0)],
+                outputs: &[TxOutTemplate::new(2500, Some(0))],
+                anchors: &[],
+                last_seen: Some(1000),
+                assume_canonical: false,
+            },
+        ],
+        exp_chain_txs: Vec::from(["a0", "b0", "c0", "d0"]),
+    },
+    // a0
+    //  \
+    //   b0
+    //    \
+    //     c0
+    Scenario {
+        name: "c0 spend a0, b0 spend a0, and a0, b0 are in the best chain",
+        tx_templates: &[
+            TxTemplate {
+                tx_name: "a0",
+                inputs: &[],
+                outputs: &[TxOutTemplate::new(10000, Some(0))],
+                anchors: &[block_id!(1, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "b0",
+                inputs: &[TxInTemplate::PrevTx("a0", 0)],
+                outputs: &[TxOutTemplate::new(5000, Some(0))],
+                anchors: &[block_id!(1, "B")],
+                last_seen: None,
+                assume_canonical: false,
+            },
+            TxTemplate {
+                tx_name: "c0",
+                inputs: &[TxInTemplate::PrevTx("b0", 0)],
+                outputs: &[TxOutTemplate::new(2500, Some(0))],
+                anchors: &[],
+                last_seen: Some(1000),
+                assume_canonical: false,
+            },
+        ],
+        exp_chain_txs: Vec::from(["a0", "b0", "c0"]),
+    },
+    //     a0
+    //    /  \
+    //   b0   b1
+    //  /  \   \
+    // c0   \   c1
+    //       \  /
+    //        d0
+    Scenario {
+        name: "c0 spend b0, b0 spend a0, d0 spends both b0 and c1, c1 spend b1, b1 spend a0, and are all in the best chain",
+        tx_templates: &[TxTemplate {
+            tx_name: "a0",
+            inputs: &[],
+            outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        }, TxTemplate {
+            tx_name: "b0",
+            inputs: &[TxInTemplate::PrevTx("a0", 0)],
+            outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "c0",
+            inputs: &[TxInTemplate::PrevTx("b0", 0)],
+            outputs: &[TxOutTemplate::new(5000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "b1",
+            inputs: &[TxInTemplate::PrevTx("a0", 1)],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "c1",
+            inputs: &[TxInTemplate::PrevTx("b1", 0)],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "d0",
+            inputs: &[TxInTemplate::PrevTx("b0", 1), TxInTemplate::PrevTx("c1", 0),],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        }],
+        exp_chain_txs: Vec::from(["a0", "b0", "c0", "b1", "c1", "d0"]),
+    },
+    //     a0   d0   e0
+    //    /         /  \
+    //   b0        f0  f1
+    //  /           \  /
+    // c0            g0
+    Scenario {
+        name: "c0 spend b0, b0 spend a0, d0 does not spend any nor is spent by, g0 spends f0, f1, and f0 and f1 spends e0, and a0, d0, and e0 are in the best chain",
+        tx_templates: &[TxTemplate {
+            tx_name: "a0",
+            inputs: &[],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        }, TxTemplate {
+            tx_name: "b0",
+            inputs: &[TxInTemplate::PrevTx("a0", 0)],
+            outputs: &[TxOutTemplate::new(5000, Some(0)), TxOutTemplate::new(10000, Some(1))],
+            anchors: &[block_id!(2, "C")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "c0",
+            inputs: &[TxInTemplate::PrevTx("b0", 0)],
+            outputs: &[TxOutTemplate::new(2500, Some(0))],
+            anchors: &[block_id!(3, "D")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "d0",
+            inputs: &[],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(3, "D")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "e0",
+            inputs: &[],
+            outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
+            anchors: &[block_id!(4, "E")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "f0",
+            inputs: &[TxInTemplate::PrevTx("e0", 0)],
+            outputs: &[TxOutTemplate::new(5000, Some(0))],
+            anchors: &[block_id!(5, "F")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "f1",
+            inputs: &[TxInTemplate::PrevTx("e0", 1)],
+            outputs: &[TxOutTemplate::new(5000, Some(0))],
+            anchors: &[block_id!(5, "F")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "g0",
+            inputs: &[TxInTemplate::PrevTx("f0", 0), TxInTemplate::PrevTx("f1", 0)],
+            outputs: &[TxOutTemplate::new(1000, Some(0))],
+            anchors: &[],
+            last_seen: Some(1000),
+            assume_canonical: false,
+        }
+        ],
+        exp_chain_txs: Vec::from(["a0", "b0", "c0", "d0", "e0", "f0", "f1", "g0"]),
+    },
+    //      a0
+    //     / \ \
+    //   e0  /  b1
+    //   /  /    \
+    //  f0 /      \
+    //   \/        \
+    //   b0         \
+    //  /  \        /
+    // c0   \     c1
+    //       \   /
+    //         d0
+    Scenario {
+        name: "c0 spend b0, b0 spends both f0 and a0, f0 spend e0, e0 spend a0, d0 spends both b0 and c1, c1 spend b1, b1 spend a0, and are all in the best chain",
+        tx_templates: &[TxTemplate {
+            tx_name: "a0",
+            inputs: &[],
+            outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1)), TxOutTemplate::new(10000, Some(2))],
+            // outputs: &[TxOutTemplate::new(10000, Some(1)), TxOutTemplate::new(10000, Some(2))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+        TxTemplate {
+            tx_name: "e0",
+            inputs: &[TxInTemplate::PrevTx("a0", 0)],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+        TxTemplate {
+            tx_name: "f0",
+            inputs: &[TxInTemplate::PrevTx("e0", 0)],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "b0",
+            inputs: &[TxInTemplate::PrevTx("f0", 0), TxInTemplate::PrevTx("a0", 1)],
+            outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "c0",
+            inputs: &[TxInTemplate::PrevTx("b0", 0)],
+            outputs: &[TxOutTemplate::new(5000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "b1",
+            inputs: &[TxInTemplate::PrevTx("a0", 2)],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "c1",
+            inputs: &[TxInTemplate::PrevTx("b1", 0)],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        },
+         TxTemplate {
+            tx_name: "d0",
+            inputs: &[TxInTemplate::PrevTx("b0", 1), TxInTemplate::PrevTx("c1", 0),  ],
+            outputs: &[TxOutTemplate::new(10000, Some(0))],
+            anchors: &[block_id!(1, "B")],
+            last_seen: None,
+            assume_canonical: false,
+        }],
+        exp_chain_txs: Vec::from(["a0", "e0", "f0", "b0", "c0", "b1", "c1", "d0"]),
+    }];
+
+    for (_, scenario) in scenarios.iter().enumerate() {
+        let env = init_graph(scenario.tx_templates.iter());
+
+        let canonical_txs = env
+            .tx_graph
+            .list_canonical_txs(&local_chain, chain_tip, env.canonicalization_params.clone())
+            .map(|tx| tx.tx_node.txid)
+            .collect::<BTreeSet<_>>();
+
+        let exp_txs = scenario
+            .exp_chain_txs
+            .iter()
+            .map(|txid| *env.tx_name_to_txid.get(txid).expect("txid must exist"))
+            .collect::<BTreeSet<_>>();
+
+        assert_eq!(
+            canonical_txs, exp_txs,
+            "\n[{}] 'list_canonical_txs' failed",
+            scenario.name
+        );
+
+        let canonical_txs = canonical_txs.iter().map(|txid| *txid).collect::<Vec<_>>();
+
+        assert!(
+            is_txs_in_topological_order(canonical_txs, env.tx_graph),
+            "\n[{}] 'list_canonical_txs' failed to output the txs in topological order",
+            scenario.name
+        );
+    }
+}