Merge rust-bitcoin/rust-bitcoin#4534: Make `FeeRate` use MvB internally

56516757ad Add code comment to amount calculation (Tobin C. Harding)
8cf1dc39b5 Fix incorrect code comment (Tobin C. Harding)
7c186e6081 Refactor fee functions (Tobin C. Harding)
bf0776e3dd Remove panic using checked arithmetic (Tobin C. Harding)
b65860067f Make fee functions const (Tobin C. Harding)
9b2fc021b2 Improve rustdocs on FeeRate (Tobin C. Harding)
1bd1e89458 Re-introduce FeeRate encapsulate module (Tobin C. Harding)
b27d8e5819 Change the internal representation of FeeRate (Tobin C. Harding)
2e0b88ba76 bitcoin: Fix dust 'fee' identifiers (Tobin C. Harding)
399bca531c Reduce the FeeRate::MAX value (Tobin C. Harding)
d174c06a4a Saturate to_fee to Amount::MAX (Tobin C. Harding)
64ac33754f Add missing argument docs (Tobin C. Harding)
fe0a448e78 Temporarily remove const from fee calc function (Tobin C. Harding)
b929022d56 Add floor/ceil versions of to_sat_per_kwu (Tobin C. Harding)
64098e4578 Remove encapsulate module from fee rate (Tobin C. Harding)

Pull request description:

  The `FeeRate` is a bit entangled with amount and weight.  Also we have an off-by-one bug caused by rounding errors and the fact that we use kwu internally.

  We can get more precision in the fee rate by internally using per million virtual bytes.

  - Fix: #4516
  - Fix: #4497
  - Fix: #3806

ACKs for top commit:
  apoelstra:
    ACK 56516757ad8874d8121dba468946546bb8fd7d4e; successfully ran local tests

Tree-SHA512: 0c6e7e2c9420886d5332e32519838d6ea415d269abda916e51d5847aa2475c87fa4abfc29b5f75e8b10c44df67ae29d823aa93f7cfabbc89eb27f2173b103242
This commit is contained in:
merge-script 2025-06-04 16:42:55 +00:00
commit a13ba99c11
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
10 changed files with 251 additions and 207 deletions

View File

