Skip to content

Commit 8a984e3

Browse files
evanlinjinclaude
andcommitted
refactor(metrics): extract shared resize_bound and excess_with_drain helpers
The same "resize trick" was duplicated in three places: LowestFee::bound, ChangelessLowestFee::bound, and changeless_waste::resize_target_lb_input_weight. Each one ran the same find-then-deselect dance plus the same rate/replace/absolute scale calculation, only differing in what they computed from `scale` at the end. Extracts: - `resize_bound(cs, target)` -> Option<(CoinSelector, Candidate, scale)>: the find + scale calculation. Callers compute their own ideal_fee / ideal_input_weight from `(cs, to_resize, scale)`. Exact-match (zero excess) is folded into the same return shape via `scale == 1.0`. - `excess_with_drain(cs, target, change_policy)`: shortcut for the "would-be excess if change were materialised" calculation that ChangePolicy uses to decide whether to add a drain. Net: -91 lines, all bound logic in one place. The `resize_target_lb_input_weight` helper in changeless_waste.rs is removed; callers use `resize_bound` directly. `Waste::lb_with_change_waste` still has its own small `excess_with_drain > min_value` feasibility check inline; the helper handles the raw call. All 31 tests still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 20ef963 commit 8a984e3

3 files changed

Lines changed: 101 additions & 148 deletions

File tree

src/metrics.rs

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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, ChangePolicy, CoinSelector, Drain, Target};
3+
use crate::{bnb::BnbMetric, float::Ordf32, Candidate, ChangePolicy, CoinSelector, Drain, Target};
44
mod lowest_fee;
55
pub use lowest_fee::*;
66
mod changeless;
@@ -34,6 +34,97 @@ fn change_lower_bound(cs: &CoinSelector, target: Target, change_policy: ChangePo
3434
}
3535
}
3636

