Skip to content

Commit 22c6d71

Browse files
committed
feat(coin_select): add DrainWeights and min_value_and_waste policy
This is a better default change policy as it minimizes waste without introducing a change output with a dust value. We update `examples_cli` to use this change policy. We introduce `DrainWeights` and refactor `change_policy` to use it.
1 parent eb079d5 commit 22c6d71

File tree

4 files changed

+137
-84
lines changed

4 files changed

+137
-84
lines changed

example-crates/example_cli/src/lib.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub use anyhow;
22
use anyhow::Context;
3-
use bdk_coin_select::{Candidate, CoinSelector};
3+
use bdk_coin_select::{Candidate, CoinSelector, Drain};
44
use bdk_file_store::Store;
55
use serde::{de::DeserializeOwned, Serialize};
66
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex};
@@ -466,13 +466,13 @@ where
466466
};
467467

468468
let target = bdk_coin_select::Target {
469-
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(1.0),
469+
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(5.0),
470470
min_fee: 0,
471471
value: transaction.output.iter().map(|txo| txo.value).sum(),
472472
};
473473

474-
let drain = bdk_coin_select::Drain {
475-
weight: {
474+
let drain_weights = bdk_coin_select::DrainWeights {
475+
output_weight: {
476476
// we calculate the weight difference of including the drain output in the base tx
477477
// this method will detect varint size changes of txout count
478478
let tx_weight = transaction.weight();
@@ -486,11 +486,14 @@ where
486486
};
487487
(tx_weight_with_drain - tx_weight).to_wu() as u32 - 1
488488
},
489-
value: 0,
490489
spend_weight: change_plan.expected_weight() as u32,
491490
};
492-
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_wu(0.25);
493-
let drain_policy = bdk_coin_select::change_policy::min_waste(drain, long_term_feerate);
491+
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_vb(1.0);
492+
let drain_policy = bdk_coin_select::change_policy::min_value_and_waste(
493+
drain_weights,
494+
change_script.dust_value().to_sat(),
495+
long_term_feerate,
496+
);
494497

495498
let mut selector = CoinSelector::new(&candidates, transaction.weight().to_wu() as u32);
496499
match cs_algorithm {
@@ -503,16 +506,10 @@ where
503506
let (final_selection, _score) = selector
504507
.branch_and_bound(metric)
505508
.take(50_000)
506-
// we only process viable solutions
509+
// skip exclusion branches (as they are not scored)
507510
.flatten()
508-
.reduce(|(best_sol, best_score), (curr_sol, curr_score)| {
509-
// we are reducing waste
510-
if curr_score < best_score {
511-
(curr_sol, curr_score)
512-
} else {
513-
(best_sol, best_score)
514-
}
515-
})
511+
// the last result is always the best score
512+
.last()
516513
.ok_or(anyhow::format_err!("no bnb solution found"))?;
517514
selector = final_selection;
518515
}
@@ -531,7 +528,13 @@ where
531528
}),
532529
CoinSelectionAlgo::BranchAndBound => unreachable!("bnb variant is matched already"),
533530
}
534-
selector.select_until_target_met(target, drain)?
531+
selector.select_until_target_met(
532+
target,
533+
Drain {
534+
weights: drain_weights,
535+
value: 0,
536+
},
537+
)?
535538
}
536539
};
537540

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't
22
use crate::float::FloatExt;
3-
use crate::{CoinSelector, Drain, FeeRate, Target};
3+
use crate::{CoinSelector, Drain, DrainWeights, FeeRate, Target};
44
use core::convert::TryInto;
55

