Merge rust-bitcoin/rust-bitcoin#6884: units: Fix div_by_fee_rate_floor precision
What changed, and why it matters
This commit fixes a precision bug in a Rust Bitcoin library function that calculates how much transaction weight a given amount of money can afford at a given fee rate. The old code rounded the fee rate up to a coarser unit (satoshis per kilo-weight-unit), which could make the result slightly too small, understating the affordable weight. The fix uses a wider intermediate calculation with satoshis per million virtual bytes, matching a previous fix for related functions. It also now properly reports overflow when the result exceeds the maximum representable weight.
Review downstream callers of `div_by_fee_rate_floor` to determine whether the previously understated weight values could have caused incorrect transaction construction, fee estimation, or coin-selection logic. Update to the patched version and run the new regression test. Consider auditing related `div_by_weight_*` functions for similar precision issues if not already addressed.
Security signals we found
Incorrect fee-rate-to-weight conversion leading to understated affordable weight
Integer rounding direction (ceil) used as divisor in floor division causing off-by-one or larger precision loss
Addition of overflow handling for Weight::MAX
Fix explicitly closes an audit issue (project-loupe/audit-rust-bitcoin#81)
Evidence from the diff
The Amount::div_by_fee_rate_floor function previously divided self.to_msat() by fee_rate.to_sat_per_kwu_ceil(). Because to_sat_per_kwu_ceil() rounds the fee rate upward, the divisor is larger than the true rate, causing the floor quotient to be smaller than the mathematically correct affordable weight. The patch changes the computation to use fee_rate.to_sat_per_mvb() and widens the intermediate values to u128, computing (sats * 4_000_000) / rate. It also adds an overflow check so that results exceeding Weight::MAX return an error instead of silently truncating. A regression test demonstrates the old behavior would have returned 3995 wu instead of the correct 3996 wu for a 1000 sat budget at 1001 sat/kvb.
Changed components
units/src/amount/unsigned.rsAmount::div_by_fee_rate_floorFeeRate conversion helpersInspect captured patch +23 / −5
### units/src/amount/tests.rs
@@ -360,6 +360,18 @@ fn amount_checked_div_by_fee_rate() {
assert_eq!(weight, Weight::from_wu(2_100_000_000_000_000_000));
}
+#[test]
+#[cfg(feature = "alloc")]
+fn div_by_fee_rate_floor_preserves_mvb_precision() {
+ // 1001 sat/kvb is 1001 sat per 4,000,000 wu, so a 1000 sat budget funds at most
+ // floor(1000 * 4,000,000 / 1,001,000) = 3996 wu.
+ let weight = sat(1000).div_by_fee_rate_floor(FeeRate::from_sat_per_kvb(1001)).unwrap();
+ assert_eq!(weight, Weight::from_wu(3996));
+
+ // A tiny fee rate over the maximum amount overflows Weight and must error.
+ assert!(Amount::MAX.div_by_fee_rate_floor(FeeRate::from_sat_per_mvb(1)).is_error());
+}
+
#[test]
#[cfg(feature = "alloc")]
fn floating_point() {
### units/src/amount/unsigned.rs
@@ -539,13 +539,19 @@ impl Amount {
///
/// # Errors
///
- /// This can fail only if `fee_rate` is zero, therefore an error returned from this method can
- /// be treated as infinity.
+ /// Returns an error if `fee_rate` is zero, or if the resulting weight would exceed
+ /// [`Weight::MAX`].
#[inline]
pub const fn div_by_fee_rate_floor(self, fee_rate: FeeRate) -> NumOpResult<Weight> {
- let msats = self.to_msat();
- match msats.checked_div(fee_rate.to_sat_per_kwu_ceil()) {
- Some(wu) => R::Valid(Weight::from_wu(wu)),
+ let rate = fee_rate.to_sat_per_mvb() as u128;
+ let sats = self.to_sat() as u128;
+ match (sats * 4_000_000).checked_div(rate) {
+ Some(wu) if wu <= const_casts::u64_to_u128(u64::MAX) =>
+ R::Valid(Weight::from_wu(wu as u64)),
+ Some(_) => R::Error(E::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Div,
+ is_negative: false,
+ })),
None => R::Error(E::while_doing(MathErrorKind::DivByZero)),
}
}Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.