Add dedicated error for when amounts aren't representable within a u64

Fixes the issue where _inputs_ could still overflow u64::MAX and cause a panic.
This commit is contained in:
Luke Parker
2025-08-09 14:06:27 -04:00
parent 54c9d19726
commit 8fcfa6d3d5
2 changed files with 42 additions and 17 deletions

View File

@@ -177,6 +177,17 @@ pub enum SendError {
/// The created transaction was too large. /// The created transaction was too large.
#[cfg_attr(feature = "std", error("too large of a transaction"))] #[cfg_attr(feature = "std", error("too large of a transaction"))]
TooLargeTransaction, TooLargeTransaction,
/// The transactions' amounts could not be represented within a `u64`.
#[cfg_attr(
feature = "std",
error("transaction amounts exceed u64::MAX (in {in_amount}, out {out_amount})")
)]
AmountsUnrepresentable {
/// The amount in (via inputs).
in_amount: u128,
/// The amount which would be out (between outputs and the fee).
out_amount: u128,
},
/// This transaction could not pay for itself. /// This transaction could not pay for itself.
#[cfg_attr( #[cfg_attr(
feature = "std", feature = "std",
@@ -300,23 +311,34 @@ impl SignableTransaction {
} }
// Make sure we have enough funds // Make sure we have enough funds
let in_amount = self.inputs.iter().map(|input| input.commitment().amount).sum::<u64>(); let weight;
let payments_amount = self {
.payments let in_amount: u128 =
.iter() self.inputs.iter().map(|input| u128::from(input.commitment().amount)).sum();
.filter_map(|payment| match payment { let payments_amount: u128 = self
InternalPayment::Payment(_, amount) => Some(*amount), .payments
InternalPayment::Change(_) => None, .iter()
}) .filter_map(|payment| match payment {
.try_fold(0, u64::checked_add); InternalPayment::Payment(_, amount) => Some(u128::from(*amount)),
let payments_amount = payments_amount.ok_or(SendError::TooManyOutputs)?; InternalPayment::Change(_) => None,
let (weight, necessary_fee) = self.weight_and_necessary_fee(); })
if payments_amount.checked_add(necessary_fee).is_none_or(|total_out| in_amount < total_out) { .sum();
Err(SendError::NotEnoughFunds { let necessary_fee;
inputs: in_amount, (weight, necessary_fee) = self.weight_and_necessary_fee();
outputs: payments_amount, let out_amount = payments_amount + u128::from(necessary_fee);
necessary_fee: Some(necessary_fee), let in_out_amount = u64::try_from(in_amount)
})?; .and_then(|in_amount| u64::try_from(out_amount).map(|out_amount| (in_amount, out_amount)));
let Ok((in_amount, out_amount)) = in_out_amount else {
Err(SendError::AmountsUnrepresentable { in_amount, out_amount })?
};
if in_amount < out_amount {
Err(SendError::NotEnoughFunds {
inputs: in_amount,
outputs: u64::try_from(payments_amount)
.expect("total out fit within u64 but not part of total out"),
necessary_fee: Some(necessary_fee),
})?;
}
} }
// The limit is half the no-penalty block size // The limit is half the no-penalty block size

View File

@@ -390,6 +390,8 @@ impl Monero {
MakeSignableTransactionResult::SignableTransaction(signable) MakeSignableTransactionResult::SignableTransaction(signable)
} }
})), })),
// AmountsUnrepresentable is unreachable on Monero without 100% of the supply before tail
// emission or fundamental corruption
Err(e) => match e { Err(e) => match e {
SendError::UnsupportedRctType => { SendError::UnsupportedRctType => {
panic!("trying to use an RctType unsupported by monero-wallet") panic!("trying to use an RctType unsupported by monero-wallet")
@@ -398,6 +400,7 @@ impl Monero {
SendError::InvalidDecoyQuantity | SendError::InvalidDecoyQuantity |
SendError::NoOutputs | SendError::NoOutputs |
SendError::TooManyOutputs | SendError::TooManyOutputs |
SendError::AmountsUnrepresentable { .. } |
SendError::NoChange | SendError::NoChange |
SendError::TooMuchArbitraryData | SendError::TooMuchArbitraryData |
SendError::TooLargeTransaction | SendError::TooLargeTransaction |