Skip to content

Commit

Permalink
WIP(coin_select): prop tests for lowest fee metric
Browse files Browse the repository at this point in the history
  • Loading branch information
evanlinjin committed Aug 18, 2023
1 parent c1a9dec commit 758d854
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 33 deletions.
6 changes: 5 additions & 1 deletion nursery/coin_select/src/bnb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
// for thing in self.queue.iter() {
// println!("{} {:?}", &thing.selector, thing.lower_bound);
// }
// let _ = std::io::stdin().read_line(&mut String::new());
// let _ = std::io::stdin().read_line(&mut alloc::string::String::new());
// }

let branch = self.queue.pop()?;
Expand Down Expand Up @@ -48,6 +48,10 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
}
}
self.best = Some(score.clone());
println!(
"\tsolution: {} lb={:?}, score={:?}",
&selector, branch.lower_bound, score
);
Some(Some((selector, score)))
}
}
Expand Down
6 changes: 3 additions & 3 deletions nursery/coin_select/src/coin_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
/// [`bnb_solutions`]: CoinSelector::bnb_solutions
#[derive(Debug, Clone)]
pub struct CoinSelector<'a> {
base_weight: u32,
pub base_weight: u32,
candidates: &'a [Candidate],
selected: Cow<'a, BTreeSet<usize>>,
banned: Cow<'a, BTreeSet<usize>>,
Expand Down Expand Up @@ -672,8 +672,8 @@ impl std::error::Error for InsufficientFunds {}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NoBnbSolution {
max_rounds: usize,
rounds: usize,
pub max_rounds: usize,
pub rounds: usize,
}

impl core::fmt::Display for NoBnbSolution {
Expand Down
54 changes: 36 additions & 18 deletions nursery/coin_select/src/metrics/lowest_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ pub struct LowestFee<'c, C> {
pub change_policy: &'c C,
}

impl<'c, C> Clone for LowestFee<'c, C> {
fn clone(&self) -> Self {
Self {
target: self.target.clone(),
long_term_feerate: self.long_term_feerate.clone(),
change_policy: self.change_policy.clone(),
}
}
}

impl<'c, C> Copy for LowestFee<'c, C> {}

impl<'c, C> LowestFee<'c, C>
where
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
Expand Down Expand Up @@ -99,7 +111,7 @@ where

// 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);
slurp(&cs, self.target, excess, finishing_input);

(cs.input_weight() as f32 + perfect_input_weight)
* self.target.feerate.spwu()
Expand All @@ -122,7 +134,7 @@ where
.find(|(cs, _, _)| cs.is_target_met(self.target, change_lb))?;
cs.deselect(slurp_index);

let perfect_excess = i64::min(
let perfect_excess = i64::max(
cs.rate_excess(self.target, Drain::none()),
cs.absolute_excess(self.target, Drain::none()),
);
Expand All @@ -135,18 +147,20 @@ where
* self.target.feerate.spwu()
+ change_weights.spend_weight as f32 * self.long_term_feerate.spwu();

println!("[target_not_met:must_have_change] LB: {}", lowest_fee);
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);
slurp(&cs, self.target, perfect_excess, candidate_to_slurp);

// 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();

println!("[target_not_met:changeless_possible] LB: {}", lowest_fee);
Some(Ordf32(lowest_fee))
}
}
Expand All @@ -157,19 +171,23 @@ where
}
}

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)
fn slurp(cs: &CoinSelector, target: Target, excess: i64, candidate: Candidate) -> f32 {
// let input_eff_sum = cs.effective_value(target.feerate) as f32;
// let eff_target = cs.base_weight as f32 * target.feerate.spwu() + target.value as f32;
let vpw = candidate.value_pwu().0;

// let perfect_weight =
// (-old_excess as f32 + eff_target - input_eff_sum) / (vpw - target.feerate.spwu());
let perfect_weight = -(excess) as f32 / (dbg!(vpw) - target.feerate.spwu());

assert!(
{
let perfect_value = (candidate.value as f32 * perfect_weight) / candidate.weight as f32;
let perfect_vpw = dbg!(dbg!(perfect_value) / dbg!(perfect_weight));
(vpw - perfect_vpw).abs() < 0.01
},
"value:weight ratio must stay the same"
);

perfect_weight
}
12 changes: 12 additions & 0 deletions nursery/coin_select/src/metrics/waste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ pub struct Waste<'c, C> {
pub change_policy: &'c C,
}

impl<'c, C> Clone for Waste<'c, C> {
fn clone(&self) -> Self {
Self {
target: self.target.clone(),
long_term_feerate: self.long_term_feerate.clone(),
change_policy: self.change_policy.clone(),
}
}
}

impl<'c, C> Copy 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
13 changes: 7 additions & 6 deletions nursery/coin_select/tests/changeless.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![allow(unused)]
use bdk_coin_select::{float::Ordf32, metrics, Candidate, CoinSelector, Drain, FeeRate, Target};
use bdk_coin_select::{
float::Ordf32, metrics, Candidate, CoinSelector, Drain, DrainWeights, FeeRate, Target,
};
use proptest::{
prelude::*,
test_runner::{RngAlgorithm, TestRng},
Expand Down Expand Up @@ -41,10 +43,9 @@ proptest! {
let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha);
let long_term_feerate = FeeRate::from_sat_per_vb(0.0f32.max(feerate - long_term_feerate_diff));
let feerate = FeeRate::from_sat_per_vb(feerate);
let drain = Drain {
weight: change_weight,
let drain = DrainWeights {
output_weight: change_weight,
spend_weight: change_spend_weight,
value: 0
};

let change_policy = bdk_coin_select::change_policy::min_waste(drain, long_term_feerate);
Expand All @@ -59,7 +60,7 @@ proptest! {
min_fee
};

let solutions = cs.branch_and_bound(metrics::Changeless {
let solutions = cs.bnb_solutions(metrics::Changeless {
target,
change_policy: &change_policy
});
Expand All @@ -78,7 +79,7 @@ proptest! {
let mut naive_select = cs.clone();
naive_select.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.effective_value(target.feerate)));
// we filter out failing onces below
let _ = naive_select.select_until_target_met(target, drain);
let _ = naive_select.select_until_target_met(target, Drain { weights: drain, value: 0 });
naive_select
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc a75fec9349c584791ef633224614369ec3721bccbe91bb24fe96978637a01098 # shrinks to n_candidates = 13, target_value = 16056, base_weight = 484, min_fee = 0, feerate = 37.709625, feerate_lt_diff = 0.0, drain_weight = 220, drain_spend_weight = 369, drain_dust = 100
cc 19a016e1ac5ec54d3b513b0e09a1a287ab49c6454313e2220b881be5bfa18df5 # shrinks to n_candidates = 0, target_value = 200, base_weight = 164, min_fee = 0, feerate = 1.0, feerate_lt_diff = 0.0, drain_weight = 100, drain_spend_weight = 250, drain_dust = 100
Loading

0 comments on commit 758d854

Please sign in to comment.