@@ -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( 2_000u64 ..30_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( 2_000u64 ..30_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,10 +71,11 @@ 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+ max_weight in common:: maybe_max_weight( 2_000u64 ..30_000 ) , // optional max tx weight cap (wu)
7275 ) {
7376 println!( "== TEST ==" ) ;
7477
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 } ;
78+ 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 } ;
7679 println!( "{:?}" , params) ;
7780
7881 let candidates = core:: iter:: repeat( Candidate {
@@ -112,15 +115,64 @@ proptest! {
112115 drain_spend_weight in 1 ..=2000_u32 , // drain spend weight (wu)
113116 drain_dust in 100 ..=1000_u64 , // drain dust (sats)
114117 n_drain_outputs in 1usize ..150 , // the number of drain outputs
118+ max_weight in common:: maybe_max_weight( 2_000u64 ..30_000 ) , // optional max tx weight cap (wu)
115119 ) {
116120
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 } ;
121+ 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 } ;
118122 let candidates = common:: gen_candidates( params. n_candidates) ;
119123 let metric = params. lowest_fee_metric( ) ;
120124 common:: compare_against_benchmarks( params, candidates, metric) ?;
121125 }
122126}
123127
128+ proptest ! {
129+ // Cheap cases (small n), so run many more than the default to stress the max_weight prune.
130+ #![ proptest_config( ProptestConfig { cases: 4096 , ..Default :: default ( ) } ) ]
131+
132+ /// Cross-check `max_weight` handling against an exact, exhaustive feasibility oracle.
133+ ///
134+ /// BnB with unlimited rounds is itself an exact feasibility detector (nothing is pruned before
135+ /// the first incumbent; the only pre-incumbent prune is the weight hard-prune). So
136+ /// `bnb_found == exact_possible` must hold — a mismatch means the prune dropped a feasible
137+ /// subtree. `n` is kept small because the exact oracle is exponential.
138+ #[ test]
139+ #[ cfg( not( debug_assertions) ) ] // too slow if compiling for debug
140+ fn bnb_respects_max_weight(
141+ n_candidates in 1 ..16_usize ,
142+ target_value in 500 ..500_000_u64 ,
143+ n_target_outputs in 1usize ..150 ,
144+ target_weight in 0 ..10_000_u32 ,
145+ replace in common:: maybe_replace( 0u64 ..10_000 ) ,
146+ feerate in 1.0 ..100.0_f32 ,
147+ feerate_lt_diff in -5.0 ..50.0_f32 ,
148+ drain_weight in 100 ..=500_u32 ,
149+ drain_spend_weight in 1 ..=2000_u32 ,
150+ drain_dust in 100 ..=1000_u64 ,
151+ n_drain_outputs in 1usize ..150 ,
152+ max_weight in common:: maybe_max_weight( 2_000u64 ..15_000 ) , // tighter -> binds often
153+ ) {
154+ 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 } ;
155+ let candidates = common:: gen_candidates( params. n_candidates) ;
156+ let target = params. target( ) ;
157+ let metric = params. lowest_fee_metric( ) ;
158+
159+ let exact_possible = common:: exact_selection_possible( & CoinSelector :: new( & candidates) , target) ;
160+
161+ let mut cs = CoinSelector :: new( & candidates) ;
162+ let bnb_found = common:: bnb_search( & mut cs, metric, usize :: MAX ) . is_ok( ) ;
163+ prop_assert_eq!(
164+ bnb_found, exact_possible,
165+ "bnb_found={} but exact_possible={} (weight prune may have dropped a feasible subtree)" ,
166+ bnb_found, exact_possible
167+ ) ;
168+
169+ // The shipped greedy `is_selection_possible` must never over-claim: a `true` is a real
170+ // witness, so it must imply the exact oracle.
171+ let greedy = CoinSelector :: new( & candidates) . is_selection_possible( target) ;
172+ prop_assert!( !greedy || exact_possible, "is_selection_possible over-claimed" ) ;
173+ }
174+ }
175+
124176/// We wrap `LowestFee` in `Changeless` to derive a metric that finds the lowest-fee changeless
125177/// solution. Constraining to changeless should never take fewer rounds than the unconstrained
126178/// `LowestFee`.
@@ -138,6 +190,7 @@ fn combined_changeless_metric() {
138190 drain_dust : 200 ,
139191 n_target_outputs : 1 ,
140192 n_drain_outputs : 1 ,
193+ max_weight : None ,
141194 } ;
142195
143196 let candidates = common:: gen_candidates ( params. n_candidates ) ;
@@ -178,6 +231,7 @@ fn does_not_create_change_below_spend_cost() {
178231 weight_sum : 200 - TX_FIXED_FIELD_WEIGHT - 1 ,
179232 n_outputs : 1 ,
180233 } ,
234+ max_weight : None ,
181235 } ;
182236
183237 let candidates = vec ! [
@@ -256,6 +310,7 @@ fn zero_fee_tx() {
256310 weight_sum : 200 - TX_FIXED_FIELD_WEIGHT - 1 ,
257311 n_outputs : 1 ,
258312 } ,
313+ max_weight : None ,
259314 } ;
260315
261316 let candidates = vec ! [
0 commit comments