Skip to content

Commit d0ba53c

Browse files
committed
Merge #265: Prefer Utxo::Local over Utxo::Foreign in OldestFirstCoinSelection
345cc6e fix(coin_selection): prefer Utxo::Local over Utxo::Foreign in OldestFirstCoinSelection (nymius) Pull request description: ### Description The comments in the `OldestFirstCoinSelection` implementation stated the following: > // For utxo that doesn't exist in DB, they will have lowest priority to be selected But this was not honored in the code. This PR enforces this and ensures the expected behaviour through the two following new tests: - `test_oldest_first_coin_selection_uses_all_optional_with_foreign_utxo_locals_sorted_first` - `test_oldest_first_coin_selection_uses_only_all_optional_local_utxos_not_a_single_foreign` Fixes #264 ### Changelog notice No public APIs are changed by these commits. ### Checklists > [!IMPORTANT] > This pull request **DOES NOT** break the existing API * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo +nightly fmt` and `cargo clippy` before committing * [x] I've added tests for the new code * [x] I've expanded docs addressing new code * [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: ValuedMammal: ACK 345cc6e oleonardolima: ACK 345cc6e Tree-SHA512: 8a538e0765ca55f870fcbc33e4c865656e9074765a5bb13c370afe2626dbf3ecbb71e8a372607e1e405a9e35f26418bfdd9c71fec018999858a5fe5680af866b
2 parents 4d3116a + 345cc6e commit d0ba53c

File tree

1 file changed

+83
-4
lines changed

1 file changed

+83
-4
lines changed

wallet/src/wallet/coin_selection.rs

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,12 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
275275
) -> Result<CoinSelectionResult, InsufficientFunds> {
276276
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
277277
// oldest to newest according to blocktime
278-
// For utxo that doesn't exist in DB, they will have lowest priority to be selected
278+
// For UTXOs that doesn't exist in DB (Utxo::Foreign), they will have lowest priority to be
279+
// selected
279280
let utxos = {
280281
optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
281-
Utxo::Local(local) => Some(local.chain_position),
282-
Utxo::Foreign { .. } => None,
282+
Utxo::Local(local) => (false, Some(local.chain_position)),
283+
Utxo::Foreign { .. } => (true, None),
283284
});
284285

285286
required_utxos
@@ -726,10 +727,11 @@ fn calculate_cs_result(
726727
mod test {
727728
use assert_matches::assert_matches;
728729
use bitcoin::hashes::Hash;
729-
use bitcoin::OutPoint;
730+
use bitcoin::{psbt, OutPoint, Sequence};
730731
use chain::{ChainPosition, ConfirmationBlockTime};
731732
use core::str::FromStr;
732733
use rand::rngs::StdRng;
734+
use std::boxed::Box;
733735

734736
use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut};
735737

@@ -778,6 +780,30 @@ mod test {
778780
)
779781
}
780782

783+
fn foreign_utxo(value: Amount, index: u32) -> WeightedUtxo {
784+
assert!(index < 10);
785+
let outpoint = OutPoint::from_str(&format!(
786+
"000000000000000000000000000000000000000000000000000000000000000{}:0",
787+
index
788+
))
789+
.unwrap();
790+
WeightedUtxo {
791+
utxo: Utxo::Foreign {
792+
outpoint,
793+
sequence: Sequence(0xFFFFFFFD),
794+
psbt_input: Box::new(psbt::Input {
795+
witness_utxo: Some(TxOut {
796+
value,
797+
script_pubkey: ScriptBuf::new(),
798+
}),
799+
non_witness_utxo: None,
800+
..Default::default()
801+
}),
802+
},
803+
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
804+
}
805+
}
806+
781807
fn utxo(
782808
value: Amount,
783809
index: u32,
@@ -1051,6 +1077,59 @@ mod test {
10511077
assert_eq!(result.fee_amount, Amount::from_sat(204));
10521078
}
10531079

1080+
#[test]
1081+
fn test_oldest_first_coin_selection_uses_all_optional_with_foreign_utxo_locals_sorted_first() {
1082+
let utxos = get_oldest_first_test_utxos();
1083+
let mut all_utxos = vec![foreign_utxo(Amount::from_sat(120_000), 1)];
1084+
all_utxos.extend_from_slice(&utxos);
1085+
let drain_script = ScriptBuf::default();
1086+
let target_amount = Amount::from_sat(619_000) + FEE_AMOUNT;
1087+
1088+
let result = OldestFirstCoinSelection
1089+
.coin_select(
1090+
vec![],
1091+
all_utxos,
1092+
FeeRate::from_sat_per_vb_unchecked(1),
1093+
target_amount,
1094+
&drain_script,
1095+
&mut thread_rng(),
1096+
)
1097+
.unwrap();
1098+
1099+
assert_eq!(result.selected.len(), 4);
1100+
assert_eq!(result.selected_amount(), Amount::from_sat(620_000));
1101+
assert_eq!(result.fee_amount, Amount::from_sat(272));
1102+
assert!(matches!(result.selected[3], Utxo::Foreign { .. }));
1103+
}
1104+
1105+
#[test]
1106+
fn test_oldest_first_coin_selection_uses_only_all_optional_local_utxos_not_a_single_foreign() {
1107+
let utxos = get_oldest_first_test_utxos();
1108+
let mut all_utxos = vec![foreign_utxo(Amount::from_sat(120_000), 1)];
1109+
all_utxos.extend_from_slice(&utxos);
1110+
let drain_script = ScriptBuf::default();
1111+
let target_amount = Amount::from_sat(499_000) + FEE_AMOUNT;
1112+
1113+
let result = OldestFirstCoinSelection
1114+
.coin_select(
1115+
vec![],
1116+
all_utxos,
1117+
FeeRate::from_sat_per_vb_unchecked(1),
1118+
target_amount,
1119+
&drain_script,
1120+
&mut thread_rng(),
1121+
)
1122+
.unwrap();
1123+
1124+
assert_eq!(result.selected.len(), 3);
1125+
assert_eq!(result.selected_amount(), Amount::from_sat(500_000));
1126+
assert_eq!(result.fee_amount, Amount::from_sat(204));
1127+
assert!(result
1128+
.selected
1129+
.iter()
1130+
.all(|utxo| matches!(utxo, Utxo::Local(..))));
1131+
}
1132+
10541133
#[test]
10551134
fn test_oldest_first_coin_selection_use_only_necessary() {
10561135
let utxos = get_oldest_first_test_utxos();

0 commit comments

Comments
 (0)