Skip to content

Commit b317c43

Browse files
committed
WIP(coin_select): prop tests for lowest fee metric
1 parent c1a9dec commit b317c43

File tree

8 files changed

+282
-33
lines changed

8 files changed

+282
-33
lines changed

nursery/coin_select/src/bnb.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
1818
// for thing in self.queue.iter() {
1919
// println!("{} {:?}", &thing.selector, thing.lower_bound);
2020
// }
21-
// let _ = std::io::stdin().read_line(&mut String::new());
21+
// let _ = std::io::stdin().read_line(&mut alloc::string::String::new());
2222
// }
2323

2424
let branch = self.queue.pop()?;
@@ -48,6 +48,7 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
4848
}
4949
}
5050
self.best = Some(score.clone());
51+
println!("\tsolution={}, score={:?}", &selector, score);
5152
Some(Some((selector, score)))
5253
}
5354
}

nursery/coin_select/src/coin_selector.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
1212
/// [`bnb_solutions`]: CoinSelector::bnb_solutions
1313
#[derive(Debug, Clone)]
1414
pub struct CoinSelector<'a> {
15-
base_weight: u32,
15+
pub base_weight: u32,
1616
candidates: &'a [Candidate],
1717
selected: Cow<'a, BTreeSet<usize>>,
1818
banned: Cow<'a, BTreeSet<usize>>,
@@ -672,8 +672,8 @@ impl std::error::Error for InsufficientFunds {}
672672

673673
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
674674
pub struct NoBnbSolution {
675-
max_rounds: usize,
676-
rounds: usize,
675+
pub max_rounds: usize,
676+
pub rounds: usize,
677677
}
678678

679679
impl core::fmt::Display for NoBnbSolution {

nursery/coin_select/src/metrics/lowest_fee.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ pub struct LowestFee<'c, C> {
99
pub change_policy: &'c C,
1010
}
1111

12+
impl<'c, C> Clone for LowestFee<'c, C> {
13+
fn clone(&self) -> Self {
14+
Self {
15+
target: self.target.clone(),
16+
long_term_feerate: self.long_term_feerate.clone(),
17+
change_policy: self.change_policy.clone(),
18+
}
19+
}
20+
}
21+
22+
impl<'c, C> Copy for LowestFee<'c, C> {}
23+
1224
impl<'c, C> LowestFee<'c, C>
1325
where
1426
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,
@@ -99,7 +111,7 @@ where
99111

100112
// change the input's weight to make it's effective value match the excess
101113
let perfect_input_weight =
102-
slurp_candidate(finishing_input, excess, self.target.feerate);
114+
slurp(&cs, self.target, excess, finishing_input);
103115

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

125-
let perfect_excess = i64::min(
137+
let perfect_excess = i64::max(
126138
cs.rate_excess(self.target, Drain::none()),
127139
cs.absolute_excess(self.target, Drain::none()),
128140
);
@@ -141,7 +153,7 @@ where
141153
None => {
142154
// use the lowest excess to find "perfect candidate weight"
143155
let perfect_input_weight =
144-
slurp_candidate(candidate_to_slurp, perfect_excess, self.target.feerate);
156+
slurp(&cs, self.target, perfect_excess, candidate_to_slurp);
145157

146158
// the perfect input weight canned the excess and we assume no change
147159
let lowest_fee =
@@ -157,19 +169,23 @@ where
157169
}
158170
}
159171

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)
172+
fn slurp(_cs: &CoinSelector, target: Target, excess: i64, candidate: Candidate) -> f32 {
173+
// let input_eff_sum = cs.effective_value(target.feerate) as f32;
174+
// let eff_target = cs.base_weight as f32 * target.feerate.spwu() + target.value as f32;
175+
let vpw = candidate.value_pwu().0;
176+
177+
// let perfect_weight =
178+
// (-old_excess as f32 + eff_target - input_eff_sum) / (vpw - target.feerate.spwu());
179+
let perfect_weight = -(excess) as f32 / (vpw - target.feerate.spwu());
180+
181+
debug_assert!(
182+
{
183+
let perfect_value = (candidate.value as f32 * perfect_weight) / candidate.weight as f32;
184+
let perfect_vpw = perfect_value / perfect_weight;
185+
(vpw - perfect_vpw).abs() < 0.01
186+
},
187+
"value:weight ratio must stay the same"
188+
);
189+
190+
perfect_weight.max(0.0)
175191
}

nursery/coin_select/src/metrics/waste.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ pub struct Waste<'c, C> {
2424
pub change_policy: &'c C,
2525
}
2626

27+
impl<'c, C> Clone for Waste<'c, C> {
28+
fn clone(&self) -> Self {
29+
Self {
30+
target: self.target.clone(),
31+
long_term_feerate: self.long_term_feerate.clone(),
32+
change_policy: self.change_policy.clone(),
33+
}
34+
}
35+
}
36+
37+
impl<'c, C> Copy for Waste<'c, C> {}
38+
2739
impl<'c, C> BnbMetric for Waste<'c, C>
2840
where
2941
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain,

nursery/coin_select/tests/changeless.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![allow(unused)]
2-
use bdk_coin_select::{float::Ordf32, metrics, Candidate, CoinSelector, Drain, FeeRate, Target};
2+
use bdk_coin_select::{
3+
float::Ordf32, metrics, Candidate, CoinSelector, Drain, DrainWeights, FeeRate, Target,
4+
};
35
use proptest::{
46
prelude::*,
57
test_runner::{RngAlgorithm, TestRng},
@@ -41,10 +43,9 @@ proptest! {
4143
let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha);
4244
let long_term_feerate = FeeRate::from_sat_per_vb(0.0f32.max(feerate - long_term_feerate_diff));
4345
let feerate = FeeRate::from_sat_per_vb(feerate);
44-
let drain = Drain {
45-
weight: change_weight,
46+
let drain = DrainWeights {
47+
output_weight: change_weight,
4648
spend_weight: change_spend_weight,
47-
value: 0
4849
};
4950

5051
let change_policy = bdk_coin_select::change_policy::min_waste(drain, long_term_feerate);
@@ -59,7 +60,7 @@ proptest! {
5960
min_fee
6061
};
6162

62-
let solutions = cs.branch_and_bound(metrics::Changeless {
63+
let solutions = cs.bnb_solutions(metrics::Changeless {
6364
target,
6465
change_policy: &change_policy
6566
});
@@ -78,7 +79,7 @@ proptest! {
7879
let mut naive_select = cs.clone();
7980
naive_select.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.effective_value(target.feerate)));
8081
// we filter out failing onces below
81-
let _ = naive_select.select_until_target_met(target, drain);
82+
let _ = naive_select.select_until_target_met(target, Drain { weights: drain, value: 0 });
8283
naive_select
8384
},
8485
];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
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
8+
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

0 commit comments

Comments
 (0)