66
/// Add a change output if the change value would be greater than or equal to `min_value`.
77
///
88
/// Note that the value field of the `drain` is ignored.
9-
pub fn min_value(mut drain: Drain, min_value: u64) -> impl Fn(&CoinSelector, Target) -> Drain {
10-
debug_assert!(drain.is_some());
9+
pub fn min_value(
10+
drain_weights: DrainWeights,
11+
min_value: u64,
12+
) -> impl Fn(&CoinSelector, Target) -> Drain {
1113
let min_value: i64 = min_value
1214
.try_into()
1315
.expect("min_value is ridiculously large");
14-
drain.value = 0;
16+
1517
move |cs, target| {
18+
let mut drain = Drain {
19+
weights: drain_weights,
20+
..Default::default()
21+
};
22+
1623
let excess = cs.excess(target, drain);
17-
if excess >= min_value {
18-
let mut drain = drain;
19-
drain.value = excess.try_into().expect(
20-
"cannot be negative since we checked it against min_value which is positive",
21-
);
22-
drain
23-
} else {
24-
Drain::none()
24+
if excess < min_value {
25+
return Drain::none();
2526
}
27+
28+
drain.value = excess
29+
.try_into()
30+
.expect("must be positive since it is greater than min_value (which is positive)");
31+
drain
2632
}
2733
}
2834

@@ -31,23 +37,49 @@ pub fn min_value(mut drain: Drain, min_value: u64) -> impl Fn(&CoinSelector, Tar
3137
/// Note that the value field of the `drain` is ignored.
3238
/// The `value` will be set to whatever needs to be to reach the given target.
3339
pub fn min_waste(
34-
mut drain: Drain,
40+
drain_weights: DrainWeights,
41+
long_term_feerate: FeeRate,
42+
) -> impl Fn(&CoinSelector, Target) -> Drain {
43+
move |cs, target| {
44+
// The output waste of a changeless solution is the excess.
45+
let waste_changeless = cs.excess(target, Drain::none());
46+
let waste_with_change = drain_weights
47+
.waste(target.feerate, long_term_feerate)
48+
.ceil() as i64;
49+
50+
if waste_changeless <= waste_with_change {
51+
return Drain::none();
52+
}
53+
54+
let mut drain = Drain {
55+
weights: drain_weights,
56+
value: 0,
57+
};
58+
drain.value = cs
59+
.excess(target, drain)
60+
.try_into()
61+
.expect("the excess must be positive because drain free excess was > waste");
62+
drain
63+
}
64+
}
65+
66+
/// Add a change output if the change value is greater than or equal to `min_value` and if it would
67+
/// reduce the overall waste of the transaction.
68+
///
69+
/// Note that the value field of the `drain` is ignored. [`Drain`] is just used for the drain weight
70+
/// and drain spend weight.
71+
pub fn min_value_and_waste(
72+
drain_weights: DrainWeights,
73+
min_value: u64,
3574
long_term_feerate: FeeRate,
3675
) -> impl Fn(&CoinSelector, Target) -> Drain {
37-
debug_assert!(drain.is_some());
38-
drain.value = 0;
76+
let min_waste_policy = crate::change_policy::min_waste(drain_weights, long_term_feerate);
3977

4078
move |cs, target| {
41-
let excess = cs.excess(target, Drain::none());
42-
if excess > drain.waste(target.feerate, long_term_feerate).ceil() as i64 {
43-
let mut drain = drain;
44-
drain.value = cs
45-
.excess(target, drain)
46-
.try_into()
47-
.expect("the excess must be positive because drain free excess was > waste");
48-
drain
49-
} else {
50-
Drain::none()
79+
let drain = min_waste_policy(cs, target);
80+
if drain.value < min_value {
81+
return Drain::none();
5182
}
83+
drain
5284
}
5385
}

nursery/coin_select/src/coin_selector.rs

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,14 @@ impl<'a> CoinSelector<'a> {
208208
self.base_weight + self.input_weight() + drain_weight
209209
}
210210

211-
/// How much the current selection overshoots the value needed to acheive `target`.
211+
/// How much the current selection overshoots the value needed to achieve `target`.
212212
///
213213
/// In order for the resulting transaction to be valid this must be 0.
214214
pub fn excess(&self, target: Target, drain: Drain) -> i64 {
215215
self.selected_value() as i64
216216
- target.value as i64
217217
- drain.value as i64
218-
- self.implied_fee(target.feerate, target.min_fee, drain.weight) as i64
218+
- self.implied_fee(target.feerate, target.min_fee, drain.weights.output_weight) as i64
219219
}
220220

221221
/// How much the current selection overshoots the value need to satisfy `target.feerate` and
@@ -224,7 +224,7 @@ impl<'a> CoinSelector<'a> {
224224
self.selected_value() as i64
225225
- target.value as i64
226226
- drain.value as i64
227-
- self.implied_fee_from_feerate(target.feerate, drain.weight) as i64
227+
- self.implied_fee_from_feerate(target.feerate, drain.weights.output_weight) as i64
228228
}
229229

230230
/// How much the current selection overshoots the value needed to satisfy `target.min_fee` and
@@ -236,11 +236,11 @@ impl<'a> CoinSelector<'a> {
236236
- target.min_fee as i64
237237
}
238238

239-
/// The feerate the transaction would have if we were to use this selection of inputs to acheive
240-
/// the `target_value`
239+
/// The feerate the transaction would have if we were to use this selection of inputs to achieve
240+
/// the `target_value`.
241241
pub fn implied_feerate(&self, target_value: u64, drain: Drain) -> FeeRate {
242242
let numerator = self.selected_value() as i64 - target_value as i64 - drain.value as i64;
243-
let denom = self.weight(drain.weight);
243+
let denom = self.weight(drain.weights.output_weight);
244244
FeeRate::from_sat_per_wu(numerator as f32 / denom as f32)
245245
}
246246

@@ -327,8 +327,8 @@ impl<'a> CoinSelector<'a> {
327327
excess_waste *= excess_discount.max(0.0).min(1.0);
328328
waste += excess_waste;
329329
} else {
330-
waste += drain.weight as f32 * target.feerate.spwu()
331-
+ drain.spend_weight as f32 * long_term_feerate.spwu();
330+
waste += drain.weights.output_weight as f32 * target.feerate.spwu()
331+
+ drain.weights.spend_weight as f32 * long_term_feerate.spwu();
332332
}
333333

334334
waste
@@ -514,6 +514,34 @@ impl Candidate {
514514
}
515515
}
516516

517+
/// A structure that represents the weight costs of a drain (a.k.a. change) output.
518+
///
519+
/// This structure can also represent multiple outputs.
520+
#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)]
521+
pub struct DrainWeights {
522+
/// The weight of adding this drain output.
523+
pub output_weight: u32,
524+
/// The weight of spending this drain output (in the future).
525+
pub spend_weight: u32,
526+
}
527+
528+
impl DrainWeights {
529+
/// The waste of adding this drain to a transaction according to the [waste metric].
530+
///
531+
/// [waste metric]: https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection
532+
pub fn waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 {
533+
self.output_weight as f32 * feerate.spwu()
534+
+ self.spend_weight as f32 * long_term_feerate.spwu()
535+
}
536+
537+
pub fn new_tr_keyspend() -> Self {
538+
Self {
539+
output_weight: TXOUT_BASE_WEIGHT + TR_SPK_WEIGHT,
540+
spend_weight: TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT,
541+
}
542+
}
543+
}
544+
517545
/// A drain (A.K.A. change) output.
518546
/// Technically it could represent multiple outputs.
519547
///
@@ -522,12 +550,10 @@ impl Candidate {
522550
/// [`change_policy`]: crate::change_policy
523551
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
524552
pub struct Drain {
525-
/// The weight of adding this drain
526-
pub weight: u32,
527-
/// The value that should be assigned to the drain
553+
/// Weight of adding drain output and spending the drain output.
554+
pub weights: DrainWeights,
555+
/// The value that should be assigned to the drain.
528556
pub value: u64,
529-
/// The weight of spending this drain
530-
pub spend_weight: u32,
531557
}
532558

533559
impl Drain {
@@ -546,19 +572,11 @@ impl Drain {
546572
!self.is_none()
547573
}
548574

549-
pub fn new_tr_keyspend() -> Self {
550-
Self {
551-
weight: TXOUT_BASE_WEIGHT + TR_SPK_WEIGHT,
552-
value: 0,
553-
spend_weight: TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT,
554-
}
555-
}
556-
557575
/// The waste of adding this drain to a transaction according to the [waste metric].
558576
///
559577
/// [waste metric]: https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection
560578
pub fn waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 {
561-
self.weight as f32 * feerate.spwu() + self.spend_weight as f32 * long_term_feerate.spwu()
579+
self.weights.waste(feerate, long_term_feerate)
562580
}
563581
}
564582

0 commit comments

Comments
 (0)