Skip to content

Commit c286a82

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

File tree

1 file changed

+87
-4
lines changed

1 file changed

+87
-4
lines changed

wallet/src/wallet/coin_selection.rs

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,19 +273,27 @@ 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 {
281282
Utxo::Local(local) => Some(local.chain_position),
282283
Utxo::Foreign { .. } => None,
283284
});
284285

286+
let (local_utxos, foreign_utxos): (Vec<WeightedUtxo>, Vec<WeightedUtxo>) =
287+
optional_utxos.into_iter().partition(|wu| match &wu.utxo {
288+
Utxo::Local(..) => true,
289+
Utxo::Foreign { .. } => false,
290+
});
291+
285292
required_utxos
286293
.into_iter()
287294
.map(|utxo| (true, utxo))
288-
.chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
295+
.chain(local_utxos.into_iter().map(|utxo| (false, utxo)))
296+
.chain(foreign_utxos.into_iter().map(|utxo| (false, utxo)))
289297
};
290298

291299
select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
@@ -726,10 +734,11 @@ fn calculate_cs_result(
726734
mod test {
727735
use assert_matches::assert_matches;
728736
use bitcoin::hashes::Hash;
729-
use bitcoin::OutPoint;
737+
use bitcoin::{psbt, OutPoint, Sequence};
730738
use chain::{ChainPosition, ConfirmationBlockTime};
731739
use core::str::FromStr;
732740
use rand::rngs::StdRng;
741+
use std::boxed::Box;
733742

734743
use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut};
735744

@@ -778,6 +787,30 @@ mod test {
778787
)
779788
}
780789

790+
fn foreign_utxo(value: Amount, index: u32) -> WeightedUtxo {
791+
assert!(index < 10);
792+
let outpoint = OutPoint::from_str(&format!(
793+
"000000000000000000000000000000000000000000000000000000000000000{}:0",
794+
index
795+
))
796+
.unwrap();
797+
WeightedUtxo {
798+
utxo: Utxo::Foreign {
799+
outpoint,
800+
sequence: Sequence(0xFFFFFFFD),
801+
psbt_input: Box::new(psbt::Input {
802+
witness_utxo: Some(TxOut {
803+
value,
804+
script_pubkey: ScriptBuf::new(),
805+
}),
806+
non_witness_utxo: None,
807+
..Default::default()
808+
}),
809+
},
810+
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
811+
}
812+
}
813+
781814
fn utxo(
782815
value: Amount,
783816
index: u32,
@@ -1051,6 +1084,56 @@ mod test {
10511084
assert_eq!(result.fee_amount, Amount::from_sat(204));
10521085
}
10531086

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

0 commit comments

Comments
 (0)