zebra/zebra-chain/src/amount.rs

556 lines
14 KiB
Rust
Raw Normal View History

//! Strongly-typed zatoshi amounts that prevent under/overflows.
2020-08-15 22:18:30 -07:00
//!
//! 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
2020-08-16 12:08:24 -07:00
//! [`Result`](std::result::Result)s.
2020-07-01 16:31:30 -07:00
use std::{
cmp::Ordering,
2020-07-01 16:31:30 -07:00
convert::{TryFrom, TryInto},
hash::{Hash, Hasher},
2020-07-01 16:31:30 -07:00
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<T, E = Error> = std::result::Result<T, E>;
2020-07-01 16:31:30 -07:00
/// A runtime validated type for representing amounts of zatoshis
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(try_from = "i64")]
#[serde(bound = "C: Constraint")]
pub struct Amount<C = NegativeAllowed>(
/// 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<C>,
);
2020-07-01 16:31:30 -07:00
impl<C> std::fmt::Debug for Amount<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(&format!("Amount<{}>", std::any::type_name::<C>()))
.field(&self.0)
.finish()
}
}
2020-07-01 16:31:30 -07:00
impl<C> Amount<C> {
/// Convert this amount to a different Amount type if it satisfies the new constraint
pub fn constrain<C2>(self) -> Result<Amount<C2>>
where
C2: Constraint,
2020-07-01 16:31:30 -07:00
{
self.0.try_into()
}
/// 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<Amount<C>>
where
C: Constraint,
{
let amount = i64::from_le_bytes(bytes);
amount.try_into()
}
/// Create a zero `Amount`
pub fn zero() -> Amount<C>
where
C: Constraint,
{
0.try_into().expect("an amount of 0 is always valid")
}
2020-07-01 16:31:30 -07:00
}
impl<C> std::ops::Add<Amount<C>> for Amount<C>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Output = Result<Amount<C>>;
fn add(self, rhs: Amount<C>) -> Self::Output {
let value = self
.0
.checked_add(rhs.0)
.expect("adding two constrained Amounts is always within an i64");
2020-07-01 16:31:30 -07:00
value.try_into()
}
}
impl<C> std::ops::Add<Amount<C>> for Result<Amount<C>>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Output = Result<Amount<C>>;
fn add(self, rhs: Amount<C>) -> Self::Output {
self? + rhs
}
}
impl<C> std::ops::Add<Result<Amount<C>>> for Amount<C>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Output = Result<Amount<C>>;
fn add(self, rhs: Result<Amount<C>>) -> Self::Output {
self + rhs?
}
}
impl<C> std::ops::AddAssign<Amount<C>> for Result<Amount<C>>
where
Amount<C>: Copy,
C: Constraint,
2020-07-01 16:31:30 -07:00
{
fn add_assign(&mut self, rhs: Amount<C>) {
if let Ok(lhs) = *self {
*self = lhs + rhs;
}
}
}
impl<C> std::ops::Sub<Amount<C>> for Amount<C>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Output = Result<Amount<C>>;
fn sub(self, rhs: Amount<C>) -> Self::Output {
let value = self
.0
.checked_sub(rhs.0)
.expect("subtracting two constrained Amounts is always within an i64");
2020-07-01 16:31:30 -07:00
value.try_into()
}
}
impl<C> std::ops::Sub<Amount<C>> for Result<Amount<C>>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Output = Result<Amount<C>>;
fn sub(self, rhs: Amount<C>) -> Self::Output {
self? - rhs
}
}
impl<C> std::ops::Sub<Result<Amount<C>>> for Amount<C>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Output = Result<Amount<C>>;
fn sub(self, rhs: Result<Amount<C>>) -> Self::Output {
self - rhs?
}
}
impl<C> std::ops::SubAssign<Amount<C>> for Result<Amount<C>>
where
Amount<C>: Copy,
C: Constraint,
2020-07-01 16:31:30 -07:00
{
fn sub_assign(&mut self, rhs: Amount<C>) {
if let Ok(lhs) = *self {
*self = lhs - rhs;
}
}
}
impl<C> From<Amount<C>> for i64 {
fn from(amount: Amount<C>) -> Self {
amount.0
}
}
impl From<Amount<NonNegative>> for u64 {
fn from(amount: Amount<NonNegative>) -> Self {
amount.0.try_into().expect("non-negative i64 fits in u64")
2020-07-01 16:31:30 -07:00
}
}
impl<C> From<Amount<C>> for jubjub::Fr {
fn from(a: Amount<C>) -> 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"))
}
}
}
2021-03-14 01:24:29 -08:00
impl<C> From<Amount<C>> for halo2::pasta::pallas::Scalar {
fn from(a: Amount<C>) -> 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()
2021-03-14 01:24:29 -08:00
} else {
halo2::pasta::pallas::Scalar::from(
u64::try_from(a.0).expect("non-negative i64 fits in u64"),
)
2021-03-14 01:24:29 -08:00
}
}
}
impl<C> TryFrom<i32> for Amount<C>
where
C: Constraint,
{
type Error = Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
C::validate(value.into()).map(|v| Self(v, PhantomData))
}
}
2020-07-01 16:31:30 -07:00
impl<C> TryFrom<i64> for Amount<C>
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Error = Error;
fn try_from(value: i64) -> Result<Self, Self::Error> {
C::validate(value).map(|v| Self(v, PhantomData))
}
}
impl<C> TryFrom<u64> for Amount<C>
2020-07-01 16:31:30 -07:00
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Error = Error;
fn try_from(value: u64) -> Result<Self, Self::Error> {
let value = value.try_into().map_err(|source| Error::Convert {
value: value.into(),
source,
})?;
C::validate(value).map(|v| Self(v, PhantomData))
2020-07-01 16:31:30 -07:00
}
}
/// Conversion from `i128` to `Amount`.
///
/// Used to handle the result of multiplying negative `Amount`s by `u64`.
impl<C> TryFrom<i128> for Amount<C>
2020-07-01 16:31:30 -07:00
where
C: Constraint,
2020-07-01 16:31:30 -07:00
{
type Error = Error;
fn try_from(value: i128) -> Result<Self, Self::Error> {
2020-07-01 16:31:30 -07:00
let value = value
.try_into()
.map_err(|source| Error::Convert { value, source })?;
C::validate(value).map(|v| Self(v, PhantomData))
}
}
impl<C> Hash for Amount<C> {
/// Amounts with the same value are equal, even if they have different constraints
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<C1, C2> PartialEq<Amount<C2>> for Amount<C1> {
fn eq(&self, other: &Amount<C2>) -> bool {
self.0.eq(&other.0)
}
}
Move the check in `transaction::check::sapling_balances_match` to `V4` deserialization (#2234) * Implement `PartialEq<i64>` for `Amount` Allows to compare an `Amount` instance directly to an integer. * Add `SerializationError::BadTransactionBalance` Error variant representing deserialization of a transaction that doesn't conform to the Sapling consensus rule where the balance MUST be zero if there aren't any shielded spends and outputs. * Validate consensus rule when deserializing Return an error if the deserialized V4 transaction has a non-zero value balance but doesn't have any Sapling shielded spends nor outputs. * Add consensus rule link to field documentation Describe how the consensus rule is validated structurally by `ShieldedData`. * Clarify that `value_balance` is zero Make the description more concise and objective. Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * Update field documentation Include information about how the consensus rule is guaranteed during serialization. Co-authored-by: teor <teor@riseup.net> * Remove `check::sapling_balances_match` function The check is redundant because the respective consensus rule is validated structurally by `ShieldedData`. * Test deserialization of invalid V4 transaction A transaction with no Sapling shielded spends and no outputs but with a non-zero balance value should fail to deserialize. * Change least-significant byte of the value balance State how the byte index is calculated, and change the least significant-byte to be non-zero. Co-authored-by: teor <teor@riseup.net>
2021-06-03 15:53:00 -07:00
impl<C> PartialEq<i64> for Amount<C> {
fn eq(&self, other: &i64) -> bool {
self.0.eq(other)
}
}
impl<C> PartialEq<Amount<C>> for i64 {
fn eq(&self, other: &Amount<C>) -> bool {
self.eq(&other.0)
}
}
impl<C> Eq for Amount<C> {}
impl<C1, C2> PartialOrd<Amount<C2>> for Amount<C1> {
fn partial_cmp(&self, other: &Amount<C2>) -> Option<Ordering> {
Some(self.0.cmp(&other.0))
}
}
impl<C> Ord for Amount<C> {
fn cmp(&self, other: &Amount<C>) -> Ordering {
self.0.cmp(&other.0)
}
}
impl<C> std::ops::Mul<u64> for Amount<C>
where
C: Constraint,
{
type Output = Result<Amount<C>>;
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<C> std::ops::Mul<Amount<C>> for u64
where
C: Constraint,
{
type Output = Result<Amount<C>>;
fn mul(self, rhs: Amount<C>) -> Self::Output {
rhs.mul(self)
}
}
impl<C> std::ops::Div<u64> for Amount<C>
where
C: Constraint,
{
type Output = Result<Amount<C>>;
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<C> std::iter::Sum<Amount<C>> for Result<Amount<C>>
where
C: Constraint,
{
fn sum<I: Iterator<Item = Amount<C>>>(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<C>> for Result<Amount<C>>
where
C: Constraint + std::marker::Copy + 'amt,
{
fn sum<I: Iterator<Item = &'amt Amount<C>>>(iter: I) -> Self {
iter.copied().sum()
}
}
impl<C> std::ops::Neg for Amount<C>
where
C: Constraint,
{
type Output = Amount<NegativeAllowed>;
fn neg(self) -> Self::Output {
Amount::<NegativeAllowed>::try_from(-self.0)
.expect("a negation of any Amount into NegativeAllowed is always valid")
}
}
#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq, Eq)]
2020-07-01 16:31:30 -07:00
#[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 {
2020-07-01 16:31:30 -07:00
value: i64,
range: RangeInclusive<i64>,
2020-07-01 16:31:30 -07:00
},
/// {value} could not be converted to an i64 Amount
2020-07-01 16:31:30 -07:00
Convert {
value: i128,
2020-07-01 16:31:30 -07:00
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,
},
2020-07-01 16:31:30 -07:00
}
Check remaining transaction value & make value balance signs match the spec (#2566) * Make Amount arithmetic more generic To modify generated amounts, we need some extra operations on `Amount`. We also need to extend existing operations to both `NonNegative` and `NegativeAllowed` amounts. * Add a constrain method for ValueBalance * Derive Eq for ValueBalance * impl Neg for ValueBalance * Make some Amount arithmetic expectations explicit * Explain why we use i128 for multiplication And expand the overflow error details. * Expand Amount::sum error details * Make amount::Error field order consistent * Rename an amount::Error variant to Constraint, so it's clearer * Add specific pool variants to ValueBalanceError * Update coinbase remaining value consensus rule comment This consensus rule was updated recently to include coinbase transactions, but Zebra doesn't check block subsidy or miner fees yet. * Add test methods for modifying transparent values and shielded value balances * Temporarily set values and value balances to zero in proptests In both generated chains and proptests that construct their own transactions. Using zero values reduces value calculation and value check test coverage. A future change will use non-zero values, and fix them so the check passes. * Add extra fields to remaining transaction value errors * Swap the transparent value balance sign to match shielded value balances This makes the signs of all the chain value pools consistent. * Use a NonNegative constraint for transparent values This fix: * makes the type signature match the consensus rules * avoids having to write code to handle negative values * Allocate total generated transaction input value to outputs If there isn't enough input value for an output, set it to zero. Temporarily reduce all generated values to avoid overflow. (We'll remove this workaround when we calculate chain value balances.) * Consistently use ValueBalanceError for ValueBalances * Make the value balance signs match the spec And rename and document methods so their signs are clearer. * Convert amount::Errors to specific pool ValueBalanceErrors * Move some error changes to the next PR * Add extra info to remaining transaction value errors (#2585) * Distinguish between overflow and negative remaining transaction value errors And make some error types cloneable. * Add methods for updating chain value pools (#2586) * Move amount::test to amount::tests:vectors * Make ValueBalance traits more consistent with Amount - implement Add and Sub variants with Result and Assign - derive Hash * Clarify some comments and expects * Create ValueBalance update methods for blocks and transactions Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
2021-08-09 10:22:26 -07:00
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(),
}
}
}
2020-08-15 22:18:30 -07:00
/// 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,
/// );
/// ```
2020-07-01 16:31:30 -07:00
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2021-05-28 05:49:28 -07:00
pub struct NegativeAllowed;
2020-07-01 16:31:30 -07:00
impl Constraint for NegativeAllowed {
2020-07-01 16:31:30 -07:00
fn valid_range() -> RangeInclusive<i64> {
-MAX_MONEY..=MAX_MONEY
}
}
2020-08-15 22:18:30 -07:00
/// 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)]
2021-05-28 05:49:28 -07:00
pub struct NonNegative;
2020-07-01 16:31:30 -07:00
impl Constraint for NonNegative {
2020-07-01 16:31:30 -07:00
fn valid_range() -> RangeInclusive<i64> {
0..=MAX_MONEY
}
}
/// Number of zatoshis in 1 ZEC
pub const COIN: i64 = 100_000_000;
2020-08-15 22:18:30 -07:00
/// The maximum zatoshi amount.
pub const MAX_MONEY: i64 = 21_000_000 * COIN;
2020-07-01 16:31:30 -07:00
/// A trait for defining constraints on `Amount`
pub trait Constraint {
2020-07-01 16:31:30 -07:00
/// Returns the range of values that are valid under this constraint
fn valid_range() -> RangeInclusive<i64>;
/// Check if an input value is within the valid range
fn validate(value: i64) -> Result<i64, Error> {
let range = Self::valid_range();
if !range.contains(&value) {
Err(Error::Constraint { value, range })
2020-07-01 16:31:30 -07:00
} else {
Ok(value)
}
}
}
impl ZcashSerialize for Amount<NegativeAllowed> {
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_i64::<LittleEndian>(self.0)
}
}
impl ZcashDeserialize for Amount<NegativeAllowed> {
fn zcash_deserialize<R: std::io::Read>(
mut reader: R,
) -> Result<Self, crate::serialization::SerializationError> {
Ok(reader.read_i64::<LittleEndian>()?.try_into()?)
}
}
impl ZcashSerialize for Amount<NonNegative> {
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
let amount = self
.0
.try_into()
.expect("constraint guarantees value is positive");
writer.write_u64::<LittleEndian>(amount)
}
}
impl ZcashDeserialize for Amount<NonNegative> {
fn zcash_deserialize<R: std::io::Read>(
mut reader: R,
) -> Result<Self, crate::serialization::SerializationError> {
Ok(reader.read_u64::<LittleEndian>()?.try_into()?)
}
}