37+
/// Shortcut for `cs.excess(target, Drain { weights: change_policy.drain_weights, value: 0 })`.
38+
///
39+
/// This is the "would-be excess" if a change output were materialised — the same quantity
40+
/// `ChangePolicy::drain_value` compares against `min_value` to decide whether to add change.
41+
fn excess_with_drain(cs: &CoinSelector<'_>, target: Target, change_policy: ChangePolicy) -> i64 {
42+
cs.excess(
43+
target,
44+
Drain {
45+
weights: change_policy.drain_weights,
46+
value: 0,
47+
},
48+
)
49+
}
50+
51+
/// Shared "resize trick" used by every bound that needs a tight lower bound on the
52+
/// `input_weight` / `selected_value` of any target-meeting descendant `D ⊇ cs`.
53+
///
54+
/// Walks the `value_pwu`-sorted unselected list until the target is first crossed, then represents
55+
/// the crossing candidate as a fractional `scale ∈ [0, 1]` that would satisfy each fee constraint
56+
/// (rate, replacement, absolute) with exactly zero excess.
57+
///
58+
/// Returns `Some((cs_after_deselect, to_resize, scale))` where:
59+
/// - `cs_after_deselect` is the selection just before crossing target (the crossing input
60+
/// deselected).
61+
/// - `to_resize` is the candidate that crossed.
62+
/// - `scale ∈ [0, 1]`. `scale == 1.0` is the special case where the find result already hit
63+
/// target with zero excess — using the full candidate is equivalent to a perfect resize.
64+
///
65+
/// Callers compute their own metric value, e.g.
66+
/// ```ignore
67+
/// let ideal_fee = scale * to_resize.value as f32 + cs_after.selected_value() as f32 - target.value() as f32;
68+
/// let ideal_iw = cs_after.input_weight() as f32 + scale * to_resize.weight as f32;
69+
/// ```
70+
///
71+
/// Returns `None` if no target-meeting descendant exists (some fee constraint cannot be satisfied
72+
/// by any candidate available).
73+
fn resize_bound<'a>(
74+
cs: &CoinSelector<'a>,
75+
target: Target,
76+
) -> Option<(CoinSelector<'a>, Candidate, f32)> {
77+
let (mut cs_iter, resize_index, to_resize) = cs
78+
.clone()
79+
.select_iter()
80+
.find(|(c, _, _)| c.is_target_met(target))?;
81+
82+
// Exact-match special case: the find result hit target with zero excess. Treat as a
83+
// "perfect" full resize so the caller's `cs + scale * to_resize.X` formula recovers the
84+
// find-result quantities exactly.
85+
if cs_iter.excess(target, Drain::NONE) == 0 {
86+
cs_iter.deselect(resize_index);
87+
return Some((cs_iter, to_resize, 1.0));
88+
}
89+
cs_iter.deselect(resize_index);
90+
91+
let mut scale = 0.0_f32;
92+
93+
let rate_excess = cs_iter.rate_excess_wu(target, Drain::NONE) as f32;
94+
if rate_excess < 0.0 {
95+
let remaining = rate_excess.abs();
96+
let ev_resized = to_resize.effective_value(target.fee.rate);
97+
if ev_resized > 0.0 {
98+
scale = scale.max(remaining / ev_resized);
99+
} else {
100+
return None;
101+
}
102+
}
103+
if let Some(replace) = target.fee.replace {
104+
let replace_excess = cs_iter.replacement_excess_wu(target, Drain::NONE) as f32;
105+
if replace_excess < 0.0 {
106+
let remaining = replace_excess.abs();
107+
let ev_resized = to_resize.effective_value(replace.incremental_relay_feerate);
108+
if ev_resized > 0.0 {
109+
scale = scale.max(remaining / ev_resized);
110+
} else {
111+
return None;
112+
}
113+
}
114+
}
115+
let absolute_excess = cs_iter.absolute_excess(target, Drain::NONE) as f32;
116+
if absolute_excess < 0.0 {
117+
let remaining = absolute_excess.abs();
118+
if to_resize.value > 0 {
119+
scale = scale.max(remaining / to_resize.value as f32);
120+
} else {
121+
return None;
122+
}
123+
}
124+
125+
Some((cs_iter, to_resize, scale))
126+
}
127+
37128
macro_rules! impl_for_tuple {
38129
($($a:ident $b:tt)*) => {
39130
impl<$($a),*> BnbMetric for ($(($a, f32)),*)

src/metrics/changeless_waste.rs

Lines changed: 5 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::change_lower_bound;
1+
use super::{change_lower_bound, excess_with_drain, resize_bound};
22
use crate::{bnb::BnbMetric, float::Ordf32, ChangePolicy, CoinSelector, Drain, FeeRate, Target};
33
use alloc::vec::Vec;
44

@@ -71,64 +71,9 @@ impl BnbMetric for ChangelessWaste {
7171
if cs.is_target_met(self.target) {
7272
return Some(Ordf32(cs.input_weight() as f32 * rate_diff));
7373
}
74-
75-
// Target not met. Same resize trick as `LowestFee::bound`: walk the sorted unselected
76-
// list until we cross the target, then pretend the crossing input was perfectly
77-
// scaled so the target is hit with zero excess. Among all subsets of unselected that
78-
// reach target, the highest-`value_pwu` candidates are the most weight-efficient — so
79-
// the resize-scaled prefix is a valid lower bound on any target-meeting descendant's
80-
// input_weight.
81-
let (mut cs, resize_index, to_resize) = cs
82-
.clone()
83-
.select_iter()
84-
.find(|(cs, _, _)| cs.is_target_met(self.target))?;
85-
86-
if cs.excess(self.target, Drain::NONE) == 0 {
87-
return Some(Ordf32(cs.waste(
88-
self.target,
89-
self.long_term_feerate,
90-
Drain::NONE,
91-
1.0,
92-
)));
93-
}
94-
cs.deselect(resize_index);
95-
96-
let mut scale = Ordf32(0.0);
97-
98-
let rate_excess = cs.rate_excess_wu(self.target, Drain::NONE) as f32;
99-
if rate_excess < 0.0 {
100-
let remaining = rate_excess.abs();
101-
let ev_resized = to_resize.effective_value(self.target.fee.rate);
102-
if ev_resized > 0.0 {
103-
scale = scale.max(Ordf32(remaining / ev_resized));
104-
} else {
105-
return None;
106-
}
107-
}
108-
if let Some(replace) = self.target.fee.replace {
109-
let replace_excess = cs.replacement_excess_wu(self.target, Drain::NONE) as f32;
110-
if replace_excess < 0.0 {
111-
let remaining = replace_excess.abs();
112-
let ev_resized = to_resize.effective_value(replace.incremental_relay_feerate);
113-
if ev_resized > 0.0 {
114-
scale = scale.max(Ordf32(remaining / ev_resized));
115-
} else {
116-
return None;
117-
}
118-
}
119-
}
120-
let absolute_excess = cs.absolute_excess(self.target, Drain::NONE) as f32;
121-
if absolute_excess < 0.0 {
122-
let remaining = absolute_excess.abs();
123-
if to_resize.value > 0 {
124-
scale = scale.max(Ordf32(remaining / to_resize.value as f32));
125-
} else {
126-
return None;
127-
}
128-
}
129-
130-
let ideal_input_weight = cs.input_weight() as f32 + scale.0 * to_resize.weight as f32;
131-
Some(Ordf32(ideal_input_weight * rate_diff))
74+
let (cs_after, to_resize, scale) = resize_bound(cs, self.target)?;
75+
let ideal_iw = cs_after.input_weight() as f32 + scale * to_resize.weight as f32;
76+
Some(Ordf32(ideal_iw * rate_diff))
13277
}
13378

13479
fn requires_ordering_by_descending_value_pwu(&self) -> bool {
@@ -159,14 +104,7 @@ fn ub_changeless_input_weight(
159104
d_all.select_all();
160105
let d_all_iw = d_all.input_weight() as f32;
161106

162-
let excess_with_drain = d_all.excess(
163-
target,
164-
Drain {
165-
weights: change_policy.drain_weights,
166-
value: 0,
167-
},
168-
);
169-
let delta = excess_with_drain - change_policy.min_value as i64;
107+
let delta = excess_with_drain(&d_all, target, change_policy) - change_policy.min_value as i64;
170108
if delta <= 0 {
171109
return d_all_iw;
172110
}

src/metrics/lowest_fee.rs

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -110,88 +110,12 @@ impl BnbMetric for LowestFee {
110110

111111
Some(current_score)
112112
} else {
113-
// Step 1: select everything up until the input that hits the target.
114-
let (mut cs, resize_index, to_resize) = cs
115-
.clone()
116-
.select_iter()
117-
.find(|(cs, _, _)| cs.is_target_met(self.target))?;
118-
119-
// If this selection is already perfect, return its score directly.
120-
if cs.excess(self.target, Drain::NONE) == 0 {
121-
return Some(self.score(&cs).unwrap());
122-
};
123-
cs.deselect(resize_index);
124-
125-
// We need to find the minimum fee we'd pay if we satisfy the feerate constraint. We do
126-
// this by imagining we had a perfect input that perfectly hit the target. The sats per
127-
// weight unit of this perfect input is the one at `slurp_index` but we'll do a scaled
128-
// resize of it to fit perfectly.
129-
//
130-
// Here's the formaula:
131-
//
132-
// target_feerate = (current_input_value - current_output_value + scale * value_resized_input) / (current_weight + scale * weight_resized_input)
133-
//
134-
// Rearranging to find `scale` we find that:
135-
//
136-
// scale = remaining_value_to_reach_feerate / effective_value_of_resized_input
137-
//
138-
// This should be intutive since we're finding out how to scale the input we're resizing to get the effective value we need.
139-
//
140-
// In the perfect scenario, no additional fee would be required to pay for rounding up when converting from weight units to
141-
// vbytes and so all fee calculations below are performed on weight units directly.
142-
let rate_excess = cs.rate_excess_wu(self.target, Drain::NONE) as f32;
143-
let mut scale = Ordf32(0.0);
144-
145-
if rate_excess < 0.0 {
146-
let remaining_value_to_reach_feerate = rate_excess.abs();
147-
let effective_value_of_resized_input =
148-
to_resize.effective_value(self.target.fee.rate);
149-
if effective_value_of_resized_input > 0.0 {
150-
let feerate_scale =
151-
remaining_value_to_reach_feerate / effective_value_of_resized_input;
152-
scale = scale.max(Ordf32(feerate_scale));
153-
} else {
154-
return None; // we can never satisfy the constraint
155-
}
156-
}
157-
158-
// We can use the same approach for replacement we just have to use the
159-
// incremental_relay_feerate.
160-
if let Some(replace) = self.target.fee.replace {
161-
let replace_excess = cs.replacement_excess_wu(self.target, Drain::NONE) as f32;
162-
if replace_excess < 0.0 {
163-
let remaining_value_to_reach_feerate = replace_excess.abs();
164-
let effective_value_of_resized_input =
165-
to_resize.effective_value(replace.incremental_relay_feerate);
166-
if effective_value_of_resized_input > 0.0 {
167-
let replace_scale =
168-
remaining_value_to_reach_feerate / effective_value_of_resized_input;
169-
scale = scale.max(Ordf32(replace_scale));
170-
} else {
171-
return None; // we can never satisfy the constraint
172-
}
173-
}
174-
}
175-
// Handle absolute fee constraint. Unlike feerate and replacement, the
176-
// absolute fee is a fixed amount (not weight-proportional), so we just
177-
// need enough raw value to cover the gap.
178-
let absolute_excess = cs.absolute_excess(self.target, Drain::NONE) as f32;
179-
if absolute_excess < 0.0 {
180-
let remaining = absolute_excess.abs();
181-
if to_resize.value > 0 {
182-
let absolute_scale = remaining / to_resize.value as f32;
183-
scale = scale.max(Ordf32(absolute_scale));
184-
} else {
185-
return None; // we can never satisfy the constraint
186-
}
187-
}
188-
189-
// `scale` could be 0 even if `is_target_met` is `false` due to the latter being based on
190-
// rounded-up vbytes.
191-
let ideal_fee = scale.0 * to_resize.value as f32 + cs.selected_value() as f32
113+
// Target not met. Use the shared resize trick to compute the ideal fee assuming a
114+
// perfectly-scaled crossing input that hits the target with zero excess.
115+
let (cs_after, to_resize, scale) = super::resize_bound(cs, self.target)?;
116+
let ideal_fee = scale * to_resize.value as f32 + cs_after.selected_value() as f32
192117
- self.target.value() as f32;
193118
assert!(ideal_fee >= 0.0);
194-
195119
Some(Ordf32(ideal_fee))
196120
}
197121
}

0 commit comments

Comments
 (0)