From e17c391a3c385802ba5fb203c68541e23b68e494 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 12 Jun 2025 10:16:29 +1000 Subject: [PATCH 1/4] Inline checked div functions back into unsigned module A while back we move all the 'fee' stuff into a separate module because I thought it would help with clarity - I was wrong. Move the checked div functions back into the `unsigned` module on the main `Amount` impl block. Internal change only - code move. --- units/src/amount/unsigned.rs | 101 ++++++++++++++++++++++++++++++++++ units/src/fee.rs | 102 ----------------------------------- 2 files changed, 101 insertions(+), 102 deletions(-) diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs index 33c97b4fc..bbc126f91 100644 --- a/units/src/amount/unsigned.rs +++ b/units/src/amount/unsigned.rs @@ -15,6 +15,7 @@ use super::{ parse_signed_to_satoshi, split_amount_and_denomination, Denomination, Display, DisplayStyle, OutOfRangeError, ParseAmountError, ParseError, SignedAmount, }; +use crate::{FeeRate, Weight}; mod encapsulate { use super::OutOfRangeError; @@ -402,6 +403,106 @@ impl Amount { SignedAmount::from_sat(self.to_sat() as i64) // Cast ok, signed amount and amount share positive range. .expect("range of Amount is within range of SignedAmount") } + + /// Checked weight floor division. + /// + /// Be aware that integer division loses the remainder if no exact division + /// can be made. See also [`Self::checked_div_by_weight_ceil`]. + /// + /// Returns [`None`] if overflow occurred. + #[must_use] + pub const fn checked_div_by_weight_floor(self, weight: Weight) -> Option { + let wu = weight.to_wu(); + if wu == 0 { + return None; + } + + // Mul by 1,000 because we use per/kwu. + match self.to_sat().checked_mul(1_000) { + Some(sats) => { + let fee_rate = sats / wu; + FeeRate::from_sat_per_kwu(fee_rate) + } + None => None, + } + } + + /// Checked weight ceiling division. + /// + /// Be aware that integer division loses the remainder if no exact division + /// can be made. This method rounds up ensuring the transaction fee rate is + /// sufficient. See also [`Self::checked_div_by_weight_floor`]. + /// + /// Returns [`None`] if overflow occurred. + /// + /// # Examples + /// + /// ``` + /// # use bitcoin_units::{amount, Amount, FeeRate, Weight}; + /// let amount = Amount::from_sat(10)?; + /// let weight = Weight::from_wu(300); + /// let fee_rate = amount.checked_div_by_weight_ceil(weight); + /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34)); + /// # Ok::<_, amount::OutOfRangeError>(()) + /// ``` + #[must_use] + pub const fn checked_div_by_weight_ceil(self, weight: Weight) -> Option { + let wu = weight.to_wu(); + if wu == 0 { + return None; + } + + // Mul by 1,000 because we use per/kwu. + if let Some(sats) = self.to_sat().checked_mul(1_000) { + // No need to used checked arithmetic because wu is non-zero. + if let Some(bump) = sats.checked_add(wu - 1) { + let fee_rate = bump / wu; + return FeeRate::from_sat_per_kwu(fee_rate); + } + } + 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 { + if let Some(msats) = self.to_sat().checked_mul(1000) { + if let Some(wu) = msats.checked_div(fee_rate.to_sat_per_kwu_ceil()) { + return Some(Weight::from_wu(wu)); + } + } + 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 { + // Use ceil because result is used as the divisor. + let rate = fee_rate.to_sat_per_kwu_ceil(); + if rate == 0 { + return None; + } + + if let Some(msats) = self.to_sat().checked_mul(1000) { + // No need to used checked arithmetic because rate is non-zero. + if let Some(bump) = msats.checked_add(rate - 1) { + let wu = bump / rate; + return Some(Weight::from_wu(wu)); + } + } + None + } } impl default::Default for Amount { diff --git a/units/src/fee.rs b/units/src/fee.rs index 8248a9957..1354fc848 100644 --- a/units/src/fee.rs +++ b/units/src/fee.rs @@ -17,108 +17,6 @@ use NumOpResult as R; use crate::{Amount, FeeRate, MathOp, NumOpError as E, NumOpResult, OptionExt, Weight}; -impl Amount { - /// Checked weight floor division. - /// - /// Be aware that integer division loses the remainder if no exact division - /// can be made. See also [`Self::checked_div_by_weight_ceil`]. - /// - /// Returns [`None`] if overflow occurred. - #[must_use] - pub const fn checked_div_by_weight_floor(self, weight: Weight) -> Option { - let wu = weight.to_wu(); - if wu == 0 { - return None; - } - - // Mul by 1,000 because we use per/kwu. - match self.to_sat().checked_mul(1_000) { - Some(sats) => { - let fee_rate = sats / wu; - FeeRate::from_sat_per_kwu(fee_rate) - } - None => None, - } - } - - /// Checked weight ceiling division. - /// - /// Be aware that integer division loses the remainder if no exact division - /// can be made. This method rounds up ensuring the transaction fee rate is - /// sufficient. See also [`Self::checked_div_by_weight_floor`]. - /// - /// Returns [`None`] if overflow occurred. - /// - /// # Examples - /// - /// ``` - /// # use bitcoin_units::{amount, Amount, FeeRate, Weight}; - /// let amount = Amount::from_sat(10)?; - /// let weight = Weight::from_wu(300); - /// let fee_rate = amount.checked_div_by_weight_ceil(weight); - /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34)); - /// # Ok::<_, amount::OutOfRangeError>(()) - /// ``` - #[must_use] - pub const fn checked_div_by_weight_ceil(self, weight: Weight) -> Option { - let wu = weight.to_wu(); - if wu == 0 { - return None; - } - - // Mul by 1,000 because we use per/kwu. - if let Some(sats) = self.to_sat().checked_mul(1_000) { - // No need to used checked arithmetic because wu is non-zero. - if let Some(bump) = sats.checked_add(wu - 1) { - let fee_rate = bump / wu; - return FeeRate::from_sat_per_kwu(fee_rate); - } - } - 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 { - if let Some(msats) = self.to_sat().checked_mul(1000) { - if let Some(wu) = msats.checked_div(fee_rate.to_sat_per_kwu_ceil()) { - return Some(Weight::from_wu(wu)); - } - } - 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 { - // Use ceil because result is used as the divisor. - let rate = fee_rate.to_sat_per_kwu_ceil(); - if rate == 0 { - return None; - } - - if let Some(msats) = self.to_sat().checked_mul(1000) { - // No need to used checked arithmetic because rate is non-zero. - if let Some(bump) = msats.checked_add(rate - 1) { - let wu = bump / rate; - return Some(Weight::from_wu(wu)); - } - } - None - } -} - impl FeeRate { /// Calculates the fee by multiplying this fee rate by weight. /// From a8610a937bb5a2db129c779deacf3ade5bde12ab Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 12 Jun 2025 10:20:05 +1000 Subject: [PATCH 2/4] Inline checked mul / to fee back into fee_rate module A while back we move all the 'fee' stuff into a separate module because I thought it would help with clarity - I was wrong. Move the checked mul and to fee functions back into the `fee_rate` module on the main `FeeRate` impl block. Internal change only - code move. --- units/src/fee.rs | 58 --------------------------------------- units/src/fee_rate/mod.rs | 58 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 59 deletions(-) diff --git a/units/src/fee.rs b/units/src/fee.rs index 1354fc848..144ad0fd8 100644 --- a/units/src/fee.rs +++ b/units/src/fee.rs @@ -17,64 +17,6 @@ use NumOpResult as R; use crate::{Amount, FeeRate, MathOp, NumOpError as E, NumOpResult, OptionExt, Weight}; -impl FeeRate { - /// Calculates the fee by multiplying this fee rate by weight. - /// - /// Computes the absolute fee amount for a given [`Weight`] at this fee rate. When the resulting - /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is - /// enough instead of falling short if rounded down. - /// - /// If the calculation would overflow we saturate to [`Amount::MAX`]. Since such a fee can never - /// be paid this is meaningful as an error case while still removing the possibility of silently - /// wrapping. - pub const fn to_fee(self, weight: Weight) -> Amount { - // No `unwrap_or()` in const context. - match self.checked_mul_by_weight(weight) { - Some(fee) => fee, - None => Amount::MAX, - } - } - - /// Calculates the fee by multiplying this fee rate by weight, in weight units, returning [`None`] - /// if an overflow occurred. - /// - /// This is equivalent to `Self::checked_mul_by_weight()`. - #[must_use] - #[deprecated(since = "TBD", note = "use `to_fee()` instead")] - pub fn fee_wu(self, weight: Weight) -> Option { self.checked_mul_by_weight(weight) } - - /// Calculates the fee by multiplying this fee rate by weight, in virtual bytes, returning [`None`] - /// if an overflow occurred. - /// - /// This is equivalent to converting `vb` to [`Weight`] using [`Weight::from_vb`] and then calling - /// `Self::fee_wu(weight)`. - #[must_use] - #[deprecated(since = "TBD", note = "use Weight::from_vb and then `to_fee()` instead")] - pub fn fee_vb(self, vb: u64) -> Option { Weight::from_vb(vb).map(|w| self.to_fee(w)) } - - /// Checked weight multiplication. - /// - /// Computes the absolute fee amount for a given [`Weight`] at this fee rate. When the resulting - /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is - /// enough instead of falling short if rounded down. - /// - /// Returns [`None`] if overflow occurred. - #[must_use] - pub const fn checked_mul_by_weight(self, weight: Weight) -> Option { - let wu = weight.to_wu(); - if let Some(fee_kwu) = self.to_sat_per_kwu_floor().checked_mul(wu) { - // Bump by 999 to do ceil division using kwu. - if let Some(bump) = fee_kwu.checked_add(999) { - let fee = bump / 1_000; - if let Ok(fee_amount) = Amount::from_sat(fee) { - return Some(fee_amount); - } - } - } - None - } -} - crate::internal_macros::impl_op_for_references! { impl ops::Mul for Weight { type Output = NumOpResult; diff --git a/units/src/fee_rate/mod.rs b/units/src/fee_rate/mod.rs index 75c516ea5..aec144041 100644 --- a/units/src/fee_rate/mod.rs +++ b/units/src/fee_rate/mod.rs @@ -13,7 +13,7 @@ use arbitrary::{Arbitrary, Unstructured}; use NumOpResult as R; -use crate::{Amount,MathOp, NumOpError as E, NumOpResult}; +use crate::{Amount, MathOp, NumOpError as E, NumOpResult, Weight}; mod encapsulate { /// Fee rate. @@ -184,6 +184,62 @@ impl FeeRate { None => None, } } + + /// Calculates the fee by multiplying this fee rate by weight. + /// + /// Computes the absolute fee amount for a given [`Weight`] at this fee rate. When the resulting + /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is + /// enough instead of falling short if rounded down. + /// + /// If the calculation would overflow we saturate to [`Amount::MAX`]. Since such a fee can never + /// be paid this is meaningful as an error case while still removing the possibility of silently + /// wrapping. + pub const fn to_fee(self, weight: Weight) -> Amount { + // No `unwrap_or()` in const context. + match self.checked_mul_by_weight(weight) { + Some(fee) => fee, + None => Amount::MAX, + } + } + + /// Calculates the fee by multiplying this fee rate by weight, in weight units, returning [`None`] + /// if an overflow occurred. + /// + /// This is equivalent to `Self::checked_mul_by_weight()`. + #[must_use] + #[deprecated(since = "TBD", note = "use `to_fee()` instead")] + pub fn fee_wu(self, weight: Weight) -> Option { self.checked_mul_by_weight(weight) } + + /// Calculates the fee by multiplying this fee rate by weight, in virtual bytes, returning [`None`] + /// if an overflow occurred. + /// + /// This is equivalent to converting `vb` to [`Weight`] using [`Weight::from_vb`] and then calling + /// `Self::fee_wu(weight)`. + #[must_use] + #[deprecated(since = "TBD", note = "use Weight::from_vb and then `to_fee()` instead")] + pub fn fee_vb(self, vb: u64) -> Option { Weight::from_vb(vb).map(|w| self.to_fee(w)) } + + /// Checked weight multiplication. + /// + /// Computes the absolute fee amount for a given [`Weight`] at this fee rate. When the resulting + /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is + /// enough instead of falling short if rounded down. + /// + /// Returns [`None`] if overflow occurred. + #[must_use] + pub const fn checked_mul_by_weight(self, weight: Weight) -> Option { + let wu = weight.to_wu(); + if let Some(fee_kwu) = self.to_sat_per_kwu_floor().checked_mul(wu) { + // Bump by 999 to do ceil division using kwu. + if let Some(bump) = fee_kwu.checked_add(999) { + let fee = bump / 1_000; + if let Ok(fee_amount) = Amount::from_sat(fee) { + return Some(fee_amount); + } + } + } + None + } } crate::internal_macros::impl_op_for_references! { From 251e6a85da1610bd3b62d686d51212cfb08a6e2c Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 12 Jun 2025 10:25:55 +1000 Subject: [PATCH 3/4] Inline checked mul function back into weight module A while back we move all the 'fee' stuff into a separate module because I thought it would help with clarity - I was wrong. Move the checked mul function back into the `weight` module on the main `Weight` impl block. Internal change only - code move. --- units/src/fee.rs | 14 -------------- units/src/weight.rs | 14 +++++++++++++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/units/src/fee.rs b/units/src/fee.rs index 144ad0fd8..76aa55309 100644 --- a/units/src/fee.rs +++ b/units/src/fee.rs @@ -181,20 +181,6 @@ crate::internal_macros::impl_op_for_references! { } } -impl Weight { - /// Checked fee rate multiplication. - /// - /// Computes the absolute fee amount for a given [`FeeRate`] at this weight. When the resulting - /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is - /// enough instead of falling short if rounded down. - /// - /// Returns [`None`] if overflow occurred. - #[must_use] - pub const fn checked_mul_by_fee_rate(self, fee_rate: FeeRate) -> Option { - fee_rate.checked_mul_by_weight(self) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/units/src/weight.rs b/units/src/weight.rs index 31da7274c..5bf700471 100644 --- a/units/src/weight.rs +++ b/units/src/weight.rs @@ -10,7 +10,7 @@ use arbitrary::{Arbitrary, Unstructured}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::CheckedSum; +use crate::{Amount, CheckedSum, FeeRate}; /// The factor that non-witness serialization data is multiplied by during weight calculation. pub const WITNESS_SCALE_FACTOR: usize = 4; @@ -162,6 +162,18 @@ impl Weight { None => None, } } + + /// Checked fee rate multiplication. + /// + /// Computes the absolute fee amount for a given [`FeeRate`] at this weight. When the resulting + /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is + /// enough instead of falling short if rounded down. + /// + /// Returns [`None`] if overflow occurred. + #[must_use] + pub const fn checked_mul_by_fee_rate(self, fee_rate: FeeRate) -> Option { + fee_rate.checked_mul_by_weight(self) + } } /// Alternative will display the unit. From 20c84ce444fe68577bf14842ebaffd796572d91f Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 12 Jun 2025 10:31:56 +1000 Subject: [PATCH 4/4] units: Make fee module public The `fee` module is now empty as far as API surface. It still holds the `core::ops` impls for fee calculation. It also does have useful rustdocs which are not currently visible online because module is private. Make the module public. Functionally all this does is provide a place for the module level docs under a discoverable name. Improve the docs by adding a bunch of links to fee related functions. --- units/src/fee.rs | 12 ++++++++++++ units/src/lib.rs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/units/src/fee.rs b/units/src/fee.rs index 76aa55309..008fd920b 100644 --- a/units/src/fee.rs +++ b/units/src/fee.rs @@ -10,6 +10,18 @@ //! //! We provide `fee.checked_div_by_weight_ceil(weight)` to calculate a minimum threshold fee rate //! required to pay at least `fee` for transaction with `weight`. +//! +//! We support various `core::ops` traits all of which return [`NumOpResult`]. +//! +//! For specific methods see: +//! +//! * [`Amount::checked_div_by_weight_floor`] +//! * [`Amount::checked_div_by_weight_ceil`] +//! * [`Amount::checked_div_by_fee_rate_floor`] +//! * [`Amount::checked_div_by_fee_rate_ceil`] +//! * [`Weight::checked_mul_by_fee_rate`] +//! * [`FeeRate::checked_mul_by_weight`] +//! * [`FeeRate::to_fee`] use core::ops; diff --git a/units/src/lib.rs b/units/src/lib.rs index 643120223..12c818650 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -34,7 +34,6 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -mod fee; mod internal_macros; mod result; @@ -48,6 +47,7 @@ pub mod _export { pub mod amount; pub mod block; +pub mod fee; pub mod fee_rate; pub mod locktime; pub mod parse;