Skip to content

Commit 115439f

Browse files
Introduce changes to support avoid_partial_spends functionality
This commit introduces many changes but they don't _really_ change anything. Tests will still pass as usual. The main change is how UTXOs are treated on coin_selection. Before, each UTXO was treated individually, but now each UTXO is its own **group**. For now, group_utxos_if_applies just creates groups with a single UTXO inside of it, but future commits will add logic to this function so it groups by pub_key
1 parent 39bae0b commit 115439f

File tree

1 file changed

+114
-51
lines changed

1 file changed

+114
-51
lines changed

crates/wallet/src/wallet/coin_selection.rs

Lines changed: 114 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ pub trait CoinSelectionAlgorithm: core::fmt::Debug {
232232
params: CoinSelectionParams<'_, R>,
233233
) -> Result<CoinSelectionResult, InsufficientFunds>;
234234
}
235+
fn group_utxos_if_applies(utxos: Vec<WeightedUtxo>, _: bool) -> Vec<Vec<WeightedUtxo>> {
236+
// No grouping: every UTXO is its own group.
237+
return utxos.into_iter().map(|u| vec![u]).collect();
238+
}
235239

236240
/// Simple and dumb coin selection
237241
///
@@ -247,21 +251,31 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection {
247251
) -> Result<CoinSelectionResult, InsufficientFunds> {
248252
let CoinSelectionParams {
249253
required_utxos,
250-
mut optional_utxos,
254+
optional_utxos,
251255
fee_rate,
252256
target_amount,
253257
drain_script,
254258
rand: _,
255-
avoid_partial_spends: _,
259+
avoid_partial_spends,
256260
} = params;
261+
let required_utxo_group =
262+
group_utxos_if_applies(required_utxos.clone(), avoid_partial_spends);
263+
let mut optional_utxos_group = group_utxos_if_applies(optional_utxos, avoid_partial_spends);
257264
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
258265
// initially smallest to largest, before being reversed with `.rev()`.
259266
let utxos = {
260-
optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
261-
required_utxos
267+
optional_utxos_group.sort_unstable_by_key(|group| {
268+
group.iter().map(|wu| wu.utxo.txout().value).sum::<Amount>()
269+
});
270+
required_utxo_group
262271
.into_iter()
263272
.map(|utxo| (true, utxo))
264-
.chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
273+
.chain(
274+
optional_utxos_group
275+
.into_iter()
276+
.rev()
277+
.map(|utxo| (false, utxo)),
278+
)
265279
};
266280

267281
select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
@@ -282,26 +296,30 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
282296
) -> Result<CoinSelectionResult, InsufficientFunds> {
283297
let CoinSelectionParams {
284298
required_utxos,
285-
mut optional_utxos,
299+
optional_utxos,
286300
fee_rate,
287301
target_amount,
288302
drain_script,
289303
rand: _,
290-
avoid_partial_spends: _,
304+
avoid_partial_spends,
291305
} = params;
306+
let required_utxo_group =
307+
group_utxos_if_applies(required_utxos.clone(), avoid_partial_spends);
308+
let mut optional_utxos_group =
309+
group_utxos_if_applies(optional_utxos.clone(), avoid_partial_spends);
292310
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
293311
// oldest to newest according to blocktime
294312
// For utxo that doesn't exist in DB, they will have lowest priority to be selected
295313
let utxos = {
296-
optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
297-
Utxo::Local(local) => Some(local.chain_position),
314+
optional_utxos_group.sort_unstable_by_key(|group| match group[0].utxo {
315+
Utxo::Local(ref local) => Some(local.chain_position),
298316
Utxo::Foreign { .. } => None,
299317
});
300318

301-
required_utxos
319+
required_utxo_group
302320
.into_iter()
303321
.map(|utxo| (true, utxo))
304-
.chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
322+
.chain(optional_utxos_group.into_iter().map(|utxo| (false, utxo)))
305323
};
306324

307325
select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
@@ -336,7 +354,7 @@ pub fn decide_change(remaining_amount: Amount, fee_rate: FeeRate, drain_script:
336354
}
337355

338356
fn select_sorted_utxos(
339-
utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
357+
utxos: impl Iterator<Item = (bool, Vec<WeightedUtxo>)>,
340358
fee_rate: FeeRate,
341359
target_amount: Amount,
342360
drain_script: &Script,
@@ -346,20 +364,23 @@ fn select_sorted_utxos(
346364
let selected = utxos
347365
.scan(
348366
(&mut selected_amount, &mut fee_amount),
349-
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
367+
|(selected_amount, fee_amount), (must_use, group)| {
350368
if must_use || **selected_amount < target_amount + **fee_amount {
351-
**fee_amount += fee_rate
352-
* TxIn::default()
353-
.segwit_weight()
354-
.checked_add(weighted_utxo.satisfaction_weight)
355-
.expect("`Weight` addition should not cause an integer overflow");
356-
**selected_amount += weighted_utxo.utxo.txout().value;
357-
Some(weighted_utxo.utxo)
369+
for weighted_utxo in &group {
370+
**fee_amount += fee_rate
371+
* TxIn::default()
372+
.segwit_weight()
373+
.checked_add(weighted_utxo.satisfaction_weight)
374+
.expect("`Weight` addition should not cause an integer overflow");
375+
**selected_amount += weighted_utxo.utxo.txout().value;
376+
}
377+
Some(group.into_iter().map(|wu| wu.utxo).collect::<Vec<_>>())
358378
} else {
359379
None
360380
}
361381
},
362382
)
383+
.flatten()
363384
.collect::<Vec<_>>();
364385

365386
let amount_needed_with_fees = target_amount + fee_amount;
@@ -469,26 +490,42 @@ impl<Cs: CoinSelectionAlgorithm> CoinSelectionAlgorithm for BranchAndBoundCoinSe
469490
rand: _,
470491
avoid_partial_spends,
471492
} = params;
493+
let required_utxo_group =
494+
group_utxos_if_applies(required_utxos.clone(), avoid_partial_spends);
495+
let optional_utxos_group =
496+
group_utxos_if_applies(optional_utxos.clone(), avoid_partial_spends);
472497
// Mapping every (UTXO, usize) to an output group
473-
let required_ogs: Vec<OutputGroup> = required_utxos
474-
.iter()
475-
.map(|u| OutputGroup::new(u.clone(), fee_rate))
498+
let required_ogs: Vec<Vec<OutputGroup>> = required_utxo_group
499+
.into_iter()
500+
.map(|group| {
501+
group
502+
.into_iter()
503+
.map(|weighted_utxo| OutputGroup::new(weighted_utxo, fee_rate))
504+
.collect()
505+
})
476506
.collect();
477507

478508
// Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative
479509
// effective value
480-
let optional_ogs: Vec<OutputGroup> = optional_utxos
481-
.iter()
482-
.map(|u| OutputGroup::new(u.clone(), fee_rate))
483-
.filter(|u| u.effective_value.is_positive())
510+
let optional_ogs: Vec<Vec<OutputGroup>> = optional_utxos_group
511+
.into_iter()
512+
.map(|group| {
513+
group
514+
.into_iter()
515+
.map(|weighted_utxo| OutputGroup::new(weighted_utxo, fee_rate))
516+
.filter(|og| og.effective_value.is_positive())
517+
.collect()
518+
})
484519
.collect();
485520

486521
let curr_value = required_ogs
487522
.iter()
523+
.flat_map(|group| group.iter())
488524
.fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
489525

490526
let curr_available_value = optional_ogs
491527
.iter()
528+
.flat_map(|group| group.iter())
492529
.fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
493530

494531
let cost_of_change = (Weight::from_vb(self.size_of_change).expect("overflow occurred")
@@ -515,9 +552,11 @@ impl<Cs: CoinSelectionAlgorithm> CoinSelectionAlgorithm for BranchAndBoundCoinSe
515552
// positive effective value), sum their value and their fee cost.
516553
let (utxo_fees, utxo_value) = required_ogs.iter().chain(optional_ogs.iter()).fold(
517554
(Amount::ZERO, Amount::ZERO),
518-
|(mut fees, mut value), utxo| {
519-
fees += utxo.fee;
520-
value += utxo.weighted_utxo.utxo.txout().value;
555+
|(mut fees, mut value), group| {
556+
for utxo in group {
557+
fees += utxo.fee;
558+
value += utxo.weighted_utxo.utxo.txout().value;
559+
}
521560
(fees, value)
522561
},
523562
);
@@ -580,8 +619,8 @@ impl<Cs> BranchAndBoundCoinSelection<Cs> {
580619
#[allow(clippy::too_many_arguments)]
581620
fn bnb(
582621
&self,
583-
required_utxos: Vec<OutputGroup>,
584-
mut optional_utxos: Vec<OutputGroup>,
622+
required_utxos: Vec<Vec<OutputGroup>>,
623+
mut optional_utxos: Vec<Vec<OutputGroup>>,
585624
mut curr_value: SignedAmount,
586625
mut curr_available_value: SignedAmount,
587626
target_amount: SignedAmount,
@@ -596,7 +635,12 @@ impl<Cs> BranchAndBoundCoinSelection<Cs> {
596635
let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
597636

598637
// Sort the utxo_pool
599-
optional_utxos.sort_unstable_by_key(|a| a.effective_value);
638+
optional_utxos.sort_unstable_by_key(|group| {
639+
group
640+
.iter()
641+
.map(|og| og.effective_value)
642+
.sum::<SignedAmount>()
643+
});
600644
optional_utxos.reverse();
601645

602646
// Contains the best selection we found
@@ -637,7 +681,10 @@ impl<Cs> BranchAndBoundCoinSelection<Cs> {
637681
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
638682
while let Some(false) = current_selection.last() {
639683
current_selection.pop();
640-
curr_available_value += optional_utxos[current_selection.len()].effective_value;
684+
curr_available_value += optional_utxos[current_selection.len()]
685+
.iter()
686+
.map(|og| og.effective_value)
687+
.sum::<SignedAmount>();
641688
}
642689

643690
if current_selection.last_mut().is_none() {
@@ -655,17 +702,26 @@ impl<Cs> BranchAndBoundCoinSelection<Cs> {
655702
}
656703

657704
let utxo = &optional_utxos[current_selection.len() - 1];
658-
curr_value -= utxo.effective_value;
705+
curr_value -= utxo
706+
.iter()
707+
.map(|og| og.effective_value)
708+
.sum::<SignedAmount>();
659709
} else {
660710
// Moving forwards, continuing down this branch
661711
let utxo = &optional_utxos[current_selection.len()];
662712

663713
// Remove this utxo from the curr_available_value utxo amount
664-
curr_available_value -= utxo.effective_value;
714+
curr_available_value -= utxo
715+
.iter()
716+
.map(|og| og.effective_value)
717+
.sum::<SignedAmount>();
665718

666719
// Inclusion branch first (Largest First Exploration)
667720
current_selection.push(true);
668-
curr_value += utxo.effective_value;
721+
curr_value += utxo
722+
.iter()
723+
.map(|og| og.effective_value)
724+
.sum::<SignedAmount>();
669725
}
670726
}
671727

@@ -679,7 +735,7 @@ impl<Cs> BranchAndBoundCoinSelection<Cs> {
679735
.into_iter()
680736
.zip(best_selection)
681737
.filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
682-
.collect::<Vec<OutputGroup>>();
738+
.collect::<Vec<Vec<OutputGroup>>>();
683739

684740
let selected_amount = best_selection_value.unwrap();
685741

@@ -707,21 +763,23 @@ impl CoinSelectionAlgorithm for SingleRandomDraw {
707763
) -> Result<CoinSelectionResult, InsufficientFunds> {
708764
let CoinSelectionParams {
709765
required_utxos,
710-
mut optional_utxos,
766+
optional_utxos,
711767
fee_rate,
712768
target_amount,
713769
drain_script,
714770
rand,
715-
avoid_partial_spends: _,
771+
avoid_partial_spends,
716772
} = params;
773+
let required_utxo_group = group_utxos_if_applies(required_utxos, avoid_partial_spends);
774+
let mut optional_utxos_group = group_utxos_if_applies(optional_utxos, avoid_partial_spends);
717775
// We put the required UTXOs first and then the randomize optional UTXOs to take as needed
718776
let utxos = {
719-
shuffle_slice(&mut optional_utxos, rand);
777+
shuffle_slice(&mut optional_utxos_group, rand);
720778

721-
required_utxos
779+
required_utxo_group
722780
.into_iter()
723781
.map(|utxo| (true, utxo))
724-
.chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
782+
.chain(optional_utxos_group.into_iter().map(|utxo| (false, utxo)))
725783
};
726784

727785
// select required UTXOs and then random optional UTXOs.
@@ -730,15 +788,20 @@ impl CoinSelectionAlgorithm for SingleRandomDraw {
730788
}
731789

732790
fn calculate_cs_result(
733-
mut selected_utxos: Vec<OutputGroup>,
734-
mut required_utxos: Vec<OutputGroup>,
791+
mut selected_utxos: Vec<Vec<OutputGroup>>,
792+
mut required_utxos: Vec<Vec<OutputGroup>>,
735793
excess: Excess,
736794
) -> CoinSelectionResult {
737795
selected_utxos.append(&mut required_utxos);
738-
let fee_amount = selected_utxos.iter().map(|u| u.fee).sum();
796+
let fee_amount = selected_utxos
797+
.iter()
798+
.flat_map(|group| group.iter())
799+
.map(|u| u.fee)
800+
.sum();
739801
let selected = selected_utxos
740802
.into_iter()
741-
.map(|u| u.weighted_utxo.utxo)
803+
.flatten()
804+
.map(|og| og.weighted_utxo.utxo)
742805
.collect::<Vec<_>>();
743806

744807
CoinSelectionResult {
@@ -1444,7 +1507,7 @@ mod test {
14441507
let target_amount = SignedAmount::from_sat(20_000) + FEE_AMOUNT.to_signed().unwrap();
14451508
let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw).bnb(
14461509
vec![],
1447-
utxos,
1510+
utxos.into_iter().map(|u| vec![u]).collect(),
14481511
SignedAmount::ZERO,
14491512
curr_available_value,
14501513
target_amount,
@@ -1477,7 +1540,7 @@ mod test {
14771540

14781541
let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw).bnb(
14791542
vec![],
1480-
utxos,
1543+
utxos.into_iter().map(|u| vec![u]).collect(),
14811544
SignedAmount::ZERO,
14821545
curr_available_value,
14831546
target_amount,
@@ -1518,7 +1581,7 @@ mod test {
15181581
let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw)
15191582
.bnb(
15201583
vec![],
1521-
utxos,
1584+
utxos.into_iter().map(|u| vec![u]).collect(),
15221585
curr_value,
15231586
curr_available_value,
15241587
target_amount,
@@ -1558,7 +1621,7 @@ mod test {
15581621
let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
15591622
.bnb(
15601623
vec![],
1561-
optional_utxos,
1624+
optional_utxos.into_iter().map(|u| vec![u]).collect(),
15621625
curr_value,
15631626
curr_available_value,
15641627
target_amount,

0 commit comments

Comments
 (0)