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