Skip to content

Commit 4023034

Browse files
committed
Merge #13: Implement lowest fee metric correctly
6ae0fdf docs: fix typos and use better wording (志宇) 9e1cecd Use ChangePolicy::min_value in lowest fee tests (LLFourn) 7360052 Write lowest fee test that hits important branch (LLFourn) 17cc8f2 Score branches before adding children (LLFourn) e30246d Fix lowest_fee metric (LLFourn) 0aef6ff Make lowest fee test fail by implementing score correctly (LLFourn) 0c66696 Rethink is_target_met (LLFourn) Pull request description: This replaces #11. This first commit just fixes the metric to make the tests fail. Note the previous calculation was overthinking it. The fee metric is just `inputs - outputs + long_term_feerate * change_weight`. Next steps: 1. Make ci actually run the tests and get them to fail. 2. Fix the metric lower bound ACKs for top commit: evanlinjin: ACK 6ae0fdf Tree-SHA512: c9c684ed95bc946e7e1ad8d65cd03f15180ba0bbc4e901d0e55145006629063fd110a3a08307f3e8c091ff875e41492bebc31895819455b58cc6a137b56103bc
2 parents f147ebc + 6ae0fdf commit 4023034

12 files changed

+303
-221
lines changed

README.md

+11-8
Original file line numberDiff line numberDiff line change
@@ -235,20 +235,23 @@ let candidates = candidate_txouts
235235
.collect::<Vec<_>>();
236236

237237
let mut selector = CoinSelector::new(&candidates, base_weight);
238-
let _result = selector
239-
.select_until_target_met(target, Drain::none());
240-
241-
// Determine what the drain output will be, based on our selection.
242-
let drain = selector.drain(target, change_policy);
243-
244-
// In theory the target must always still be met at this point
245-
assert!(selector.is_target_met(target, drain));
238+
selector
239+
.select_until_target_met(target, Drain::none())
240+
.expect("we've got enough coins");
246241

