Merge rust-bitcoin/rust-bitcoin#6812: units: Separate mathematical operation and failure mode in `NumOpError`
What changed, and why it matters
This is a code-quality refactor in the rust-bitcoin library. It renames and restructures how arithmetic errors (overflow, divide-by-zero, remainder-by-zero) are reported, so callers can tell what specifically went wrong. It does not change the actual safety checks—overflows and divisions by zero were already caught before this change. The patch also fixes a minor behavior quirk where combining an error result with a valid value would overwrite the original error type; now the original error is preserved.
No immediate security action required. This is an API refactor. Downstream users relying on NumOpError::is_overflow() or is_div_by_zero() will need to update their code, and should verify that the new error-preservation behavior in AddAssign/SubAssign matches their expectations.
Security signals we found
Refactor of error reporting for checked arithmetic operations
Removal of public is_overflow()/is_div_by_zero() predicates on NumOpError
Preservation of original error kind in NumOpResult AddAssign/SubAssign
No change to checked_add, checked_sub, checked_mul, checked_div, checked_rem logic
No new unsafe code, no new dependencies, no network or serialization changes
Evidence from the diff
The commit replaces the internal MathOp field of NumOpError with a new MathErrorKind enum that distinguishes Overflow { op, is_negative }, DivByZero, and RemByZero. It updates all call sites in Amount, SignedAmount, FeeRate, and helper macros to construct the new error kind. The public NumOpError API loses is_overflow()/is_div_by_zero() but keeps operation(). AddAssign/SubAssign for NumOpResult are changed so that if the left-hand side is already an error, it is preserved rather than replaced by a new Add/Sub error. Tests and docs are updated accordingly. No arithmetic bounds or checked_* behavior are changed.
Changed components
units/src/result.rsunits/src/amount/ops.rsunits/src/amount/unsigned.rsunits/src/amount/tests.rsunits/src/fee_rate/mod.rsunits/src/internal_macros.rsInspect captured patch +283 / −143
### units/src/amount/ops.rs
@@ -12,7 +12,7 @@ use crate::internal_macros::{
impl_add_assign_for_results, impl_div_assign, impl_mul_assign, impl_rem_assign,
impl_sub_assign_for_results,
};
-use crate::result::{MathOp, NumOpError, NumOpResult, OptionExt};
+use crate::result::{MathErrorKind, MathOp, NumOpError, NumOpResult, OptionExt};
impl From<Amount> for NumOpResult<Amount> {
#[inline]
@@ -36,7 +36,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Add<Amount> for Amount {
type Output = NumOpResult<Amount>;
- fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathOp::Add) }
+ fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathErrorKind::Overflow { op: MathOp::Add, is_negative: false }) }
}
impl ops::Add<NumOpResult<Amount>> for Amount {
type Output = NumOpResult<Amount>;
@@ -47,7 +47,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Sub<Amount> for Amount {
type Output = NumOpResult<Amount>;
- fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathOp::Sub) }
+ fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathErrorKind::Overflow { op: MathOp::Sub, is_negative: true }) }
}
impl ops::Sub<NumOpResult<Amount>> for Amount {
type Output = NumOpResult<Amount>;
@@ -63,7 +63,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Mul<u64> for Amount {
type Output = NumOpResult<Amount>;
- fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathOp::Mul) }
+ fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathErrorKind::Overflow { op: MathOp::Mul, is_negative: false }) }
}
impl ops::Mul<u64> for NumOpResult<Amount> {
type Output = NumOpResult<Amount>;
@@ -73,7 +73,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Mul<Amount> for u64 {
type Output = NumOpResult<Amount>;
- fn mul(self, rhs: Amount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathOp::Mul) }
+ fn mul(self, rhs: Amount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathErrorKind::Overflow { op: MathOp::Mul, is_negative: false }) }
}
impl ops::Mul<NumOpResult<Amount>> for u64 {
type Output = NumOpResult<Amount>;
@@ -84,7 +84,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Div<u64> for Amount {
type Output = NumOpResult<Amount>;
- fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathOp::Div) }
+ fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathErrorKind::DivByZero) }
}
impl ops::Div<u64> for NumOpResult<Amount> {
type Output = NumOpResult<Amount>;
@@ -95,7 +95,7 @@ crate::internal_macros::impl_op_for_references! {
type Output = NumOpResult<u64>;
fn div(self, rhs: Amount) -> Self::Output {
- self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathOp::Div)
+ self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathErrorKind::DivByZero)
}
}
impl ops::Div<NonZeroU64> for Amount {
@@ -111,7 +111,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Rem<u64> for Amount {
type Output = NumOpResult<Amount>;
- fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathOp::Rem) }
+ fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathErrorKind::RemByZero) }
}
impl ops::Rem<NonZeroU64> for Amount {
type Output = Amount;
@@ -132,7 +132,10 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Add<SignedAmount> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
- fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathOp::Add) }
+ fn add(self, rhs: SignedAmount) -> Self::Output {
+ let kind = MathErrorKind::Overflow { op: MathOp::Add, is_negative: rhs.is_negative() };
+ self.checked_add(rhs).valid_or_error(kind)
+ }
}
impl ops::Add<NumOpResult<SignedAmount>> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
@@ -143,7 +146,10 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Sub<SignedAmount> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
- fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathOp::Sub) }
+ fn sub(self, rhs: SignedAmount) -> Self::Output {
+ let kind = MathErrorKind::Overflow { op: MathOp::Sub, is_negative: !rhs.is_negative() };
+ self.checked_sub(rhs).valid_or_error(kind)
+ }
}
impl ops::Sub<NumOpResult<SignedAmount>> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
@@ -159,7 +165,10 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Mul<i64> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
- fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathOp::Mul) }
+ fn mul(self, rhs: i64) -> Self::Output {
+ let is_negative = self.is_negative() != rhs.is_negative();
+ self.checked_mul(rhs).valid_or_error(MathErrorKind::Overflow { op: MathOp::Mul, is_negative })
+ }
}
impl ops::Mul<i64> for NumOpResult<SignedAmount> {
type Output = NumOpResult<SignedAmount>;
@@ -169,7 +178,10 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Mul<SignedAmount> for i64 {
type Output = NumOpResult<SignedAmount>;
- fn mul(self, rhs: SignedAmount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathOp::Mul) }
+ fn mul(self, rhs: SignedAmount) -> Self::Output {
+ let is_negative = self.is_negative() != rhs.is_negative();
+ rhs.checked_mul(self).valid_or_error(MathErrorKind::Overflow { op: MathOp::Mul, is_negative })
+ }
}
impl ops::Mul<NumOpResult<SignedAmount>> for i64 {
type Output = NumOpResult<SignedAmount>;
@@ -180,7 +192,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Div<i64> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
- fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathOp::Div) }
+ fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathErrorKind::DivByZero) }
}
impl ops::Div<i64> for NumOpResult<SignedAmount> {
type Output = NumOpResult<SignedAmount>;
@@ -191,7 +203,7 @@ crate::internal_macros::impl_op_for_references! {
type Output = NumOpResult<i64>;
fn div(self, rhs: SignedAmount) -> Self::Output {
- self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathOp::Div)
+ self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathErrorKind::DivByZero)
}
}
impl ops::Div<NonZeroI64> for SignedAmount {
@@ -207,7 +219,7 @@ crate::internal_macros::impl_op_for_references! {
impl ops::Rem<i64> for SignedAmount {
type Output = NumOpResult<SignedAmount>;
- fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathOp::Rem) }
+ fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathErrorKind::RemByZero) }
}
impl ops::Rem<NonZeroI64> for SignedAmount {
type Output = SignedAmount;
@@ -283,7 +295,10 @@ impl<T: Into<Self>> core::iter::Sum<T> for NumOpResult<Amount> {
{
iter.fold(Self::Valid(Amount::ZERO), |acc, amount| match (acc, amount.into()) {
(Self::Valid(lhs), Self::Valid(rhs)) => lhs + rhs,
- (_, _) => Self::Error(NumOpError::while_doing(MathOp::Add)),
+ (_, _) => Self::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ })),
})
}
}
@@ -294,7 +309,10 @@ impl<'a> core::iter::Sum<&'a Self> for NumOpResult<Amount> {
{
iter.fold(Self::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
(Self::Valid(lhs), Self::Valid(rhs)) => lhs + rhs,
- (_, _) => Self::Error(NumOpError::while_doing(MathOp::Add)),
+ (_, _) => Self::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ })),
})
}
}
@@ -306,7 +324,10 @@ impl<T: Into<Self>> core::iter::Sum<T> for NumOpResult<SignedAmount> {
{
iter.fold(Self::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount.into()) {
(Self::Valid(lhs), Self::Valid(rhs)) => lhs + rhs,
- (_, _) => Self::Error(NumOpError::while_doing(MathOp::Add)),
+ (_, _) => Self::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ })),
})
}
}
@@ -317,7 +338,10 @@ impl<'a> core::iter::Sum<&'a Self> for NumOpResult<SignedAmount> {
{
iter.fold(Self::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
(Self::Valid(lhs), Self::Valid(rhs)) => lhs + rhs,
- (_, _) => Self::Error(NumOpError::while_doing(MathOp::Add)),
+ (_, _) => Self::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ })),
})
}
}
@@ -362,7 +386,10 @@ mod tests {
fn sum_amount_with_error_propagation() {
let amounts = [
NumOpResult::Valid(Amount::from_sat_u32(100)),
- NumOpResult::Error(NumOpError::while_doing(MathOp::Add)),
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ })),
NumOpResult::Valid(Amount::from_sat_u32(200)),
];
@@ -410,7 +437,10 @@ mod tests {
fn sum_signed_amount_with_error_propagation() {
let amounts = [
NumOpResult::Valid(SignedAmount::from_sat_i32(100)),
- NumOpResult::Error(NumOpError::while_doing(MathOp::Add)),
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ })),
NumOpResult::Valid(SignedAmount::from_sat_i32(200)),
];
@@ -426,15 +456,21 @@ mod tests {
res += Amount::from_sat_u32(50);
assert_eq!(res, NumOpResult::Valid(Amount::from_sat_u32(150)));
- let add_err = NumOpResult::Error(NumOpError::while_doing(MathOp::Add));
+ let add_err = NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
res += add_err; // Add an error result
assert_eq!(res, add_err);
let mut res = sat + sat;
res -= Amount::from_sat_u32(20);
assert_eq!(res, NumOpResult::Valid(Amount::from_sat_u32(80)));
- let sub_err = NumOpResult::Error(NumOpError::while_doing(MathOp::Sub));
+ let sub_err = NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Sub,
+ is_negative: true,
+ }));
res -= sub_err; // Subtract an error result
assert_eq!(res, sub_err);
}
@@ -447,15 +483,21 @@ mod tests {
res += SignedAmount::from_sat_i32(-30);
assert_eq!(res, NumOpResult::Valid(SignedAmount::from_sat_i32(70)));
- let add_err = NumOpResult::Error(NumOpError::while_doing(MathOp::Add));
+ let add_err = NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
res += add_err; // Add an error result
assert_eq!(res, add_err);
let mut res = ssat + ssat;
res -= SignedAmount::from_sat_i32(25);
assert_eq!(res, NumOpResult::Valid(SignedAmount::from_sat_i32(75)));
- let sub_err = NumOpResult::Error(NumOpError::while_doing(MathOp::Sub));
+ let sub_err = NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Sub,
+ is_negative: true,
+ }));
res -= sub_err; // Subtract an error result
assert_eq!(res, sub_err);
}
@@ -471,10 +513,10 @@ mod tests {
assert_eq!(res, NumOpResult::Valid(Amount::from_sat_u32(2)));
res %= 0_u64;
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Rem)));
+ assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathErrorKind::RemByZero)));
res %= 5_u64;
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Rem)));
+ assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathErrorKind::RemByZero)));
}
#[test]
@@ -504,10 +546,10 @@ mod tests {
assert_eq!(res, NumOpResult::Valid(SignedAmount::from_sat_i32(-2)));
res %= 0_i64;
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Rem)));
+ assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathErrorKind::RemByZero)));
res %= 5_i64;
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Rem)));
+ assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathErrorKind::RemByZero)));
}
#[test]
@@ -540,9 +582,19 @@ mod tests {
assert_eq!(res, NumOpResult::Valid(Amount::from_sat_u32(25)));
// An error result stays an error, keeping the original operation.
- let mut res: NumOpResult<Amount> = NumOpResult::Error(NumOpError::while_doing(MathOp::Add));
+ let mut res: NumOpResult<Amount> =
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
res /= NonZeroU64::new(2).unwrap();
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Add)));
+ assert_eq!(
+ res,
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false
+ }))
+ );
}
#[test]
@@ -563,29 +615,46 @@ mod tests {
assert_eq!(res, NumOpResult::Valid(SignedAmount::from_sat_i32(-25)));
let mut res: NumOpResult<SignedAmount> =
- NumOpResult::Error(NumOpError::while_doing(MathOp::Sub));
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Sub,
+ is_negative: true,
+ }));
res /= NonZeroI64::new(2).unwrap();
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Sub)));
+ assert_eq!(
+ res,
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Sub,
+ is_negative: true
+ }))
+ );
}
#[test]
fn op_assign_amount_error() {
- let mut res: NumOpResult<Amount> = NumOpResult::Error(NumOpError::while_doing(MathOp::Add));
-
- // Adding a valid amount to an error should make an Add error
+ let mut res: NumOpResult<Amount> =
+ NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Mul,
+ is_negative: false,
+ }));
+ let orig_res = res;
+
+ // All assign ops to an error should preserve the error
res += Amount::from_sat_u32(10);
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Add)));
+ assert_eq!(res, orig_res);
- // Adding an error to an error change to an Add error
- res += NumOpResult::Error(NumOpError::while_doing(MathOp::Sub));
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Add)));
+ res += NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Sub,
+ is_negative: true,
+ }));
+ assert_eq!(res, orig_res);
- // Subtracting a valid amount from an error should make a Sub error
res -= Amount::from_sat_u32(10);
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Sub)));
+ assert_eq!(res, orig_res);
- // Subtracting an error from an error change to a Sub error
- res -= NumOpResult::Error(NumOpError::while_doing(MathOp::Add));
- assert_eq!(res, NumOpResult::Error(NumOpError::while_doing(MathOp::Sub)));
+ res -= NumOpResult::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
+ assert_eq!(res, orig_res);
}
}
### units/src/amount/tests.rs
@@ -11,7 +11,7 @@ use core::num::{NonZeroI64, NonZeroU64};
use std::panic;
use super::*;
-use crate::result::{MathOp, NumOpError, NumOpResult};
+use crate::result::{MathErrorKind, MathOp, NumOpError, NumOpResult};
#[cfg(feature = "alloc")]
use crate::FeeRate;
use crate::Weight;
@@ -1450,20 +1450,16 @@ fn amount_op_result_sum() {
fn math_op_errors() {
let overflow = Amount::MAX + Amount::from_sat(1).unwrap();
if let NumOpResult::Error(err) = overflow {
- assert!(err.operation().is_overflow());
- assert!(err.is_overflow());
- assert!(!err.operation().is_div_by_zero());
- assert!(!err.is_div_by_zero());
+ assert!(matches!(err.operation(), MathOp::Add));
+ assert!(matches!(err.0, MathErrorKind::Overflow { .. }));
} else {
panic!("Expected an overflow error, but got a valid result");
}
let div_by_zero = Amount::from_sat(10).unwrap() / Amount::ZERO;
if let NumOpResult::Error(err) = div_by_zero {
- assert!(!err.operation().is_overflow());
- assert!(!err.is_overflow());
- assert!(err.operation().is_div_by_zero());
- assert!(err.is_div_by_zero());
+ assert!(matches!(err.operation(), MathOp::Div));
+ assert!(matches!(err.0, MathErrorKind::DivByZero));
} else {
panic!("Expected a division by zero error, but got a valid result");
}
@@ -1654,20 +1650,26 @@ fn checked_rem() {
fn amount_div_by_weight_floor_error() {
// Division by zero weight returns error
let err = sat(100).div_by_weight_floor(Weight::ZERO).unwrap_err();
- assert_eq!(err, NumOpError::while_doing(MathOp::Div));
+ assert_eq!(err, NumOpError::while_doing(MathErrorKind::DivByZero));
// Overflow case: Amount::MAX * 1000 overflows
let err = Amount::MAX.div_by_weight_floor(Weight::from_wu(1)).unwrap_err();
- assert_eq!(err, NumOpError::while_doing(MathOp::Mul));
+ assert_eq!(
+ err,
+ NumOpError::while_doing(MathErrorKind::Overflow { op: MathOp::Div, is_negative: false })
+ );
}
#[test]
fn amount_div_by_weight_ceil_error() {
// Division by zero weight returns error
let err = sat(100).div_by_weight_ceil(Weight::ZERO).unwrap_err();
- assert_eq!(err, NumOpError::while_doing(MathOp::Div));
+ assert_eq!(err, NumOpError::while_doing(MathErrorKind::DivByZero));
// Overflow case: Amount::MAX * 1000 overflows
let err = Amount::MAX.div_by_weight_ceil(Weight::from_wu(1)).unwrap_err();
- assert_eq!(err, NumOpError::while_doing(MathOp::Mul));
+ assert_eq!(
+ err,
+ NumOpError::while_doing(MathErrorKind::Overflow { op: MathOp::Div, is_negative: false })
+ );
}
### units/src/amount/unsigned.rs
@@ -19,7 +19,7 @@ use super::{
parse_signed_to_satoshi, split_amount_and_denomination, Denomination, Display, DisplayStyle,
OutOfRangeError, ParseAmountError, ParseError, SignedAmount,
};
-use crate::result::{MathOp, NumOpError as E, NumOpResult};
+use crate::result::{MathErrorKind, MathOp, NumOpError as E, NumOpResult};
use crate::{parse_int, FeeRate, Weight};
mod encapsulate {
@@ -471,11 +471,10 @@ impl Amount {
if let Ok(amount) = Self::from_sat(fee_rate) {
return FeeRate::from_per_kwu(amount);
},
- None => return R::Error(E::while_doing(MathOp::Div)),
+ None => return R::Error(E::while_doing(MathErrorKind::DivByZero)),
}
}
- // Use `MathOp::Mul` because `Div` implies div by zero.
- R::Error(E::while_doing(MathOp::Mul))
+ R::Error(E::while_doing(MathErrorKind::Overflow { op: MathOp::Div, is_negative: false }))
}
/// Checked weight ceiling division.
@@ -498,7 +497,7 @@ impl Amount {
pub const fn div_by_weight_ceil(self, weight: Weight) -> NumOpResult<FeeRate> {
let wu = weight.to_wu();
if wu == 0 {
- return R::Error(E::while_doing(MathOp::Div));
+ return R::Error(E::while_doing(MathErrorKind::DivByZero));
}
// Mul by 1,000 because we use per/kwu.
@@ -509,8 +508,7 @@ impl Amount {
return FeeRate::from_per_kwu(amount);
}
}
- // Use `MathOp::Mul` because `Div` implies div by zero.
- R::Error(E::while_doing(MathOp::Mul))
+ R::Error(E::while_doing(MathErrorKind::Overflow { op: MathOp::Div, is_negative: false }))
}
/// Checked fee rate floor division.
@@ -524,7 +522,7 @@ impl Amount {
let msats = self.to_sat() * 1_000;
match msats.checked_div(fee_rate.to_sat_per_kwu_ceil()) {
Some(wu) => R::Valid(Weight::from_wu(wu)),
- None => R::Error(E::while_doing(MathOp::Div)),
+ None => R::Error(E::while_doing(MathErrorKind::DivByZero)),
}
}
@@ -538,7 +536,7 @@ impl Amount {
let rate = fee_rate.to_sat_per_kwu_ceil();
// Early return so we do not have to use checked arithmetic below.
if rate == 0 {
- return R::Error(E::while_doing(MathOp::Div));
+ return R::Error(E::while_doing(MathErrorKind::DivByZero));
}
debug_assert!(Self::MAX.to_sat().checked_mul(1_000).is_some());
### units/src/fee_rate/mod.rs
@@ -12,7 +12,7 @@ use core::ops;
use arbitrary::{Arbitrary, Unstructured};
use NumOpResult as R;
-use crate::result::{MathOp, NumOpError as E, NumOpResult};
+use crate::result::{MathErrorKind, MathOp, NumOpError as E, NumOpResult};
use crate::{Amount, Weight};
mod encapsulate {
@@ -80,7 +80,10 @@ impl FeeRate {
// No `map()` in const context.
match rate.checked_mul(4_000) {
Some(per_mvb) => R::Valid(Self::from_sat_per_mvb(per_mvb.to_sat())),
- None => R::Error(E::while_doing(MathOp::Mul)),
+ None => R::Error(E::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Mul,
+ is_negative: false,
+ })),
}
}
@@ -97,7 +100,10 @@ impl FeeRate {
// 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())),
- None => R::Error(E::while_doing(MathOp::Mul)),
+ None => R::Error(E::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Mul,
+ is_negative: false,
+ })),
}
}
@@ -114,7 +120,10 @@ impl FeeRate {
// No `map()` in const context.
match rate.checked_mul(1_000) {
Some(per_mvb) => R::Valid(Self::from_sat_per_mvb(per_mvb.to_sat())),
- None => R::Error(E::while_doing(MathOp::Mul)),
+ None => R::Error(E::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Mul,
+ is_negative: false,
+ })),
}
}
@@ -218,7 +227,10 @@ impl FeeRate {
return NumOpResult::Valid(fee_amount);
}
}
- NumOpResult::Error(E::while_doing(MathOp::Mul))
+ NumOpResult::Error(E::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Mul,
+ is_negative: false,
+ }))
}
}
### units/src/internal_macros.rs
@@ -103,9 +103,8 @@ macro_rules! impl_add_assign_for_results {
impl ops::AddAssign<$ty> for NumOpResult<$ty> {
#[inline]
fn add_assign(&mut self, rhs: $ty) {
- match self {
- Self::Error(_) => *self = Self::Error(NumOpError::while_doing(MathOp::Add)),
- Self::Valid(ref lhs) => *self = lhs + rhs,
+ if let Self::Valid(ref lhs) = self {
+ *self = lhs + rhs
}
}
}
@@ -115,7 +114,8 @@ macro_rules! impl_add_assign_for_results {
fn add_assign(&mut self, rhs: Self) {
match (&self, rhs) {
(Self::Valid(_), Self::Valid(rhs)) => *self += rhs,
- (_, _) => *self = Self::Error(NumOpError::while_doing(MathOp::Add)),
+ (Self::Valid(_), Self::Error(err)) => *self = Self::Error(err),
+ (Self::Error(_), _) => (), // If the lhs is an error, preserve it
}
}
}
@@ -137,9 +137,8 @@ macro_rules! impl_sub_assign_for_results {
impl ops::SubAssign<$ty> for NumOpResult<$ty> {
#[inline]
fn sub_assign(&mut self, rhs: $ty) {
- match self {
- Self::Error(_) => *self = Self::Error(NumOpError::while_doing(MathOp::Sub)),
- Self::Valid(ref lhs) => *self = lhs - rhs,
+ if let Self::Valid(ref lhs) = self {
+ *self = lhs - rhs
}
}
}
@@ -149,7 +148,8 @@ macro_rules! impl_sub_assign_for_results {
fn sub_assign(&mut self, rhs: Self) {
match (&self, rhs) {
(Self::Valid(_), Self::Valid(rhs)) => *self -= rhs,
- (_, _) => *self = Self::Error(NumOpError::while_doing(MathOp::Sub)),
+ (Self::Valid(_), Self::Error(err)) => *self = Self::Error(err),
+ (Self::Error(_), _) => (), // If the lhs is an error, preserve it
}
}
}
### units/src/result.rs
@@ -54,9 +54,9 @@ pub use self::error::NumOpError;
/// # Ok::<_, amount::OutOfRangeError>(())
/// ```
///
-/// ### Divide-by-zero (overflow in [`Div`] or [`Rem`])
+/// ### Failure in chained operations
///
-/// In some instances one may wish to differentiate div-by-zero from overflow.
+/// In some instances one may wish to differentiate one math op failure from another.
///
/// ```
/// # use bitcoin_units::{Amount, FeeRate, NumOpResult, result::NumOpError};
@@ -70,12 +70,12 @@ pub use self::error::NumOpError;
/// let max_fee = a + b;
/// let _fee = match max_fee / fee_rate {
/// NumOpResult::Valid(fee) => fee,
-/// NumOpResult::Error(e) if e.is_div_by_zero() => {
-/// // Do something when div by zero.
+/// NumOpResult::Error(e) if e.operation().is_division() => {
+/// // Do something when division fails (div by zero or perhaps MIN / -1).
/// return Err(e);
/// },
/// NumOpResult::Error(e) => {
-/// // We separate div-by-zero from overflow in case it needs to be handled separately.
+/// // And something else if the addition overflowed.
/// //
/// // This branch could be hit since `max_fee` came from some previous calculation. And if
/// // an input to that calculation was from the user then overflow could be an attack vector.
@@ -248,7 +248,7 @@ crate::internal_macros::impl_op_for_references! {
fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
- (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
+ (_, _) => R::Error(NumOpError::while_doing(MathErrorKind::Overflow { op: MathOp::Add, is_negative: false })),
}
}
}
@@ -271,7 +271,7 @@ crate::internal_macros::impl_op_for_references! {
fn sub(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(R::Valid(lhs), R::Valid(rhs)) => lhs - rhs,
- (_, _) => R::Error(NumOpError::while_doing(MathOp::Sub)),
+ (_, _) => R::Error(NumOpError::while_doing(MathErrorKind::Overflow { op: MathOp::Sub, is_negative: true })),
}
}
}
@@ -306,7 +306,8 @@ impl<T: ops::AddAssign + Copy> ops::AddAssign<Self> for NumOpResult<T> {
fn add_assign(&mut self, rhs: Self) {
match (&self, rhs) {
(Self::Valid(_), Self::Valid(rhs)) => *self += rhs,
- (_, _) => *self = Self::Error(NumOpError::while_doing(MathOp::Add)),
+ (Self::Valid(_), Self::Error(err)) => *self = Self::Error(err),
+ (Self::Error(_), _) => (), // If the lhs is an error, preserve it
}
}
}
@@ -326,24 +327,25 @@ impl<T: ops::SubAssign + Copy> ops::SubAssign<Self> for NumOpResult<T> {
fn sub_assign(&mut self, rhs: Self) {
match (&self, rhs) {
(Self::Valid(_), Self::Valid(rhs)) => *self -= rhs,
- (_, _) => *self = Self::Error(NumOpError::while_doing(MathOp::Sub)),
+ (Self::Valid(_), Self::Error(err)) => *self = Self::Error(err),
+ (Self::Error(_), _) => (), // If the lhs is an error, preserve it
}
}
}
pub(crate) trait OptionExt<T> {
- fn valid_or_error(self, op: MathOp) -> NumOpResult<T>;
+ fn valid_or_error(self, kind: MathErrorKind) -> NumOpResult<T>;
}
macro_rules! impl_opt_ext {
($($ty:ident),* $(,)?) => {
$(
impl OptionExt<$ty> for Option<$ty> {
#[inline]
- fn valid_or_error(self, op: MathOp) -> NumOpResult<$ty> {
+ fn valid_or_error(self, kind: MathErrorKind) -> NumOpResult<$ty> {
match self {
Some(amount) => R::Valid(amount),
- None => R::Error(NumOpError(op)),
+ None => R::Error(NumOpError::while_doing(kind)),
}
}
}
@@ -375,16 +377,6 @@ pub enum MathOp {
}
impl MathOp {
- /// Returns `true` if this operation error'ed due to overflow.
- #[inline]
- pub fn is_overflow(self) -> bool {
- matches!(self, Self::Add | Self::Sub | Self::Mul | Self::Neg)
- }
-
- /// Returns `true` if this operation error'ed due to division by zero.
- #[inline]
- pub fn is_div_by_zero(self) -> bool { !self.is_overflow() }
-
/// Returns `true` if this operation error'ed due to addition.
#[inline]
pub fn is_addition(self) -> bool { self == Self::Add }
@@ -397,6 +389,14 @@ impl MathOp {
#[inline]
pub fn is_multiplication(self) -> bool { self == Self::Mul }
+ /// Returns `true` if this operation error'ed due to division.
+ #[inline]
+ pub fn is_division(self) -> bool { self == Self::Div }
+
+ /// Returns `true` if this operation error'ed due to remainder.
+ #[inline]
+ pub fn is_remainder(self) -> bool { self == Self::Rem }
+
/// Returns `true` if this operation error'ed due to negation.
#[inline]
pub fn is_negation(self) -> bool { self == Self::Neg }
@@ -417,34 +417,46 @@ impl fmt::Display for MathOp {
}
}
+/// The kind of error that happened during a mathematical operation failure.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum MathErrorKind {
+ // The result overflowed.
+ Overflow {
+ op: MathOp,
+ is_negative: bool,
+ },
+ /// A division by zero was performed.
+ DivByZero,
+ /// A remainder by zero was performed.
+ RemByZero,
+}
+
/// Error types for mathematical operations.
pub mod error {
use core::convert::Infallible;
use core::fmt;
- use super::MathOp;
+ use super::{MathErrorKind, MathOp};
/// Error returned when a mathematical operation fails.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
- pub struct NumOpError(pub(super) MathOp);
+ pub struct NumOpError(pub(crate) MathErrorKind);
impl NumOpError {
- /// Constructs a [`NumOpError`] caused by `op`.
- #[inline]
- pub(crate) const fn while_doing(op: MathOp) -> Self { Self(op) }
-
- /// Returns `true` if this operation error'ed due to overflow.
- #[inline]
- pub fn is_overflow(self) -> bool { self.0.is_overflow() }
-
- /// Returns `true` if this operation error'ed due to division by zero.
+ /// Constructs a [`NumOpError`] caused by `kind`.
#[inline]
- pub fn is_div_by_zero(self) -> bool { self.0.is_div_by_zero() }
+ pub(crate) const fn while_doing(kind: MathErrorKind) -> Self { Self(kind) }
/// Returns the [`MathOp`] that caused this error.
#[inline]
- pub fn operation(self) -> MathOp { self.0 }
+ pub fn operation(self) -> MathOp {
+ match self.0 {
+ MathErrorKind::DivByZero => MathOp::Div,
+ MathErrorKind::RemByZero => MathOp::Rem,
+ MathErrorKind::Overflow { op, is_negative: _ } => op,
+ }
+ }
}
impl From<Infallible> for NumOpError {
@@ -455,7 +467,15 @@ pub mod error {
impl fmt::Display for NumOpError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "math operation '{}' gave an invalid numeric result", self.operation())
+ match self.0 {
+ MathErrorKind::Overflow { op, is_negative: false } =>
+ write!(f, "failed to {}: the result would be too large", op),
+ MathErrorKind::Overflow { op, is_negative: true } =>
+ write!(f, "failed to {}: the result would be too small", op),
+ MathErrorKind::DivByZero => write!(f, "division by zero"),
+ MathErrorKind::RemByZero =>
+ write!(f, "attempt to compute the remainder of division by zero"),
+ }
}
}
@@ -475,7 +495,7 @@ impl<'a, T: Arbitrary<'a>> Arbitrary<'a> for NumOpResult<T> {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
match bool::arbitrary(u)? {
true => Ok(Self::Valid(T::arbitrary(u)?)),
- false => Ok(Self::Error(NumOpError(MathOp::arbitrary(u)?))),
+ false => Ok(Self::Error(NumOpError(MathErrorKind::arbitrary(u)?))),
}
}
}
@@ -496,29 +516,31 @@ impl<'a> Arbitrary<'a> for MathOp {
}
}
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for MathErrorKind {
+ #[inline]
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ let choice = u.int_in_range(0..=2)?;
+ match choice {
+ 0 => Ok(Self::Overflow { op: MathOp::arbitrary(u)?, is_negative: bool::arbitrary(u)? }),
+ 1 => Ok(Self::RemByZero),
+ _ => Ok(Self::DivByZero),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
use alloc::string::ToString;
#[cfg(feature = "std")]
use std::error::Error;
- use crate::result::{MathOp, NumOpError, NumOpResult};
+ use crate::result::{MathErrorKind, MathOp, NumOpError, NumOpResult};
use crate::{Amount, FeeRate, Weight};
#[test]
fn mathop_predicates() {
- assert!(MathOp::Add.is_overflow());
- assert!(MathOp::Sub.is_overflow());
- assert!(MathOp::Mul.is_overflow());
- assert!(MathOp::Neg.is_overflow());
- assert!(!MathOp::Div.is_overflow());
- assert!(!MathOp::Rem.is_overflow());
-
- assert!(MathOp::Div.is_div_by_zero());
- assert!(MathOp::Rem.is_div_by_zero());
- assert!(!MathOp::Add.is_div_by_zero());
-
assert!(MathOp::Add.is_addition());
assert!(!MathOp::Sub.is_addition());
@@ -530,6 +552,12 @@ mod tests {
assert!(MathOp::Neg.is_negation());
assert!(!MathOp::Add.is_negation());
+
+ assert!(MathOp::Div.is_division());
+ assert!(!MathOp::Rem.is_division());
+
+ assert!(MathOp::Rem.is_remainder());
+ assert!(!MathOp::Div.is_remainder());
}
#[test]
@@ -540,7 +568,10 @@ mod tests {
assert_eq!(new_value, NumOpResult::Valid(Weight::from_wu(10_000)));
// op is not evaluated for error results
- let res = NumOpResult::<Weight>::Error(NumOpError::while_doing(MathOp::Add));
+ let res = NumOpResult::<Weight>::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
let res_err = res.map(|_| {
panic!("map should not evaluate for wrapped error values");
});
@@ -566,8 +597,11 @@ mod tests {
#[test]
#[should_panic(expected = "test error message")]
fn mathop_expect_panics_on_error() {
- NumOpResult::<Amount>::Error(NumOpError::while_doing(MathOp::Add))
- .expect("test error message");
+ NumOpResult::<Amount>::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }))
+ .expect("test error message");
}
#[test]
@@ -589,18 +623,31 @@ mod tests {
#[test]
#[should_panic(expected = "")]
fn mathop_unwrap_panics_on_err() {
- NumOpResult::<Amount>::Error(NumOpError::while_doing(MathOp::Add)).unwrap();
+ NumOpResult::<Amount>::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }))
+ .unwrap();
}
#[test]
fn mathop_unwrap_err() {
let errs = [
- NumOpError::while_doing(MathOp::Add),
- NumOpError::while_doing(MathOp::Sub),
- NumOpError::while_doing(MathOp::Mul),
- NumOpError::while_doing(MathOp::Div),
- NumOpError::while_doing(MathOp::Neg),
- NumOpError::while_doing(MathOp::Rem),
+ NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }),
+ NumOpError::while_doing(MathErrorKind::Overflow { op: MathOp::Sub, is_negative: true }),
+ NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Mul,
+ is_negative: false,
+ }),
+ NumOpError::while_doing(MathErrorKind::DivByZero),
+ NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Neg,
+ is_negative: false,
+ }),
+ NumOpError::while_doing(MathErrorKind::RemByZero),
];
for err in errs {
assert_eq!(NumOpResult::<Amount>::Error(err).unwrap_err(), err);
@@ -619,7 +666,10 @@ mod tests {
let base_amount = Amount::from_sat_u32(100);
// default is returned for error results
- let res = NumOpResult::<Amount>::Error(NumOpError::while_doing(MathOp::Add));
+ let res = NumOpResult::<Amount>::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
let res_default = res.unwrap_or(base_amount);
assert_eq!(res_default, base_amount);
@@ -634,7 +684,10 @@ mod tests {
let base_amount = Amount::from_sat_u32(100);
// op is evaluated for error results
- let res = NumOpResult::<Amount>::Error(NumOpError::while_doing(MathOp::Add));
+ let res = NumOpResult::<Amount>::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
let res_default = res.unwrap_or_else(|| base_amount);
assert_eq!(res_default, base_amount);
@@ -651,7 +704,10 @@ mod tests {
let amt = Amount::from_sat_u32(150);
assert_eq!(NumOpResult::Valid(amt).ok(), Some(amt));
- let err = NumOpError::while_doing(MathOp::Add);
+ let err = NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ });
assert_eq!(NumOpResult::<Amount>::Error(err).ok(), None);
}
@@ -663,7 +719,10 @@ mod tests {
assert_eq!(new_value, NumOpResult::Valid(Amount::from_sat_u32(150)));
// op is not evaluated for error results
- let res = NumOpResult::<Amount>::Error(NumOpError::while_doing(MathOp::Add));
+ let res = NumOpResult::<Amount>::Error(NumOpError::while_doing(MathErrorKind::Overflow {
+ op: MathOp::Add,
+ is_negative: false,
+ }));
let res_err = res.and_then(|_| {
panic!("and_then should not evaluate for wrapped error values");
});Why this scored 19/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.