Repository: rust-bitcoin/rust-bitcoin (language: rust)

Issue:
Implement max transaction weight = fee / fee_rate
We already support `fee = fee_rate * weight` and also `fee rate = fee / weight` so it seems logical to support div by fee rate.

Note that division results in a threshold. For `fee / weight` we want the ceiling because floor would result in too small a fee rate. Following similar logic, for `fee / fee rate` I believe we want floor otherwise a larger weight would be too big resulting in a higher fee.

If #3813 merges then when doing this issue remember to update the new module rustdoc. 



Discussion / hints:
Probably wait to see if #3813 gets traction.
Likely will look something like this. 

```rust

    /// Checked fee rate division.
    ///
    /// Computes the maximum weight for a transaction with `fee_rate` assuming this amount
    /// represents a transaction fee.
    ///
    /// Does integer division i.e., rounds down. This ensures the weight returned is a maximum
    /// threshold ensuring a transaction requires less fees than this amount.
    ///
    /// # Returns
    ///
    /// Returns [`None`] if `fee_rate` is zero or the division results in overflow.
    #[must_use]
    pub const fn checked_div_by_fee_rate(self, fee_rate: FeeRate) -> Option<Weight> {
        let fee = match self.to_sat().checked_mul(1_000) {
            Some(fee) => fee,
            None => panic!("unreachable, MAX_MONEY times 1,000 cannot overflow"),
        };
        match fee.checked_div(fee_rate.to_sat_per_kwu()) {
            Some(weight) => Some(Weight::from_wu(weight)),
            None => None,
        }
    }


impl ops::Div<FeeRate> for Amount {
    type Output = Weight;

    /// Truncating integer division.
    ///
    /// # Panics
    ///
    /// This operation will panic if `fee_rate` is zero or the division results in overflow.
    fn div(self, rhs: FeeRate) -> Self::Output { self.checked_div_by_fee_rate(rhs).unwrap() }
}
```
> For `fee / weight` we want the ceiling because floor would result in too small

