Skip to content

Commit

Permalink
feat(coin_select): Implement LowestFee metric
Browse files Browse the repository at this point in the history
This is an initial non-tested implementation.
  • Loading branch information
evanlinjin committed Aug 16, 2023
1 parent 671efc5 commit c1a9dec
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 21 deletions.
8 changes: 4 additions & 4 deletions example-crates/example_cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ where
};

let target = bdk_coin_select::Target {
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(5.0),
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(2.0),
min_fee: 0,
value: transaction.output.iter().map(|txo| txo.value).sum(),
};
Expand All @@ -488,7 +488,7 @@ where
},
spend_weight: change_plan.expected_weight() as u32,
};
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_vb(1.0);
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_vb(5.0);
let drain_policy = bdk_coin_select::change_policy::min_value_and_waste(
drain_weights,
change_script.dust_value().to_sat(),
Expand All @@ -498,12 +498,12 @@ where
let mut selector = CoinSelector::new(&candidates, transaction.weight().to_wu() as u32);
match cs_algorithm {
CoinSelectionAlgo::BranchAndBound => {
let metric = bdk_coin_select::metrics::Waste {
let metric = bdk_coin_select::metrics::LowestFee {
target,
long_term_feerate,
change_policy: &drain_policy,
};
selector.run_bnb(metric, 50_000)?;
selector.run_bnb(metric, 100_000)?;
}
CoinSelectionAlgo::LargestFirst => {
selector.sort_candidates_by_key(|(_, c)| Reverse(c.value))
Expand Down
8 changes: 4 additions & 4 deletions nursery/coin_select/src/bnb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ use super::CoinSelector;
use alloc::collections::BinaryHeap;

#[derive(Debug)]
pub(crate) struct BnbIter<'a, M: BnBMetric> {
pub(crate) struct BnbIter<'a, M: BnbMetric> {
queue: BinaryHeap<Branch<'a, M::Score>>,
best: Option<M::Score>,
/// The `BnBMetric` that will score each selection
metric: M,
}

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

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

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

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

fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score>;
Expand Down
3 changes: 3 additions & 0 deletions nursery/coin_select/src/change_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub fn min_value(
///
/// Note that the value field of the `drain` is ignored.
/// The `value` will be set to whatever needs to be to reach the given target.
///
/// **WARNING:** This may result in a change output that is below dust limit. It is recommended to
/// use [`min_value_and_waste`].
pub fn min_waste(
drain_weights: DrainWeights,
long_term_feerate: FeeRate,
Expand Down
10 changes: 6 additions & 4 deletions nursery/coin_select/src/coin_selector.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;
#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't
use crate::float::FloatExt;
use crate::{bnb::BnBMetric, float::Ordf32, FeeRate};
use crate::{bnb::BnbMetric, float::Ordf32, FeeRate};
use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};

/// [`CoinSelector`] is responsible for selecting and deselecting from a set of canididates.
Expand Down Expand Up @@ -462,7 +462,7 @@ impl<'a> CoinSelector<'a> {
/// and score. Each subsequent solution of the iterator guarantees a higher score than the last.
///
/// Most of the time, you would want to use [`CoinSelector::run_bnb`] instead.
pub fn bnb_solutions<M: BnBMetric>(
pub fn bnb_solutions<M: BnbMetric>(
&self,
metric: M,
) -> impl Iterator<Item = Option<(CoinSelector<'a>, M::Score)>> {
Expand All @@ -475,7 +475,7 @@ impl<'a> CoinSelector<'a> {
/// [`NoBnbSolution`].
///
/// To access to raw bnb iterator, use [`CoinSelector::bnb_solutions`].
pub fn run_bnb<M: BnBMetric>(
pub fn run_bnb<M: BnbMetric>(
&mut self,
metric: M,
max_rounds: usize,
Expand Down Expand Up @@ -631,7 +631,9 @@ impl Drain {
}
}

/// The `SelectIter` allows you to select candidates by calling `.next`.
/// The `SelectIter` allows you to select candidates by calling [`Iterator::next`].
///
/// The [`Iterator::Item`] is a tuple of `(selector, last_selected_index, last_selected_candidate)`.
pub struct SelectIter<'a> {
cs: CoinSelector<'a>,
}
Expand Down
8 changes: 5 additions & 3 deletions nursery/coin_select/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Branch and bound metrics that can be passed to [`CoinSelector::bnb_solutions`] or
//! [`CoinSelector::run_bnb`].
use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, Target};
use crate::{bnb::BnbMetric, float::Ordf32, CoinSelector, Drain, Target};
mod waste;
pub use waste::*;
mod lowest_fee;
pub use lowest_fee::*;
mod changeless;
pub use changeless::*;

Expand Down Expand Up @@ -38,8 +40,8 @@ fn change_lower_bound<'a>(

macro_rules! impl_for_tuple {
($($a:ident $b:tt)*) => {
impl<$($a),*> BnBMetric for ($($a),*)
where $($a: BnBMetric),*
impl<$($a),*> BnbMetric for ($($a),*)
where $($a: BnbMetric),*
{
type Score=($(<$a>::Score),*);

Expand Down
4 changes: 2 additions & 2 deletions nursery/coin_select/src/metrics/changeless.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use super::change_lower_bound;
use crate::{bnb::BnBMetric, CoinSelector, Drain, Target};
use crate::{bnb::BnbMetric, CoinSelector, Drain, Target};

pub struct Changeless<'c, C> {
pub target: Target,
pub change_policy: &'c C,
}

impl<'c, C> BnBMetric for Changeless<'c, C>
impl<'c, C> BnbMetric for Changeless<'c, C>
where
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
{
Expand Down
175 changes: 175 additions & 0 deletions nursery/coin_select/src/metrics/lowest_fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
use crate::{
float::Ordf32, metrics::change_lower_bound, BnbMetric, Candidate, CoinSelector, Drain,
DrainWeights, FeeRate, Target,
};

pub struct LowestFee<'c, C> {
pub target: Target,
pub long_term_feerate: FeeRate,
pub change_policy: &'c C,
}

impl<'c, C> LowestFee<'c, C>
where
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
{
fn calculate_metric(&self, cs: &CoinSelector<'_>, drain_weights: Option<DrainWeights>) -> f32 {
match drain_weights {
// with change
Some(drain_weights) => {
(cs.input_weight() + drain_weights.output_weight) as f32
* self.target.feerate.spwu()
+ drain_weights.spend_weight as f32 * self.long_term_feerate.spwu()
}
// changeless
None => {
cs.input_weight() as f32 * self.target.feerate.spwu()
+ (cs.selected_value() - self.target.value) as f32
}
}
}
}

impl<'c, C> BnbMetric for LowestFee<'c, C>
where
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
{
type Score = Ordf32;

fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score> {
let drain = (self.change_policy)(cs, self.target);
if !cs.is_target_met(self.target, drain) {
return None;
}

let drain_weights = if drain.is_some() {
Some(drain.weights)
} else {
None
};

Some(Ordf32(self.calculate_metric(cs, drain_weights)))
}

fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<Self::Score> {
// this either returns:
// * None: change output may or may not exist
// * Some: change output must exist from this branch onwards
let change_lb = change_lower_bound(cs, self.target, &self.change_policy);
let change_lb_weights = if change_lb.is_some() {
Some(change_lb.weights)
} else {
None
};

if cs.is_target_met(self.target, change_lb) {
// Target is met, is it possible to add further inputs to remove drain output?
// If we do, can we get a better score?

// First lower bound candidate is just the selection itself.
let mut lower_bound = self.calculate_metric(cs, change_lb_weights);

// Since a changeless solution may exist, we should try reduce the excess
if change_lb.is_none() {
let selection_with_as_much_negative_ev_as_possible = cs
.clone()
.select_iter()
.rev()
.take_while(|(cs, _, candidate)| {
candidate.effective_value(self.target.feerate).0 < 0.0
&& cs.is_target_met(self.target, Drain::none())
})
.last()
.map(|(cs, _, _)| cs);

if let Some(cs) = selection_with_as_much_negative_ev_as_possible {
// we have selected as much "real" inputs as possible, is it possible to select
// one more with the perfect weight?
let can_do_better_by_slurping =
cs.unselected().next_back().and_then(|(_, candidate)| {
if candidate.effective_value(self.target.feerate).0 < 0.0 {
Some(candidate)
} else {
None
}
});
let lower_bound_changeless = match can_do_better_by_slurping {
Some(finishing_input) => {
let excess = cs.rate_excess(self.target, Drain::none());

// change the input's weight to make it's effective value match the excess
let perfect_input_weight =
slurp_candidate(finishing_input, excess, self.target.feerate);

(cs.input_weight() as f32 + perfect_input_weight)
* self.target.feerate.spwu()
}
None => self.calculate_metric(&cs, None),
};

lower_bound = lower_bound.min(lower_bound_changeless)
}
}

return Some(Ordf32(lower_bound));
}

// target is not met yet
// select until we just exceed target, then we slurp the last selection
let (mut cs, slurp_index, candidate_to_slurp) = cs
.clone()
.select_iter()
.find(|(cs, _, _)| cs.is_target_met(self.target, change_lb))?;
cs.deselect(slurp_index);

let perfect_excess = i64::min(
cs.rate_excess(self.target, Drain::none()),
cs.absolute_excess(self.target, Drain::none()),
);

match change_lb_weights {
// must have change!
Some(change_weights) => {
// [todo] This will not be perfect, just a placeholder for now
let lowest_fee = (cs.input_weight() + change_weights.output_weight) as f32
* self.target.feerate.spwu()
+ change_weights.spend_weight as f32 * self.long_term_feerate.spwu();

Some(Ordf32(lowest_fee))
}
// can be changeless!
None => {
// use the lowest excess to find "perfect candidate weight"
let perfect_input_weight =
slurp_candidate(candidate_to_slurp, perfect_excess, self.target.feerate);

// the perfect input weight canned the excess and we assume no change
let lowest_fee =
(cs.input_weight() as f32 + perfect_input_weight) * self.target.feerate.spwu();

Some(Ordf32(lowest_fee))
}
}
}

fn requires_ordering_by_descending_value_pwu(&self) -> bool {
true
}
}

fn slurp_candidate(candidate: Candidate, excess: i64, feerate: FeeRate) -> f32 {
let candidate_weight = candidate.weight as f32;

// this equation is dervied from:
// * `input_effective_value = input_value - input_weight * feerate`
// * `input_value * new_input_weight = new_input_value * input_weight`
// (ensure we have the same value:weight ratio)
// where we want `input_effective_value` to match `-excess`.
let perfect_weight = -(candidate_weight * excess as f32)
/ (candidate.value as f32 - candidate_weight * feerate.spwu());

debug_assert!(perfect_weight <= candidate_weight);

// we can't allow the weight to go negative
perfect_weight.min(0.0)
}
9 changes: 7 additions & 2 deletions nursery/coin_select/src/metrics/waste.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::change_lower_bound;
use crate::{bnb::BnBMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRate, Target};
use crate::{bnb::BnbMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRate, Target};

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

impl<'c, C> BnBMetric for Waste<'c, C>
impl<'c, C> BnbMetric for Waste<'c, C>
where
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
{
Expand Down Expand Up @@ -150,8 +150,10 @@ where

let weight_to_satisfy_abs =
remaining_abs.min(0) as f32 / to_slurp.value_pwu().0;

let weight_to_satisfy_rate =
slurp_wv(to_slurp, remaining_rate.min(0), self.target.feerate);

let weight_to_satisfy = weight_to_satisfy_abs.max(weight_to_satisfy_rate);
debug_assert!(weight_to_satisfy <= to_slurp.weight as f32);
weight_to_satisfy
Expand Down Expand Up @@ -224,6 +226,9 @@ where
}
}

/// Returns the "perfect weight" for this candidate to slurp up a given value with `feerate` while
/// not changing the candidate's value/weight ratio.
///
/// Used to pretend that a candidate had precisely `value_to_slurp` + fee needed to include it. It
/// tells you how much weight such a perfect candidate would have if it had the same value per
/// weight unit as `candidate`. This is useful for estimating a lower weight bound for a perfect
Expand Down
4 changes: 2 additions & 2 deletions nursery/coin_select/tests/bnb.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bdk_coin_select::{BnBMetric, Candidate, CoinSelector, Drain, FeeRate, Target};
use bdk_coin_select::{BnbMetric, Candidate, CoinSelector, Drain, FeeRate, Target};
#[macro_use]
extern crate alloc;

Expand Down Expand Up @@ -30,7 +30,7 @@ struct MinExcessThenWeight {
target: Target,
}

impl BnBMetric for MinExcessThenWeight {
impl BnbMetric for MinExcessThenWeight {
type Score = (i64, u32);

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

0 comments on commit c1a9dec

Please sign in to comment.