From 5e1c0189b151b19296fb3fc32ddefbf1397d4309 Mon Sep 17 00:00:00 2001 From: Arowolokehinde Date: Fri, 14 Aug 2026 16:48:35 +0100 Subject: [PATCH] fix: clamp ideal_fee to 0.0 in LowestFee::bound to prevent f32 rounding panic --- src/metrics/lowest_fee.rs | 9 +++++---- tests/lowest_fee.rs | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/metrics/lowest_fee.rs b/src/metrics/lowest_fee.rs index cade774..1ec068b 100644 --- a/src/metrics/lowest_fee.rs +++ b/src/metrics/lowest_fee.rs @@ -261,10 +261,11 @@ impl BnbMetric for LowestFee { } // `scale` could be 0 even if `is_funded` is `false` due to the latter being based on - // rounded-up vbytes. - let ideal_fee = scale.0 * to_resize.value as f32 + cs.selected_value() as f32 - - cs.target().value() as f32; - assert!(ideal_fee >= 0.0); + // rounded-up vbytes. f32 rounding can also push the sum slightly below zero; 0 is + // always an admissible lower bound for a fee, so clamp. + let ideal_fee = (scale.0 * to_resize.value as f32 + cs.selected_value() as f32 + - cs.target().value() as f32) + .max(0.0); Some(Ordf32(ideal_fee)) } diff --git a/tests/lowest_fee.rs b/tests/lowest_fee.rs index af6120a..0c49a52 100644 --- a/tests/lowest_fee.rs +++ b/tests/lowest_fee.rs @@ -450,3 +450,28 @@ fn identical_candidates_are_excluded_together_when_interleaved() { .count(); assert!(rounds <= 100, "took {} rounds", rounds); } + +#[test] +fn bound_does_not_panic_on_f32_rounding() { + // At a zero feerate, the bound's ideal fee for an empty selection is + // `(target / value) * value - target`. In f32, `(1 / 41) * 41` rounds to `1 - 2^-24`, so this + // came out slightly negative and tripped the old `assert!` (#57). + let candidates = [Candidate::new_tr_keyspend(41)]; + let target = Target { + fee: TargetFee::ZERO, + outputs: TargetOutputs { + value_sum: 1, + weight_sum: 0, + n_outputs: 1, + }, + max_weight: None, + }; + let mut metric = LowestFee { + long_term_feerate: FeeRate::ZERO, + dust_relay_feerate: FeeRate::ZERO, + drain_weights: DrainWeights::TR_KEYSPEND, + }; + let cs = CoinSelector::new(&candidates, target); + let bound = metric.bound(&cs).expect("target is reachable"); + assert_eq!(bound.0, 0.0); +}