From 8281b9040cd56b6ef40ddbbc36638c15e8fc2059 Mon Sep 17 00:00:00 2001 From: Jane Lusby Date: Wed, 1 Jul 2020 16:31:30 -0700 Subject: [PATCH] Start work on new Amount type (#554) --- Cargo.lock | 13 + zebra-chain/Cargo.toml | 2 + zebra-chain/src/serialization.rs | 7 + zebra-chain/src/transaction.rs | 5 +- zebra-chain/src/transaction/joinsplit.rs | 8 +- zebra-chain/src/transaction/serialize.rs | 17 +- zebra-chain/src/transaction/tests.rs | 2 +- .../src/transaction/tests/arbitrary.rs | 9 +- zebra-chain/src/transaction/transparent.rs | 8 +- zebra-chain/src/types.rs | 46 ++- zebra-chain/src/types/amount.rs | 387 ++++++++++++++++++ 11 files changed, 459 insertions(+), 45 deletions(-) create mode 100644 zebra-chain/src/types/amount.rs diff --git a/Cargo.lock b/Cargo.lock index f40583812..58c0c9210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,6 +408,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "displaydoc" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6269d127174b18c665e683e23c2c55d3735fadbec4181c7c70b0450b764bfa5" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "syn 1.0.33", +] + [[package]] name = "dtoa" version = "0.4.6" @@ -2309,6 +2320,8 @@ dependencies = [ "bs58", "byteorder", "chrono", + "color-eyre", + "displaydoc", "ed25519-zebra", "equihash", "futures", diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 209fb5511..aeef1ea22 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -30,8 +30,10 @@ serde-big-array = "0.3.0" ed25519-zebra = "0.4" redjubjub = "0.1" equihash = { git = "https://github.com/ZcashFoundation/librustzcash.git", branch = "equihash-crate" } +displaydoc = "0.1.6" [dev-dependencies] proptest = "0.10" proptest-derive = "0.2.0" zebra-test = { path = "../zebra-test/" } +color-eyre = "0.5" diff --git a/zebra-chain/src/serialization.rs b/zebra-chain/src/serialization.rs index 6bd1f6106..e60762a15 100644 --- a/zebra-chain/src/serialization.rs +++ b/zebra-chain/src/serialization.rs @@ -23,6 +23,13 @@ pub enum SerializationError { // XXX refine errors #[error("parse error: {0}")] Parse(&'static str), + /// An error caused when validating a zatoshi `Amount` + #[error("input couldn't be parsed as a zatoshi `Amount`")] + Amount { + /// The source error indicating how the num failed to validate + #[from] + source: crate::types::amount::Error, + }, } /// Consensus-critical serialization for Zcash. diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index e5d603d1b..570b28efe 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -17,7 +17,7 @@ pub use shielded_data::{Output, ShieldedData, Spend}; pub use transparent::{CoinbaseData, OutPoint, TransparentInput, TransparentOutput}; use crate::proofs::{Bctv14Proof, Groth16Proof}; -use crate::types::{BlockHeight, LockTime}; +use crate::types::{amount::Amount, BlockHeight, LockTime}; /// A Zcash transaction. /// @@ -82,8 +82,7 @@ pub enum Transaction { /// The latest block height that this transaction can be added to the chain. expiry_height: BlockHeight, /// The net value of Sapling spend transfers minus output transfers. - // XXX refine this to an Amount type. - value_balance: i64, + value_balance: Amount, /// The shielded data for this transaction, if any. shielded_data: Option, /// The JoinSplit data for this transaction, if any. diff --git a/zebra-chain/src/transaction/joinsplit.rs b/zebra-chain/src/transaction/joinsplit.rs index 8a530a740..15814366b 100644 --- a/zebra-chain/src/transaction/joinsplit.rs +++ b/zebra-chain/src/transaction/joinsplit.rs @@ -1,3 +1,4 @@ +use crate::types::amount::{Amount, NonNegative}; use crate::{ed25519_zebra, notes::sprout, proofs::ZkSnarkProof}; use serde::{Deserialize, Serialize}; @@ -8,14 +9,11 @@ use serde::{Deserialize, Serialize}; pub struct JoinSplit { /// A value that the JoinSplit transfer removes from the transparent value /// pool. - /// - /// XXX refine to an Amount - pub vpub_old: u64, + pub vpub_old: Amount, /// A value that the JoinSplit transfer inserts into the transparent value /// pool. /// - /// XXX refine to an Amount - pub vpub_new: u64, + pub vpub_new: Amount, /// A root of the Sprout note commitment tree at some block height in the /// past, or the root produced by a previous JoinSplit transfer in this /// transaction. diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 88d991e4d..666375e3c 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -3,6 +3,7 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use std::{ + convert::TryInto, io::{self, Read}, sync::Arc, }; @@ -215,7 +216,7 @@ impl ZcashDeserialize for TransparentInput { impl ZcashSerialize for TransparentOutput { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - writer.write_u64::(self.value)?; + writer.write_u64::(self.value.into())?; self.pk_script.zcash_serialize(&mut writer)?; Ok(()) } @@ -224,7 +225,7 @@ impl ZcashSerialize for TransparentOutput { impl ZcashDeserialize for TransparentOutput { fn zcash_deserialize(mut reader: R) -> Result { Ok(TransparentOutput { - value: reader.read_u64::()?, + value: reader.read_u64::()?.try_into()?, pk_script: Script::zcash_deserialize(&mut reader)?, }) } @@ -232,8 +233,8 @@ impl ZcashDeserialize for TransparentOutput { impl ZcashSerialize for JoinSplit

{ fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - writer.write_u64::(self.vpub_old)?; - writer.write_u64::(self.vpub_new)?; + writer.write_u64::(self.vpub_old.into())?; + writer.write_u64::(self.vpub_new.into())?; writer.write_all(&self.anchor[..])?; writer.write_all(&self.nullifiers[0][..])?; writer.write_all(&self.nullifiers[1][..])?; @@ -253,8 +254,8 @@ impl ZcashSerialize for JoinSplit

{ impl ZcashDeserialize for JoinSplit

{ fn zcash_deserialize(mut reader: R) -> Result { Ok(JoinSplit::

{ - vpub_old: reader.read_u64::()?, - vpub_new: reader.read_u64::()?, + vpub_old: reader.read_u64::()?.try_into()?, + vpub_new: reader.read_u64::()?.try_into()?, anchor: reader.read_32_bytes()?, nullifiers: [reader.read_32_bytes()?, reader.read_32_bytes()?], commitments: [reader.read_32_bytes()?, reader.read_32_bytes()?], @@ -433,7 +434,7 @@ impl ZcashSerialize for Transaction { outputs.zcash_serialize(&mut writer)?; lock_time.zcash_serialize(&mut writer)?; writer.write_u32::(expiry_height.0)?; - writer.write_i64::(*value_balance)?; + writer.write_i64::((*value_balance).into())?; // The previous match arms serialize in one go, because the // internal structure happens to nicely line up with the @@ -540,7 +541,7 @@ impl ZcashDeserialize for Transaction { let outputs = Vec::zcash_deserialize(&mut reader)?; let lock_time = LockTime::zcash_deserialize(&mut reader)?; let expiry_height = BlockHeight(reader.read_u32::()?); - let value_balance = reader.read_i64::()?; + let value_balance = reader.read_i64::()?.try_into()?; let mut shielded_spends = Vec::zcash_deserialize(&mut reader)?; let mut shielded_outputs = Vec::zcash_deserialize(&mut reader)?; let joinsplit_data = OptV4JSD::zcash_deserialize(&mut reader)?; diff --git a/zebra-chain/src/transaction/tests.rs b/zebra-chain/src/transaction/tests.rs index 73872bc79..bae89c687 100644 --- a/zebra-chain/src/transaction/tests.rs +++ b/zebra-chain/src/transaction/tests.rs @@ -69,7 +69,7 @@ impl Transaction { vec(any::(), 0..10), any::(), any::(), - any::(), + any::(), option::of(any::()), option::of(any::>()), ) diff --git a/zebra-chain/src/transaction/tests/arbitrary.rs b/zebra-chain/src/transaction/tests/arbitrary.rs index 8a30d7680..0340389b0 100644 --- a/zebra-chain/src/transaction/tests/arbitrary.rs +++ b/zebra-chain/src/transaction/tests/arbitrary.rs @@ -6,7 +6,10 @@ use crate::{ CoinbaseData, JoinSplit, JoinSplitData, OutPoint, Output, ShieldedData, Spend, Transaction, TransparentInput, }, - types::{BlockHeight, Script}, + types::{ + amount::{Amount, NonNegative}, + BlockHeight, Script, + }, }; use futures::future::Either; use proptest::{array, collection::vec, prelude::*}; @@ -16,8 +19,8 @@ impl Arbitrary for JoinSplit

{ fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - any::(), - any::(), + any::>(), + any::>(), array::uniform32(any::()), array::uniform2(array::uniform32(any::())), array::uniform2(array::uniform32(any::())), diff --git a/zebra-chain/src/transaction/transparent.rs b/zebra-chain/src/transaction/transparent.rs index 6d25abd24..39dae8faa 100644 --- a/zebra-chain/src/transaction/transparent.rs +++ b/zebra-chain/src/transaction/transparent.rs @@ -4,7 +4,10 @@ #[cfg(test)] use proptest_derive::Arbitrary; -use crate::types::{BlockHeight, Script}; +use crate::types::{ + amount::{Amount, NonNegative}, + BlockHeight, Script, +}; use super::TransactionHash; @@ -80,8 +83,7 @@ pub enum TransparentInput { pub struct TransparentOutput { /// Transaction value. // At https://en.bitcoin.it/wiki/Protocol_documentation#tx, this is an i64. - // XXX refine to Amount ? - pub value: u64, + pub value: Amount, /// Usually contains the public key as a Bitcoin script setting up /// conditions to claim this output. diff --git a/zebra-chain/src/types.rs b/zebra-chain/src/types.rs index 7e7bc79e7..fde98a026 100644 --- a/zebra-chain/src/types.rs +++ b/zebra-chain/src/types.rs @@ -15,28 +15,7 @@ use crate::serialization::{ ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize, }; -/// A 4-byte checksum using truncated double-SHA256 (two rounds of SHA256). -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct Sha256dChecksum(pub [u8; 4]); - -impl<'a> From<&'a [u8]> for Sha256dChecksum { - fn from(bytes: &'a [u8]) -> Self { - use sha2::{Digest, Sha256}; - let hash1 = Sha256::digest(bytes); - let hash2 = Sha256::digest(&hash1); - let mut checksum = [0u8; 4]; - checksum[0..4].copy_from_slice(&hash2[0..4]); - Self(checksum) - } -} - -impl fmt::Debug for Sha256dChecksum { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("Sha256dChecksum") - .field(&hex::encode(&self.0)) - .finish() - } -} +pub mod amount; /// A u32 which represents a block height value. /// @@ -145,6 +124,29 @@ impl ZcashDeserialize for Script { } } +/// A 4-byte checksum using truncated double-SHA256 (two rounds of SHA256). +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct Sha256dChecksum(pub [u8; 4]); + +impl<'a> From<&'a [u8]> for Sha256dChecksum { + fn from(bytes: &'a [u8]) -> Self { + use sha2::{Digest, Sha256}; + let hash1 = Sha256::digest(bytes); + let hash2 = Sha256::digest(&hash1); + let mut checksum = [0u8; 4]; + checksum[0..4].copy_from_slice(&hash2[0..4]); + Self(checksum) + } +} + +impl fmt::Debug for Sha256dChecksum { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Sha256dChecksum") + .field(&hex::encode(&self.0)) + .finish() + } +} + #[cfg(test)] use proptest::prelude::*; diff --git a/zebra-chain/src/types/amount.rs b/zebra-chain/src/types/amount.rs new file mode 100644 index 000000000..31e5f8bb1 --- /dev/null +++ b/zebra-chain/src/types/amount.rs @@ -0,0 +1,387 @@ +//! Module of types for working with validated zatoshi Amounts +use std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, + ops::RangeInclusive, +}; + +type Result = std::result::Result; + +/// A runtime validated type for representing amounts of zatoshis +#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +pub struct Amount(i64, PhantomData); + +impl Amount { + /// Convert this amount to a different Amount type if it satisfies the new constraint + pub fn constrain(self) -> Result> + where + C2: AmountConstraint, + { + self.0.try_into() + } +} + +impl std::ops::Add> for Amount +where + C: AmountConstraint, +{ + type Output = Result>; + + fn add(self, rhs: Amount) -> Self::Output { + let value = self.0 + rhs.0; + value.try_into() + } +} + +impl std::ops::Add> for Result> +where + C: AmountConstraint, +{ + type Output = Result>; + + fn add(self, rhs: Amount) -> Self::Output { + self? + rhs + } +} + +impl std::ops::Add>> for Amount +where + C: AmountConstraint, +{ + type Output = Result>; + + fn add(self, rhs: Result>) -> Self::Output { + self + rhs? + } +} + +impl std::ops::AddAssign> for Result> +where + Amount: Copy, + C: AmountConstraint, +{ + fn add_assign(&mut self, rhs: Amount) { + if let Ok(lhs) = *self { + *self = lhs + rhs; + } + } +} + +impl std::ops::Sub> for Amount +where + C: AmountConstraint, +{ + type Output = Result>; + + fn sub(self, rhs: Amount) -> Self::Output { + let value = self.0 - rhs.0; + value.try_into() + } +} + +impl std::ops::Sub> for Result> +where + C: AmountConstraint, +{ + type Output = Result>; + + fn sub(self, rhs: Amount) -> Self::Output { + self? - rhs + } +} + +impl std::ops::Sub>> for Amount +where + C: AmountConstraint, +{ + type Output = Result>; + + fn sub(self, rhs: Result>) -> Self::Output { + self - rhs? + } +} + +impl std::ops::SubAssign> for Result> +where + Amount: Copy, + C: AmountConstraint, +{ + 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 as _ + } +} + +impl TryFrom for Amount +where + C: AmountConstraint, +{ + type Error = Error; + + fn try_from(value: i64) -> Result { + C::validate(value).map(|v| Self(v, PhantomData)) + } +} + +impl TryFrom for Amount +where + C: AmountConstraint, +{ + type Error = Error; + + fn try_from(value: i32) -> Result { + C::validate(value as _).map(|v| Self(v, PhantomData)) + } +} + +impl TryFrom for Amount +where + C: AmountConstraint, +{ + type Error = Error; + + fn try_from(value: u64) -> Result { + let value = value + .try_into() + .map_err(|source| Error::Convert { value, source })?; + + C::validate(value).map(|v| Self(v, PhantomData)) + } +} + +#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)] +#[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:?} + Contains { + range: RangeInclusive, + value: i64, + }, + /// u64 {value} could not be converted to an i64 Amount + Convert { + value: u64, + source: std::num::TryFromIntError, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Marker type for `Amount` that restricts the values to `-MAX_MONEY..=MAX_MONEY` +pub enum NegativeAllowed {} + +impl AmountConstraint for NegativeAllowed { + fn valid_range() -> RangeInclusive { + -MAX_MONEY..=MAX_MONEY + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Marker type for `Amount` that restricts the value to positive numbers `0..=MAX_MONEY` +pub enum NonNegative {} + +impl AmountConstraint for NonNegative { + fn valid_range() -> RangeInclusive { + 0..=MAX_MONEY + } +} + +/// The max amount of money that can be obtained in zatoshis +pub const MAX_MONEY: i64 = 21_000_000 * 100_000_000; + +/// A trait for defining constraints on `Amount` +pub trait AmountConstraint { + /// 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::Contains { range, value }) + } else { + Ok(value) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use color_eyre::eyre::Result; + use proptest::prelude::*; + use std::fmt; + + impl Arbitrary for Amount + where + C: AmountConstraint + fmt::Debug, + { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + C::valid_range().prop_map(|v| Self(v, PhantomData)).boxed() + } + + type Strategy = BoxedStrategy; + } + + #[test] + fn test_add_bare() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + + let zero: Amount = 0.try_into()?; + let new_zero = one + neg_one; + + assert_eq!(zero, new_zero?); + + Ok(()) + } + + #[test] + fn test_add_opt_lhs() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let one = Ok(one); + let neg_one: Amount = (-1).try_into()?; + + let zero: Amount = 0.try_into()?; + let new_zero = one + neg_one; + + assert_eq!(zero, new_zero?); + + Ok(()) + } + + #[test] + fn test_add_opt_rhs() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + let neg_one = Ok(neg_one); + + let zero: Amount = 0.try_into()?; + let new_zero = one + neg_one; + + assert_eq!(zero, new_zero?); + + Ok(()) + } + + #[test] + fn test_add_opt_both() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let one = Ok(one); + let neg_one: Amount = (-1).try_into()?; + let neg_one = Ok(neg_one); + + let zero: Amount = 0.try_into()?; + let new_zero = one.and_then(|one| one + neg_one); + + assert_eq!(zero, new_zero?); + + Ok(()) + } + + #[test] + fn test_add_assign() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let neg_one: Amount = (-1).try_into()?; + let mut neg_one = Ok(neg_one); + + let zero: Amount = 0.try_into()?; + neg_one += one; + let new_zero = neg_one; + + assert_eq!(Ok(zero), new_zero); + + Ok(()) + } + + #[test] + fn test_sub_bare() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let zero: Amount = 0.try_into()?; + + let neg_one: Amount = (-1).try_into()?; + let new_neg_one = zero - one; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) + } + + #[test] + fn test_sub_opt_lhs() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let one = Ok(one); + let zero: Amount = 0.try_into()?; + + let neg_one: Amount = (-1).try_into()?; + let new_neg_one = zero - one; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) + } + + #[test] + fn test_sub_opt_rhs() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let zero: Amount = 0.try_into()?; + let zero = Ok(zero); + + let neg_one: Amount = (-1).try_into()?; + let new_neg_one = zero - one; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) + } + + #[test] + fn test_sub_assign() -> Result<()> { + zebra_test::init(); + let one: Amount = 1.try_into()?; + let zero: Amount = 0.try_into()?; + let mut zero = Ok(zero); + + let neg_one: Amount = (-1).try_into()?; + zero -= one; + let new_neg_one = zero; + + assert_eq!(Ok(neg_one), new_neg_one); + + Ok(()) + } + + #[test] + fn add_with_diff_constraints() -> Result<()> { + let one = Amount::::try_from(1)?; + let zero = Amount::::try_from(0)?; + + (zero - one.constrain()).expect("should allow negative"); + (zero.constrain() - one).expect_err("shouldn't allow negative"); + + Ok(()) + } +}