Skip to content

Commit d23d07b

Browse files
committed
fix(coin_selection): prefer Utxo::Local over Utxo::Foreign in OldestFirstCoinSelection
Fixes #264
1 parent 00dafe7 commit d23d07b

File tree

1 file changed

+81
-5
lines changed

1 file changed

+81
-5
lines changed

wallet/src/wallet/coin_selection.rs

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,14 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
273273
drain_script: &Script,
274274
_: &mut R,
275275
) -> Result<CoinSelectionResult, InsufficientFunds> {
276-
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
276+
// 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,56 @@ 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!(matches!(result.selected[2], Utxo::Local(..)));
1128+
}
1129+
10541130
#[test]
10551131
fn test_oldest_first_coin_selection_use_only_necessary() {
10561132
let utxos = get_oldest_first_test_utxos();

0 commit comments

Comments
 (0)