//! Strongly-typed zatoshi amounts that prevent under/overflows. //! //! The [`Amount`] type is parameterized by a [`Constraint`] implementation that //! declares the range of allowed values. In contrast to regular arithmetic //! operations, which return values, arithmetic on [`Amount`]s returns //! [`Result`](std::result::Result)s. use std::{ cmp::Ordering, fmt, hash::{Hash, Hasher}, marker::PhantomData, ops::RangeInclusive, }; use crate::serialization::{ZcashDeserialize, ZcashSerialize}; use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt}; #[cfg(any(test, feature = "proptest-impl"))] pub mod arbitrary; #[cfg(test)] mod tests; /// The result of an amount operation. pub type Result = std::result::Result; /// A runtime validated type for representing amounts of zatoshis // // TODO: // - remove the default NegativeAllowed bound, to make consensus rule reviews easier // - put a Constraint bound on the type generic, not just some implementations #[derive(Clone, Copy, Serialize, Deserialize, Default)] #[serde(try_from = "i64")] #[serde(into = "i64")] #[serde(bound = "C: Constraint + Clone")] pub struct Amount( /// The inner amount value. i64, /// Used for [`Constraint`] type inference. /// /// # Correctness /// /// This internal Zebra marker type is not consensus-critical. /// And it should be ignored during testing. (And other internal uses.) #[serde(skip)] PhantomData, ); impl fmt::Display for Amount { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let zats = self.zatoshis(); f.pad_integral(zats > 0, "", &zats.to_string()) } } impl fmt::Debug for Amount { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple(&format!("Amount<{}>", std::any::type_name::())) .field(&self.0) .finish() } } impl Amount { /// Convert this amount to a different Amount type if it satisfies the new constraint pub fn constrain(self) -> Result> where C2: Constraint, { self.0.try_into() } /// Returns the number of zatoshis in this amount. pub fn zatoshis(&self) -> i64 { self.0 } /// To little endian byte array pub fn to_bytes(&self) -> [u8; 8] { let mut buf: [u8; 8] = [0; 8]; LittleEndian::write_i64(&mut buf, self.0); buf } /// From little endian byte array pub fn from_bytes(bytes: [u8; 8]) -> Result> where C: Constraint, { let amount = i64::from_le_bytes(bytes); amount.try_into() } /// Create a zero `Amount` pub fn zero() -> Amount where C: Constraint, { 0.try_into().expect("an amount of 0 is always valid") } } impl std::ops::Add> for Amount where C: Constraint, { type Output = Result>; fn add(self, rhs: Amount) -> Self::Output { let value = self .0 .checked_add(rhs.0) .expect("adding two constrained Amounts is always within an i64"); value.try_into() } } impl std::ops::Add> for Result> where C: Constraint, { type Output = Result>; fn add(self, rhs: Amount) -> Self::Output { self? + rhs } } impl std::ops::Add>> for Amount where C: Constraint, { type Output = Result>; fn add(self, rhs: Result>) -> Self::Output { self + rhs? } } impl std::ops::AddAssign> for Result> where Amount: Copy, C: Constraint, { fn add_assign(&mut self, rhs: Amount) { if let Ok(lhs) = *self { *self = lhs + rhs; } } } impl std::ops::Sub> for Amount where C: Constraint, { type Output = Result>; fn sub(self, rhs: Amount) -> Self::Output { let value = self .0 .checked_sub(rhs.0) .expect("subtracting two constrained Amounts is always within an i64"); value.try_into() } } impl std::ops::Sub> for Result> where C: Constraint, { type Output = Result>; fn sub(self, rhs: Amount) -> Self::Output { self? - rhs } } impl std::ops::Sub>> for Amount where C: Constraint, { type Output = Result>; fn sub(self, rhs: Result>) -> Self::Output { self - rhs? } } impl std::ops::SubAssign> for Result> where Amount: Copy, C: Constraint, { fn sub_assign(&mut self, rhs: Amount) { if let Ok(lhs) = *self { *self = lhs - rhs; } } } impl From> for i64 { fn from(amount: Amount) -> Self { amount.0 } } impl From> for u64 { fn from(amount: Amount) -> Self { amount.0.try_into().expect("non-negative i64 fits in u64") } } impl From> for jubjub::Fr { fn from(a: Amount) -> jubjub::Fr { // TODO: this isn't constant time -- does that matter? if a.0 < 0 { let abs_amount = i128::from(a.0) .checked_abs() .expect("absolute i64 fits in i128"); let abs_amount = u64::try_from(abs_amount).expect("absolute i64 fits in u64"); jubjub::Fr::from(abs_amount).neg() } else { jubjub::Fr::from(u64::try_from(a.0).expect("non-negative i64 fits in u64")) } } } impl From> for halo2::pasta::pallas::Scalar { fn from(a: Amount) -> halo2::pasta::pallas::Scalar { // TODO: this isn't constant time -- does that matter? if a.0 < 0 { let abs_amount = i128::from(a.0) .checked_abs() .expect("absolute i64 fits in i128"); let abs_amount = u64::try_from(abs_amount).expect("absolute i64 fits in u64"); halo2::pasta::pallas::Scalar::from(abs_amount).neg() } else { halo2::pasta::pallas::Scalar::from( u64::try_from(a.0).expect("non-negative i64 fits in u64"), ) } } } impl TryFrom for Amount where C: Constraint, { type Error = Error; fn try_from(value: i32) -> Result { C::validate(value.into()).map(|v| Self(v, PhantomData)) } } impl TryFrom for Amount where C: Constraint, { type Error = Error; fn try_from(value: i64) -> Result { C::validate(value).map(|v| Self(v, PhantomData)) } } impl TryFrom for Amount where C: Constraint, { type Error = Error; fn try_from(value: u64) -> Result { let value = value.try_into().map_err(|source| Error::Convert { value: value.into(), source, })?; C::validate(value).map(|v| Self(v, PhantomData)) } } /// Conversion from `i128` to `Amount`. /// /// Used to handle the result of multiplying negative `Amount`s by `u64`. impl TryFrom for Amount where C: Constraint, { type Error = Error; fn try_from(value: i128) -> Result { let value = value .try_into() .map_err(|source| Error::Convert { value, source })?; C::validate(value).map(|v| Self(v, PhantomData)) } } impl Hash for Amount { /// Amounts with the same value are equal, even if they have different constraints fn hash(&self, state: &mut H) { self.0.hash(state); } } impl PartialEq> for Amount { fn eq(&self, other: &Amount) -> bool { self.0.eq(&other.0) } } impl PartialEq for Amount { fn eq(&self, other: &i64) -> bool { self.0.eq(other) } } impl PartialEq> for i64 { fn eq(&self, other: &Amount) -> bool { self.eq(&other.0) } } impl Eq for Amount {} impl PartialOrd> for Amount { fn partial_cmp(&self, other: &Amount) -> Option { Some(self.0.cmp(&other.0)) } } impl Ord for Amount { fn cmp(&self, other: &Amount) -> Ordering { self.0.cmp(&other.0) } } impl std::ops::Mul for Amount where C: Constraint, { type Output = Result>; fn mul(self, rhs: u64) -> Self::Output { // use i128 for multiplication, so we can handle negative Amounts let value = i128::from(self.0) .checked_mul(i128::from(rhs)) .expect("multiplying i64 by u64 can't overflow i128"); value.try_into().map_err(|_| Error::MultiplicationOverflow { amount: self.0, multiplier: rhs, overflowing_result: value, }) } } impl std::ops::Mul> for u64 where C: Constraint, { type Output = Result>; fn mul(self, rhs: Amount) -> Self::Output { rhs.mul(self) } } impl std::ops::Div for Amount where C: Constraint, { type Output = Result>; fn div(self, rhs: u64) -> Self::Output { let quotient = i128::from(self.0) .checked_div(i128::from(rhs)) .ok_or(Error::DivideByZero { amount: self.0 })?; Ok(quotient .try_into() .expect("division by a positive integer always stays within the constraint")) } } impl std::iter::Sum> for Result> where C: Constraint, { fn sum>>(mut iter: I) -> Self { let sum = iter.try_fold(Amount::zero(), |acc, amount| acc + amount); match sum { Ok(sum) => Ok(sum), Err(Error::Constraint { value, .. }) => Err(Error::SumOverflow { partial_sum: value, remaining_items: iter.count(), }), Err(unexpected_error) => unreachable!("unexpected Add error: {:?}", unexpected_error), } } } impl<'amt, C> std::iter::Sum<&'amt Amount> for Result> where C: Constraint + Copy + 'amt, { fn sum>>(iter: I) -> Self { iter.copied().sum() } } // TODO: add infalliable impls for NonNegative <-> NegativeOrZero, // when Rust uses trait output types to disambiguate overlapping impls. impl std::ops::Neg for Amount where C: Constraint, { type Output = Amount; fn neg(self) -> Self::Output { Amount::::try_from(-self.0) .expect("a negation of any Amount into NegativeAllowed is always valid") } } #[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq, Eq)] #[allow(missing_docs)] /// Errors that can be returned when validating `Amount`s pub enum Error { /// input {value} is outside of valid range for zatoshi Amount, valid_range={range:?} Constraint { value: i64, range: RangeInclusive, }, /// {value} could not be converted to an i64 Amount Convert { value: i128, source: std::num::TryFromIntError, }, /// i64 overflow when multiplying i64 amount {amount} by u64 {multiplier}, overflowing result {overflowing_result} MultiplicationOverflow { amount: i64, multiplier: u64, overflowing_result: i128, }, /// cannot divide amount {amount} by zero DivideByZero { amount: i64 }, /// i64 overflow when summing i64 amounts, partial_sum: {partial_sum}, remaining items: {remaining_items} SumOverflow { partial_sum: i64, remaining_items: usize, }, } impl Error { /// Returns the invalid value for this error. /// /// This value may be an initial input value, partially calculated value, /// or an overflowing or underflowing value. pub fn invalid_value(&self) -> i128 { use Error::*; match self.clone() { Constraint { value, .. } => value.into(), Convert { value, .. } => value, MultiplicationOverflow { overflowing_result, .. } => overflowing_result, DivideByZero { amount } => amount.into(), SumOverflow { partial_sum, .. } => partial_sum.into(), } } } /// Marker type for `Amount` that allows negative values. /// /// ``` /// # use zebra_chain::amount::{Constraint, MAX_MONEY, NegativeAllowed}; /// assert_eq!( /// NegativeAllowed::valid_range(), /// -MAX_MONEY..=MAX_MONEY, /// ); /// ``` #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] pub struct NegativeAllowed; impl Constraint for NegativeAllowed { fn valid_range() -> RangeInclusive { -MAX_MONEY..=MAX_MONEY } } /// Marker type for `Amount` that requires nonnegative values. /// /// ``` /// # use zebra_chain::amount::{Constraint, MAX_MONEY, NonNegative}; /// assert_eq!( /// NonNegative::valid_range(), /// 0..=MAX_MONEY, /// ); /// ``` #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)] pub struct NonNegative; impl Constraint for NonNegative { fn valid_range() -> RangeInclusive { 0..=MAX_MONEY } } /// Marker type for `Amount` that requires negative or zero values. /// /// Used for coinbase transactions in `getblocktemplate` RPCs. /// /// ``` /// # use zebra_chain::amount::{Constraint, MAX_MONEY, NegativeOrZero}; /// assert_eq!( /// NegativeOrZero::valid_range(), /// -MAX_MONEY..=0, /// ); /// ``` #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct NegativeOrZero; impl Constraint for NegativeOrZero { fn valid_range() -> RangeInclusive { -MAX_MONEY..=0 } } /// Number of zatoshis in 1 ZEC pub const COIN: i64 = 100_000_000; /// The maximum zatoshi amount. pub const MAX_MONEY: i64 = 21_000_000 * COIN; /// A trait for defining constraints on `Amount` pub trait Constraint { /// Returns the range of values that are valid under this constraint fn valid_range() -> RangeInclusive; /// Check if an input value is within the valid range fn validate(value: i64) -> Result { let range = Self::valid_range(); if !range.contains(&value) { Err(Error::Constraint { value, range }) } else { Ok(value) } } } impl ZcashSerialize for Amount { fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { writer.write_i64::(self.0) } } impl ZcashDeserialize for Amount { fn zcash_deserialize( mut reader: R, ) -> Result { Ok(reader.read_i64::()?.try_into()?) } } impl ZcashSerialize for Amount { #[allow(clippy::unwrap_in_result)] fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { let amount = self .0 .try_into() .expect("constraint guarantees value is positive"); writer.write_u64::(amount) } } impl ZcashDeserialize for Amount { fn zcash_deserialize( mut reader: R, ) -> Result { Ok(reader.read_u64::()?.try_into()?) } }