247242
// Get a list of coins that are selected.
248243
let selected_coins = selector
249244
.apply_selection(&candidate_txouts)
250245
.collect::<Vec<_>>();
251246
assert_eq!(selected_coins.len(), 1);
247+
248+
// Determine whether we should add a change output.
249+
let drain = selector.drain(target, change_policy);
250+
251+
if drain.is_some() {
252+
// add our change output to the transaction
253+
let change_value = drain.value;
254+
}
252255
```
253256

254257
# Minimum Supported Rust Version (MSRV)

src/bnb.rs

+19-17
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,22 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
5151

5252
let selector = branch.selector;
5353

54-
self.insert_new_branches(&selector);
55-
56-
if branch.is_exclusion {
57-
return Some(None);
54+
let mut return_val = None;
55+
if !branch.is_exclusion {
56+
if let Some(score) = self.metric.score(&selector) {
57+
let better = match self.best {
58+
Some(best_score) => score < best_score,
59+
None => true,
60+
};
61+
if better {
62+
self.best = Some(score);
63+
return_val = Some(score);
64+
}
65+
};
5866
}
5967

60-
let score = match self.metric.score(&selector) {
61-
Some(score) => score,
62-
None => return Some(None),
63-
};
64-
65-
if let Some(best_score) = &self.best {
66-
if score >= *best_score {
67-
return Some(None);
68-
}
69-
}
70-
self.best = Some(score);
71-
Some(Some((selector, score)))
68+
self.insert_new_branches(&selector);
69+
Some(return_val.map(|score| (selector, score)))
7270
}
7371
}
7472

@@ -92,7 +90,11 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> {
9290
fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a>, is_exclusion: bool) {
9391
let bound = self.metric.bound(cs);
9492
if let Some(bound) = bound {
95-
if self.best.is_none() || self.best.as_ref().unwrap() >= &bound {
93+
let is_good_enough = match self.best {
94+
Some(best) => best > bound,
95+
None => true,
96+
};
97+
if is_good_enough {
9698
let branch = Branch {
9799
lower_bound: bound,
98100
selector: cs.clone(),

src/coin_selector.rs

+61-36
Original file line numberDiff line numberDiff line change
@@ -169,29 +169,10 @@ impl<'a> CoinSelector<'a> {
169169
/// enough value.
170170
///
171171
/// [`ban`]: Self::ban
172-
pub fn is_selection_possible(&self, target: Target, drain: Drain) -> bool {
172+
pub fn is_selection_possible(&self, target: Target) -> bool {
173173
let mut test = self.clone();
174174
test.select_all_effective(target.feerate);
175-
test.is_target_met(target, drain)
176-
}
177-
178-
/// Is meeting the target *plausible* with this `change_policy`.
179-
/// Note this will respect [`ban`]ned candidates.
180-
///
181-
/// This is very similar to [`is_selection_possible`] except that you pass in a change policy.
182-
/// This method will give the right answer as long as `change_policy` is monotone but otherwise
183-
/// can it can give false negatives.
184-
///
185-
/// [`ban`]: Self::ban
186-
/// [`is_selection_possible`]: Self::is_selection_possible
187-
pub fn is_selection_plausible_with_change_policy(
188-
&self,
189-
target: Target,
190-
change_policy: &impl Fn(&CoinSelector<'a>, Target) -> Drain,
191-
) -> bool {
192-
let mut test = self.clone();
193-
test.select_all_effective(target.feerate);
194-
test.is_target_met(target, change_policy(&test, target))
175+
test.is_target_met(target)
195176
}
196177

197178
/// Returns true if no candidates have been selected.
@@ -283,6 +264,14 @@ impl<'a> CoinSelector<'a> {
283264
(self.weight(drain_weight) as f32 * feerate.spwu()).ceil() as u64
284265
}
285266

267+
/// The actual fee the selection would pay if it was used in a transaction that had
268+
/// `target_value` value for outputs and change output of `drain_value`.
269+
///
270+
/// This can be negative when the selection is invalid (outputs are greater than inputs).
271+
pub fn fee(&self, target_value: u64, drain_value: u64) -> i64 {
272+
self.selected_value() as i64 - target_value as i64 - drain_value as i64
273+
}
274+
286275
/// The value of the current selected inputs minus the fee needed to pay for the selected inputs
287276
pub fn effective_value(&self, feerate: FeeRate) -> i64 {
288277
self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
@@ -330,7 +319,9 @@ impl<'a> CoinSelector<'a> {
330319

331320
/// Sorts the candidates by descending value per weight unit, tie-breaking with value.
332321
pub fn sort_candidates_by_descending_value_pwu(&mut self) {
333-
self.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse((wv.value_pwu(), wv.value)));
322+
self.sort_candidates_by_key(|(_, wv)| {
323+
core::cmp::Reverse((Ordf32(wv.value_pwu()), wv.value))
324+
});
334325
}
335326

336327
/// The waste created by the current selection as measured by the [waste metric].
@@ -405,19 +396,30 @@ impl<'a> CoinSelector<'a> {
405396
self.unselected_indices().next().is_none()
406397
}
407398

408-
/// Whether the constraints of `Target` have been met if we include the `drain` ouput.
409-
pub fn is_target_met(&self, target: Target, drain: Drain) -> bool {
399+
/// Whether the constraints of `Target` have been met if we include a specific `drain` ouput.
400+
///
401+
/// Note if [`is_target_met`] is true and the `drain` is produced from the [`drain`] method then
402+
/// this method will also always be true.
403+
///
404+
/// [`is_target_met`]: Self::is_target_met
405+
/// [`drain`]: Self::drain
406+
pub fn is_target_met_with_drain(&self, target: Target, drain: Drain) -> bool {
410407
self.excess(target, drain) >= 0
411408
}
412409

410+
/// Whether the constraints of `Target` have been met.
411+
pub fn is_target_met(&self, target: Target) -> bool {
412+
self.is_target_met_with_drain(target, Drain::none())
413+
}
414+
413415
/// Whether the constrains of `Target` have been met if we include the drain (change) output
414416
/// when `change_policy` decides it should be present.
415417
pub fn is_target_met_with_change_policy(
416418
&self,
417419
target: Target,
418420
change_policy: ChangePolicy,
419421
) -> bool {
420-
self.is_target_met(target, self.drain(target, change_policy))
422+
self.is_target_met_with_drain(target, self.drain(target, change_policy))
421423
}
422424

423425
/// Select all unselected candidates
@@ -442,17 +444,34 @@ impl<'a> CoinSelector<'a> {
442444
},
443445
);
444446
if excess > change_policy.min_value as i64 {
447+
debug_assert_eq!(
448+
self.is_target_met(target),
449+
self.is_target_met_with_drain(
450+
target,
451+
Drain {
452+
weights: change_policy.drain_weights,
453+
value: excess as u64
454+
}
455+
),
456+
"if the target is met without a drain it must be met after adding the drain"
457+
);
445458
Some(excess as u64)
446459
} else {
447460
None
448461
}
449462
}
450463

451-
/// Convienince method that calls [`drain_value`] and converts the result into `Drain` by using
452-
/// the provided `DrainWeights`. Note carefully that the `change_policy` should have been
453-
/// calculated with the same `DrainWeights`.
464+
/// Figures out whether the current selection should have a change output given the
465+
/// `change_policy`. If it shouldn't then it will return a `Drain` where [`Drain::is_none`] is
466+
/// true. The value of the `Drain` will be the same as [`drain_value`].
467+
///
468+
/// If [`is_target_met`] returns true for this selection then [`is_target_met_with_drain`] will
469+
/// also be true if you pass in the drain returned from this method.
454470
///
455471
/// [`drain_value`]: Self::drain_value
472+
/// [`is_target_met_with_drain`]: Self::is_target_met_with_drain
473+
/// [`is_target_met`]: Self::is_target_met
474+
#[must_use]
456475
pub fn drain(&self, target: Target, change_policy: ChangePolicy) -> Drain {
457476
match self.drain_value(target, change_policy) {
458477
Some(value) => Drain {
@@ -470,7 +489,7 @@ impl<'a> CoinSelector<'a> {
470489
for cand_index in self.candidate_order.iter() {
471490
if self.selected.contains(cand_index)
472491
|| self.banned.contains(cand_index)
473-
|| self.candidates[*cand_index].effective_value(feerate) <= Ordf32(0.0)
492+
|| self.candidates[*cand_index].effective_value(feerate) <= 0.0
474493
{
475494
continue;
476495
}
@@ -486,7 +505,7 @@ impl<'a> CoinSelector<'a> {
486505
target: Target,
487506
drain: Drain,
488507
) -> Result<(), InsufficientFunds> {
489-
self.select_until(|cs| cs.is_target_met(target, drain))
508+
self.select_until(|cs| cs.is_target_met_with_drain(target, drain))
490509
.ok_or_else(|| InsufficientFunds {
491510
missing: self.excess(target, drain).unsigned_abs(),
492511
})
@@ -615,13 +634,13 @@ impl Candidate {
615634
}
616635

617636
/// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`.
618-
pub fn effective_value(&self, feerate: FeeRate) -> Ordf32 {
619-
Ordf32(self.value as f32 - (self.weight as f32 * feerate.spwu()))
637+
pub fn effective_value(&self, feerate: FeeRate) -> f32 {
638+
self.value as f32 - (self.weight as f32 * feerate.spwu())
620639
}
621640

622641
/// Value per weight unit
623-
pub fn value_pwu(&self) -> Ordf32 {
624-
Ordf32(self.value as f32 / self.weight as f32)
642+
pub fn value_pwu(&self) -> f32 {
643+
self.value as f32 / self.weight as f32
625644
}
626645
}
627646

@@ -647,11 +666,17 @@ impl DrainWeights {
647666
+ self.spend_weight as f32 * long_term_feerate.spwu()
648667
}
649668

650-
/// Create [`DrainWeights`] that represents a drain output with a taproot keyspend.
669+
/// The fee you will pay to spend these change output(s) in the future.
670+
pub fn spend_fee(&self, long_term_feerate: FeeRate) -> u64 {
671+
(self.spend_weight as f32 * long_term_feerate.spwu()).ceil() as u64
672+
}
673+
674+
/// Create [`DrainWeights`] that represents a drain output that will be spent with a taproot
675+
/// keyspend
651676
pub fn new_tr_keyspend() -> Self {
652677
Self {
653678
output_weight: TXOUT_BASE_WEIGHT + TR_SPK_WEIGHT,
654-
spend_weight: TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT,
679+
spend_weight: TR_KEYSPEND_TXIN_WEIGHT,
655680
}
656681
}
657682
}

