11use crate :: { bnb:: BnbMetric , float:: Ordf32 , CoinSelector , Drain , DrainWeights , FeeRate , Target } ;
2+ use alloc:: vec:: Vec ;
23
34/// Metric that minimizes the [waste metric] subject to the constraint that the selection produces
45/// no change output.
@@ -41,34 +42,60 @@ impl ChangelessWaste {
4142 ///
4243 /// [`LowestFee`]: crate::metrics::LowestFee
4344 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- ) ;
45+ let excess_with_drain_weight = self . excess_with_drain_weight ( cs, target) ;
5346
5447 // Adding change is only worth it if the value we'd recover exceeds the future cost of
5548 // 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 {
49+ if excess_with_drain_weight <= self . drain_spend_cost ( ) as i64 {
6050 return None ;
6151 }
6252
6353 // ...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 {
54+ if excess_with_drain_weight < self . dust_threshold ( ) as i64 {
6655 return None ;
6756 }
6857
6958 Some ( excess_with_drain_weight. unsigned_abs ( ) )
7059 }
7160
61+ /// The excess of `cs` after accounting for the weight (but not value) of a would-be change
62+ /// output. This is the quantity the change decision (see [`drain_value`]) is made on.
63+ ///
64+ /// [`drain_value`]: Self::drain_value
65+ fn excess_with_drain_weight ( & self , cs : & CoinSelector < ' _ > , target : Target ) -> i64 {
66+ // The change output pays for its own weight, so the value we'd actually recover is the
67+ // excess remaining after accounting for that weight.
68+ cs. excess (
69+ target,
70+ Drain {
71+ weights : self . drain_weights ,
72+ value : 0 ,
73+ } ,
74+ )
75+ }
76+
77+ /// The future fee of spending a would-be change output. Change below this is never worthwhile.
78+ fn drain_spend_cost ( & self ) -> u64 {
79+ self . long_term_feerate
80+ . implied_fee_wu ( self . drain_weights . spend_weight )
81+ }
82+
83+ /// The dust threshold of a would-be change output. Change below this is never created.
84+ fn dust_threshold ( & self ) -> u64 {
85+ self . drain_weights . dust_threshold ( self . dust_relay_feerate )
86+ }
87+
88+ /// The largest `excess_with_drain_weight` for which a selection is still changeless.
89+ ///
90+ /// A selection is changeless (see [`drain_value`]) when its excess is `<= drain_spend_cost` OR
91+ /// `< dust_threshold`. The union of those two regions is `excess <= max(drain_spend_cost,
92+ /// dust_threshold - 1)`, so this is the inclusive upper edge of the changeless region.
93+ ///
94+ /// [`drain_value`]: Self::drain_value
95+ fn changeless_max_excess ( & self ) -> i64 {
96+ ( self . drain_spend_cost ( ) as i64 ) . max ( self . dust_threshold ( ) as i64 - 1 )
97+ }
98+
7299 /// Whether every selection reachable down this branch (the current one and any superset of it)
73100 /// would have a change output — so no changeless solution exists here and the branch can be
74101 /// pruned.
@@ -98,6 +125,72 @@ impl ChangelessWaste {
98125
99126 self . drain_value ( & least_excess, target) . is_some ( )
100127 }
128+
129+ /// LP-relaxed upper bound on `D.input_weight` for changeless `D ⊇ cs` (used by the
130+ /// `rate_diff < 0` branch of [`bound`]).
131+ ///
132+ /// Construct `D_all = cs ∪ all unselected`. If `D_all` itself is changeless, the UB is
133+ /// `D_all.input_weight`. Otherwise we must exclude enough excess-contributing
134+ /// (positive-`effective_value`) candidates to drop `excess_with_drain_weight` down to
135+ /// [`changeless_max_excess`]. To MAXIMIZE the remaining `input_weight` we MINIMIZE the excluded
136+ /// weight, sorting positive-`ev` candidates by `ev / weight` descending and removing
137+ /// fractionally until the required `delta` is met.
138+ ///
139+ /// The LP relaxation gives a value `>=` any integer solution's excluded weight, so
140+ /// `D_all.input_weight - LP_min` is a safe UB for any feasible `D.input_weight`. The
141+ /// `input_weight()` segwit/varint corrections only ever ADD weight to the parent, never
142+ /// subtract from a subset — so the additive subtraction is safe in the UB direction.
143+ ///
144+ /// [`bound`]: BnbMetric::bound
145+ /// [`changeless_max_excess`]: Self::changeless_max_excess
146+ fn ub_changeless_input_weight ( & self , cs : & CoinSelector < ' _ > , target : Target ) -> f32 {
147+ let mut d_all = cs. clone ( ) ;
148+ d_all. select_all ( ) ;
149+ let d_all_iw = d_all. input_weight ( ) as f32 ;
150+
151+ let delta = self . excess_with_drain_weight ( & d_all, target) - self . changeless_max_excess ( ) ;
152+ if delta <= 0 {
153+ return d_all_iw;
154+ }
155+ let mut remaining = delta as f32 ;
156+
157+ let mut pos: Vec < ( f32 , f32 ) > = cs
158+ . unselected ( )
159+ . filter_map ( |( _, c) | {
160+ let ev = c. effective_value ( target. fee . rate ) ;
161+ if ev > 0.0 {
162+ Some ( ( ev, c. weight as f32 ) )
163+ } else {
164+ None
165+ }
166+ } )
167+ . collect ( ) ;
168+ pos. sort_by ( |a, b| {
169+ let r_a = a. 0 / a. 1 ;
170+ let r_b = b. 0 / b. 1 ;
171+ r_b. partial_cmp ( & r_a) . unwrap_or ( core:: cmp:: Ordering :: Equal )
172+ } ) ;
173+
174+ let mut removed_weight = 0.0_f32 ;
175+ for ( ev, w) in pos {
176+ if remaining <= 0.0 {
177+ break ;
178+ }
179+ if ev >= remaining {
180+ removed_weight += w * ( remaining / ev) ;
181+ remaining = 0.0 ;
182+ } else {
183+ removed_weight += w;
184+ remaining -= ev;
185+ }
186+ }
187+ if remaining > 0.0 {
188+ // Unreachable when `change_unavoidable = false` (which the caller already checked).
189+ // Fall back to the loose `D_all`-based bound rather than fabricating a tight one.
190+ return d_all_iw;
191+ }
192+ d_all_iw - removed_weight
193+ }
101194}
102195
103196impl BnbMetric for ChangelessWaste {
@@ -130,37 +223,34 @@ impl BnbMetric for ChangelessWaste {
130223 // score(D) = D.input_weight * rate_diff + max(0, D.excess)
131224 //
132225 // 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` .
226+ // bound therefore reduces to bounding `D.input_weight` in the right direction .
134227
135228 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) ) ;
229+ // rate_diff < 0: we want an UPPER bound on `D.input_weight`. `all_selected` is a
230+ // safe but loose UB; we tighten by LP-relaxed knapsack over candidates that
231+ // *must* be excluded to keep the selection changeless.
232+ let ub = self . ub_changeless_input_weight ( cs, target) ;
233+ return Some ( Ordf32 ( ub * rate_diff) ) ;
142234 }
143235
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.
236+ // rate_diff >= 0: we want a LOWER bound on `D. input_weight`. `cs.input_weight` is a
237+ // safe baseline (input_weight is monotone non-decreasing). Tighten with the resize
238+ // trick when target is not yet met.
147239 if cs. is_target_met ( target) {
148240 return Some ( Ordf32 ( cs. input_weight ( ) as f32 * rate_diff) ) ;
149241 }
150242
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.
243+ // Target not met. Same resize trick as `LowestFee::bound`: walk the sorted unselected
244+ // list until we cross the target, then pretend the crossing input was perfectly
245+ // scaled so the target is hit with zero excess. Among all subsets of unselected that
246+ // reach target, the highest-`value_pwu` candidates are the most weight-efficient — so
247+ // the resize-scaled prefix is a valid lower bound on any target-meeting descendant's
248+ // input_weight.
157249 let ( mut cs, resize_index, to_resize) = cs
158250 . clone ( )
159251 . select_iter ( )
160252 . find ( |( cs, _, _) | cs. is_target_met ( target) ) ?;
161253
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`).
164254 if cs. excess ( target, Drain :: NONE ) == 0 {
165255 return Some ( Ordf32 ( cs. waste (
166256 target,
@@ -171,7 +261,6 @@ impl BnbMetric for ChangelessWaste {
171261 }
172262 cs. deselect ( resize_index) ;
173263
174- // Compute the smallest `scale` of `to_resize` that satisfies each fee constraint.
175264 let mut scale = Ordf32 ( 0.0 ) ;
176265
177266 let rate_excess = cs. rate_excess_wu ( target, Drain :: NONE ) as f32 ;
0 commit comments