@ -289,8 +289,8 @@ crate::internal_macros::define_extension_trait! {
/// To use the default Bitcoin Core value, use [`minimal_non_dust`].
///
/// [`minimal_non_dust`]: Script::minimal_non_dust
fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> Option<Amount> {
self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu() * 4)
fn minimal_non_dust_custom(&self, dust_relay: FeeRate) -> Option<Amount> {
self.minimal_non_dust_internal(dust_relay.to_sat_per_kvb_ceil())
}
/// Counts the sigops for this Script using accurate counting.
@ -407,10 +407,10 @@ mod sealed {
crate::internal_macros::define_extension_trait! {
pub(crate) trait ScriptExtPriv impl for Script {
fn minimal_non_dust_internal(&self, dust_relay_fee: u64) -> Option<Amount> {
fn minimal_non_dust_internal(&self, dust_relay_fee_rate_per_kvb: u64) -> Option<Amount> {
// This must never be lower than Bitcoin Core's GetDustThreshold() (as of v0.21) as it may
// otherwise allow users to create transactions which likely can never be broadcast/confirmed.
let sats = dust_relay_fee
let sats = dust_relay_fee_rate_per_kvb
.checked_mul(if self.is_op_return() {
0
} else if self.is_witness_program() {

View File

@ -18,7 +18,6 @@ use hashes::sha256d;
use internals::{compact_size, write_err, ToU64};
use io::{BufRead, Write};
use primitives::Sequence;
use units::NumOpResult;
use super::Weight;
use crate::consensus::{self, encode, Decodable, Encodable};
@ -776,19 +775,18 @@ impl Decodable for Transaction {
///
/// * `fee_rate` - the fee rate of the transaction being created.
/// * `input_weight_prediction` - the predicted input weight.
///
/// # Returns
///
/// This will return [`NumOpResult::Error`] if the fee calculation (fee_rate * weight) overflows.
/// Otherwise, [`NumOpResult::Valid`] will wrap the successful calculation.
/// * `value` - The value of the output we are spending.
pub fn effective_value(
fee_rate: FeeRate,
input_weight_prediction: InputWeightPrediction,
value: Amount,
) -> NumOpResult<SignedAmount> {
) -> SignedAmount {
let weight = input_weight_prediction.total_weight();
let fee = fee_rate.to_fee(weight);
fee_rate.to_fee(weight).map(Amount::to_signed).and_then(|fee| value.to_signed() - fee) // Cannot overflow.
// Cannot overflow because after conversion to signed Amount::MIN - Amount::MAX
// still fits in SignedAmount::MAX (0 - MAX = -MAX).
(value.to_signed() - fee.to_signed()).expect("cannot overflow")
}
/// Predicts the weight of a to-be-constructed transaction.
@ -1691,11 +1689,10 @@ mod tests {
#[test]
fn effective_value_happy_path() {
let value = "1 cBTC".parse::<Amount>().unwrap();
let fee_rate = FeeRate::from_sat_per_kwu(10);
let effective_value =
effective_value(fee_rate, InputWeightPrediction::P2WPKH_MAX, value).unwrap();
let fee_rate = FeeRate::from_sat_per_kwu(10).unwrap();
let effective_value = effective_value(fee_rate, InputWeightPrediction::P2WPKH_MAX, value);
// 10 sat/kwu * 272 wu = 4 sats (rounding up)
// 10 sat/kwu * 272 wu = 3 sats (rounding up)
let expected_fee = "3 sats".parse::<SignedAmount>().unwrap();
let expected_effective_value = (value.to_signed() - expected_fee).unwrap();
assert_eq!(effective_value, expected_effective_value);
@ -1705,7 +1702,8 @@ mod tests {
fn effective_value_fee_rate_does_not_overflow() {
let eff_value =
effective_value(FeeRate::MAX, InputWeightPrediction::P2WPKH_MAX, Amount::ZERO);
assert!(eff_value.is_error());
let want = SignedAmount::from_sat(-1254378597012250).unwrap(); // U64::MAX / 4_000 because of FeeRate::MAX
assert_eq!(eff_value, want)
}
#[test]

View File

@ -1178,8 +1178,11 @@ impl fmt::Display for ExtractTxError {
use ExtractTxError::*;
match *self {
AbsurdFeeRate { fee_rate, .. } =>
write!(f, "an absurdly high fee rate of {} sat/kwu", fee_rate.to_sat_per_kwu()),
AbsurdFeeRate { fee_rate, .. } => write!(
f,
"an absurdly high fee rate of {} sat/kwu",
fee_rate.to_sat_per_kwu_floor()
),
MissingInputValue { .. } => write!(
f,
"one of the inputs lacked value information (witness_utxo or non_witness_utxo)"
@ -1353,9 +1356,15 @@ mod tests {
use crate::Sequence;
/// Fee rate in sat/kwu for a high-fee PSBT with an input=5_000_000_000_000, output=1000
const ABSURD_FEE_RATE: FeeRate = FeeRate::from_sat_per_kwu(15_060_240_960_843);
/// Fee rate which is just below absurd threshold (1 sat/kwu less)
const JUST_BELOW_ABSURD_FEE_RATE: FeeRate = FeeRate::from_sat_per_kwu(15_060_240_960_842);
const ABSURD_FEE_RATE: FeeRate = match FeeRate::from_sat_per_kwu(15_060_240_960_843) {
Some(fee_rate) => fee_rate,
None => panic!("unreachable - no unwrap in Rust 1.63 in const"),
};
const JUST_BELOW_ABSURD_FEE_RATE: FeeRate = match FeeRate::from_sat_per_kwu(15_060_240_960_842)
{
Some(fee_rate) => fee_rate,
None => panic!("unreachable - no unwrap in Rust 1.63 in const"),
};
#[track_caller]
pub fn hex_psbt(s: &str) -> Result<Psbt, crate::psbt::error::Error> {
@ -1472,7 +1481,7 @@ mod tests {
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
_ => panic!(""),
}),
Err(FeeRate::from_sat_per_kwu(6250003)) // 6250000 is 25k sat/vbyte
Err(FeeRate::from_sat_per_kwu(6250003).unwrap()) // 6250000 is 25k sat/vbyte
);
// Lowering the input satoshis by 1 lowers the sat/kwu by 3

View File

@ -17,7 +17,7 @@ fn do_test(data: &[u8]) {
let _ = script.count_sigops_legacy();
let _ = script.minimal_non_dust();
let fee_rate = FeeRate::from_sat_per_kwu(consume_u64(&mut new_data));
let fee_rate = FeeRate::from_sat_per_kwu(consume_u64(&mut new_data)).unwrap();
let _ = script.minimal_non_dust_custom(fee_rate);
let mut b = script::Builder::new();

View File

@ -270,13 +270,13 @@ fn amount_checked_div_by_weight_ceil() {
let weight = Weight::from_kwu(1).unwrap();
let fee_rate = sat(1).checked_div_by_weight_ceil(weight).unwrap();
// 1 sats / 1,000 wu = 1 sats/kwu
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1).unwrap());
let weight = Weight::from_wu(381);
let fee_rate = sat(329).checked_div_by_weight_ceil(weight).unwrap();
// 329 sats / 381 wu = 863.5 sats/kwu
// round up to 864
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864).unwrap());
let fee_rate = Amount::ONE_SAT.checked_div_by_weight_ceil(Weight::ZERO);
assert!(fee_rate.is_none());
@ -288,13 +288,13 @@ fn amount_checked_div_by_weight_floor() {
let weight = Weight::from_kwu(1).unwrap();
let fee_rate = sat(1).checked_div_by_weight_floor(weight).unwrap();
// 1 sats / 1,000 wu = 1 sats/kwu
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1).unwrap());
let weight = Weight::from_wu(381);
let fee_rate = sat(329).checked_div_by_weight_floor(weight).unwrap();
// 329 sats / 381 wu = 863.5 sats/kwu
// round down to 863
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863).unwrap());
let fee_rate = Amount::ONE_SAT.checked_div_by_weight_floor(Weight::ZERO);
assert!(fee_rate.is_none());
@ -304,7 +304,7 @@ fn amount_checked_div_by_weight_floor() {
#[test]
fn amount_checked_div_by_fee_rate() {
let amount = sat(1000);
let fee_rate = FeeRate::from_sat_per_kwu(2);
let fee_rate = FeeRate::from_sat_per_kwu(2).unwrap();
// Test floor division
let weight = amount.checked_div_by_fee_rate_floor(fee_rate).unwrap();
@ -317,20 +317,20 @@ fn amount_checked_div_by_fee_rate() {
// Test truncation behavior
let amount = sat(1000);
let fee_rate = FeeRate::from_sat_per_kwu(3);
let fee_rate = FeeRate::from_sat_per_kwu(3).unwrap();
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);
let zero_fee_rate = FeeRate::from_sat_per_kwu(0).unwrap();
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 small_fee_rate = FeeRate::from_sat_per_kwu(1).unwrap();
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));

View File

@ -26,12 +26,17 @@ impl Amount {
/// Returns [`None`] if overflow occurred.
#[must_use]
pub const fn checked_div_by_weight_floor(self, weight: Weight) -> Option<FeeRate> {
// No `?` operator in const context.
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(res) => match res.checked_div(weight.to_wu()) {
Some(fee_rate) => Some(FeeRate::from_sat_per_kwu(fee_rate)),
None => None,
},
Some(sats) => {
let fee_rate = sats / wu;
FeeRate::from_sat_per_kwu(fee_rate)
}
None => None,
}
}
@ -51,20 +56,22 @@ impl Amount {
/// 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, Some(FeeRate::from_sat_per_kwu(34)));
/// 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<FeeRate> {
let wu = weight.to_wu();
// No `?` operator in const context.
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) {
if let Some(wu_minus_one) = wu.checked_sub(1) {
if let Some(sats_plus_wu_minus_one) = sats.checked_add(wu_minus_one) {
if let Some(fee_rate) = sats_plus_wu_minus_one.checked_div(wu) {
return Some(FeeRate::from_sat_per_kwu(fee_rate));
}
}
// 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
@ -79,14 +86,13 @@ impl 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,
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.
///
@ -96,30 +102,39 @@ impl Amount {
/// 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,
// 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, in weight units, returning
/// [`NumOpResult::Error`] if an overflow occurred.
/// Calculates the fee by multiplying this fee rate by weight.
///
/// This is equivalent to `Self::checked_mul_by_weight()`.
pub const fn to_fee(self, weight: Weight) -> NumOpResult<Amount> {
self.checked_mul_by_weight(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`]
@ -128,9 +143,7 @@ impl FeeRate {
/// 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<Amount> {
self.checked_mul_by_weight(weight).ok()
}
pub fn fee_wu(self, weight: Weight) -> Option<Amount> { 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.
@ -139,9 +152,7 @@ impl FeeRate {
/// `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<Amount> {
Weight::from_vb(vb).and_then(|w| self.to_fee(w).ok())
}
pub fn fee_vb(self, vb: u64) -> Option<Amount> { Weight::from_vb(vb).map(|w| self.to_fee(w)) }
/// Checked weight multiplication.
///
@ -149,16 +160,19 @@ impl FeeRate {
/// 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 [`NumOpResult::Error`] if overflow occurred.
pub const fn checked_mul_by_weight(self, weight: Weight) -> NumOpResult<Amount> {
if let Some(fee) = self.to_sat_per_kwu().checked_mul(weight.to_wu()) {
if let Some(round_up) = fee.checked_add(999) {
if let Ok(ret) = Amount::from_sat(round_up / 1_000) {
return NumOpResult::Valid(ret);
/// Returns [`None`] if overflow occurred.
pub const fn checked_mul_by_weight(self, weight: Weight) -> Option<Amount> {
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);
}
}
}
NumOpResult::Error(E::while_doing(MathOp::Mul))
None
}
}
@ -166,7 +180,10 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Mul<FeeRate> for Weight {
type Output = NumOpResult<Amount>;
fn mul(self, rhs: FeeRate) -> Self::Output {
rhs.checked_mul_by_weight(self)
match rhs.checked_mul_by_weight(self) {
Some(amount) => R::Valid(amount),
None => R::Error(E::while_doing(MathOp::Mul)),
}
}
}
impl ops::Mul<FeeRate> for NumOpResult<Weight> {
@ -203,7 +220,10 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Mul<Weight> for FeeRate {
type Output = NumOpResult<Amount>;
fn mul(self, rhs: Weight) -> Self::Output {
self.checked_mul_by_weight(rhs)
match self.checked_mul_by_weight(rhs) {
Some(amount) => R::Valid(amount),
None => R::Error(E::while_doing(MathOp::Mul)),
}
}
}
impl ops::Mul<Weight> for NumOpResult<FeeRate> {
@ -328,7 +348,7 @@ impl Weight {
/// enough instead of falling short if rounded down.
///
/// Returns [`None`] if overflow occurred.
pub const fn checked_mul_by_fee_rate(self, fee_rate: FeeRate) -> NumOpResult<Amount> {
pub const fn checked_mul_by_fee_rate(self, fee_rate: FeeRate) -> Option<Amount> {
fee_rate.checked_mul_by_weight(self)
}
}
@ -340,17 +360,14 @@ mod tests {
#[test]
fn fee_rate_div_by_weight() {
let fee_rate = (Amount::from_sat_u32(329) / Weight::from_wu(381)).unwrap();
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863).unwrap());
}
#[test]
fn fee_wu() {
let operation = FeeRate::from_sat_per_kwu(10).to_fee(Weight::MAX).unwrap_err().operation();
assert!(operation.is_multiplication());
let fee_rate = FeeRate::from_sat_per_vb(2).unwrap();
let weight = Weight::from_vb(3).unwrap();
assert_eq!(fee_rate.to_fee(weight).unwrap(), Amount::from_sat_u32(6));
assert_eq!(fee_rate.to_fee(weight), Amount::from_sat_u32(6));
}
#[test]
@ -362,8 +379,8 @@ mod tests {
.expect("expected Amount");
assert_eq!(Amount::from_sat_u32(100), fee);
let fee = FeeRate::from_sat_per_kwu(10).checked_mul_by_weight(Weight::MAX);
assert!(fee.is_error());
let fee = FeeRate::from_sat_per_kwu(10).unwrap().checked_mul_by_weight(Weight::MAX);
assert!(fee.is_none());
let weight = Weight::from_vb(3).unwrap();
let fee_rate = FeeRate::from_sat_per_vb(3).unwrap();
@ -371,7 +388,7 @@ mod tests {
assert_eq!(Amount::from_sat_u32(9), fee);
let weight = Weight::from_wu(381);
let fee_rate = FeeRate::from_sat_per_kwu(864);
let fee_rate = FeeRate::from_sat_per_kwu(864).unwrap();
let fee = weight.checked_mul_by_fee_rate(fee_rate).unwrap();
// 381 * 0.864 yields 329.18.
// The result is then rounded up to 330.
@ -398,7 +415,7 @@ mod tests {
fn amount_div_by_fee_rate() {
// Test exact division
let amount = Amount::from_sat_u32(1000);
let fee_rate = FeeRate::from_sat_per_kwu(2);
let fee_rate = FeeRate::from_sat_per_kwu(2).unwrap();
let weight = (amount / fee_rate).unwrap();
assert_eq!(weight, Weight::from_wu(500_000));
@ -412,7 +429,7 @@ mod tests {
// Test truncation behavior
let amount = Amount::from_sat_u32(1000);
let fee_rate = FeeRate::from_sat_per_kwu(3);
let fee_rate = FeeRate::from_sat_per_kwu(3).unwrap();
let weight = (amount / fee_rate).unwrap();
// 1000 * 1000 = 1,000,000 msats
// 1,000,000 / 3 = 333,333.33... wu
@ -424,7 +441,7 @@ mod tests {
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);
let zero_rate = FeeRate::from_sat_per_kwu(0).unwrap();
assert!(amount.checked_div_by_fee_rate_floor(zero_rate).is_none());
assert!(amount.checked_div_by_fee_rate_ceil(zero_rate).is_none());
}

View File

@ -14,74 +14,98 @@ use arbitrary::{Arbitrary, Unstructured};
mod encapsulate {
/// Fee rate.
///
/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection
/// against mixing up the types as well as basic formatting features.
/// This is an integer newtype representing fee rate. It provides protection
/// against mixing up the types, conversion functions, and basic formatting.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct FeeRate(u64);
impl FeeRate {
/// Constructs a new [`FeeRate`] from satoshis per 1000 weight units.
pub const fn from_sat_per_kwu(sat_kwu: u64) -> Self { FeeRate(sat_kwu) }
/// Constructs a new [`FeeRate`] from satoshis per 1,000,000 virtual bytes.
pub(crate) const fn from_sat_per_mvb(sat_mvb: u64) -> Self { Self(sat_mvb) }
/// Returns raw fee rate.
///
/// Can be used instead of `into()` to avoid inference issues.
pub const fn to_sat_per_kwu(self) -> u64 { self.0 }
/// Converts to sat/MvB.
pub(crate) const fn to_sat_per_mvb(self) -> u64 { self.0 }
}
}
#[doc(inline)]
pub use encapsulate::FeeRate;
impl FeeRate {
/// 0 sat/kwu.
/// The zero fee rate.
///
/// Equivalent to [`MIN`](Self::MIN), may better express intent in some contexts.
pub const ZERO: FeeRate = FeeRate::from_sat_per_kwu(0);
pub const ZERO: FeeRate = FeeRate::from_sat_per_mvb(0);
/// Minimum possible value (0 sat/kwu).
/// The minimum possible value.
///
/// Equivalent to [`ZERO`](Self::ZERO), may better express intent in some contexts.
pub const MIN: FeeRate = FeeRate::ZERO;
/// Maximum possible value.
pub const MAX: FeeRate = FeeRate::from_sat_per_kwu(u64::MAX);
/// The maximum possible value.
pub const MAX: FeeRate = FeeRate::from_sat_per_mvb(u64::MAX);
/// Minimum fee rate required to broadcast a transaction.
/// The minimum fee rate required to broadcast a transaction.
///
/// The value matches the default Bitcoin Core policy at the time of library release.
pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb_u32(1);
/// Fee rate used to compute dust amount.
/// The fee rate used to compute dust amount.
pub const DUST: FeeRate = FeeRate::from_sat_per_vb_u32(3);
/// Constructs a new [`FeeRate`] from satoshis per 1000 weight units.
pub const fn from_sat_per_kwu(sat_kwu: u64) -> Option<Self> {
// No `map()` in const context.
match sat_kwu.checked_mul(4_000) {
Some(fee_rate) => Some(FeeRate::from_sat_per_mvb(fee_rate)),
None => None,
}
}
/// Constructs a new [`FeeRate`] from satoshis per virtual bytes.
///
/// # Errors
///
/// Returns [`None`] on arithmetic overflow.
pub fn from_sat_per_vb(sat_vb: u64) -> Option<Self> {
// 1 vb == 4 wu
// 1 sat/vb == 1/4 sat/wu
// sat/vb * 1000 / 4 == sat/kwu
Some(FeeRate::from_sat_per_kwu(sat_vb.checked_mul(1000 / 4)?))
pub const fn from_sat_per_vb(sat_vb: u64) -> Option<Self> {
// No `map()` in const context.
match sat_vb.checked_mul(1_000_000) {
Some(fee_rate) => Some(FeeRate::from_sat_per_mvb(fee_rate)),
None => None,
}
}
/// Constructs a new [`FeeRate`] from satoshis per virtual bytes.
pub const fn from_sat_per_vb_u32(sat_vb: u32) -> Self {
let sat_vb = sat_vb as u64; // No `Into` in const context.
FeeRate::from_sat_per_kwu(sat_vb * (1000 / 4))
FeeRate::from_sat_per_mvb(sat_vb * 1_000_000)
}
/// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes).
pub const fn from_sat_per_kvb(sat_kvb: u64) -> Self { FeeRate::from_sat_per_kwu(sat_kvb / 4) }
pub const fn from_sat_per_kvb(sat_kvb: u64) -> Option<Self> {
// No `map()` in const context.
match sat_kvb.checked_mul(1_000) {
Some(fee_rate) => Some(FeeRate::from_sat_per_mvb(fee_rate)),
None => None,
}
}
/// Converts to sat/kwu rounding down.
pub const fn to_sat_per_kwu_floor(self) -> u64 { self.to_sat_per_mvb() / 4_000 }
/// Converts to sat/kwu rounding up.
pub const fn to_sat_per_kwu_ceil(self) -> u64 { (self.to_sat_per_mvb() + 3_999) / 4_000 }
/// Converts to sat/vB rounding down.
pub const fn to_sat_per_vb_floor(self) -> u64 { self.to_sat_per_kwu() / (1000 / 4) }
pub const fn to_sat_per_vb_floor(self) -> u64 { self.to_sat_per_mvb() / 1_000_000 }
/// Converts to sat/vB rounding up.
pub const fn to_sat_per_vb_ceil(self) -> u64 {
(self.to_sat_per_kwu() + (1000 / 4 - 1)) / (1000 / 4)
}
pub const fn to_sat_per_vb_ceil(self) -> u64 { (self.to_sat_per_mvb() + 999_999) / 1_000_000 }
/// Converts to sat/kvb rounding down.
pub const fn to_sat_per_kvb_floor(self) -> u64 { self.to_sat_per_mvb() / 1_000 }
/// Converts to sat/kvb rounding up.
pub const fn to_sat_per_kvb_ceil(self) -> u64 { (self.to_sat_per_mvb() + 999) / 1_000 }
/// Checked multiplication.
///
@ -89,8 +113,8 @@ impl FeeRate {
#[must_use]
pub const fn checked_mul(self, rhs: u64) -> Option<Self> {
// No `map()` in const context.
match self.to_sat_per_kwu().checked_mul(rhs) {
Some(res) => Some(Self::from_sat_per_kwu(res)),
match self.to_sat_per_mvb().checked_mul(rhs) {
Some(res) => Some(Self::from_sat_per_mvb(res)),
None => None,
}
}
@ -101,8 +125,8 @@ impl FeeRate {
#[must_use]
pub const fn checked_div(self, rhs: u64) -> Option<Self> {
// No `map()` in const context.
match self.to_sat_per_kwu().checked_div(rhs) {
Some(res) => Some(Self::from_sat_per_kwu(res)),
match self.to_sat_per_mvb().checked_div(rhs) {
Some(res) => Some(Self::from_sat_per_mvb(res)),
None => None,
}
}
@ -113,8 +137,8 @@ impl FeeRate {
#[must_use]
pub const fn checked_add(self, rhs: FeeRate) -> Option<Self> {
// No `map()` in const context.
match self.to_sat_per_kwu().checked_add(rhs.to_sat_per_kwu()) {
Some(res) => Some(Self::from_sat_per_kwu(res)),
match self.to_sat_per_mvb().checked_add(rhs.to_sat_per_mvb()) {
Some(res) => Some(Self::from_sat_per_mvb(res)),
None => None,
}
}
@ -125,8 +149,8 @@ impl FeeRate {
#[must_use]
pub const fn checked_sub(self, rhs: FeeRate) -> Option<Self> {
// No `map()` in const context.
match self.to_sat_per_kwu().checked_sub(rhs.to_sat_per_kwu()) {
Some(res) => Some(Self::from_sat_per_kwu(res)),
match self.to_sat_per_mvb().checked_sub(rhs.to_sat_per_mvb()) {
Some(res) => Some(Self::from_sat_per_mvb(res)),
None => None,
}
}
@ -136,19 +160,19 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Add<FeeRate> for FeeRate {
type Output = FeeRate;
fn add(self, rhs: FeeRate) -> Self::Output { FeeRate::from_sat_per_kwu(self.to_sat_per_kwu() + rhs.to_sat_per_kwu()) }
fn add(self, rhs: FeeRate) -> Self::Output { FeeRate::from_sat_per_mvb(self.to_sat_per_mvb() + rhs.to_sat_per_mvb()) }
}
impl ops::Sub<FeeRate> for FeeRate {
type Output = FeeRate;
fn sub(self, rhs: FeeRate) -> Self::Output { FeeRate::from_sat_per_kwu(self.to_sat_per_kwu() - rhs.to_sat_per_kwu()) }
fn sub(self, rhs: FeeRate) -> Self::Output { FeeRate::from_sat_per_mvb(self.to_sat_per_mvb() - rhs.to_sat_per_mvb()) }
}
impl ops::Div<NonZeroU64> for FeeRate {
type Output = FeeRate;
fn div(self, rhs: NonZeroU64) -> Self::Output{ Self::from_sat_per_kwu(self.to_sat_per_kwu() / rhs.get()) }
fn div(self, rhs: NonZeroU64) -> Self::Output{ Self::from_sat_per_mvb(self.to_sat_per_mvb() / rhs.get()) }
}
}
crate::internal_macros::impl_add_assign!(FeeRate);
@ -159,7 +183,7 @@ impl core::iter::Sum for FeeRate {
where
I: Iterator<Item = Self>,
{
FeeRate::from_sat_per_kwu(iter.map(FeeRate::to_sat_per_kwu).sum())
FeeRate::from_sat_per_mvb(iter.map(FeeRate::to_sat_per_mvb).sum())
}
}
@ -168,7 +192,7 @@ impl<'a> core::iter::Sum<&'a FeeRate> for FeeRate {
where
I: Iterator<Item = &'a FeeRate>,
{
FeeRate::from_sat_per_kwu(iter.map(|f| FeeRate::to_sat_per_kwu(*f)).sum())
FeeRate::from_sat_per_mvb(iter.map(|f| FeeRate::to_sat_per_mvb(*f)).sum())
}
}
@ -181,7 +205,7 @@ impl<'a> Arbitrary<'a> for FeeRate {
1 => Ok(FeeRate::BROADCAST_MIN),
2 => Ok(FeeRate::DUST),
3 => Ok(FeeRate::MAX),
_ => Ok(FeeRate::from_sat_per_kwu(u64::arbitrary(u)?)),
_ => Ok(FeeRate::from_sat_per_mvb(u64::arbitrary(u)?)),
}
}
}
@ -195,18 +219,18 @@ mod tests {
#[test]
#[allow(clippy::op_ref)]
fn feerate_div_nonzero() {
let rate = FeeRate::from_sat_per_kwu(200);
let rate = FeeRate::from_sat_per_kwu(200).unwrap();
let divisor = NonZeroU64::new(2).unwrap();
assert_eq!(rate / divisor, FeeRate::from_sat_per_kwu(100));
assert_eq!(&rate / &divisor, FeeRate::from_sat_per_kwu(100));
assert_eq!(rate / divisor, FeeRate::from_sat_per_kwu(100).unwrap());
assert_eq!(&rate / &divisor, FeeRate::from_sat_per_kwu(100).unwrap());
}
#[test]
#[allow(clippy::op_ref)]
fn addition() {
let one = FeeRate::from_sat_per_kwu(1);
let two = FeeRate::from_sat_per_kwu(2);
let three = FeeRate::from_sat_per_kwu(3);
let one = FeeRate::from_sat_per_kwu(1).unwrap();
let two = FeeRate::from_sat_per_kwu(2).unwrap();
let three = FeeRate::from_sat_per_kwu(3).unwrap();
assert!(one + two == three);
assert!(&one + two == three);
@ -217,9 +241,9 @@ mod tests {
#[test]
#[allow(clippy::op_ref)]
fn subtract() {
let three = FeeRate::from_sat_per_kwu(3);
let seven = FeeRate::from_sat_per_kwu(7);
let ten = FeeRate::from_sat_per_kwu(10);
let three = FeeRate::from_sat_per_kwu(3).unwrap();
let seven = FeeRate::from_sat_per_kwu(7).unwrap();
let ten = FeeRate::from_sat_per_kwu(10).unwrap();
assert_eq!(ten - seven, three);
assert_eq!(&ten - seven, three);
@ -229,43 +253,44 @@ mod tests {
#[test]
fn add_assign() {
let mut f = FeeRate::from_sat_per_kwu(1);
f += FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(3));
let mut f = FeeRate::from_sat_per_kwu(1).unwrap();
f += FeeRate::from_sat_per_kwu(2).unwrap();
assert_eq!(f, FeeRate::from_sat_per_kwu(3).unwrap());
let mut f = FeeRate::from_sat_per_kwu(1);
f += &FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(3));
let mut f = FeeRate::from_sat_per_kwu(1).unwrap();
f += &FeeRate::from_sat_per_kwu(2).unwrap();
assert_eq!(f, FeeRate::from_sat_per_kwu(3).unwrap());
}
#[test]
fn sub_assign() {
let mut f = FeeRate::from_sat_per_kwu(3);
f -= FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(1));
let mut f = FeeRate::from_sat_per_kwu(3).unwrap();
f -= FeeRate::from_sat_per_kwu(2).unwrap();
assert_eq!(f, FeeRate::from_sat_per_kwu(1).unwrap());
let mut f = FeeRate::from_sat_per_kwu(3);
f -= &FeeRate::from_sat_per_kwu(2);
assert_eq!(f, FeeRate::from_sat_per_kwu(1));
let mut f = FeeRate::from_sat_per_kwu(3).unwrap();
f -= &FeeRate::from_sat_per_kwu(2).unwrap();
assert_eq!(f, FeeRate::from_sat_per_kwu(1).unwrap());
}
#[test]
fn checked_add() {
let one = FeeRate::from_sat_per_kwu(1);
let two = FeeRate::from_sat_per_kwu(2);
let three = FeeRate::from_sat_per_kwu(3);
let one = FeeRate::from_sat_per_kwu(1).unwrap();
let two = FeeRate::from_sat_per_kwu(2).unwrap();
let three = FeeRate::from_sat_per_kwu(3).unwrap();
assert_eq!(one.checked_add(two).unwrap(), three);
let fee_rate = FeeRate::from_sat_per_kwu(u64::MAX).checked_add(one);
assert!(FeeRate::from_sat_per_kvb(u64::MAX).is_none()); // sanity check.
let fee_rate = FeeRate::from_sat_per_mvb(u64::MAX).checked_add(one);
assert!(fee_rate.is_none());
}
#[test]
fn checked_sub() {
let one = FeeRate::from_sat_per_kwu(1);
let two = FeeRate::from_sat_per_kwu(2);
let three = FeeRate::from_sat_per_kwu(3);
let one = FeeRate::from_sat_per_kwu(1).unwrap();
let two = FeeRate::from_sat_per_kwu(2).unwrap();
let three = FeeRate::from_sat_per_kwu(3).unwrap();
assert_eq!(three.checked_sub(two).unwrap(), one);
let fee_rate = FeeRate::ZERO.checked_sub(one);
@ -274,23 +299,23 @@ mod tests {
#[test]
fn fee_rate_const() {
assert_eq!(FeeRate::ZERO.to_sat_per_kwu(), 0);
assert_eq!(FeeRate::MIN.to_sat_per_kwu(), u64::MIN);
assert_eq!(FeeRate::MAX.to_sat_per_kwu(), u64::MAX);
assert_eq!(FeeRate::BROADCAST_MIN.to_sat_per_kwu(), 250);
assert_eq!(FeeRate::DUST.to_sat_per_kwu(), 750);
assert_eq!(FeeRate::ZERO.to_sat_per_kwu_floor(), 0);
assert_eq!(FeeRate::MIN.to_sat_per_kwu_floor(), u64::MIN);
assert_eq!(FeeRate::MAX.to_sat_per_kwu_floor(), u64::MAX / 4_000);
assert_eq!(FeeRate::BROADCAST_MIN.to_sat_per_kwu_floor(), 250);
assert_eq!(FeeRate::DUST.to_sat_per_kwu_floor(), 750);
}
#[test]
fn fee_rate_from_sat_per_vb() {
let fee_rate = FeeRate::from_sat_per_vb(10).expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500).unwrap());
}
#[test]
fn fee_rate_from_sat_per_kvb() {
let fee_rate = FeeRate::from_sat_per_kvb(11);
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2));
let fee_rate = FeeRate::from_sat_per_kvb(11).unwrap();
assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(11_000));
}
#[test]
@ -302,7 +327,7 @@ mod tests {
#[test]
fn from_sat_per_vb_u32() {
let fee_rate = FeeRate::from_sat_per_vb_u32(10);
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500));
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500).unwrap());
}
#[test]
@ -311,29 +336,40 @@ mod tests {
#[test]
fn raw_feerate() {
let fee_rate = FeeRate::from_sat_per_kwu(749);
assert_eq!(fee_rate.to_sat_per_kwu(), 749);
let fee_rate = FeeRate::from_sat_per_kwu(749).unwrap();
assert_eq!(fee_rate.to_sat_per_kwu_floor(), 749);
assert_eq!(fee_rate.to_sat_per_vb_floor(), 2);
assert_eq!(fee_rate.to_sat_per_vb_ceil(), 3);
}
#[test]
fn checked_mul() {
let fee_rate =
FeeRate::from_sat_per_kwu(10).checked_mul(10).expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(100));
let fee_rate = FeeRate::from_sat_per_kwu(10)
.unwrap()
.checked_mul(10)
.expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(100).unwrap());
let fee_rate = FeeRate::from_sat_per_kwu(10).checked_mul(u64::MAX);
let fee_rate = FeeRate::from_sat_per_kwu(10).unwrap().checked_mul(u64::MAX);
assert!(fee_rate.is_none());
}
#[test]
fn checked_div() {
let fee_rate =
FeeRate::from_sat_per_kwu(10).checked_div(10).expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1));
let fee_rate = FeeRate::from_sat_per_kwu(10)
.unwrap()
.checked_div(10)
.expect("expected feerate in sat/kwu");
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(1).unwrap());
let fee_rate = FeeRate::from_sat_per_kwu(10).checked_div(0);
let fee_rate = FeeRate::from_sat_per_kwu(10).unwrap().checked_div(0);
assert!(fee_rate.is_none());
}
#[test]
fn mvb() {
let fee_rate = FeeRate::from_sat_per_mvb(1_234_567);
let got = fee_rate.to_sat_per_mvb();
assert_eq!(got, 1_234_567);
}
}

View File

@ -18,7 +18,7 @@
//!
//! #[derive(Serialize, Deserialize)]
//! pub struct Foo {
//! #[serde(with = "fee_rate::serde::as_sat_per_kwu")]
//! #[serde(with = "fee_rate::serde::as_sat_per_kwu_floor")]
//! pub fee_rate: FeeRate,
//! }
//! ```
@ -26,7 +26,7 @@
use core::convert::Infallible;
use core::fmt;
pub mod as_sat_per_kwu {
pub mod as_sat_per_kwu_floor {
//! Serialize and deserialize [`FeeRate`] denominated in satoshis per 1000 weight units.
//!
//! Use with `#[serde(with = "fee_rate::serde::as_sat_per_kwu")]`.
@ -36,11 +36,12 @@ pub mod as_sat_per_kwu {
use crate::FeeRate;
pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> {
u64::serialize(&f.to_sat_per_kwu(), s)
u64::serialize(&f.to_sat_per_kwu_floor(), s)
}
pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> {
Ok(FeeRate::from_sat_per_kwu(u64::deserialize(d)?))
FeeRate::from_sat_per_kwu(u64::deserialize(d)?)
.ok_or_else(|| serde::de::Error::custom("overflowed sats/kwu"))
}
pub mod opt {
@ -57,7 +58,7 @@ pub mod as_sat_per_kwu {
#[allow(clippy::ref_option)] // API forced by serde.
pub fn serialize<S: Serializer>(f: &Option<FeeRate>, s: S) -> Result<S::Ok, S::Error> {
match *f {
Some(f) => s.serialize_some(&f.to_sat_per_kwu()),
Some(f) => s.serialize_some(&f.to_sat_per_kwu_floor()),
None => s.serialize_none(),
}
}

View File

@ -90,23 +90,6 @@ pub enum NumOpResult<T> {
impl<T> NumOpResult<T> {
/// Maps a `NumOpResult<T>` to `NumOpResult<U>` by applying a function to a
/// contained [`NumOpResult::Valid`] value, leaving a [`NumOpResult::Error`] value untouched.
///
/// # Examples
///
/// ```
/// use bitcoin_units::{FeeRate, Amount, Weight, SignedAmount};
///
/// let fee_rate = FeeRate::from_sat_per_vb(1).unwrap();
/// let weight = Weight::from_wu(1000);
/// let amount = Amount::from_sat_u32(1_000_000);
///
/// let amount_after_fee = fee_rate
/// .to_fee(weight) // (1 sat/ 4 wu) * (1000 wu) = 250 sat fee
/// .map(|fee| fee.to_signed())
/// .and_then(|fee| amount.to_signed() - fee);
///
/// assert_eq!(amount_after_fee.unwrap(), SignedAmount::from_sat_i32(999_750))
/// ```
#[inline]
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> NumOpResult<U> {
match self {

View File

@ -37,13 +37,13 @@ struct Serde {
vb_floor: FeeRate,
#[serde(with = "fee_rate::serde::as_sat_per_vb_ceil")]
vb_ceil: FeeRate,
#[serde(with = "fee_rate::serde::as_sat_per_kwu")]
#[serde(with = "fee_rate::serde::as_sat_per_kwu_floor")]
kwu: FeeRate,
#[serde(with = "fee_rate::serde::as_sat_per_vb_floor::opt")]
opt_vb_floor: Option<FeeRate>,
#[serde(with = "fee_rate::serde::as_sat_per_vb_ceil::opt")]
opt_vb_ceil: Option<FeeRate>,
#[serde(with = "fee_rate::serde::as_sat_per_kwu::opt")]
#[serde(with = "fee_rate::serde::as_sat_per_kwu_floor::opt")]
opt_kwu: Option<FeeRate>,
a: BlockHeight,