src/metrics.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ fn change_lower_bound(cs: &CoinSelector, target: Target, change_policy: ChangePo
2525
let mut least_excess = cs.clone();
2626
cs.unselected()
2727
.rev()
28-
.take_while(|(_, wv)| wv.effective_value(target.feerate) < Ordf32(0.0))
28+
.take_while(|(_, wv)| wv.effective_value(target.feerate) < 0.0)
2929
.for_each(|(index, _)| {
3030
least_excess.select(index);
3131
});

src/metrics/changeless.rs

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use super::change_lower_bound;
2-
use crate::{
3-
bnb::BnbMetric, change_policy::ChangePolicy, float::Ordf32, CoinSelector, Drain, Target,
4-
};
2+
use crate::{bnb::BnbMetric, change_policy::ChangePolicy, float::Ordf32, CoinSelector, Target};
53

64
/// Metric for finding changeless solutions only.
75
pub struct Changeless {
@@ -13,7 +11,7 @@ pub struct Changeless {
1311

1412
impl BnbMetric for Changeless {
1513
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32> {
16-
if cs.is_target_met(self.target, Drain::none())
14+
if cs.is_target_met(self.target)
1715
&& cs.drain_value(self.target, self.change_policy).is_none()
1816
{
1917
Some(Ordf32(0.0))

0 commit comments

Comments
 (0)