Merge rust-bitcoin/rust-bitcoin#3893: feat: add Amount division by FeeRate
7482fcd934
Run just check-api (jrakibi)bcc38c40e0
Add Amount division by FeeRate (jrakibi) Pull request description: Add checked_div_by_fee_rate method to Amount that computes the maximum weight for a transaction with a given fee rate. This complements the existing `fee = fee_rate * weight `and `fee_rate = fee / weight` operations - Add `checked_div_by_fee_rate` method that returns Option<Weight> - Implement` Div<FeeRate>` for Amount for operator syntax support - Use `floor` division to ensure weight doesn't exceed intended fee This allows calculating the maximum transaction weight possible for a given fee amount and fee rate. Closes #3814 ACKs for top commit: apoelstra: ACK 7482fcd934c09e3cd6cd25fd4328960b02f8e976; successfully ran local tests tcharding: ACK7482fcd934
Tree-SHA512: 622ca42bde1f67782a3c2996efcba0132fedb5e984f594603aece974de6acdeb4b22d63239d29d46fb8623c8082841c33b1d5b9ad2ea51e2f63e6f5d859daa7e
This commit is contained in:
commit
11f5012758
|
@ -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
|
||||
|
@ -722,6 +723,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>
|
||||
|
@ -816,6 +819,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)
|
||||
|
@ -1289,6 +1293,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
|
||||
|
|
|
@ -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
|
||||
|
@ -660,6 +661,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>
|
||||
|
@ -750,6 +753,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)
|
||||
|
@ -1117,6 +1121,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
|
||||
|
|
|
@ -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
|
||||
|
@ -644,6 +645,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>
|
||||
|
@ -732,6 +735,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)
|
||||
|
@ -1069,6 +1073,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
|
||||
|
|
|
@ -246,6 +246,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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue