Skip to content

Commit 44abb68

Browse files
committed
Merge #390: fix(coin_selection): calculate_cs_result returns the required UTXOs first
8a5e763 test(tx_builder): Add `test_add_utxo_final_outpoint_retained` (valued mammal) 0b5d927 test(wallet): Add `test_tx_ordering_untouched_preserves_insertion_ordering_bnb_success` (valued mammal) e020391 fix(coin_selection): `calculate_cs_result` returns the required UTXOs first (valued mammal) Pull request description: ### Description Follow-up to #262 that addresses transaction input ordering when BnB finds a solution. Previously `calculate_cs_result` produced a CoinSelectionResult by appending the required UTXOs onto the selected ones, which changed the expected order of transaction inputs. `calculate_cs_result` now returns the required UTXOs before the newly selected ones. This behavior aligns with the expectation that the order of manually selected inputs should be preserved in the final transaction whenever `TxOrdering::Untouched` is specified. For related discussion refer to #244 (comment). ### Changelog notice Fixed - wallet: Fixed order of selected UTXOs for `BranchAndBoundCoinSelection`, required UTXOs come first ### Checklists #### All Submissions: * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) #### Bugfixes: * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: 110CodingP: ACK [`8a5e763`](8a5e763). Tree-SHA512: 4ebf33f7d1fe6e6dcfff89e218aeefcec92ae8ae78c2589d7f496b433991122f48100be38ffa52b8fc2d67feb679d567a707dea8681810167eac1a6a04f9dcc0
2 parents fbf803a + 8a5e763 commit 44abb68

3 files changed

Lines changed: 107 additions & 15 deletions

File tree

src/wallet/coin_selection.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -704,16 +704,17 @@ impl CoinSelectionAlgorithm for SingleRandomDraw {
704704
}
705705

