Skip to content

Commit 15065b7

Browse files
committed
Return NumOpResult when calculating fee on Amount
Currently we call the `Amount` fee calculation functions `div_by_foo` and return an `Option` to indicate error. We have the `NumOpResult` type that better indicates the error case. Includes a bunch of changes to the `psbt` tests because extracting the transaction from a PSBT has a bunch of overflow paths that need testing caused by fee calculation.
1 parent 75106e6 commit 15065b7

File tree

5 files changed

+75
-81
lines changed

5 files changed

+75
-81
lines changed

bitcoin/src/psbt/mod.rs

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,17 +1348,6 @@ mod tests {
13481348
use crate::witness::Witness;
13491349
use crate::Sequence;
13501350

1351-
/// Fee rate in sat/kwu for a high-fee PSBT with an input=5_000_000_000_000, output=1000
1352-
const ABSURD_FEE_RATE: FeeRate = match FeeRate::from_sat_per_kwu(15_060_240_960_843) {
1353-
Some(fee_rate) => fee_rate,
1354-
None => panic!("unreachable - no unwrap in Rust 1.63 in const"),
1355-
};
1356-
const JUST_BELOW_ABSURD_FEE_RATE: FeeRate = match FeeRate::from_sat_per_kwu(15_060_240_960_842)
1357-
{
1358-
Some(fee_rate) => fee_rate,
1359-
None => panic!("unreachable - no unwrap in Rust 1.63 in const"),
1360-
};
1361-
13621351
#[track_caller]
13631352
pub fn hex_psbt(s: &str) -> Result<Psbt, crate::psbt::error::Error> {
13641353
let r = Vec::from_hex(s);
@@ -1442,31 +1431,43 @@ mod tests {
14421431

14431432
#[test]
14441433
fn psbt_high_fee_checks() {
1445-
let psbt = psbt_with_values(5_000_000_000_000, 1000);
1434+
let psbt = psbt_with_values(Amount::MAX.to_sat(), 1000);
1435+
1436+
// We cannot create an expected fee rate to test against because `FeeRate::from_sat_per_mvb` is private.
1437+
// Large fee rate errors if we pass in 1 sat/vb so just use this to get the error fee rate returned.
1438+
let error_fee_rate = psbt
1439+
.clone()
1440+
.extract_tx_with_fee_rate_limit(FeeRate::from_sat_per_vb_u32(1))
1441+
.map_err(|e| match e {
1442+
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
1443+
_ => panic!(""),
1444+
})
1445+
.unwrap_err();
1446+
1447+
// In `internal_extract_tx_with_fee_rate_limit` when we do fee / weight
1448+
// we manually saturate to `FeeRate::MAX`.
1449+
assert!(psbt.clone().extract_tx_with_fee_rate_limit(FeeRate::MAX).is_ok());
1450+
1451+
// These error because the fee rate is above the limit as expected.
14461452
assert_eq!(
14471453
psbt.clone().extract_tx().map_err(|e| match e {
14481454
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
14491455
_ => panic!(""),
14501456
}),
1451-
Err(ABSURD_FEE_RATE)
1457+
Err(error_fee_rate)
14521458
);
14531459
assert_eq!(
14541460
psbt.clone().extract_tx_fee_rate_limit().map_err(|e| match e {
14551461
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
14561462
_ => panic!(""),
14571463
}),
1458-
Err(ABSURD_FEE_RATE)
1459-
);
1460-
assert_eq!(
1461-
psbt.clone().extract_tx_with_fee_rate_limit(JUST_BELOW_ABSURD_FEE_RATE).map_err(|e| {
1462-
match e {
1463-
ExtractTxError::AbsurdFeeRate { fee_rate, .. } => fee_rate,
1464-
_ => panic!(""),
1465-
}
1466-
}),
1467-
Err(ABSURD_FEE_RATE)
1464+
Err(error_fee_rate)
14681465
);
1469-
assert!(psbt.extract_tx_with_fee_rate_limit(ABSURD_FEE_RATE).is_ok());
1466+
1467+
// No one is using an ~50 BTC fee so if we can handle this
1468+
// then the `FeeRate` restrictions are fine for PSBT usage.
1469+
let psbt = psbt_with_values(Amount::from_btc_u16(50).to_sat(), 1000); // fee = 50 BTC - 1000 sats
1470+
assert!(psbt.extract_tx_with_fee_rate_limit(FeeRate::MAX).is_ok());
14701471

14711472
// Testing that extract_tx will error at 25k sat/vbyte (6250000 sat/kwu)
14721473
assert_eq!(

units/src/amount/tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ fn amount_checked_div_by_weight_ceil() {
279279
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(864).unwrap());
280280

281281
let fee_rate = Amount::ONE_SAT.div_by_weight_ceil(Weight::ZERO);
282-
assert!(fee_rate.is_none());
282+
assert!(fee_rate.is_error());
283283
}
284284

285285
#[cfg(feature = "alloc")]
@@ -297,7 +297,7 @@ fn amount_checked_div_by_weight_floor() {
297297
assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863).unwrap());
298298

299299
let fee_rate = Amount::ONE_SAT.div_by_weight_floor(Weight::ZERO);
300-
assert!(fee_rate.is_none());
300+
assert!(fee_rate.is_error());
301301
}
302302

303303
#[cfg(feature = "alloc")]
@@ -325,8 +325,8 @@ fn amount_checked_div_by_fee_rate() {
325325

326326
// Test division by zero
327327
let zero_fee_rate = FeeRate::from_sat_per_kwu(0).unwrap();
328-
assert!(amount.div_by_fee_rate_floor(zero_fee_rate).is_none());
329-
assert!(amount.div_by_fee_rate_ceil(zero_fee_rate).is_none());
328+
assert!(amount.div_by_fee_rate_floor(zero_fee_rate).is_error());
329+
assert!(amount.div_by_fee_rate_ceil(zero_fee_rate).is_error());
330330

331331
// Test with maximum amount
332332
let max_amount = Amount::MAX;

units/src/amount/unsigned.rs

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ use core::{default, fmt};
99

1010
#[cfg(feature = "arbitrary")]
1111
use arbitrary::{Arbitrary, Unstructured};
12+
use NumOpResult as R;
1213

1314
use super::error::{ParseAmountErrorInner, ParseErrorInner};
1415
use super::{
1516
parse_signed_to_satoshi, split_amount_and_denomination, Denomination, Display, DisplayStyle,
1617
OutOfRangeError, ParseAmountError, ParseError, SignedAmount,
1718
};
18-
use crate::{FeeRate, Weight};
19+
use crate::{FeeRate, MathOp, NumOpError as E, NumOpResult, Weight};
1920

2021
mod encapsulate {
2122
use super::OutOfRangeError;
@@ -407,101 +408,96 @@ impl Amount {
407408
/// Checked weight floor division.
408409
///
409410
/// Be aware that integer division loses the remainder if no exact division
410-
/// can be made. See also [`Self::checked_div_by_weight_ceil`].
411-
///
412-
/// Returns [`None`] if overflow occurred.
413-
#[must_use]
414-
pub const fn div_by_weight_floor(self, weight: Weight) -> Option<FeeRate> {
411+
/// can be made. See also [`Self::div_by_weight_ceil`].
412+
pub const fn div_by_weight_floor(self, weight: Weight) -> NumOpResult<FeeRate> {
415413
let wu = weight.to_wu();
416-
if wu == 0 {
417-
return None;
418-
}
419414

420415
// Mul by 1,000 because we use per/kwu.
421-
match self.to_sat().checked_mul(1_000) {
422-
Some(sats) => {
423-
let fee_rate = sats / wu;
424-
FeeRate::from_sat_per_kwu(fee_rate)
416+
if let Some(sats) = self.to_sat().checked_mul(1_000) {
417+
match sats.checked_div(wu) {
418+
Some(fee_rate) =>
419+
if let Ok(amount) = Amount::from_sat(fee_rate) {
420+
return FeeRate::from_per_kwu(amount);
421+
},
422+
None => return R::Error(E::while_doing(MathOp::Div)),
425423
}
426-
None => None,
427424
}
425+
// Use `MathOp::Mul` because `Div` implies div by zero.
426+
R::Error(E::while_doing(MathOp::Mul))
428427
}
429428

430429
/// Checked weight ceiling division.
431430
///
432431
/// Be aware that integer division loses the remainder if no exact division
433432
/// can be made. This method rounds up ensuring the transaction fee rate is
434-
/// sufficient. See also [`Self::checked_div_by_weight_floor`].
435-
///
436-
/// Returns [`None`] if overflow occurred.
433+
/// sufficient. See also [`Self::div_by_weight_floor`].
437434
///
438435
/// # Examples
439436
///
440437
/// ```
441438
/// # use bitcoin_units::{amount, Amount, FeeRate, Weight};
442439
/// let amount = Amount::from_sat(10)?;
443440
/// let weight = Weight::from_wu(300);
444-
/// let fee_rate = amount.div_by_weight_ceil(weight);
445-
/// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34));
441+
/// let fee_rate = amount.div_by_weight_ceil(weight).expect("valid fee rate");
442+
/// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34).expect("valid fee rate"));
446443
/// # Ok::<_, amount::OutOfRangeError>(())
447444
/// ```
448-
#[must_use]
449-
pub const fn div_by_weight_ceil(self, weight: Weight) -> Option<FeeRate> {
445+
pub const fn div_by_weight_ceil(self, weight: Weight) -> NumOpResult<FeeRate> {
450446
let wu = weight.to_wu();
451447
if wu == 0 {
452-
return None;
448+
return R::Error(E::while_doing(MathOp::Div));
453449
}
454450

455451
// Mul by 1,000 because we use per/kwu.
456452
if let Some(sats) = self.to_sat().checked_mul(1_000) {
457453
// No need to used checked arithmetic because wu is non-zero.
458454
if let Some(bump) = sats.checked_add(wu - 1) {
459455
let fee_rate = bump / wu;
460-
return FeeRate::from_sat_per_kwu(fee_rate);
456+
if let Ok(amount) = Amount::from_sat(fee_rate) {
457+
return FeeRate::from_per_kwu(amount);
458+
}
461459
}
462460
}
463-
None
461+
// Use `MathOp::Mul` because `Div` implies div by zero.
462+
R::Error(E::while_doing(MathOp::Mul))
464463
}
465464

466465
/// Checked fee rate floor division.
467466
///
468467
/// Computes the maximum weight that would result in a fee less than or equal to this amount
469468
/// at the given `fee_rate`. Uses floor division to ensure the resulting weight doesn't cause
470469
/// the fee to exceed the amount.
471-
///
472-
/// Returns [`None`] if overflow occurred or if `fee_rate` is zero.
473-
#[must_use]
474-
pub const fn div_by_fee_rate_floor(self, fee_rate: FeeRate) -> Option<Weight> {
475-
if let Some(msats) = self.to_sat().checked_mul(1000) {
476-
if let Some(wu) = msats.checked_div(fee_rate.to_sat_per_kwu_ceil()) {
477-
return Some(Weight::from_wu(wu));
478-
}
470+
pub const fn div_by_fee_rate_floor(self, fee_rate: FeeRate) -> NumOpResult<Weight> {
471+
debug_assert!(Amount::MAX.to_sat().checked_mul(1_000).is_some());
472+
let msats = self.to_sat() * 1_000;
473+
match msats.checked_div(fee_rate.to_sat_per_kwu_ceil()) {
474+
Some(wu) => R::Valid(Weight::from_wu(wu)),
475+
None => R::Error(E::while_doing(MathOp::Div)),
479476
}
480-
None
481477
}
482478

483479
/// Checked fee rate ceiling division.
484480
///
485481
/// Computes the minimum weight that would result in a fee greater than or equal to this amount
486482
/// at the given `fee_rate`. Uses ceiling division to ensure the resulting weight is sufficient.
487-
///
488-
/// Returns [`None`] if overflow occurred or if `fee_rate` is zero.
489-
#[must_use]
490-
pub const fn div_by_fee_rate_ceil(self, fee_rate: FeeRate) -> Option<Weight> {
483+
pub const fn div_by_fee_rate_ceil(self, fee_rate: FeeRate) -> NumOpResult<Weight> {
491484
// Use ceil because result is used as the divisor.
492485
let rate = fee_rate.to_sat_per_kwu_ceil();
486+
// Early return so we do not have to use checked arithmetic below.
493487
if rate == 0 {
494-
return None;
488+
return R::Error(E::while_doing(MathOp::Div));
495489
}
496490

497-
if let Some(msats) = self.to_sat().checked_mul(1000) {
498-
// No need to used checked arithmetic because rate is non-zero.
499-
if let Some(bump) = msats.checked_add(rate - 1) {
491+
debug_assert!(Amount::MAX.to_sat().checked_mul(1_000).is_some());
492+
let msats = self.to_sat() * 1_000;
493+
match msats.checked_add(rate - 1) {
494+
Some(bump) => {
500495
let wu = bump / rate;
501-
return Some(Weight::from_wu(wu));
496+
NumOpResult::Valid(Weight::from_wu(wu))
502497
}
498+
// Use `MathOp::Add` because `Div` implies div by zero.
499+
None => R::Error(E::while_doing(MathOp::Add)),
503500
}
504-
None
505501
}
506502
}
507503

units/src/fee.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use core::ops;
2727

2828
use NumOpResult as R;
2929

30-
use crate::{Amount, FeeRate, MathOp, NumOpError as E, NumOpResult, OptionExt, Weight};
30+
use crate::{Amount, FeeRate, MathOp, NumOpError as E, NumOpResult, Weight};
3131

3232
crate::internal_macros::impl_op_for_references! {
3333
impl ops::Mul<FeeRate> for Weight {
@@ -114,7 +114,7 @@ crate::internal_macros::impl_op_for_references! {
114114
type Output = NumOpResult<FeeRate>;
115115

116116
fn div(self, rhs: Weight) -> Self::Output {
117-
self.div_by_weight_floor(rhs).valid_or_error(MathOp::Div)
117+
self.div_by_weight_floor(rhs)
118118
}
119119
}
120120
impl ops::Div<Weight> for NumOpResult<Amount> {
@@ -155,7 +155,7 @@ crate::internal_macros::impl_op_for_references! {
155155
type Output = NumOpResult<Weight>;
156156

157157
fn div(self, rhs: FeeRate) -> Self::Output {
158-
self.div_by_fee_rate_floor(rhs).valid_or_error(MathOp::Div)
158+
self.div_by_fee_rate_floor(rhs)
159159
}
160160
}
161161
impl ops::Div<FeeRate> for NumOpResult<Amount> {
@@ -213,10 +213,8 @@ mod tests {
213213
#[test]
214214
fn weight_mul() {
215215
let weight = Weight::from_vb(10).unwrap();
216-
let fee: Amount = FeeRate::from_sat_per_vb(10)
217-
.unwrap()
218-
.mul_by_weight(weight)
219-
.expect("expected Amount");
216+
let fee: Amount =
217+
FeeRate::from_sat_per_vb(10).unwrap().mul_by_weight(weight).expect("expected Amount");
220218
assert_eq!(Amount::from_sat_u32(100), fee);
221219

222220
let fee = FeeRate::from_sat_per_kwu(10).unwrap().mul_by_weight(Weight::MAX);
@@ -282,7 +280,7 @@ mod tests {
282280

283281
// Test that division by zero returns None
284282
let zero_rate = FeeRate::from_sat_per_kwu(0).unwrap();
285-
assert!(amount.div_by_fee_rate_floor(zero_rate).is_none());
286-
assert!(amount.div_by_fee_rate_ceil(zero_rate).is_none());
283+
assert!(amount.div_by_fee_rate_floor(zero_rate).is_error());
284+
assert!(amount.div_by_fee_rate_ceil(zero_rate).is_error());
287285
}
288286
}

units/src/fee_rate/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use core::ops;
1010

1111
#[cfg(feature = "arbitrary")]
1212
use arbitrary::{Arbitrary, Unstructured};
13-
1413
use NumOpResult as R;
1514

1615
use crate::{Amount, MathOp, NumOpError as E, NumOpResult, Weight};

0 commit comments

Comments
 (0)