Skip to content

Commit ef5cf30

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

File tree

1 file changed

+84
-6
lines changed

1 file changed

+84
-6
lines changed

wallet/src/wallet/coin_selection.rs

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,16 @@ 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 = {
280-
optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
281-
Utxo::Local(local) => Some(local.chain_position),
282-
Utxo::Foreign { .. } => None,
281+
optional_utxos.sort_unstable_by_key(|wu| {
282+
match &wu.utxo {
283+
Utxo::Local(local) => (false, Some(local.chain_position)),
284+
Utxo::Foreign { .. } => (true, None),
285+
}
283286
});
284287

285288
required_utxos
@@ -726,10 +729,11 @@ fn calculate_cs_result(
726729
mod test {
727730
use assert_matches::assert_matches;
728731
use bitcoin::hashes::Hash;
729-
use bitcoin::OutPoint;
732+
use bitcoin::{psbt, OutPoint, Sequence};
730733
use chain::{ChainPosition, ConfirmationBlockTime};
731734
use core::str::FromStr;
732735
use rand::rngs::StdRng;
736+
use std::boxed::Box;
733737

734738
use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut};
735739

@@ -778,6 +782,30 @@ mod test {
778782
)
779783
}
780784

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

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

0 commit comments

Comments
 (0)