706706
fn calculate_cs_result(
707-
mut selected_utxos: Vec<OutputGroup>,
708-
mut required_utxos: Vec<OutputGroup>,
707+
selected_utxos: Vec<OutputGroup>,
708+
required_utxos: Vec<OutputGroup>,
709709
excess: Excess,
710710
) -> CoinSelectionResult {
711-
selected_utxos.append(&mut required_utxos);
712-
let fee_amount = selected_utxos.iter().map(|u| u.fee).sum();
713-
let selected = selected_utxos
711+
let mut selected = required_utxos;
712+
selected.extend(selected_utxos);
713+
let fee_amount = selected.iter().map(|u| u.fee).sum();
714+
let selected = selected
714715
.into_iter()
715-
.map(|u| u.weighted_utxo.utxo)
716-
.collect::<Vec<_>>();
716+
.map(|output_group| output_group.weighted_utxo.utxo)
717+
.collect();
717718

718719
CoinSelectionResult {
719720
selected,

src/wallet/tx_builder.rs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ mod test {
931931
};
932932
}
933933

934+
use crate::test_utils::*;
934935
use bitcoin::consensus::deserialize;
935936
use bitcoin::hex::FromHex;
936937
use bitcoin::TxOut;
@@ -1156,7 +1157,6 @@ mod test {
11561157

11571158
#[test]
11581159
fn test_exclude_unconfirmed() {
1159-
use crate::test_utils::*;
11601160
use bdk_chain::BlockId;
11611161
use bitcoin::{hashes::Hash, BlockHash, Network};
11621162

@@ -1246,7 +1246,6 @@ mod test {
12461246

12471247
#[test]
12481248
fn test_build_fee_bump_remove_change_output_single_desc() {
1249-
use crate::test_utils::*;
12501249
use bdk_chain::BlockId;
12511250
use bitcoin::{hashes::Hash, BlockHash, Network};
12521251

@@ -1292,8 +1291,6 @@ mod test {
12921291

12931292
#[test]
12941293
fn duplicated_utxos_in_add_utxos_are_only_added_once() {
1295-
use crate::test_utils::get_funded_wallet_wpkh;
1296-
12971294
let (mut wallet, _) = get_funded_wallet_wpkh();
12981295
let utxo = wallet.list_unspent().next().unwrap();
12991296
let op = utxo.outpoint;
@@ -1306,7 +1303,6 @@ mod test {
13061303

13071304
#[test]
13081305
fn not_duplicated_utxos_in_required_list() {
1309-
use crate::test_utils::get_funded_wallet_wpkh;
13101306
let (mut wallet1, _) = get_funded_wallet_wpkh();
13111307
let utxo1 @ LocalOutput { outpoint, .. } = wallet1.list_unspent().next().unwrap();
13121308
let mut builder = wallet1.build_tx();
@@ -1321,10 +1317,54 @@ mod test {
13211317
assert_eq!(vec![fake_weighted_utxo], builder.params.utxos);
13221318
}
13231319

1320+
// This test demonstrates that `add_utxo` only considers the final insertion.
13241321
#[test]
1325-
fn not_duplicated_foreign_utxos_with_same_outpoint_but_different_weight() {
1326-
use crate::test_utils::{get_funded_wallet_single, get_funded_wallet_wpkh, get_test_wpkh};
1322+
fn test_add_utxo_final_outpoint_retained() {
1323+
// Create empty wallet
1324+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
1325+
let mut wallet = Wallet::create(desc, change_desc)
1326+
.network(bdk_wallet::bitcoin::Network::Regtest)
1327+
.create_wallet_no_persist()
1328+
.unwrap();
1329+
1330+
let outpoint_0 = receive_output(
1331+
&mut wallet,
1332+
Amount::from_sat(35_000),
1333+
ReceiveTo::Mempool(50),
1334+
);
1335+
let outpoint_1 = receive_output(
1336+
&mut wallet,
1337+
Amount::from_sat(25_200),
1338+
ReceiveTo::Mempool(100),
1339+
);
1340+
1341+
let send_to = wallet.next_unused_address(KeychainKind::External).address;
1342+
let mut tx_builder = wallet.build_tx();
1343+
tx_builder
1344+
.add_utxo(outpoint_0)
1345+
.unwrap()
1346+
.add_utxo(outpoint_1)
1347+
.unwrap()
1348+
.add_utxo(outpoint_0)
1349+
.unwrap()
1350+
.add_recipient(send_to.script_pubkey(), Amount::from_sat(60_000))
1351+
.fee_rate(FeeRate::from_sat_per_vb(1).unwrap())
1352+
.ordering(crate::TxOrdering::Untouched);
1353+
let psbt = tx_builder.finish().unwrap();
1354+
1355+
assert_eq!(
1356+
psbt.unsigned_tx
1357+
.input
1358+
.iter()
1359+
.map(|txin| txin.previous_output)
1360+
.collect::<Vec<_>>(),
1361+
vec![outpoint_1, outpoint_0],
1362+
"Last outpoint added should be retained"
1363+
);
1364+
}
13271365

1366+
#[test]
1367+
fn not_duplicated_foreign_utxos_with_same_outpoint_but_different_weight() {
13281368
// Use two different wallets to avoid adding local UTXOs
13291369
let (wallet1, txid1) = get_funded_wallet_wpkh();
13301370
let (mut wallet2, txid2) = get_funded_wallet_single(get_test_wpkh());
@@ -1380,7 +1420,6 @@ mod test {
13801420
// Test that local outputs have precedence over utxos added via `add_foreign_utxo`
13811421
#[test]
13821422
fn test_local_utxos_have_precedence_over_foreign_utxos() {
1383-
use crate::test_utils::get_funded_wallet_wpkh;
13841423
let (mut wallet, _) = get_funded_wallet_wpkh();
13851424

13861425
let utxo = wallet.list_unspent().next().unwrap();

tests/wallet.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2961,3 +2961,55 @@ fn test_tx_ordering_untouched_preserves_insertion_ordering() {
29612961
// Check vout is sorted by recipient insertion order
29622962
assert!(txouts == vec![400, 300, 500]);
29632963
}
2964+
2965+
// BnB coin selection should find a solution using the optional UTXO.
2966+
// This demonstrates that `calculate_cs_result` correctly orders required UTXOs before selected
2967+
// ones.
2968+
#[test]
2969+
fn test_tx_ordering_untouched_preserves_insertion_ordering_bnb_success() {
2970+
// Create empty wallet
2971+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
2972+
let mut wallet = Wallet::create(desc, change_desc)
2973+
.network(bdk_wallet::bitcoin::Network::Regtest)
2974+
.create_wallet_no_persist()
2975+
.unwrap();
2976+
2977+
// Set up UTXOs with specific values so BnB can find an exact match (avoiding change).
2978+
// - outpoint_0 (required): 35,000 sat - not enough alone
2979+
// - outpoint_1 (optional): 25,200 sat
2980+
// - Target: 60,000 sat
2981+
// - Expected fee: 200 sat
2982+
2983+
let outpoint_0 = receive_output(
2984+
&mut wallet,
2985+
Amount::from_sat(35_000),
2986+
ReceiveTo::Mempool(50),
2987+
);
2988+
let outpoint_1 = receive_output(
2989+
&mut wallet,
2990+
Amount::from_sat(25_200),
2991+
ReceiveTo::Mempool(100),
2992+
);
2993+
2994+
let send_to = wallet.next_unused_address(KeychainKind::External).address;
2995+
let mut tx_builder = wallet.build_tx();
2996+
tx_builder
2997+
.add_utxo(outpoint_0)
2998+
.unwrap()
2999+
.add_recipient(send_to.script_pubkey(), Amount::from_sat(60_000))
3000+
.fee_rate(FeeRate::from_sat_per_vb(1).unwrap())
3001+
.ordering(bdk_wallet::TxOrdering::Untouched);
3002+
let psbt = tx_builder.finish().unwrap();
3003+
3004+
// Verify that both UTXOs are selected in the correct order:
3005+
// required (outpoint_0) should appear before optional (outpoint_1)
3006+
assert_eq!(
3007+
psbt.unsigned_tx
3008+
.input
3009+
.iter()
3010+
.map(|txin| txin.previous_output)
3011+
.collect::<Vec<_>>(),
3012+
vec![outpoint_0, outpoint_1],
3013+
"UTXOs should be ordered with required first, then selected"
3014+
);
3015+
}

0 commit comments

Comments
 (0)