11use super :: change_lower_bound;
22use crate :: { bnb:: BnbMetric , float:: Ordf32 , ChangePolicy , CoinSelector , Drain , FeeRate , Target } ;
3+ use alloc:: vec:: Vec ;
34
45/// Metric that minimizes the [waste metric] subject to the constraint that the selection produces
56/// no change output.
@@ -54,37 +55,34 @@ impl BnbMetric for ChangelessWaste {
5455 // score(D) = D.input_weight * rate_diff + max(0, D.excess)
5556 //
5657 // and `D.excess >= 0` (target met), so `score(D) >= D.input_weight * rate_diff`. The
57- // bound therefore reduces to finding a lower bound on `D.input_weight * rate_diff` .
58+ // bound therefore reduces to bounding `D.input_weight` in the right direction .
5859
5960 if rate_diff < 0.0 {
60- // rate_diff < 0: the most negative `D.input_weight * rate_diff` comes from the
61- // largest possible input_weight, which is bounded by selecting every candidate.
62- // (`D` need not actually be feasible — we only need an LB on its score.)
63- let mut all = cs. clone ( ) ;
64- all. select_all ( ) ;
65- return Some ( Ordf32 ( all. input_weight ( ) as f32 * rate_diff) ) ;
61+ // rate_diff < 0: we want an UPPER bound on `D.input_weight`. `all_selected` is a
62+ // safe but loose UB; we tighten by LP-relaxed knapsack over candidates that
63+ // *must* be excluded to keep the selection changeless.
64+ let ub = ub_changeless_input_weight ( cs, self . target , self . change_policy ) ;
65+ return Some ( Ordf32 ( ub * rate_diff) ) ;
6666 }
6767
68- // rate_diff >= 0: smaller input_weight gives a smaller ` input_weight * rate_diff`, so we
69- // want a lower bound on `D.input_weight`. `D.input_weight >= cs.input_weight` always
70- // (selecting only grows), but we can do much better when the target is not yet met.
68+ // rate_diff >= 0: we want a LOWER bound on `D. input_weight`. `cs.input_weight` is a
69+ // safe baseline (input_weight is monotone non-decreasing). Tighten with the resize
70+ // trick when target is not yet met.
7171 if cs. is_target_met ( self . target ) {
7272 return Some ( Ordf32 ( cs. input_weight ( ) as f32 * rate_diff) ) ;
7373 }
7474
75- // Target not met. Use the same resize trick as `LowestFee::bound`: walk the sorted
76- // unselected list until we cross the target, then pretend the crossing input was
77- // perfectly scaled so that the target is hit with zero excess. Among all subsets of
78- // unselected that reach target, the highest-`value_pwu` candidates are the most
79- // weight-efficient — so the resize-scaled prefix is a valid lower bound on any
80- // target-meeting descendant's input_weight.
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.
8181 let ( mut cs, resize_index, to_resize) = cs
8282 . clone ( )
8383 . select_iter ( )
8484 . find ( |( cs, _, _) | cs. is_target_met ( self . target ) ) ?;
8585
86- // If the find selection already hits target exactly, that's the minimum-weight
87- // target-meeting subset; the bound is its waste (with `Drain::NONE`).
8886 if cs. excess ( self . target , Drain :: NONE ) == 0 {
8987 return Some ( Ordf32 ( cs. waste (
9088 self . target ,
@@ -95,7 +93,6 @@ impl BnbMetric for ChangelessWaste {
9593 }
9694 cs. deselect ( resize_index) ;
9795
98- // Compute the smallest `scale` of `to_resize` that satisfies each fee constraint.
9996 let mut scale = Ordf32 ( 0.0 ) ;
10097
10198 let rate_excess = cs. rate_excess_wu ( self . target , Drain :: NONE ) as f32 ;
@@ -138,3 +135,77 @@ impl BnbMetric for ChangelessWaste {
138135 true
139136 }
140137}
138+
139+ /// LP-relaxed upper bound on `D.input_weight` for changeless `D ⊇ cs` (used by the
140+ /// `rate_diff < 0` branch of `ChangelessWaste::bound`).
141+ ///
142+ /// Construct `D_all = cs ∪ all unselected`. If `D_all` itself is changeless, the UB is
143+ /// `D_all.input_weight`. Otherwise we must exclude enough excess-contributing
144+ /// (positive-`ev_feerate`) candidates to drop `excess(drain_weights)` below
145+ /// `change_policy.min_value`. To MAXIMIZE the remaining `input_weight` we MINIMIZE the
146+ /// excluded weight, sorting positive-`ev_feerate` candidates by `ev_feerate / weight`
147+ /// descending and removing fractionally until the required `delta` is met.
148+ ///
149+ /// The LP relaxation gives a value `>=` any integer solution's excluded weight, so
150+ /// `D_all.input_weight - LP_min` is a safe UB for any feasible `D.input_weight`. The
151+ /// `input_weight()` segwit/varint corrections only ever ADD weight to the parent, never
152+ /// subtract from a subset — so the additive subtraction is safe in the UB direction.
153+ fn ub_changeless_input_weight (
154+ cs : & CoinSelector < ' _ > ,
155+ target : Target ,
156+ change_policy : ChangePolicy ,
157+ ) -> f32 {
158+ let mut d_all = cs. clone ( ) ;
159+ d_all. select_all ( ) ;
160+ let d_all_iw = d_all. input_weight ( ) as f32 ;
161+
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 ;
170+ if delta <= 0 {
171+ return d_all_iw;
172+ }
173+ let mut remaining = delta as f32 ;
174+
175+ let mut pos: Vec < ( f32 , f32 ) > = cs
176+ . unselected ( )
177+ . filter_map ( |( _, c) | {
178+ let ev = c. effective_value ( target. fee . rate ) ;
179+ if ev > 0.0 {
180+ Some ( ( ev, c. weight as f32 ) )
181+ } else {
182+ None
183+ }
184+ } )
185+ . collect ( ) ;
186+ pos. sort_by ( |a, b| {
187+ let r_a = a. 0 / a. 1 ;
188+ let r_b = b. 0 / b. 1 ;
189+ r_b. partial_cmp ( & r_a) . unwrap_or ( core:: cmp:: Ordering :: Equal )
190+ } ) ;
191+
192+ let mut removed_weight = 0.0_f32 ;
193+ for ( ev, w) in pos {
194+ if remaining <= 0.0 {
195+ break ;
196+ }
197+ if ev >= remaining {
198+ removed_weight += w * ( remaining / ev) ;
199+ remaining = 0.0 ;
200+ } else {
201+ removed_weight += w;
202+ remaining -= ev;
203+ }
204+ }
205+ if remaining > 0.0 {
206+ // Unreachable when `change_lower_bound = None` (which we already checked). Fall back
207+ // to the loose `D_all`-based bound rather than fabricating a tight one.
208+ return d_all_iw;
209+ }
210+ d_all_iw - removed_weight
211+ }
0 commit comments