Merge rust-bitcoin/rust-bitcoin#6921: units: fix div_by_fee_rate_ceil precision
What changed, and why it matters
This commit fixes a rounding bug in how the rust-bitcoin library calculates the minimum transaction weight needed to pay a given fee at a given fee rate. The old code rounded the fee rate up too early, which could produce a weight slightly smaller than actually required. In practice, that means a wallet relying on this function might think a transaction meets its fee target when it actually falls a tiny bit short. The fix performs the division at full precision, matching a similar recent fix for the floor-division variant.
Review any code that calls div_by_fee_rate_ceil to confirm it now produces weights large enough to meet fee targets; update to this patch release. No immediate incident response is indicated, but fee-calculation correctness is safety-critical for wallet software.
Security signals we found
Incorrect fee-weight calculation due to premature integer rounding
Potential transaction fee shortfall when using div_by_fee_rate_ceil
Overflow protection added for Amount::MAX * 4_000_000 intermediate value
Matches analogous precision fix in div_by_fee_rate_floor (#6884)
Evidence from the diff
The Amount::div_by_fee_rate_ceil function previously converted the FeeRate to sat/kwu using to_sat_per_kwu_ceil() before dividing. That pre-rounds the divisor upward, which can make the resulting weight too small to cover the original fee budget. The patch changes the computation to use to_sat_per_mvb() (full u64 precision) promoted to u128, computes (sats * 4_000_000).div_ceil(rate), and only then checks for overflow against Weight::MAX. It also updates the documented error conditions and adds unit tests verifying full precision and overflow behavior.
Changed components
units/src/amount/unsigned.rsAmount::div_by_fee_rate_ceilFeeRate conversion helpersInspect captured patch +38 / −8
### units/src/amount/tests.rs
@@ -393,6 +393,28 @@ fn div_by_fee_rate_floor_preserves_mvb_precision() {
assert!(Amount::MAX.div_by_fee_rate_floor(FeeRate::from_sat_per_mvb(1)).is_error());
}
+#[test]
+#[cfg(feature = "alloc")]
+fn div_by_fee_rate_ceil_preserves_mvb_precision() {
+ // `div_by_fee_rate_ceil` must compute the minimum weight at full FeeRate precision.
+ let budget = Amount::from_sat(100).unwrap();
+ let rate = FeeRate::from_sat_per_kvb_u32(1);
+ let weight = budget.div_by_fee_rate_ceil(rate).unwrap();
+ assert!(
+ weight >= Weight::from_wu(400_000),
+ "minimum sufficient weight for 100 sat at 1 sat/kvb is 400_000 wu, got {} wu",
+ weight.to_wu()
+ );
+
+ // 1001 sat/kvb is 1001 sat per 4,000,000 wu, so a 1000 sat budget requires at least
+ // ceil(1000 * 4,000,000 / 1,001,000) = ceil(3996.0039...) = 3997 wu.
+ let weight = sat(1000).div_by_fee_rate_ceil(FeeRate::from_sat_per_kvb_u32(1001)).unwrap();
+ assert_eq!(weight, Weight::from_wu(3997));
+
+ // A tiny fee rate over the maximum amount overflows Weight and must error.
+ assert!(Amount::MAX.div_by_fee_rate_ceil(FeeRate::from_sat_per_mvb(1)).is_error());
+}
+
#[test]
#[cfg(feature = "alloc")]
fn floating_point() {
### units/src/amount/unsigned.rs
@@ -563,19 +563,27 @@ 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_ceil(self, fee_rate: FeeRate) -> NumOpResult<Weight> {
- // Use ceil because result is used as the divisor.
- let rate = fee_rate.to_sat_per_kwu_ceil();
- // Early return so we do not have to use checked arithmetic below.
+ // Operate on u128 to gracefully handle potential intermediate overflow
+ // case below, e.g. Amount::MAX * 4_000_000 > u64::MAX.
+ let rate = const_casts::u64_to_u128(fee_rate.to_sat_per_mvb());
+ let sats = const_casts::u64_to_u128(self.to_sat());
if rate == 0 {
return R::Error(E::while_doing(MathErrorKind::DivByZero));
}
-
- let msats = self.to_msat();
- NumOpResult::Valid(Weight::from_wu(msats.div_ceil(rate)))
+ // Save division until the end to keep precision (no intermediate integer rounding).
+ let wu = (sats * 4_000_000).div_ceil(rate);
+ if wu <= const_casts::u64_to_u128(u64::MAX) {
+ R::Valid(Weight::from_wu(wu as u64))
+ } else {
+ R::Error(E::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Div,
+ is_negative: false,
+ }))
+ }
}
}
Why this scored 48/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.