Merge rust-bitcoin/rust-bitcoin#6061: Units improvements
What changed, and why it matters
This is a routine code-quality and API-expansion pull request for the rust-bitcoin library's 'units' crate. It widens some fee-rate constructors from 32-bit to 64-bit inputs, renames the old 32-bit versions, improves serde error messages, and removes an artificial restriction. The changes are mostly additive or renaming; they do not appear to fix a known vulnerability and the commit message does not describe any security issue.
Treat as a normal API-maintenance merge. Reviewers should verify that the new u64 constructors correctly propagate overflow errors and that the renamed `_u32` methods preserve previous behavior. Downstream projects should update call sites because `from_sat_per_vb`/`from_sat_per_kwu`/`from_sat_per_kvb` signatures have changed. No urgent security action is indicated by the supplied materials.
Security signals we found
API surface change: new u64 fee-rate constructors return NumOpResult to prevent silent overflow
Renaming of existing u32 constructors to *_u32 may break downstream callers at compile time (breaking API change)
Removal of artificial restriction in from_per_vb allows larger valid inputs
Serde deserialization now uses custom Visitor with explicit range expectations
No mention of vulnerability, CVE, or security bug in commit or PR description
Evidence from the diff
The merge commit bundles five commits: (1) better serde deserialization error messages for Amount/SignedAmount using custom visitors; (2-3) new u64-taking FeeRate constructors from_sat_per_vb, from_sat_per_kvb, from_sat_per_kwu returning NumOpResult, while renaming the original u32-taking methods with _u32 suffixes; (4) test deduplication; (5) removal of an artificial restriction in from_per_vb by using rate.to_sat().checked_mul(1_000_000) instead of rate.checked_mul. Call sites are updated to use the renamed _u32 methods. The p2p FeeFilter decoder now constructs fee rates directly from large kvb values rather than multiplying a 1 sat/kvb base rate. No CVE, advisory, or security disclosure is referenced.
Changed components
units/src/fee_rate/mod.rsunits/src/fee_rate/serde.rsunits/src/amount/serde.rsp2p/src/message.rsunits/src/amount/unsigned.rsunits/src/fee.rsunits/src/result.rsbitcoin/src/blockdata/script/tests.rsbitcoin/src/blockdata/transaction.rsunits/src/amount/tests.rsunits/tests/serde.rsInspect captured patch +266 / −122
### bitcoin/src/blockdata/script/tests.rs
@@ -705,7 +705,7 @@ fn default_dust_value() {
assert!(script_p2wpkh.is_p2wpkh());
assert_eq!(script_p2wpkh.minimal_non_dust(), Amount::from_sat_u32(294));
assert_eq!(
- script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb(6)),
+ script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_u32(6)),
Some(Amount::from_sat_u32(588))
);
@@ -719,7 +719,7 @@ fn default_dust_value() {
assert!(script_p2pkh.is_p2pkh());
assert_eq!(script_p2pkh.minimal_non_dust(), Amount::from_sat_u32(546));
assert_eq!(
- script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb(6)),
+ script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_u32(6)),
Some(Amount::from_sat_u32(1092))
);
}
### bitcoin/src/blockdata/transaction.rs
@@ -1396,7 +1396,7 @@ 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 fee_rate = FeeRate::from_sat_per_kwu_u32(10);
let effective_value = effective_value(fee_rate, InputWeightPrediction::P2WPKH_MAX, value);
// 10 sat/kwu * 272 wu = 3 sats (rounding up)
### p2p/src/message.rs
@@ -474,8 +474,7 @@ crate::decoder_newtype! {
// We can't directly construct using kvb with any public constructors on FeeRate
// because the rate can be up to Amount::MAX_MONEY which overflows all of them.
// Instead, we construct a 1 sat/kvb and multiply by our kvb.
- let fee_rate = FeeRate::from_sat_per_kvb(1)
- .checked_mul(kvb)
+ let fee_rate = FeeRate::from_sat_per_kvb(kvb)
.expect("Amount::MAX_MONEY * 1000 < u64::MAX");
Ok(FeeFilter(fee_rate))
@@ -490,12 +489,14 @@ impl encoding::Decode for FeeFilter {
#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for FeeFilter {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ const MAX_SAT_PER_KVB: u64 = 18_446_744_073_709_551;
+
let choice = u.int_in_range(0..=3)?;
match choice {
0 => Ok(Self(FeeRate::MIN)),
1 => Ok(Self(FeeRate::BROADCAST_MIN)),
2 => Ok(Self(FeeRate::DUST)),
- _ => Ok(Self(FeeRate::from_sat_per_kvb(u.int_in_range(0..=u32::MAX)?))),
+ _ => Ok(Self(FeeRate::from_sat_per_kvb(u.int_in_range(0..=MAX_SAT_PER_KVB)?).unwrap())),
}
}
}
@@ -2850,7 +2851,7 @@ mod test {
);
let bytes = max_money.to_le_bytes();
let max_feerate = FeeFilter(
- FeeRate::from_sat_per_kvb(1).checked_mul(Amount::MAX_MONEY.to_sat()).unwrap(),
+ FeeRate::from_sat_per_kvb(Amount::MAX_MONEY.to_sat()).unwrap(),
);
assert_eq!(encoding::decode_from_slice::<FeeFilter>(&bytes).unwrap(), max_feerate);
### units/src/amount/serde.rs
@@ -61,10 +61,22 @@ pub mod as_sat {
//! [`Amount`]: crate::Amount
//! [`SignedAmount`]: crate::SignedAmount
- use serde::{Deserialize, Deserializer, Serialize, Serializer};
+ use core::fmt;
+ use core::marker::PhantomData;
+ use serde::{Deserializer, Serialize, Serializer};
use crate::SignedAmount;
+ fn is_signed<T: TryFrom<SignedAmount>>() -> bool {
+ T::try_from(-SignedAmount::ONE_SAT).is_ok()
+ }
+
+ #[test]
+ fn is_signed_correct() {
+ assert!(!is_signed::<crate::Amount>());
+ assert!(is_signed::<crate::SignedAmount>());
+ }
+
#[inline]
pub fn serialize<A, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error>
where
@@ -80,10 +92,62 @@ pub mod as_sat {
A: TryFrom<SignedAmount>,
<A as TryFrom<SignedAmount>>::Error: core::fmt::Display,
{
- let sat = i64::deserialize(d)?;
- let amount = SignedAmount::from_sat(sat).map_err(serde::de::Error::custom)?;
+ fn expecting<T: TryFrom<SignedAmount>>() -> &'static str {
+ if is_signed::<T>() {
+ "an integer between -2100000000000000 and 2100000000000000 inclusive"
+ } else {
+ "an integer between 0 and 2100000000000000 inclusive"
+ }
+ }
- A::try_from(amount).map_err(serde::de::Error::custom)
+ // We use custom visitor to have better control over error messages
+ struct Visitor<T>(PhantomData<fn() -> T>);
+
+ impl<'de, T> serde::de::Visitor<'de> for Visitor<T> where T: TryFrom<SignedAmount> {
+ type Value = T;
+
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(expecting::<T>())
+ }
+
+ fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
+ fn range_error<T, E1, E2: serde::de::Error>(value: i64) -> impl FnOnce(E1) -> E2
+ where T: TryFrom<SignedAmount>
+ {
+ move |_| {
+ let unexpected = serde::de::Unexpected::Signed(value);
+ E2::invalid_value(unexpected, &expecting::<T>())
+ }
+ }
+
+ SignedAmount::from_sat(value)
+ .map_err(range_error::<T, _, _>(value))?
+ .try_into()
+ .map_err(range_error::<T, _, _>(value))
+ }
+
+ fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
+ fn range_error<T, E1, E2: serde::de::Error>(value: u64) -> impl FnOnce(E1) -> E2
+ where T: TryFrom<SignedAmount>
+ {
+ move |_| {
+ let unexpected = serde::de::Unexpected::Unsigned(value);
+ E2::invalid_value(unexpected, &expecting::<T>())
+ }
+ }
+
+ let signed = i64::try_from(value).map_err(range_error::<T, _, _>(value))?;
+ SignedAmount::from_sat(signed)
+ .map_err(range_error::<T, _, _>(value))?
+ .try_into()
+ .map_err(range_error::<T, _, _>(value))
+ }
+ }
+ if is_signed::<A>() {
+ d.deserialize_i64(Visitor(PhantomData))
+ } else {
+ d.deserialize_u64(Visitor(PhantomData))
+ }
}
pub mod opt {
@@ -95,6 +159,7 @@ pub mod as_sat {
use core::fmt;
use core::marker::PhantomData;
+ use super::is_signed;
use serde::{de, Deserializer, Serialize, Serializer};
use crate::SignedAmount;
@@ -113,6 +178,14 @@ pub mod as_sat {
A: TryFrom<SignedAmount>,
<A as TryFrom<SignedAmount>>::Error: core::fmt::Display,
{
+ fn expecting<T: TryFrom<SignedAmount>>() -> &'static str {
+ if is_signed::<T>() {
+ "an optional integer between -2100000000000000 and 2100000000000000 inclusive"
+ } else {
+ "an optional integer between 0 and 2100000000000000 inclusive"
+ }
+ }
+
struct VisitOptAmt<X>(PhantomData<X>);
impl<'de, X> de::Visitor<'de> for VisitOptAmt<X>
@@ -123,7 +196,7 @@ pub mod as_sat {
type Value = Option<X>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- write!(formatter, "an Option<i64>")
+ formatter.write_str(expecting::<X>())
}
#[inline]
@@ -159,6 +232,7 @@ pub mod as_sat {
use alloc::vec::Vec;
use core::fmt;
use core::marker::PhantomData;
+ use super::is_signed;
use serde::de::{self, SeqAccess};
use serde::{Deserialize, Deserializer, Serializer};
@@ -178,6 +252,14 @@ pub mod as_sat {
A: TryFrom<SignedAmount>,
<A as TryFrom<SignedAmount>>::Error: core::fmt::Display,
{
+ fn expecting<T: TryFrom<SignedAmount>>() -> &'static str {
+ if is_signed::<T>() {
+ "an sequence of integers between -2100000000000000 and 2100000000000000 inclusive"
+ } else {
+ "an sequence of integers between 0 and 2100000000000000 inclusive"
+ }
+ }
+
struct VisitVec<X>(PhantomData<X>);
impl<'de, X> de::Visitor<'de> for VisitVec<X>
@@ -188,7 +270,7 @@ pub mod as_sat {
type Value = Vec<X>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "a sequence of i64")
+ f.write_str(expecting::<X>())
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
@@ -197,11 +279,11 @@ pub mod as_sat {
{
#[derive(Deserialize)]
#[serde(transparent)]
- struct Wrapper(#[serde(with = "super")] SignedAmount);
+ struct Wrapper<T: TryFrom<SignedAmount>>(#[serde(with = "super")] T) where T::Error: core::fmt::Display;
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
- while let Some(wrapped) = seq.next_element::<Wrapper>()? {
- out.push(X::try_from(wrapped.0).map_err(de::Error::custom)?);
+ while let Some(wrapped) = seq.next_element::<Wrapper<X>>()? {
+ out.push(wrapped.0);
}
Ok(out)
}
### units/src/amount/tests.rs
@@ -286,13 +286,16 @@ fn positive_sub() {
assert!(ssat(10).positive_sub(ssat(11)).is_none());
}
+#[cfg(feature = "alloc")]
+const ONE_SAT_PER_KWU: FeeRate = FeeRate::from_sat_per_kwu_u32(1);
+
#[test]
#[cfg(feature = "alloc")]
fn amount_checked_div_by_weight_ceil() {
let weight = Weight::from_kwu(1).unwrap();
let fee_rate = sat(1).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, ONE_SAT_PER_KWU);
let weight = Weight::from_wu(381);
let fee_rate = sat(329).div_by_weight_ceil(weight).unwrap();
@@ -311,7 +314,7 @@ fn amount_checked_div_by_weight_floor() {
let weight = Weight::from_kwu(1).unwrap();
let fee_rate = sat(1).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, ONE_SAT_PER_KWU);
let weight = Weight::from_wu(381);
let fee_rate = sat(329).div_by_weight_floor(weight).unwrap();
@@ -328,7 +331,7 @@ fn amount_checked_div_by_weight_floor() {
#[cfg(feature = "alloc")]
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_u32(2);
// Test floor division
let weight = amount.div_by_fee_rate_floor(fee_rate).unwrap();
@@ -341,21 +344,19 @@ 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_u32(3);
let floor_weight = amount.div_by_fee_rate_floor(fee_rate).unwrap();
let ceil_weight = amount.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.div_by_fee_rate_floor(zero_fee_rate).is_error());
- assert!(amount.div_by_fee_rate_ceil(zero_fee_rate).is_error());
+ assert!(amount.div_by_fee_rate_floor(FeeRate::ZERO).is_error());
+ assert!(amount.div_by_fee_rate_ceil(FeeRate::ZERO).is_error());
// Test with maximum amount
let max_amount = Amount::MAX;
- let small_fee_rate = FeeRate::from_sat_per_kwu(1);
- let weight = max_amount.div_by_fee_rate_floor(small_fee_rate).unwrap();
+ let weight = max_amount.div_by_fee_rate_floor(ONE_SAT_PER_KWU).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));
}
@@ -365,7 +366,7 @@ fn amount_checked_div_by_fee_rate() {
fn div_by_fee_rate_floor_preserves_mvb_precision() {
// 1001 sat/kvb is 1001 sat per 4,000,000 wu, so a 1000 sat budget funds at most
// floor(1000 * 4,000,000 / 1,001,000) = 3996 wu.
- let weight = sat(1000).div_by_fee_rate_floor(FeeRate::from_sat_per_kvb(1001)).unwrap();
+ let weight = sat(1000).div_by_fee_rate_floor(FeeRate::from_sat_per_kvb_u32(1001)).unwrap();
assert_eq!(weight, Weight::from_wu(3996));
// A tiny fee rate over the maximum amount overflows Weight and must error.
### units/src/amount/unsigned.rs
@@ -508,7 +508,7 @@ impl Amount {
/// let amount = Amount::from_sat(10)?;
/// let weight = Weight::from_wu(200);
/// let fee_rate = amount.div_by_weight_ceil(weight).expect("valid fee rate");
- /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(50));
+ /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu_u32(50));
/// # Ok::<_, amount::OutOfRangeError>(())
/// ```
#[inline]
### units/src/fee.rs
@@ -195,7 +195,7 @@ mod tests {
#[test]
fn fee_wu() {
- let fee_rate = FeeRate::from_sat_per_vb(2);
+ let fee_rate = FeeRate::from_sat_per_vb_u32(2);
let weight = Weight::from_vb(3).unwrap();
assert_eq!(fee_rate.to_fee(weight), Amount::from_sat_u32(6));
}
@@ -204,19 +204,19 @@ mod tests {
fn weight_mul() {
let weight = Weight::from_vb(10).unwrap();
let fee: Amount =
- FeeRate::from_sat_per_vb(10).mul_by_weight(weight).expect("expected Amount");
+ FeeRate::from_sat_per_vb_u32(10).mul_by_weight(weight).expect("expected Amount");
assert_eq!(Amount::from_sat_u32(100), fee);
- let fee = FeeRate::from_sat_per_kwu(10).mul_by_weight(Weight::MAX);
+ let fee = FeeRate::from_sat_per_kwu_u32(10).mul_by_weight(Weight::MAX);
assert!(fee.is_error());
let weight = Weight::from_vb(3).unwrap();
- let fee_rate = FeeRate::from_sat_per_vb(3);
+ let fee_rate = FeeRate::from_sat_per_vb_u32(3);
let fee = fee_rate.mul_by_weight(weight).unwrap();
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_u32(864);
let fee = weight.mul_by_fee_rate(fee_rate).unwrap();
// 381 * 0.864 yields 329.18.
// The result is then rounded up to 330.
@@ -226,7 +226,7 @@ mod tests {
#[test]
#[allow(clippy::op_ref)]
fn multiply() {
- let two = FeeRate::from_sat_per_vb(2);
+ let two = FeeRate::from_sat_per_vb_u32(2);
let three = Weight::from_vb(3).unwrap();
let six = Amount::from_sat_u32(6);
@@ -243,7 +243,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_u32(2);
let weight = amount / fee_rate;
assert_eq!(weight.unwrap(), Weight::from_wu(500_000));
@@ -257,7 +257,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_u32(3);
let weight = amount / fee_rate;
// 1000 * 1000 = 1,000,000 msats
// 1,000,000 / 3 = 333,333.33... wu
@@ -269,8 +269,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);
- assert!(amount.div_by_fee_rate_floor(zero_rate).is_error());
- assert!(amount.div_by_fee_rate_ceil(zero_rate).is_error());
+ assert!(amount.div_by_fee_rate_floor(FeeRate::ZERO).is_error());
+ assert!(amount.div_by_fee_rate_ceil(FeeRate::ZERO).is_error());
}
}
### units/src/fee_rate/mod.rs
@@ -62,14 +62,24 @@ impl FeeRate {
/// 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: Self = Self::from_sat_per_vb(1);
+ pub const BROADCAST_MIN: Self = Self::from_sat_per_vb_u32(1);
/// The fee rate used to compute dust amount.
- pub const DUST: Self = Self::from_sat_per_vb(3);
+ pub const DUST: Self = Self::from_sat_per_vb_u32(3);
/// Constructs a new [`FeeRate`] from satoshis per 1,000 weight units.
#[inline]
- pub const fn from_sat_per_kwu(sat_kwu: u32) -> Self {
+ pub const fn from_sat_per_kwu(sat_kwu: u64) -> NumOpResult<Self> {
+ // No `map()` in const context.
+ match sat_kwu.checked_mul(4_000) {
+ Some(fee_rate) => R::Valid(Self::from_sat_per_mvb(fee_rate)),
+ None => R::Error(E::while_doing(MathErrorKind::Overflow { op: MathOp::Mul, is_negative: false })),
+ }
+ }
+
+ /// Constructs a new [`FeeRate`] from satoshis per 1000 weight units.
+ #[inline]
+ pub const fn from_sat_per_kwu_u32(sat_kwu: u32) -> Self {
let fee_rate = (const_casts::u32_to_u64(sat_kwu)) * 4_000;
Self::from_sat_per_mvb(fee_rate)
}
@@ -82,7 +92,17 @@ impl FeeRate {
/// Constructs a new [`FeeRate`] from satoshis per virtual byte.
#[inline]
- pub const fn from_sat_per_vb(sat_vb: u32) -> Self {
+ pub const fn from_sat_per_vb(sat_vb: u64) -> NumOpResult<Self> {
+ // No `map()` in const context.
+ match sat_vb.checked_mul(1_000_000) {
+ Some(fee_rate) => R::Valid(Self::from_sat_per_mvb(fee_rate)),
+ None => R::Error(E::while_doing(MathErrorKind::Overflow { op: MathOp::Mul, is_negative: false })),
+ }
+ }
+
+ /// Constructs a new [`FeeRate`] from satoshis per virtual byte.
+ #[inline]
+ pub const fn from_sat_per_vb_u32(sat_vb: u32) -> Self {
let fee_rate = (const_casts::u32_to_u64(sat_vb)) * 1_000_000;
Self::from_sat_per_mvb(fee_rate)
}
@@ -91,8 +111,8 @@ impl FeeRate {
#[inline]
pub const fn from_per_vb(rate: Amount) -> NumOpResult<Self> {
// No `map()` in const context.
- match rate.checked_mul(1_000_000) {
- Some(per_mvb) => R::Valid(Self::from_sat_per_mvb(per_mvb.to_sat())),
+ match rate.to_sat().checked_mul(1_000_000) {
+ Some(per_mvb) => R::Valid(Self::from_sat_per_mvb(per_mvb)),
None => R::Error(E::while_doing(MathErrorKind::Overflow {
op: MathOp::Mul,
is_negative: false,
@@ -102,7 +122,17 @@ impl FeeRate {
/// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes).
#[inline]
- pub const fn from_sat_per_kvb(sat_kvb: u32) -> Self {
+ pub const fn from_sat_per_kvb(sat_kvb: u64) -> NumOpResult<Self> {
+ // No `map()` in const context.
+ match sat_kvb.checked_mul(1_000) {
+ Some(fee_rate) => R::Valid(Self::from_sat_per_mvb(fee_rate)),
+ None => R::Error(E::while_doing(MathErrorKind::Overflow { op: MathOp::Mul, is_negative: false })),
+ }
+ }
+
+ /// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes).
+ #[inline]
+ pub const fn from_sat_per_kvb_u32(sat_kvb: u32) -> Self {
let fee_rate = (const_casts::u32_to_u64(sat_kvb)) * 1_000;
Self::from_sat_per_mvb(fee_rate)
}
@@ -288,101 +318,93 @@ mod tests {
use super::*;
+ const ONE: FeeRate = FeeRate::from_sat_per_kwu_u32(1);
+ const TWO: FeeRate = FeeRate::from_sat_per_kwu_u32(2);
+ const THREE: FeeRate = FeeRate::from_sat_per_kwu_u32(3);
+ const TEN: FeeRate = FeeRate::from_sat_per_kwu_u32(10);
+ const ONE_HUNDRED: FeeRate = FeeRate::from_sat_per_kwu_u32(100);
+
#[test]
#[allow(clippy::op_ref)]
fn feerate_div_nonzero() {
- let rate = FeeRate::from_sat_per_kwu(200);
+
+ let rate = FeeRate::from_sat_per_kwu_u32(200);
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, ONE_HUNDRED);
+ assert_eq!(&rate / &divisor, ONE_HUNDRED);
}
#[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);
-
- assert!(one + two == three);
- assert!(&one + two == three);
- assert!(one + &two == three);
- assert!(&one + &two == three);
+ assert!(ONE + TWO == THREE);
+ assert!(&ONE + TWO == THREE);
+ assert!(ONE + &TWO == THREE);
+ assert!(&ONE + &TWO == THREE);
}
#[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);
-
- assert_eq!(ten - seven, three);
- assert_eq!(&ten - seven, three);
- assert_eq!(ten - &seven, three);
- assert_eq!(&ten - &seven, three);
+ assert_eq!(THREE - TWO, ONE);
+ assert_eq!(&THREE - TWO, ONE);
+ assert_eq!(THREE - &TWO, ONE);
+ assert_eq!(&THREE - &TWO, ONE);
}
#[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 = ONE;
+ f += TWO;
+ assert_eq!(f, THREE);
- 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 = ONE;
+ f += &TWO;
+ assert_eq!(f, THREE);
- let mut f = NumOpResult::Valid(FeeRate::from_sat_per_kwu(1));
- f += FeeRate::from_sat_per_kwu(2);
- assert_eq!(f, NumOpResult::Valid(FeeRate::from_sat_per_kwu(3)));
+ let mut f = NumOpResult::Valid(ONE);
+ f += TWO;
+ assert_eq!(f, NumOpResult::Valid(THREE));
- let mut f = NumOpResult::Valid(FeeRate::from_sat_per_kwu(1));
- f += NumOpResult::Valid(FeeRate::from_sat_per_kwu(2));
- assert_eq!(f, NumOpResult::Valid(FeeRate::from_sat_per_kwu(3)));
+ let mut f = NumOpResult::Valid(ONE);
+ f += NumOpResult::Valid(TWO);
+ assert_eq!(f, NumOpResult::Valid(THREE));
}
#[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 = THREE;
+ f -= TWO;
+ assert_eq!(f, ONE);
- 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 = THREE;
+ f -= &TWO;
+ assert_eq!(f, ONE);
- let mut f = NumOpResult::Valid(FeeRate::from_sat_per_kwu(3));
- f -= FeeRate::from_sat_per_kwu(2);
- assert_eq!(f, NumOpResult::Valid(FeeRate::from_sat_per_kwu(1)));
+ let mut f = NumOpResult::Valid(THREE);
+ f -= TWO;
+ assert_eq!(f, NumOpResult::Valid(ONE));
- let mut f = NumOpResult::Valid(FeeRate::from_sat_per_kwu(3));
- f -= NumOpResult::Valid(FeeRate::from_sat_per_kwu(2));
- assert_eq!(f, NumOpResult::Valid(FeeRate::from_sat_per_kwu(1)));
+ let mut f = NumOpResult::Valid(THREE);
+ f -= NumOpResult::Valid(TWO);
+ assert_eq!(f, NumOpResult::Valid(ONE));
}
#[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);
-
- assert_eq!(one.checked_add(two).unwrap(), three);
+ assert_eq!(ONE.checked_add(TWO).unwrap(), THREE);
// Sanity check - no overflow adding one to per kvb max.
- let _ = FeeRate::from_sat_per_kvb(u32::MAX).checked_add(one).unwrap();
- let fee_rate = FeeRate::from_sat_per_mvb(u64::MAX).checked_add(one);
+ let _ = FeeRate::from_sat_per_kvb_u32(u32::MAX).checked_add(ONE).unwrap();
+ 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);
- assert_eq!(three.checked_sub(two).unwrap(), one);
+ assert_eq!(THREE.checked_sub(TWO).unwrap(), ONE);
- let fee_rate = FeeRate::ZERO.checked_sub(one);
+ let fee_rate = FeeRate::ZERO.checked_sub(ONE);
assert!(fee_rate.is_none());
}
@@ -397,13 +419,28 @@ mod tests {
#[test]
fn fee_rate_from_sat_per_vb() {
- let fee_rate = FeeRate::from_sat_per_vb(10);
- assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(2500));
+ let fee_rate = FeeRate::from_sat_per_vb(10).unwrap();
+ assert_eq!(fee_rate, FeeRate::from_sat_per_kwu_u32(2500));
+ }
+
+ #[test]
+ fn fee_rate_from_sat_per_vb_u32() {
+ let fee_rate = FeeRate::from_sat_per_vb_u32(10);
+ assert_eq!(fee_rate, FeeRate::from_sat_per_kwu_u32(2500));
}
#[test]
fn fee_rate_from_sat_per_kvb() {
- let fee_rate = FeeRate::from_sat_per_kvb(11);
+ let fee_rate = FeeRate::from_sat_per_kvb(11).unwrap();
+ assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(11_000));
+ // More than 21M btc/kvB but still in range of u64
+ let fee_rate = FeeRate::from_sat_per_kvb(16_800_000_000_000_000).unwrap();
+ assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(16_800_000_000_000_000_000));
+ }
+
+ #[test]
+ fn fee_rate_from_sat_per_kvb_u32() {
+ let fee_rate = FeeRate::from_sat_per_kvb_u32(11);
assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(11_000));
}
@@ -413,6 +450,13 @@ mod tests {
assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(11_000));
}
+ #[test]
+ fn fee_rate_from_sat_per_kwu() {
+ // More than 21M btc/kwu but still in range of u64
+ let fee_rate = FeeRate::from_sat_per_kwu(4_200_000_000_000_000).unwrap();
+ assert_eq!(fee_rate, FeeRate::from_sat_per_mvb(16_800_000_000_000_000_000));
+ }
+
#[test]
fn fee_rate_to_sat_per_x() {
let fee_rate = FeeRate::from_sat_per_mvb(2_000_400);
@@ -435,23 +479,34 @@ mod tests {
assert_eq!(max.to_sat_per_kvb_ceil(), u64::MAX / 1_000 + 1);
}
+ #[test]
+ fn fee_rate_from_per_x() {
+ let from_per_kwu = FeeRate::from_per_kwu(Amount::MAX);
+ assert_eq!(from_per_kwu, FeeRate::from_sat_per_mvb(8_400_000_000_000_000_000));
+ let from_per_kvb = FeeRate::from_per_kvb(Amount::MAX);
+ assert_eq!(from_per_kvb, FeeRate::from_sat_per_mvb(2_100_000_000_000_000_000));
+ assert!(FeeRate::from_per_vb(Amount::MAX).is_error());
+ let amount = Amount::from_sat(18_446_744_073_709).unwrap();
+ let from_per_vb = FeeRate::from_per_vb(amount).unwrap();
+ assert_eq!(from_per_vb, FeeRate::from_sat_per_mvb(18_446_744_073_709_000_000));
+ }
+
#[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 = TEN.checked_mul(10).expect("expected feerate in sat/kwu");
+ assert_eq!(fee_rate, ONE_HUNDRED);
- let fee_rate = FeeRate::from_sat_per_kwu(10).checked_mul(u64::MAX);
+ let fee_rate = TEN.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));
+ TEN.checked_div(10).expect("expected feerate in sat/kwu");
+ assert_eq!(fee_rate, ONE);
- let fee_rate = FeeRate::from_sat_per_kwu(10).checked_div(0);
+ let fee_rate = TEN.checked_div(0);
assert!(fee_rate.is_none());
}
@@ -464,15 +519,15 @@ mod tests {
#[test]
fn mul_by_weight_round_up() {
- let fee_rate = FeeRate::from_sat_per_kvb(10);
+ let fee_rate = FeeRate::from_sat_per_kvb_u32(10);
let weight = Weight::from_wu(500);
let fee = fee_rate.mul_by_weight(weight).expect("expected fee amount");
assert_eq!(fee, Amount::from_sat(2).unwrap());
}
#[test]
fn mul_by_weight_preserves_mvb_precision() {
- let fee_rate = FeeRate::from_sat_per_kvb(101);
+ let fee_rate = FeeRate::from_sat_per_kvb_u32(101);
let weight = Weight::from_wu(4_000);
assert_eq!(fee_rate.mul_by_weight(weight).unwrap(), Amount::from_sat(101).unwrap());
}
### units/src/fee_rate/serde.rs
@@ -33,7 +33,7 @@ pub mod as_sat_per_kwu_floor {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
- use crate::{Amount, FeeRate};
+ use crate::FeeRate;
#[inline]
pub fn serialize<S: Serializer>(f: &FeeRate, s: S) -> Result<S::Ok, S::Error> {
@@ -42,10 +42,16 @@ pub mod as_sat_per_kwu_floor {
#[inline]
pub fn deserialize<'d, D: Deserializer<'d>>(d: D) -> Result<FeeRate, D::Error> {
- let sat = u64::deserialize(d)?;
- let amt =
- Amount::from_sat(sat).map_err(|_| serde::de::Error::custom("amount out of range"))?;
- Ok(FeeRate::from_per_kwu(amt))
+ use crate::serde::de::Error;
+
+ let sat_per_kwu = u64::deserialize(d)?;
+ FeeRate::from_sat_per_kwu(sat_per_kwu)
+ .into_result()
+ .map_err(|_| {
+ let unexpected = serde::de::Unexpected::Unsigned(sat_per_kwu);
+ let expected = &"non-negative integer up to 4611686018427387";
+ D::Error::invalid_value(unexpected, expected)
+ })
}
pub mod opt {
### units/src/result.rs
@@ -64,7 +64,7 @@ pub use self::error::NumOpError;
/// let a = Amount::from_sat(123).expect("valid amount");
/// let b = Amount::from_sat(467).expect("valid amount");
/// // Fee rate for transaction.
-/// let fee_rate = FeeRate::from_sat_per_vb(1);
+/// let fee_rate = FeeRate::from_sat_per_vb_u32(1);
///
/// // Somewhat contrived example to show addition operator chained with division.
/// let max_fee = a + b;
@@ -566,7 +566,7 @@ mod tests {
fn mathop_map() {
// op is evaluated for valid results
let res = NumOpResult::Valid(Amount::from_sat_u32(100));
- let new_value = res.map(|val| (val / FeeRate::from_sat_per_kwu(10)).unwrap());
+ let new_value = res.map(|val| (val / FeeRate::from_sat_per_kwu_u32(10)).unwrap());
assert_eq!(new_value, NumOpResult::Valid(Weight::from_wu(10_000)));
// op is not evaluated for error results
### units/tests/serde.rs
@@ -417,10 +417,10 @@ fn serde_amount_as_str_opt() {
}
#[track_caller]
-fn fee_rate_vb(vb: u32) -> FeeRate { FeeRate::from_sat_per_vb(vb) }
+fn fee_rate_vb(vb: u32) -> FeeRate { FeeRate::from_sat_per_vb_u32(vb) }
#[track_caller]
-fn fee_rate_kwu(vb: u32) -> FeeRate { FeeRate::from_sat_per_kwu(vb) }
+fn fee_rate_kwu(vb: u32) -> FeeRate { FeeRate::from_sat_per_kwu_u32(vb) }
#[test]
#[cfg(feature = "serde")]
@@ -578,7 +578,7 @@ fn serde_fee_rate_floor_vs_ceil() {
fee_rate: FeeRate,
}
- let fee_rate = FeeRate::from_sat_per_kwu(251);
+ let fee_rate = FeeRate::from_sat_per_kwu_u32(251);
let floor = Floor { fee_rate };
let ceil = Ceil { fee_rate };Why this scored 35/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.