My two cents: this is true, *but* users expect `/` to be truncating, so will do stuff like `(fee + weight - 1) / weight` (I know this isn't allowed since you can't add fees to weight, but they'll do some equivalent that compiles).

I would like to have explicit `_floor` and `_ceil` division methods for this, but if we implement `/` we should implement it in terms of the `_floor` one. And document that users probably want to use the explicitly-named `_ceil` method.
Ah perhaps I didn't write the issue clear enough. Currently `rate = fee / weight` uses floor division and we do have `_ceil` and `_floor` versions and a comment on `ops::Div` saying "you probably want to use _ceil". However for `weight = fee / rate` I believe one would typically want floor. If that is true then we don't need to provide `_ceil` and `ops::Div` can go right ahead and truncate as expected.
I think we ought to provide `_ceil` anyway (and `_floor` for symmetry) "just in case".

But ok, awesome that we don't need to provide explicit guidance docs for this.
Righto. That has the advantage also of bringing attention to the issue irrespective of what one is dividing by.
I'll work on this issue



Fix patch:
diff --git a/api/units/all-features.txt b/api/units/all-features.txt
index 0559a49bc..27831e202 100644
--- a/api/units/all-features.txt
+++ b/api/units/all-features.txt
@@ -505,6 +505,7 @@ impl core::ops::arith::AddAssign<&bitcoin_units::SignedAmount> for bitcoin_units
 impl core::ops::arith::AddAssign<&bitcoin_units::fee_rate::FeeRate> for bitcoin_units::fee_rate::FeeRate
 impl core::ops::arith::AddAssign<&bitcoin_units::weight::Weight> for bitcoin_units::weight::Weight
 impl core::ops::arith::Div for bitcoin_units::weight::Weight
+impl core::ops::arith::Div<bitcoin_units::fee_rate::FeeRate> for bitcoin_units::Amount
 impl core::ops::arith::Div<bitcoin_units::weight::Weight> for bitcoin_units::Amount
 impl core::ops::arith::Div<i64> for bitcoin_units::SignedAmount
 impl core::ops::arith::Div<u64> for bitcoin_units::Amount
@@ -720,6 +721,8 @@ pub const bitcoin_units::weight::Weight::WITNESS_SCALE_FACTOR: u64
 pub const bitcoin_units::weight::Weight::ZERO: bitcoin_units::weight::Weight
 pub const fn bitcoin_units::Amount::checked_add(self, rhs: bitcoin_units::Amount) -> core::option::Option<bitcoin_units::Amount>
 pub const fn bitcoin_units::Amount::checked_div(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
+pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_ceil(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option<bitcoin_units::weight::Weight>
+pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_floor(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option<bitcoin_units::weight::Weight>
 pub const fn bitcoin_units::Amount::checked_div_by_weight_ceil(self, weight: bitcoin_units::weight::Weight) -> core::option::Option<bitcoin_units::fee_rate::FeeRate>
 pub const fn bitcoin_units::Amount::checked_div_by_weight_floor(self, weight: bitcoin_units::weight::Weight) -> core::option::Option<bitcoin_units::fee_rate::FeeRate>
 pub const fn bitcoin_units::Amount::checked_mul(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
@@ -814,6 +817,7 @@ pub fn bitcoin_units::Amount::des_sat<'d, D: serde::de::Deserializer<'d>>(d: D,
 pub fn bitcoin_units::Amount::des_str<'d, D: serde::de::Deserializer<'d>>(d: D, _: private::Token) -> core::result::Result<Self, <D as serde::de::Deserializer>::Error>
 pub fn bitcoin_units::Amount::display_dynamic(self) -> bitcoin_units::amount::Display
 pub fn bitcoin_units::Amount::display_in(self, denomination: bitcoin_units::amount::Denomination) -> bitcoin_units::amount::Display
+pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::fee_rate::FeeRate) -> Self::Output
 pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::Amount::div(self, rhs: u64) -> Self::Output
 pub fn bitcoin_units::Amount::div_assign(&mut self, rhs: u64)
@@ -1285,6 +1289,7 @@ pub type bitcoin_units::Amount::Err = bitcoin_units::amount::ParseError
 pub type bitcoin_units::Amount::Error = bitcoin_units::amount::OutOfRangeError
 pub type bitcoin_units::Amount::Output = bitcoin_units::Amount
 pub type bitcoin_units::Amount::Output = bitcoin_units::fee_rate::FeeRate
+pub type bitcoin_units::Amount::Output = bitcoin_units::weight::Weight
 pub type bitcoin_units::SignedAmount::Err = bitcoin_units::amount::ParseError
 pub type bitcoin_units::SignedAmount::Output = bitcoin_units::SignedAmount
 pub type bitcoin_units::amount::Denomination::Err = bitcoin_units::amount::ParseDenominationError
diff --git a/api/units/alloc-only.txt b/api/units/alloc-only.txt
index a44702c7a..8b5ae4bed 100644
--- a/api/units/alloc-only.txt
+++ b/api/units/alloc-only.txt
@@ -470,6 +470,7 @@ impl core::ops::arith::AddAssign<&bitcoin_units::SignedAmount> for bitcoin_units
 impl core::ops::arith::AddAssign<&bitcoin_units::fee_rate::FeeRate> for bitcoin_units::fee_rate::FeeRate
 impl core::ops::arith::AddAssign<&bitcoin_units::weight::Weight> for bitcoin_units::weight::Weight
 impl core::ops::arith::Div for bitcoin_units::weight::Weight
+impl core::ops::arith::Div<bitcoin_units::fee_rate::FeeRate> for bitcoin_units::Amount
 impl core::ops::arith::Div<bitcoin_units::weight::Weight> for bitcoin_units::Amount
 impl core::ops::arith::Div<i64> for bitcoin_units::SignedAmount
 impl core::ops::arith::Div<u64> for bitcoin_units::Amount
@@ -658,6 +659,8 @@ pub const bitcoin_units::weight::Weight::WITNESS_SCALE_FACTOR: u64
 pub const bitcoin_units::weight::Weight::ZERO: bitcoin_units::weight::Weight
 pub const fn bitcoin_units::Amount::checked_add(self, rhs: bitcoin_units::Amount) -> core::option::Option<bitcoin_units::Amount>
 pub const fn bitcoin_units::Amount::checked_div(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
+pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_ceil(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option<bitcoin_units::weight::Weight>
+pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_floor(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option<bitcoin_units::weight::Weight>
 pub const fn bitcoin_units::Amount::checked_div_by_weight_ceil(self, weight: bitcoin_units::weight::Weight) -> core::option::Option<bitcoin_units::fee_rate::FeeRate>
 pub const fn bitcoin_units::Amount::checked_div_by_weight_floor(self, weight: bitcoin_units::weight::Weight) -> core::option::Option<bitcoin_units::fee_rate::FeeRate>
 pub const fn bitcoin_units::Amount::checked_mul(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
@@ -748,6 +751,7 @@ pub fn bitcoin_units::Amount::cmp(&self, other: &bitcoin_units::Amount) -> core:
 pub fn bitcoin_units::Amount::default() -> Self
 pub fn bitcoin_units::Amount::display_dynamic(self) -> bitcoin_units::amount::Display
 pub fn bitcoin_units::Amount::display_in(self, denomination: bitcoin_units::amount::Denomination) -> bitcoin_units::amount::Display
+pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::fee_rate::FeeRate) -> Self::Output
 pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::Amount::div(self, rhs: u64) -> Self::Output
 pub fn bitcoin_units::Amount::div_assign(&mut self, rhs: u64)
@@ -1113,6 +1117,7 @@ pub type bitcoin_units::Amount::Err = bitcoin_units::amount::ParseError
 pub type bitcoin_units::Amount::Error = bitcoin_units::amount::OutOfRangeError
 pub type bitcoin_units::Amount::Output = bitcoin_units::Amount
 pub type bitcoin_units::Amount::Output = bitcoin_units::fee_rate::FeeRate
+pub type bitcoin_units::Amount::Output = bitcoin_units::weight::Weight
 pub type bitcoin_units::SignedAmount::Err = bitcoin_units::amount::ParseError
 pub type bitcoin_units::SignedAmount::Output = bitcoin_units::SignedAmount
 pub type bitcoin_units::amount::Denomination::Err = bitcoin_units::amount::ParseDenominationError
diff --git a/api/units/no-features.txt b/api/units/no-features.txt
index 39e957f3e..a23e0e4d2 100644
--- a/api/units/no-features.txt
+++ b/api/units/no-features.txt
@@ -454,6 +454,7 @@ impl core::ops::arith::AddAssign<&bitcoin_units::SignedAmount> for bitcoin_units
 impl core::ops::arith::AddAssign<&bitcoin_units::fee_rate::FeeRate> for bitcoin_units::fee_rate::FeeRate
 impl core::ops::arith::AddAssign<&bitcoin_units::weight::Weight> for bitcoin_units::weight::Weight
 impl core::ops::arith::Div for bitcoin_units::weight::Weight
+impl core::ops::arith::Div<bitcoin_units::fee_rate::FeeRate> for bitcoin_units::Amount
 impl core::ops::arith::Div<bitcoin_units::weight::Weight> for bitcoin_units::Amount
 impl core::ops::arith::Div<i64> for bitcoin_units::SignedAmount
 impl core::ops::arith::Div<u64> for bitcoin_units::Amount
@@ -642,6 +643,8 @@ pub const bitcoin_units::weight::Weight::WITNESS_SCALE_FACTOR: u64
 pub const bitcoin_units::weight::Weight::ZERO: bitcoin_units::weight::Weight
 pub const fn bitcoin_units::Amount::checked_add(self, rhs: bitcoin_units::Amount) -> core::option::Option<bitcoin_units::Amount>
 pub const fn bitcoin_units::Amount::checked_div(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
+pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_ceil(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option<bitcoin_units::weight::Weight>
+pub const fn bitcoin_units::Amount::checked_div_by_fee_rate_floor(self, fee_rate: bitcoin_units::fee_rate::FeeRate) -> core::option::Option<bitcoin_units::weight::Weight>
 pub const fn bitcoin_units::Amount::checked_mul(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
 pub const fn bitcoin_units::Amount::checked_rem(self, rhs: u64) -> core::option::Option<bitcoin_units::Amount>
 pub const fn bitcoin_units::Amount::checked_sub(self, rhs: bitcoin_units::Amount) -> core::option::Option<bitcoin_units::Amount>
@@ -730,6 +733,7 @@ pub fn bitcoin_units::Amount::cmp(&self, other: &bitcoin_units::Amount) -> core:
 pub fn bitcoin_units::Amount::default() -> Self
 pub fn bitcoin_units::Amount::display_dynamic(self) -> bitcoin_units::amount::Display
 pub fn bitcoin_units::Amount::display_in(self, denomination: bitcoin_units::amount::Denomination) -> bitcoin_units::amount::Display
+pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::fee_rate::FeeRate) -> Self::Output
 pub fn bitcoin_units::Amount::div(self, rhs: bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::Amount::div(self, rhs: u64) -> Self::Output
 pub fn bitcoin_units::Amount::div_assign(&mut self, rhs: u64)
@@ -1067,6 +1071,7 @@ pub type bitcoin_units::Amount::Err = bitcoin_units::amount::ParseError
 pub type bitcoin_units::Amount::Error = bitcoin_units::amount::OutOfRangeError
 pub type bitcoin_units::Amount::Output = bitcoin_units::Amount
 pub type bitcoin_units::Amount::Output = bitcoin_units::fee_rate::FeeRate
+pub type bitcoin_units::Amount::Output = bitcoin_units::weight::Weight
 pub type bitcoin_units::SignedAmount::Err = bitcoin_units::amount::ParseError
 pub type bitcoin_units::SignedAmount::Output = bitcoin_units::SignedAmount
 pub type bitcoin_units::amount::Denomination::Err = bitcoin_units::amount::ParseDenominationError
diff --git a/units/src/fee.rs b/units/src/fee.rs
index fba121055..a1475f9bd 100644
--- a/units/src/fee.rs
+++ b/units/src/fee.rs
@@ -68,6 +68,48 @@ impl Amount {
             None => None,
         }
     }
+
+    /// Checked fee rate floor division.
+    ///
+    /// Computes the maximum weight that would result in a fee less than or equal to this amount
+    /// at the given `fee_rate`. Uses floor division to ensure the resulting weight doesn't cause
+    /// the fee to exceed the amount.
+    ///
+    /// Returns [`None`] if overflow occurred or if `fee_rate` is zero.
+    #[must_use]
+    pub const fn checked_div_by_fee_rate_floor(self, fee_rate: FeeRate) -> Option<Weight> {
+        match self.to_sat().checked_mul(1000) {
+            Some(amount_msats) => match amount_msats.checked_div(fee_rate.to_sat_per_kwu()) {
+                Some(wu) => Some(Weight::from_wu(wu)),
+                None => None,
+            },
+            None => None,
+        }
+    }
+
+    /// Checked fee rate ceiling division.
+    ///
+    /// Computes the minimum weight that would result in a fee greater than or equal to this amount
+    /// at the given `fee_rate`. Uses ceiling division to ensure the resulting weight is sufficient.
+    ///
+    /// Returns [`None`] if overflow occurred or if `fee_rate` is zero.
+    #[must_use]
+    pub const fn checked_div_by_fee_rate_ceil(self, fee_rate: FeeRate) -> Option<Weight> {
+        let rate = fee_rate.to_sat_per_kwu();
+        match self.to_sat().checked_mul(1000) {
+            Some(amount_msats) => match rate.checked_sub(1) {
+                Some(rate_minus_one) => match amount_msats.checked_add(rate_minus_one) {
+                    Some(rounded_msats) => match rounded_msats.checked_div(rate) {
+                        Some(wu) => Some(Weight::from_wu(wu)),
+                        None => None,
+                    },
+                    None => None,
+                },
+                None => None,
+            },
+            None => None,
+        }
+    }
 }
 
 impl FeeRate {
@@ -136,6 +178,21 @@ impl ops::Div<Weight> for Amount {
     }
 }
 
+impl ops::Div<FeeRate> for Amount {
+    type Output = Weight;
+
+    /// Truncating integer division.
+    ///
+    /// # Panics
+    ///
+    /// This operation will panic if `fee_rate` is zero or the division results in overflow.
+    ///
+    /// Note: This uses floor division. For ceiling division use [`Amount::checked_div_by_fee_rate_ceil`].
+    fn div(self, rhs: FeeRate) -> Self::Output {
+        self.checked_div_by_fee_rate_floor(rhs).unwrap()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -199,4 +256,31 @@ mod tests {
 
         assert_eq!(two * three, six);
     }
+
+    #[test]
+    fn amount_div_by_fee_rate() {
+        // Test exact division
+        let amount = Amount::from_sat(1000);
+        let fee_rate = FeeRate::from_sat_per_kwu(2);
+        let weight = amount / fee_rate;
+        assert_eq!(weight, Weight::from_wu(500_000));
+
+        // Test truncation behavior
+        let amount = Amount::from_sat(1000);
+        let fee_rate = FeeRate::from_sat_per_kwu(3);
+        let weight = amount / fee_rate;
+        // 1000 * 1000 = 1,000,000 msats
+        // 1,000,000 / 3 = 333,333.33... wu
+        // Should truncate down to 333,333 wu
+        assert_eq!(weight, Weight::from_wu(333_333));
+
+        // Verify that ceiling division gives different result
+        let ceil_weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap();
+        assert_eq!(ceil_weight, Weight::from_wu(333_334));
+
+        // Test that division by zero returns None
+        let zero_rate = FeeRate::from_sat_per_kwu(0);
+        assert!(amount.checked_div_by_fee_rate_floor(zero_rate).is_none());
+        assert!(amount.checked_div_by_fee_rate_ceil(zero_rate).is_none());
+    }
 }


Test patch:
diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs
index ecd813ffd..b8374456b 100644
--- a/units/src/amount/tests.rs
+++ b/units/src/amount/tests.rs
@@ -220,6 +220,48 @@ fn amount_checked_div_by_weight_floor() {
     assert!(fee_rate.is_none());
 }
 
+#[cfg(feature = "alloc")]
+#[test]
+fn amount_checked_div_by_fee_rate() {
+    let amount = Amount::from_sat(1000);
+    let fee_rate = FeeRate::from_sat_per_kwu(2);
+
+    // Test floor division
+    let weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap();
+    // 1000 sats / (2 sats/kwu) = 500,000 wu
+    assert_eq!(weight, Weight::from_wu(500_000));
+
+    // Test ceiling division
+    let weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap();
+    assert_eq!(weight, Weight::from_wu(500_000)); // Same result for exact division
+
+    // Test truncation behavior
+    let amount = Amount::from_sat(1000);
+    let fee_rate = FeeRate::from_sat_per_kwu(3);
+    let floor_weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap();
+    let ceil_weight = amount.checked_div_by_fee_rate_ceil(fee_rate).unwrap();
+    assert_eq!(floor_weight, Weight::from_wu(333_333));
+    assert_eq!(ceil_weight, Weight::from_wu(333_334));
+
+    // Test division by zero
+    let zero_fee_rate = FeeRate::from_sat_per_kwu(0);
+    assert!(amount.checked_div_by_fee_rate_floor(zero_fee_rate).is_none());
+    assert!(amount.checked_div_by_fee_rate_ceil(zero_fee_rate).is_none());
+
+    // Test with maximum amount
+    let max_amount = Amount::MAX;
+    let small_fee_rate = FeeRate::from_sat_per_kwu(1);
+    let weight = max_amount.checked_div_by_fee_rate_floor(small_fee_rate).unwrap();
+    // 21_000_000_0000_0000 sats / (1 sat/kwu) = 2_100_000_000_000_000_000 wu
+    assert_eq!(weight, Weight::from_wu(2_100_000_000_000_000_000));
+
+    // Test overflow case
+    let tiny_fee_rate = FeeRate::from_sat_per_kwu(1);
+    let large_amount = Amount::from_sat(u64::MAX);
+    assert!(large_amount.checked_div_by_fee_rate_floor(tiny_fee_rate).is_none());
+    assert!(large_amount.checked_div_by_fee_rate_ceil(tiny_fee_rate).is_none());
+}
+
 #[cfg(feature = "alloc")]
 #[test]
 fn floating_point() {