From 4169060037bddb5a41c485134ffa7dd2f2680638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 23 Jan 2025 17:03:27 +1100 Subject: [PATCH] text(chain,wallet): Test `assume_canonical` mod Also change `TxTemplate` API to allow for testing with `assume_canonical`. --- crates/chain/benches/canonicalization.rs | 8 +- crates/chain/tests/common/tx_template.rs | 47 +++-- crates/chain/tests/test_tx_graph.rs | 3 +- crates/chain/tests/test_tx_graph_conflicts.rs | 191 ++++++++++++++++-- crates/wallet/tests/wallet.rs | 18 +- 5 files changed, 228 insertions(+), 39 deletions(-) diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index 2f7b27e08..addbca2c3 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -91,9 +91,11 @@ fn setup(f: F) -> (KeychainTxGraph, Lo } fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) { - let txs = tx_graph - .graph() - .list_canonical_txs(chain, chain.tip().block_id(), CanonicalizationMods::NONE); + let txs = tx_graph.graph().list_canonical_txs( + chain, + chain.tip().block_id(), + CanonicalizationMods::NONE, + ); assert_eq!(txs.count(), exp_txs); } diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 0b0e2fd9e..f5885f8d5 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -4,7 +4,7 @@ use bdk_testenv::utils::DESCRIPTORS; use rand::distributions::{Alphanumeric, DistString}; use std::collections::HashMap; -use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor}; +use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, CanonicalizationMods}; use bitcoin::{ locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, @@ -24,6 +24,7 @@ pub struct TxTemplate<'a, A> { pub outputs: &'a [TxOutTemplate], pub anchors: &'a [A], pub last_seen: Option, + pub assume_canonical: bool, } #[allow(dead_code)] @@ -51,16 +52,24 @@ impl TxOutTemplate { } } +#[allow(dead_code)] +pub struct TxTemplateEnv<'a, A> { + pub tx_graph: TxGraph, + pub indexer: SpkTxOutIndex, + pub txid_to_name: HashMap<&'a str, Txid>, + pub canonicalization_mods: CanonicalizationMods, +} + #[allow(dead_code)] pub fn init_graph<'a, A: Anchor + Clone + 'a>( tx_templates: impl IntoIterator>, -) -> (TxGraph, SpkTxOutIndex, HashMap<&'a str, Txid>) { +) -> TxTemplateEnv<'a, A> { let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[2]).unwrap(); - let mut graph = TxGraph::::default(); - let mut spk_index = SpkTxOutIndex::default(); + let mut tx_graph = TxGraph::::default(); + let mut indexer = SpkTxOutIndex::default(); (0..10).for_each(|index| { - spk_index.insert_spk( + indexer.insert_spk( index, descriptor .at_derivation_index(index) @@ -68,8 +77,9 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( .script_pubkey(), ); }); - let mut tx_ids = HashMap::<&'a str, Txid>::new(); + let mut txid_to_name = HashMap::<&'a str, Txid>::new(); + let mut canonicalization_mods = CanonicalizationMods::default(); for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { let tx = Transaction { version: transaction::Version::non_standard(0), @@ -98,7 +108,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( witness: Witness::new(), }, TxInTemplate::PrevTx(prev_name, prev_vout) => { - let prev_txid = tx_ids.get(prev_name).expect( + let prev_txid = txid_to_name.get(prev_name).expect( "txin template must spend from tx of template that comes before", ); TxIn { @@ -120,21 +130,30 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( }, Some(index) => TxOut { value: Amount::from_sat(output.value), - script_pubkey: spk_index.spk_at_index(index).unwrap(), + script_pubkey: indexer.spk_at_index(index).unwrap(), }, }) .collect(), }; - tx_ids.insert(tx_tmp.tx_name, tx.compute_txid()); - spk_index.scan(&tx); - let _ = graph.insert_tx(tx.clone()); + let txid = tx.compute_txid(); + if tx_tmp.assume_canonical { + canonicalization_mods.assume_canonical.push(txid); + } + txid_to_name.insert(tx_tmp.tx_name, txid); + indexer.scan(&tx); + let _ = tx_graph.insert_tx(tx.clone()); for anchor in tx_tmp.anchors.iter() { - let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone()); + let _ = tx_graph.insert_anchor(txid, anchor.clone()); } if let Some(last_seen) = tx_tmp.last_seen { - let _ = graph.insert_seen_at(tx.compute_txid(), last_seen); + let _ = tx_graph.insert_seen_at(txid, last_seen); } } - (graph, spk_index, tx_ids) + TxTemplateEnv { + tx_graph, + indexer, + txid_to_name, + canonicalization_mods, + } } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 6bdb632ad..f2124fc1d 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1211,6 +1211,7 @@ fn call_map_anchors_with_non_deterministic_anchor() { outputs: &[TxOutTemplate::new(10000, Some(1))], anchors: &[block_id!(1, "A")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx2", @@ -1227,7 +1228,7 @@ fn call_map_anchors_with_non_deterministic_anchor() { ..Default::default() }, ]; - let (graph, _, _) = init_graph(&template); + let graph = init_graph(&template).tx_graph; let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor { anchor_block: a, // A non-deterministic value diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 20b8732b7..10025a1b3 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -3,7 +3,7 @@ #[macro_use] mod common; -use bdk_chain::{Balance, BlockId, CanonicalizationMods}; +use bdk_chain::{Balance, BlockId}; use bdk_testenv::{block_id, hash, local_chain}; use bitcoin::{Amount, OutPoint, ScriptBuf}; use common::*; @@ -59,6 +59,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(1))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "unconfirmed_conflict", @@ -130,6 +131,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -165,6 +167,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -207,6 +210,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -221,6 +225,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(30000, Some(2))], anchors: &[block_id!(4, "Orphaned Block")], last_seen: Some(300), + ..Default::default() }, ], exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]), @@ -242,6 +247,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -256,6 +262,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(30000, Some(2))], anchors: &[block_id!(4, "Orphaned Block")], last_seen: Some(100), + ..Default::default() }, ], exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]), @@ -277,6 +284,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "tx_conflict_1", @@ -371,6 +379,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -459,6 +468,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -504,6 +514,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -549,6 +560,7 @@ fn test_tx_conflict_handling() { outputs: &[TxOutTemplate::new(10000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, + ..Default::default() }, TxTemplate { tx_name: "B", @@ -686,20 +698,161 @@ fn test_tx_conflict_handling() { exp_chain_txouts: HashSet::from([("tx", 0)]), exp_unspents: HashSet::from([("tx", 0)]), exp_balance: Balance { trusted_pending: Amount::from_sat(9000), ..Default::default() } - } + }, + Scenario { + name: "assume-canonical-tx displaces unconfirmed chain", + tx_templates: &[ + TxTemplate { + tx_name: "root", + inputs: &[TxInTemplate::Bogus], + outputs: &[ + TxOutTemplate::new(21_000, Some(0)), + TxOutTemplate::new(21_000, Some(1)), + ], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "unconfirmed", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(20_000, Some(1))], + last_seen: Some(2), + ..Default::default() + }, + TxTemplate { + tx_name: "unconfirmed_descendant", + inputs: &[ + TxInTemplate::PrevTx("unconfirmed", 0), + TxInTemplate::PrevTx("root", 1), + ], + outputs: &[TxOutTemplate::new(28_000, Some(2))], + last_seen: Some(2), + ..Default::default() + }, + TxTemplate { + tx_name: "assume_canonical", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(19_000, Some(3))], + assume_canonical: true, + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["root", "assume_canonical"]), + exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]), + exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::from_sat(19_000), + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_sat(21_000), + }, + }, + Scenario { + name: "assume-canonical-tx displaces confirmed chain", + tx_templates: &[ + TxTemplate { + tx_name: "root", + inputs: &[TxInTemplate::Bogus], + outputs: &[ + TxOutTemplate::new(21_000, Some(0)), + TxOutTemplate::new(21_000, Some(1)), + ], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "confirmed", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(20_000, Some(1))], + anchors: &[block_id!(2, "C")], + ..Default::default() + }, + TxTemplate { + tx_name: "confirmed_descendant", + inputs: &[ + TxInTemplate::PrevTx("confirmed", 0), + TxInTemplate::PrevTx("root", 1), + ], + outputs: &[TxOutTemplate::new(28_000, Some(2))], + anchors: &[block_id!(3, "D")], + ..Default::default() + }, + TxTemplate { + tx_name: "assume_canonical", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(19_000, Some(3))], + assume_canonical: true, + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["root", "assume_canonical"]), + exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]), + exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::from_sat(19_000), + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_sat(21_000), + }, + }, + Scenario { + name: "assume-canonical txs respects order", + tx_templates: &[ + TxTemplate { + tx_name: "root", + inputs: &[TxInTemplate::Bogus], + outputs: &[ + TxOutTemplate::new(21_000, Some(0)), + ], + anchors: &[block_id!(1, "B")], + ..Default::default() + }, + TxTemplate { + tx_name: "assume_a", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(20_000, Some(1))], + assume_canonical: true, + ..Default::default() + }, + TxTemplate { + tx_name: "assume_b", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(19_000, Some(1))], + assume_canonical: true, + ..Default::default() + }, + TxTemplate { + tx_name: "assume_c", + inputs: &[TxInTemplate::PrevTx("root", 0)], + outputs: &[TxOutTemplate::new(18_000, Some(1))], + assume_canonical: true, + ..Default::default() + }, + ], + exp_chain_txs: HashSet::from(["root", "assume_c"]), + exp_chain_txouts: HashSet::from([("root", 0), ("assume_c", 0)]), + exp_unspents: HashSet::from([("assume_c", 0)]), + exp_balance: Balance { + immature: Amount::ZERO, + trusted_pending: Amount::from_sat(18_000), + untrusted_pending: Amount::ZERO, + confirmed: Amount::ZERO, + }, + }, ]; for scenario in scenarios { - let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter()); + let env = init_graph(scenario.tx_templates.iter()); - let txs = tx_graph - .list_canonical_txs(&local_chain, chain_tip, CanonicalizationMods::NONE) + let txs = env + .tx_graph + .list_canonical_txs(&local_chain, chain_tip, env.canonicalization_mods.clone()) .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")) + .map(|txid| *env.txid_to_name.get(txid).expect("txid must exist")) .collect::>(); assert_eq!( txs, exp_txs, @@ -707,12 +860,13 @@ fn test_tx_conflict_handling() { scenario.name ); - let txouts = tx_graph + let txouts = env + .tx_graph .filter_chain_txouts( &local_chain, chain_tip, - CanonicalizationMods::NONE, - spk_index.outpoints().iter().cloned(), + env.canonicalization_mods.clone(), + env.indexer.outpoints().iter().cloned(), ) .map(|(_, full_txout)| full_txout.outpoint) .collect::>(); @@ -720,7 +874,7 @@ fn test_tx_conflict_handling() { .exp_chain_txouts .iter() .map(|(txid, vout)| OutPoint { - txid: *exp_tx_ids.get(txid).expect("txid must exist"), + txid: *env.txid_to_name.get(txid).expect("txid must exist"), vout: *vout, }) .collect::>(); @@ -730,12 +884,13 @@ fn test_tx_conflict_handling() { scenario.name ); - let utxos = tx_graph + let utxos = env + .tx_graph .filter_chain_unspents( &local_chain, chain_tip, - CanonicalizationMods::NONE, - spk_index.outpoints().iter().cloned(), + env.canonicalization_mods.clone(), + env.indexer.outpoints().iter().cloned(), ) .map(|(_, full_txout)| full_txout.outpoint) .collect::>(); @@ -743,7 +898,7 @@ fn test_tx_conflict_handling() { .exp_unspents .iter() .map(|(txid, vout)| OutPoint { - txid: *exp_tx_ids.get(txid).expect("txid must exist"), + txid: *env.txid_to_name.get(txid).expect("txid must exist"), vout: *vout, }) .collect::>(); @@ -753,12 +908,12 @@ fn test_tx_conflict_handling() { scenario.name ); - let balance = tx_graph.balance( + let balance = env.tx_graph.balance( &local_chain, chain_tip, - CanonicalizationMods::NONE, - spk_index.outpoints().iter().cloned(), - |_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(), + env.canonicalization_mods.clone(), + env.indexer.outpoints().iter().cloned(), + |_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(), ); assert_eq!( balance, scenario.exp_balance, diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index a7ca189cd..b38efeb45 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -4264,7 +4264,11 @@ fn test_wallet_transactions_relevant() { let chain_tip = test_wallet.local_chain().tip().block_id(); let canonical_tx_count_before = test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) + .list_canonical_txs( + test_wallet.local_chain(), + chain_tip, + CanonicalizationMods::NONE, + ) .count(); // add not relevant transaction to test wallet @@ -4281,7 +4285,11 @@ fn test_wallet_transactions_relevant() { let full_tx_count_after = test_wallet.tx_graph().full_txs().count(); let canonical_tx_count_after = test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) + .list_canonical_txs( + test_wallet.local_chain(), + chain_tip, + CanonicalizationMods::NONE, + ) .count(); assert_eq!(relevant_tx_count_before, relevant_tx_count_after); @@ -4290,7 +4298,11 @@ fn test_wallet_transactions_relevant() { .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); assert!(test_wallet .tx_graph() - .list_canonical_txs(test_wallet.local_chain(), chain_tip, CanonicalizationMods::NONE) + .list_canonical_txs( + test_wallet.local_chain(), + chain_tip, + CanonicalizationMods::NONE + ) .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); assert!(full_tx_count_before < full_tx_count_after); assert!(canonical_tx_count_before < canonical_tx_count_after);