Skip to content

Commit be29c34

Browse files
committed
WIP: lowest fee
1 parent 671efc5 commit be29c34

File tree

9 files changed

+208
-21
lines changed

9 files changed

+208
-21
lines changed

example-crates/example_cli/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ where
466466
};
467467

468468
let target = bdk_coin_select::Target {
469-
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(5.0),
469+
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(2.0),
470470
min_fee: 0,
471471
value: transaction.output.iter().map(|txo| txo.value).sum(),
472472
};
@@ -488,7 +488,7 @@ where
488488
},
489489
spend_weight: change_plan.expected_weight() as u32,
490490
};
491-
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_vb(1.0);
491+
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_vb(5.0);
492492
let drain_policy = bdk_coin_select::change_policy::min_value_and_waste(
493493
drain_weights,
494494
change_script.dust_value().to_sat(),
@@ -498,12 +498,12 @@ where
498498
let mut selector = CoinSelector::new(&candidates, transaction.weight().to_wu() as u32);
499499
match cs_algorithm {
500500
CoinSelectionAlgo::BranchAndBound => {
501-
let metric = bdk_coin_select::metrics::Waste {
501+
let metric = bdk_coin_select::metrics::LowestFee {
502502
target,
503503
long_term_feerate,
504504
change_policy: &drain_policy,
505505
};
506-
selector.run_bnb(metric, 50_000)?;
506+
selector.run_bnb(metric, 100_000)?;
507507
}
508508
CoinSelectionAlgo::LargestFirst => {
509509
selector.sort_candidates_by_key(|(_, c)| Reverse(c.value))

nursery/coin_select/src/bnb.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ use super::CoinSelector;
22
use alloc::collections::BinaryHeap;
33

44
#[derive(Debug)]
5-
pub(crate) struct BnbIter<'a, M: BnBMetric> {
5+
pub(crate) struct BnbIter<'a, M: BnbMetric> {
66
queue: BinaryHeap<Branch<'a, M::Score>>,
77
best: Option<M::Score>,
88
/// The `BnBMetric` that will score each selection
99
metric: M,
1010
}
1111

12-
impl<'a, M: BnBMetric> Iterator for BnbIter<'a, M> {
12+
impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
1313
type Item = Option<(CoinSelector<'a>, M::Score)>;
1414

1515
fn next(&mut self) -> Option<Self::Item> {
@@ -52,7 +52,7 @@ impl<'a, M: BnBMetric> Iterator for BnbIter<'a, M> {
5252
}
5353
}
5454

55-
impl<'a, M: BnBMetric> BnbIter<'a, M> {
55+
impl<'a, M: BnbMetric> BnbIter<'a, M> {
5656
pub fn new(mut selector: CoinSelector<'a>, metric: M) -> Self {
5757
let mut iter = BnbIter {
5858
queue: BinaryHeap::default(),
@@ -131,7 +131,7 @@ impl<'a, O: PartialEq> PartialEq for Branch<'a, O> {
131131
impl<'a, O: PartialEq> Eq for Branch<'a, O> {}
132132

133133
/// A branch and bound metric
134-
pub trait BnBMetric {
134+
pub trait BnbMetric {
135135
type Score: Ord + Clone + core::fmt::Debug;
136136

137137
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score>;

nursery/coin_select/src/change_policy.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub fn min_value(
3636
///
3737
/// Note that the value field of the `drain` is ignored.
3838
/// The `value` will be set to whatever needs to be to reach the given target.
39+
///
40+
/// **WARNING:** This may result in a change output that is below dust limit. It is recommended to
41+
/// use [`min_value_and_waste`].
3942
pub fn min_waste(
4043
drain_weights: DrainWeights,
4144
long_term_feerate: FeeRate,

nursery/coin_select/src/coin_selector.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::*;
22
#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't
33
use crate::float::FloatExt;
4-
use crate::{bnb::BnBMetric, float::Ordf32, FeeRate};
4+
use crate::{bnb::BnbMetric, float::Ordf32, FeeRate};
55
use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
66

77
/// [`CoinSelector`] is responsible for selecting and deselecting from a set of canididates.
@@ -462,7 +462,7 @@ impl<'a> CoinSelector<'a> {
462462
/// and score. Each subsequent solution of the iterator guarantees a higher score than the last.
463463
///
464464
/// Most of the time, you would want to use [`CoinSelector::run_bnb`] instead.
465-
pub fn bnb_solutions<M: BnBMetric>(
465+
pub fn bnb_solutions<M: BnbMetric>(
466466
&self,
467467
metric: M,
468468
) -> impl Iterator<Item = Option<(CoinSelector<'a>, M::Score)>> {
@@ -475,7 +475,7 @@ impl<'a> CoinSelector<'a> {
475475
/// [`NoBnbSolution`].
476476
///
477477
/// To access to raw bnb iterator, use [`CoinSelector::bnb_solutions`].
478-
pub fn run_bnb<M: BnBMetric>(
478+
pub fn run_bnb<M: BnbMetric>(
479479
&mut self,
480480
metric: M,
481481
max_rounds: usize,
@@ -631,7 +631,9 @@ impl Drain {
631631
}
632632
}
633633

634-
/// The `SelectIter` allows you to select candidates by calling `.next`.
634+
/// The `SelectIter` allows you to select candidates by calling [`Iterator::next`].
635+
///
636+
/// The [`Iterator::Item`] is a tuple of `(selector, last_selected_index, last_selected_candidate)`.
635637
pub struct SelectIter<'a> {
636638
cs: CoinSelector<'a>,
637639
}

nursery/coin_select/src/metrics.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
//! Branch and bound metrics that can be passed to [`CoinSelector::bnb_solutions`] or
22
//! [`CoinSelector::run_bnb`].
3-
use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, Target};
3+
use crate::{bnb::BnbMetric, float::Ordf32, CoinSelector, Drain, Target};
44
mod waste;
55
pub use waste::*;
6+
mod lowest_fee;
7+
pub use lowest_fee::*;
68
mod changeless;
79
pub use changeless::*;
810

@@ -38,8 +40,8 @@ fn change_lower_bound<'a>(
3840

3941
macro_rules! impl_for_tuple {
4042
($($a:ident $b:tt)*) => {
41-
impl<$($a),*> BnBMetric for ($($a),*)
42-
where $($a: BnBMetric),*
43+
impl<$($a),*> BnbMetric for ($($a),*)
44+
where $($a: BnbMetric),*
4345
{
4446
type Score=($(<$a>::Score),*);
4547

nursery/coin_select/src/metrics/changeless.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use super::change_lower_bound;
2-
use crate::{bnb::BnBMetric, CoinSelector, Drain, Target};
2+
use crate::{bnb::BnbMetric, CoinSelector, Drain, Target};
33

44
pub struct Changeless<'c, C> {
55
pub target: Target,
66
pub change_policy: &'c C,
77
}
88

9-
impl<'c, C> BnBMetric for Changeless<'c, C>
9+
impl<'c, C> BnbMetric for Changeless<'c, C>
1010
where
1111
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
1212
{
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use crate::{
2+
float::Ordf32, metrics::change_lower_bound, BnbMetric, Candidate, CoinSelector, Drain,
3+
DrainWeights, FeeRate, Target,
4+
};
5+
6+
pub struct LowestFee<'c, C> {
7+
pub target: Target,
8+
pub long_term_feerate: FeeRate,
9+
pub change_policy: &'c C,
10+
}
11+
12+
impl<'c, C> LowestFee<'c, C>
13+
where
14+
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
15+
{
16+
fn calculate_metric(&self, cs: &CoinSelector<'_>, drain_weights: Option<DrainWeights>) -> f32 {
17+
match drain_weights {
18+
// with change
19+
Some(drain_weights) => {
20+
(cs.input_weight() + drain_weights.output_weight) as f32
21+
* self.target.feerate.spwu()
22+
+ drain_weights.spend_weight as f32 * self.long_term_feerate.spwu()
23+
}
24+
// changeless
25+
None => {
26+
cs.input_weight() as f32 * self.target.feerate.spwu()
27+
+ (cs.selected_value() - self.target.value) as f32
28+
}
29+
}
30+
}
31+
}
32+
33+
impl<'c, C> BnbMetric for LowestFee<'c, C>
34+
where
35+
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
36+
{
37+
type Score = Ordf32;
38+
39+
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score> {
40+
let drain = (self.change_policy)(cs, self.target);
41+
if !cs.is_target_met(self.target, drain) {
42+
return None;
43+
}
44+
45+
let drain_weights = if drain.is_some() {
46+
Some(drain.weights)
47+
} else {
48+
None
49+
};
50+
51+
Some(Ordf32(self.calculate_metric(cs, drain_weights)))
52+
}
53+
54+
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score> {
55+
// this either returns:
56+
// * None: change output may or may not exist
57+
// * Some: change output must exist from this branch onwards
58+
let change_lb = change_lower_bound(cs, self.target, &self.change_policy);
59+
let change_lb_weights = if change_lb.is_some() {
60+
Some(change_lb.weights)
61+
} else {
62+
None
63+
};
64+
65+
if cs.is_target_met(self.target, change_lb) {
66+
// Target is met, is it possible to add further inputs to remove drain output?
67+
// If we do, can we get a better score?
68+
69+
// First lower bound candidate is just the selection itself.
70+
let mut lower_bound = self.calculate_metric(cs, change_lb_weights);
71+
72+
// Since a changeless solution may exist, we should try reduce the excess
73+
if change_lb.is_none() {
74+
let selection_with_as_much_negative_ev_as_possible = cs
75+
.clone()
76+
.select_iter()
77+
.rev()
78+
.take_while(|(cs, _, candidate)| {
79+
candidate.effective_value(self.target.feerate).0 < 0.0
80+
&& cs.is_target_met(self.target, Drain::none())
81+
})
82+
.last()
83+
.map(|(cs, _, _)| cs);
84+
85+
if let Some(cs) = selection_with_as_much_negative_ev_as_possible {
86+
// we have selected as much "real" inputs as possible, is it possible to select
87+
// one more with the perfect weight?
88+
let can_do_better_by_slurping =
89+
cs.unselected().next_back().and_then(|(_, candidate)| {
90+
if candidate.effective_value(self.target.feerate).0 < 0.0 {
91+
Some(candidate)
92+
} else {
93+
None
94+
}
95+
});
96+
let lower_bound_changeless = match can_do_better_by_slurping {
97+
Some(finishing_input) => {
98+
let excess = cs.rate_excess(self.target, Drain::none());
99+
100+
// change the input's weight to make it's effective value match the excess
101+
let perfect_input_weight =
102+
slurp_candidate(finishing_input, excess, self.target.feerate);
103+
104+
(cs.input_weight() as f32 + perfect_input_weight)
105+
* self.target.feerate.spwu()
106+
}
107+
None => self.calculate_metric(&cs, None),
108+
};
109+
110+
lower_bound = lower_bound.min(lower_bound_changeless)
111+
}
112+
}
113+
114+
return Some(Ordf32(lower_bound));
115+
}
116+
117+
// target is not met yet
118+
// select until we just exceed target, then we slurp the last selection
119+
let (mut cs, slurp_index, candidate_to_slurp) = cs
120+
.clone()
121+
.select_iter()
122+
.find(|(cs, _, _)| cs.is_target_met(self.target, change_lb))?;
123+
cs.deselect(slurp_index);
124+
125+
let perfect_excess = i64::min(
126+
cs.rate_excess(self.target, Drain::none()),
127+
cs.absolute_excess(self.target, Drain::none()),
128+
);
129+
130+
match change_lb_weights {
131+
// must have change!
132+
Some(change_weights) => {
133+
// [todo] This will not be perfect, just a placeholder for now
134+
let lowest_fee = (cs.input_weight() + change_weights.output_weight) as f32
135+
* self.target.feerate.spwu()
136+
+ change_weights.spend_weight as f32 * self.long_term_feerate.spwu();
137+
138+
Some(Ordf32(lowest_fee))
139+
}
140+
// can be changeless!
141+
None => {
142+
// use the lowest excess to find "perfect candidate weight"
143+
let perfect_input_weight =
144+
slurp_candidate(candidate_to_slurp, perfect_excess, self.target.feerate);
145+
146+
// the perfect input weight canned the excess and we assume no change
147+
let lowest_fee =
148+
(cs.input_weight() as f32 + perfect_input_weight) * self.target.feerate.spwu();
149+
150+
Some(Ordf32(lowest_fee))
151+
}
152+
}
153+
}
154+
155+
fn requires_ordering_by_descending_value_pwu(&self) -> bool {
156+
true
157+
}
158+
}
159+
160+
fn slurp_candidate(candidate: Candidate, excess: i64, feerate: FeeRate) -> f32 {
161+
let candidate_weight = candidate.weight as f32;
162+
163+
// this equation is dervied from:
164+
// * `input_effective_value = input_value - input_weight * feerate`
165+
// * `input_value * new_input_weight = new_input_value * input_weight`
166+
// (ensure we have the same value:weight ratio)
167+
// where we want `input_effective_value` to match `-excess`.
168+
let perfect_weight = -(candidate_weight * excess as f32)
169+
/ (candidate.value as f32 - candidate_weight * feerate.spwu());
170+
171+
debug_assert!(perfect_weight <= candidate_weight);
172+
173+
// we can't allow the weight to go negative
174+
perfect_weight.min(0.0)
175+
}

nursery/coin_select/src/metrics/waste.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::change_lower_bound;
2-
use crate::{bnb::BnBMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRate, Target};
2+
use crate::{bnb::BnbMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRate, Target};
33

44
/// The "waste" metric used by bitcoin core.
55
///
@@ -24,7 +24,7 @@ pub struct Waste<'c, C> {
2424
pub change_policy: &'c C,
2525
}
2626

27-
impl<'c, C> BnBMetric for Waste<'c, C>
27+
impl<'c, C> BnbMetric for Waste<'c, C>
2828
where
2929
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
3030
{
@@ -150,8 +150,10 @@ where
150150

151151
let weight_to_satisfy_abs =
152152
remaining_abs.min(0) as f32 / to_slurp.value_pwu().0;
153+
153154
let weight_to_satisfy_rate =
154155
slurp_wv(to_slurp, remaining_rate.min(0), self.target.feerate);
156+
155157
let weight_to_satisfy = weight_to_satisfy_abs.max(weight_to_satisfy_rate);
156158
debug_assert!(weight_to_satisfy <= to_slurp.weight as f32);
157159
weight_to_satisfy
@@ -224,6 +226,9 @@ where
224226
}
225227
}
226228

229+
/// Returns the "perfect weight" for this candidate to slurp up a given value with `feerate` while
230+
/// not changing the candidate's value/weight ratio.
231+
///
227232
/// Used to pretend that a candidate had precisely `value_to_slurp` + fee needed to include it. It
228233
/// tells you how much weight such a perfect candidate would have if it had the same value per
229234
/// weight unit as `candidate`. This is useful for estimating a lower weight bound for a perfect

nursery/coin_select/tests/bnb.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bdk_coin_select::{BnBMetric, Candidate, CoinSelector, Drain, FeeRate, Target};
1+
use bdk_coin_select::{BnbMetric, Candidate, CoinSelector, Drain, FeeRate, Target};
22
#[macro_use]
33
extern crate alloc;
44

@@ -30,7 +30,7 @@ struct MinExcessThenWeight {
3030
target: Target,
3131
}
3232

33-
impl BnBMetric for MinExcessThenWeight {
33+
impl BnbMetric for MinExcessThenWeight {
3434
type Score = (i64, u32);
3535

3636
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score> {

0 commit comments

Comments
 (0)