@@ -27,8 +27,9 @@ proptest! {
2727 drain_spend_weight in 1 ..=2000_u32 , // drain spend weight (wu)
2828 drain_dust in 100 ..=1000_u64 , // drain dust (sats)
2929 n_drain_outputs in 1usize ..150 , // the number of drain outputs
30+ max_weight in common:: maybe_max_weight( 500u64 ..4_000 ) , // optional max tx weight cap (wu)
3031 ) {
31- let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs } ;
32+ let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs , max_weight } ;
3233 let candidates = common:: gen_candidates( params. n_candidates) ;
3334 let metric = params. lowest_fee_metric( ) ;
3435 common:: can_eventually_find_best_solution( params, candidates, metric) ?;
@@ -48,8 +49,9 @@ proptest! {
4849 drain_spend_weight in 1 ..=2000_u32 , // drain spend weight (wu)
4950 drain_dust in 100 ..=1000_u64 , // drain dust (sats)
5051 n_drain_outputs in 1usize ..150 , // the number of drain outputs
52+ max_weight in common:: maybe_max_weight( 500u64 ..4_000 ) , // optional max tx weight cap (wu)
5153 ) {
52- let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs } ;
54+ let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs , max_weight } ;
5355 let candidates = common:: gen_candidates( params. n_candidates) ;
5456 let metric = params. lowest_fee_metric( ) ;
5557 common:: ensure_bound_is_not_too_tight( params, candidates, metric) ?;
@@ -69,20 +71,25 @@ proptest! {
6971 drain_spend_weight in 1 ..=2000_u32 , // drain spend weight (wu)
7072 drain_dust in 100 ..=1000_u64 , // drain dust (sats)
7173 n_drain_outputs in 1usize ..150 , // the number of drain outputs
74+ // No `max_weight` here: `n` is too large for the exhaustive oracle, and this test's
75+ // impossibility check relies on the (value-only) `is_selection_possible`, so a weight cap
76+ // (which BnB could fail on while value is reachable) would break it. Cap handling is
77+ // covered by `bnb_respects_max_weight` and `can_eventually_find_best_solution`.
7278 ) {
7379 println!( "== TEST ==" ) ;
7480
75- let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs } ;
81+ let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs, max_weight : None } ;
7682 println!( "{:?}" , params) ;
7783
78- let candidates = core:: iter:: repeat( Candidate {
84+ let candidates = vec![
85+ Candidate {
7986 value: 20_000 ,
8087 weight: ( 32 + 4 + 4 + 1 ) * 4 + 64 + 32 ,
8188 input_count: 1 ,
8289 is_segwit: true ,
83- } )
84- . take ( params. n_candidates)
85- . collect :: < Vec <_>> ( ) ;
90+ } ;
91+ params. n_candidates
92+ ] ;
8693
8794 let mut cs = CoinSelector :: new( & candidates) ;
8895
@@ -101,7 +108,9 @@ proptest! {
101108 #[ test]
102109 #[ cfg( not( debug_assertions) ) ] // too slow if compiling for debug
103110 fn compare_against_benchmarks(
104- n_candidates in 0 ..50_usize , // candidates (n)
111+ // `n` is kept small: the no-solution branch asserts against `exact_selection_possible`,
112+ // an exhaustive O(2^n) oracle (needed because it must be exact w.r.t. the weight cap).
113+ n_candidates in 0 ..16_usize , // candidates (n)
105114 target_value in 500 ..1_000_000_u64 , // target value (sats)
106115 n_target_outputs in 1usize ..150 , // the number of outputs we're funding
107116 target_weight in 0 ..10_000_u32 , // the sum of the weight of the outputs (wu)
@@ -112,15 +121,59 @@ proptest! {
112121 drain_spend_weight in 1 ..=2000_u32 , // drain spend weight (wu)
113122 drain_dust in 100 ..=1000_u64 , // drain dust (sats)
114123 n_drain_outputs in 1usize ..150 , // the number of drain outputs
124+ max_weight in common:: maybe_max_weight( 500u64 ..4_000 ) , // optional max tx weight cap (wu)
115125 ) {
116126
117- let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs } ;
127+ let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs , max_weight } ;
118128 let candidates = common:: gen_candidates( params. n_candidates) ;
119129 let metric = params. lowest_fee_metric( ) ;
120130 common:: compare_against_benchmarks( params, candidates, metric) ?;
121131 }
122132}
123133
134+ proptest ! {
135+ // Cheap cases (small n), so run many more than the default to stress the max_weight prune.
136+ #![ proptest_config( ProptestConfig { cases: 512 , ..Default :: default ( ) } ) ]
137+
138+ /// Cross-check `max_weight` handling against an exact, exhaustive feasibility oracle.
139+ ///
140+ /// BnB with unlimited rounds is itself an exact feasibility detector (nothing is pruned before
141+ /// the first incumbent; the only pre-incumbent prune is the weight hard-prune). So
142+ /// `bnb_found == exact_possible` must hold — a mismatch means the prune dropped a feasible
143+ /// subtree. `n` is kept small because the exact oracle is exponential.
144+ #[ test]
145+ #[ cfg( not( debug_assertions) ) ] // too slow if compiling for debug
146+ fn bnb_respects_max_weight(
147+ n_candidates in 1 ..12_usize ,
148+ target_value in 500 ..500_000_u64 ,
149+ n_target_outputs in 1usize ..150 ,
150+ target_weight in 0 ..10_000_u32 ,
151+ replace in common:: maybe_replace( 0u64 ..10_000 ) ,
152+ feerate in 1.0 ..100.0_f32 ,
153+ feerate_lt_diff in -5.0 ..50.0_f32 ,
154+ drain_weight in 100 ..=500_u32 ,
155+ drain_spend_weight in 1 ..=2000_u32 ,
156+ drain_dust in 100 ..=1000_u64 ,
157+ n_drain_outputs in 1usize ..150 ,
158+ max_weight in common:: maybe_max_weight( 500u64 ..4_000 ) , // TRUC-tight -> binds often, small DP
159+ ) {
160+ let params = common:: StrategyParams { n_candidates, target_value, n_target_outputs, target_weight, replace, feerate, feerate_lt_diff, drain_weight, drain_spend_weight, drain_dust, n_drain_outputs, max_weight } ;
161+ let candidates = common:: gen_candidates( params. n_candidates) ;
162+ let target = params. target( ) ;
163+ let metric = params. lowest_fee_metric( ) ;
164+
165+ let exact_possible = common:: exact_selection_possible( & CoinSelector :: new( & candidates) , target) ;
166+
167+ let mut cs = CoinSelector :: new( & candidates) ;
168+ let bnb_found = common:: bnb_search( & mut cs, target, metric, usize :: MAX ) . is_ok( ) ;
169+ prop_assert_eq!(
170+ bnb_found, exact_possible,
171+ "bnb_found={} but exact_possible={} (weight prune may have dropped a feasible subtree)" ,
172+ bnb_found, exact_possible
173+ ) ;
174+ }
175+ }
176+
124177/// We wrap `LowestFee` in `Changeless` to derive a metric that finds the lowest-fee changeless
125178/// solution. Constraining to changeless should never take fewer rounds than the unconstrained
126179/// `LowestFee`.
@@ -138,6 +191,7 @@ fn combined_changeless_metric() {
138191 drain_dust : 200 ,
139192 n_target_outputs : 1 ,
140193 n_drain_outputs : 1 ,
194+ max_weight : None ,
141195 } ;
142196
143197 let candidates = common:: gen_candidates ( params. n_candidates ) ;
@@ -177,6 +231,7 @@ fn does_not_create_change_below_spend_cost() {
177231 weight_sum : 200 - TX_FIXED_FIELD_WEIGHT - 1 ,
178232 n_outputs : 1 ,
179233 } ,
234+ max_weight : None ,
180235 } ;
181236
182237 let candidates = vec ! [
@@ -259,6 +314,7 @@ fn zero_fee_tx() {
259314 weight_sum : 200 - TX_FIXED_FIELD_WEIGHT - 1 ,
260315 n_outputs : 1 ,
261316 } ,
317+ max_weight : None ,
262318 } ;
263319
264320 let candidates = vec ! [
0 commit comments