|
| 1 | +use crate::{bnb::BnbMetric, float::Ordf32, CoinSelector, Drain, DrainWeights, FeeRate, Target}; |
| 2 | + |
| 3 | +/// Metric that minimizes the [waste metric] subject to the constraint that the selection produces |
| 4 | +/// no change output. |
| 5 | +/// |
| 6 | +/// For a changeless selection, waste reduces to: |
| 7 | +/// |
| 8 | +/// > `input_weight * (feerate - long_term_feerate) + max(0, excess)` |
| 9 | +/// |
| 10 | +/// Excess in a changeless transaction goes to the miner as fees and is therefore fully counted as |
| 11 | +/// waste. |
| 12 | +/// |
| 13 | +/// Restricting to changeless solutions removes the non-monotonic discontinuity that the general |
| 14 | +/// (with-change) waste metric has when an input flips the change output on or off, which makes a |
| 15 | +/// correct bound much easier to construct. |
| 16 | +/// |
| 17 | +/// Like [`LowestFee`], `ChangelessWaste` decides for itself whether a selection *would* have a |
| 18 | +/// change output (using the same rule: change is worthwhile when the recovered excess outweighs the |
| 19 | +/// future cost of spending it and clears the dust threshold). Selections that would have change are |
| 20 | +/// rejected, so only genuinely changeless selections are scored. |
| 21 | +/// |
| 22 | +/// [waste metric]: https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection |
| 23 | +/// [`LowestFee`]: crate::metrics::LowestFee |
| 24 | +#[derive(Clone, Copy, Debug)] |
| 25 | +pub struct ChangelessWaste { |
| 26 | + /// The estimated feerate needed to spend a change output later. This is used by the metric |
| 27 | + /// even though the scored selections do not have a change output — the long-term feerate |
| 28 | + /// defines the `feerate - long_term_feerate` weight cost of each input. |
| 29 | + pub long_term_feerate: FeeRate, |
| 30 | + /// The feerate used to determine the dust threshold of the change output. |
| 31 | + pub dust_relay_feerate: FeeRate, |
| 32 | + /// The weights of the change output that would be added. |
| 33 | + pub drain_weights: DrainWeights, |
| 34 | +} |
| 35 | + |
| 36 | +impl ChangelessWaste { |
| 37 | + /// The value the change output would have, or `None` if this selection should be changeless. |
| 38 | + /// |
| 39 | + /// This is the same change decision as [`LowestFee`]: the metric owns its change policy instead |
| 40 | + /// of taking one as input. |
| 41 | + /// |
| 42 | + /// [`LowestFee`]: crate::metrics::LowestFee |
| 43 | + fn drain_value(&self, cs: &CoinSelector<'_>, target: Target) -> Option<u64> { |
| 44 | + // The change output pays for its own weight, so the value we'd actually recover is the |
| 45 | + // excess remaining after accounting for that weight. |
| 46 | + let excess_with_drain_weight = cs.excess( |
| 47 | + target, |
| 48 | + Drain { |
| 49 | + weights: self.drain_weights, |
| 50 | + value: 0, |
| 51 | + }, |
| 52 | + ); |
| 53 | + |
| 54 | + // Adding change is only worth it if the value we'd recover exceeds the future cost of |
| 55 | + // spending it (i.e. it lowers the long-term fee). |
| 56 | + let drain_spend_cost = self |
| 57 | + .long_term_feerate |
| 58 | + .implied_fee_wu(self.drain_weights.spend_weight); |
| 59 | + if excess_with_drain_weight <= drain_spend_cost as i64 { |
| 60 | + return None; |
| 61 | + } |
| 62 | + |
| 63 | + // ...and only if the change output would not be dust. |
| 64 | + let dust_threshold = self.drain_weights.dust_threshold(self.dust_relay_feerate); |
| 65 | + if excess_with_drain_weight < dust_threshold as i64 { |
| 66 | + return None; |
| 67 | + } |
| 68 | + |
| 69 | + Some(excess_with_drain_weight.unsigned_abs()) |
| 70 | + } |
| 71 | + |
| 72 | + /// Whether every selection reachable down this branch (the current one and any superset of it) |
| 73 | + /// would have a change output — so no changeless solution exists here and the branch can be |
| 74 | + /// pruned. |
| 75 | + /// |
| 76 | + /// The change decision is assumed monotone in the excess (see [`drain_value`]), so the |
| 77 | + /// reachable selection least likely to have change is the one with the smallest excess: the |
| 78 | + /// current selection plus every remaining negative-effective-value candidate. If even that |
| 79 | + /// selection still has change, then so does every reachable selection. |
| 80 | + /// |
| 81 | + /// NOTE: this relies on candidates being sorted so that all negative effective value candidates |
| 82 | + /// are next to each other, which [`requires_ordering_by_descending_value_pwu`] guarantees. |
| 83 | + /// |
| 84 | + /// [`drain_value`]: Self::drain_value |
| 85 | + /// [`requires_ordering_by_descending_value_pwu`]: BnbMetric::requires_ordering_by_descending_value_pwu |
| 86 | + fn change_unavoidable(&mut self, cs: &CoinSelector<'_>, target: Target) -> bool { |
| 87 | + if self.drain_value(cs, target).is_none() { |
| 88 | + return false; |
| 89 | + } |
| 90 | + |
| 91 | + let mut least_excess = cs.clone(); |
| 92 | + cs.unselected() |
| 93 | + .rev() |
| 94 | + .take_while(|(_, wv)| wv.effective_value(target.fee.rate) < 0.0) |
| 95 | + .for_each(|(index, _)| { |
| 96 | + least_excess.select(index); |
| 97 | + }); |
| 98 | + |
| 99 | + self.drain_value(&least_excess, target).is_some() |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +impl BnbMetric for ChangelessWaste { |
| 104 | + fn drain(&mut self, _cs: &CoinSelector<'_>, _target: Target) -> Drain { |
| 105 | + // By definition a changeless selection never has a change output. |
| 106 | + Drain::NONE |
| 107 | + } |
| 108 | + |
| 109 | + fn score(&mut self, cs: &CoinSelector<'_>, target: Target) -> Option<Ordf32> { |
| 110 | + if !cs.is_target_met(target) { |
| 111 | + return None; |
| 112 | + } |
| 113 | + // Reject selections that would have change — this metric only scores changeless solutions. |
| 114 | + if self.drain_value(cs, target).is_some() { |
| 115 | + return None; |
| 116 | + } |
| 117 | + let waste = cs.waste(target, self.long_term_feerate, Drain::NONE, 1.0); |
| 118 | + Some(Ordf32(waste)) |
| 119 | + } |
| 120 | + |
| 121 | + fn bound(&mut self, cs: &CoinSelector<'_>, target: Target) -> Option<Ordf32> { |
| 122 | + // Prune branches where every descendant is forced to have a change output. |
| 123 | + if self.change_unavoidable(cs, target) { |
| 124 | + return None; |
| 125 | + } |
| 126 | + |
| 127 | + let rate_diff = target.fee.rate.spwu() - self.long_term_feerate.spwu(); |
| 128 | + |
| 129 | + // For any changeless target-meeting descendant D ⊇ cs: |
| 130 | + // score(D) = D.input_weight * rate_diff + max(0, D.excess) |
| 131 | + // |
| 132 | + // and `D.excess >= 0` (target met), so `score(D) >= D.input_weight * rate_diff`. The |
| 133 | + // bound therefore reduces to finding a lower bound on `D.input_weight * rate_diff`. |
| 134 | + |
| 135 | + if rate_diff < 0.0 { |
| 136 | + // rate_diff < 0: the most negative `D.input_weight * rate_diff` comes from the |
| 137 | + // largest possible input_weight, which is bounded by selecting every candidate. |
| 138 | + // (`D` need not actually be feasible — we only need an LB on its score.) |
| 139 | + let mut all = cs.clone(); |
| 140 | + all.select_all(); |
| 141 | + return Some(Ordf32(all.input_weight() as f32 * rate_diff)); |
| 142 | + } |
| 143 | + |
| 144 | + // rate_diff >= 0: smaller input_weight gives a smaller `input_weight * rate_diff`, so we |
| 145 | + // want a lower bound on `D.input_weight`. `D.input_weight >= cs.input_weight` always |
| 146 | + // (selecting only grows), but we can do much better when the target is not yet met. |
| 147 | + if cs.is_target_met(target) { |
| 148 | + return Some(Ordf32(cs.input_weight() as f32 * rate_diff)); |
| 149 | + } |
| 150 | + |
| 151 | + // Target not met. Use the same resize trick as `LowestFee::bound`: walk the sorted |
| 152 | + // unselected list until we cross the target, then pretend the crossing input was |
| 153 | + // perfectly scaled so that the target is hit with zero excess. Among all subsets of |
| 154 | + // unselected that reach target, the highest-`value_pwu` candidates are the most |
| 155 | + // weight-efficient — so the resize-scaled prefix is a valid lower bound on any |
| 156 | + // target-meeting descendant's input_weight. |
| 157 | + let (mut cs, resize_index, to_resize) = cs |
| 158 | + .clone() |
| 159 | + .select_iter() |
| 160 | + .find(|(cs, _, _)| cs.is_target_met(target))?; |
| 161 | + |
| 162 | + // If the find selection already hits target exactly, that's the minimum-weight |
| 163 | + // target-meeting subset; the bound is its waste (with `Drain::NONE`). |
| 164 | + if cs.excess(target, Drain::NONE) == 0 { |
| 165 | + return Some(Ordf32(cs.waste( |
| 166 | + target, |
| 167 | + self.long_term_feerate, |
| 168 | + Drain::NONE, |
| 169 | + 1.0, |
| 170 | + ))); |
| 171 | + } |
| 172 | + cs.deselect(resize_index); |
| 173 | + |
| 174 | + // Compute the smallest `scale` of `to_resize` that satisfies each fee constraint. |
| 175 | + let mut scale = Ordf32(0.0); |
| 176 | + |
| 177 | + let rate_excess = cs.rate_excess_wu(target, Drain::NONE) as f32; |
| 178 | + if rate_excess < 0.0 { |
| 179 | + let remaining = rate_excess.abs(); |
| 180 | + let ev_resized = to_resize.effective_value(target.fee.rate); |
| 181 | + if ev_resized > 0.0 { |
| 182 | + scale = scale.max(Ordf32(remaining / ev_resized)); |
| 183 | + } else { |
| 184 | + return None; |
| 185 | + } |
| 186 | + } |
| 187 | + if let Some(replace) = target.fee.replace { |
| 188 | + let replace_excess = cs.replacement_excess_wu(target, Drain::NONE) as f32; |
| 189 | + if replace_excess < 0.0 { |
| 190 | + let remaining = replace_excess.abs(); |
| 191 | + let ev_resized = to_resize.effective_value(replace.incremental_relay_feerate); |
| 192 | + if ev_resized > 0.0 { |
| 193 | + scale = scale.max(Ordf32(remaining / ev_resized)); |
| 194 | + } else { |
| 195 | + return None; |
| 196 | + } |
| 197 | + } |
| 198 | + } |
| 199 | + let absolute_excess = cs.absolute_excess(target, Drain::NONE) as f32; |
| 200 | + if absolute_excess < 0.0 { |
| 201 | + let remaining = absolute_excess.abs(); |
| 202 | + if to_resize.value > 0 { |
| 203 | + scale = scale.max(Ordf32(remaining / to_resize.value as f32)); |
| 204 | + } else { |
| 205 | + return None; |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + let ideal_input_weight = cs.input_weight() as f32 + scale.0 * to_resize.weight as f32; |
| 210 | + Some(Ordf32(ideal_input_weight * rate_diff)) |
| 211 | + } |
| 212 | + |
| 213 | + fn requires_ordering_by_descending_value_pwu(&self) -> bool { |
| 214 | + true |
| 215 | + } |
| 216 | +} |
0 commit comments