diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 23314272c..0be893bbf 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -16,6 +16,12 @@ use std::{ use crate::serialization::{ZcashDeserialize, ZcashSerialize}; use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt}; +#[cfg(any(test, feature = "proptest-impl"))] +pub mod arbitrary; + +#[cfg(test)] +mod tests; + type Result = std::result::Result; /// A runtime validated type for representing amounts of zatoshis @@ -48,6 +54,15 @@ impl Amount { 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 @@ -452,22 +467,7 @@ impl ZcashDeserialize for Amount { } } -#[cfg(any(test, feature = "proptest-impl"))] -use proptest::prelude::*; -#[cfg(any(test, feature = "proptest-impl"))] -impl Arbitrary for Amount -where - C: Constraint + std::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; -} - +// TODO: move to tests::vectors after PR #2577 merges #[cfg(test)] mod test { use crate::serialization::ZcashDeserializeInto; diff --git a/zebra-chain/src/amount/arbitrary.rs b/zebra-chain/src/amount/arbitrary.rs new file mode 100644 index 000000000..9431f3362 --- /dev/null +++ b/zebra-chain/src/amount/arbitrary.rs @@ -0,0 +1,18 @@ +//! Randomised test case generation for amounts. + +use proptest::prelude::*; + +use crate::amount::*; + +impl Arbitrary for Amount +where + C: Constraint + std::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; +} diff --git a/zebra-chain/src/amount/tests.rs b/zebra-chain/src/amount/tests.rs new file mode 100644 index 000000000..0ca4ffe62 --- /dev/null +++ b/zebra-chain/src/amount/tests.rs @@ -0,0 +1,3 @@ +//! Tests for amounts + +mod prop; diff --git a/zebra-chain/src/amount/tests/prop.rs b/zebra-chain/src/amount/tests/prop.rs new file mode 100644 index 000000000..bb75397d4 --- /dev/null +++ b/zebra-chain/src/amount/tests/prop.rs @@ -0,0 +1,27 @@ +//! Randomised property tests for amounts. + +use proptest::prelude::*; + +use crate::amount::*; + +proptest! { + #[test] + fn amount_serialization(amount in any::>()) { + zebra_test::init(); + + let bytes = amount.to_bytes(); + let serialized_amount = Amount::::from_bytes(bytes)?; + + prop_assert_eq!(amount, serialized_amount); + } + + #[test] + fn amount_deserialization(bytes in any::<[u8; 8]>()) { + zebra_test::init(); + + if let Ok(deserialized) = Amount::::from_bytes(bytes) { + let bytes2 = deserialized.to_bytes(); + prop_assert_eq!(bytes, bytes2); + } + } +} diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index 497047891..834b4458d 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -2,6 +2,8 @@ use crate::amount::{Amount, Constraint, Error, NonNegative}; +use std::convert::TryInto; + #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; @@ -128,6 +130,51 @@ where orchard: zero, } } + + /// To byte array + pub fn to_bytes(self) -> [u8; 32] { + let transparent = self.transparent.to_bytes(); + let sprout = self.sprout.to_bytes(); + let sapling = self.sapling.to_bytes(); + let orchard = self.orchard.to_bytes(); + match [transparent, sprout, sapling, orchard].concat().try_into() { + Ok(bytes) => bytes, + _ => unreachable!( + "Four [u8; 8] should always concat with no error into a single [u8; 32]" + ), + } + } + + /// From byte array + pub fn from_bytes(bytes: [u8; 32]) -> Result, ValueBalanceError> { + let transparent = Amount::from_bytes( + bytes[0..8] + .try_into() + .expect("Extracting the first quarter of a [u8; 32] should always succeed"), + )?; + let sprout = Amount::from_bytes( + bytes[8..16] + .try_into() + .expect("Extracting the second quarter of a [u8; 32] should always succeed"), + )?; + let sapling = Amount::from_bytes( + bytes[16..24] + .try_into() + .expect("Extracting the third quarter of a [u8; 32] should always succeed"), + )?; + let orchard = Amount::from_bytes( + bytes[24..32] + .try_into() + .expect("Extracting the last quarter of a [u8; 32] should always succeed"), + )?; + + Ok(ValueBalance { + transparent, + sprout, + sapling, + orchard, + }) + } } #[derive(thiserror::Error, Debug, Clone, PartialEq)] diff --git a/zebra-chain/src/value_balance/tests.rs b/zebra-chain/src/value_balance/tests.rs index 2bf82ef4e..89c8943cb 100644 --- a/zebra-chain/src/value_balance/tests.rs +++ b/zebra-chain/src/value_balance/tests.rs @@ -1 +1,3 @@ +//! Tests for value balances. + mod prop; diff --git a/zebra-chain/src/value_balance/tests/prop.rs b/zebra-chain/src/value_balance/tests/prop.rs index 010976ca4..9806d4318 100644 --- a/zebra-chain/src/value_balance/tests/prop.rs +++ b/zebra-chain/src/value_balance/tests/prop.rs @@ -1,9 +1,12 @@ -use crate::{amount::*, value_balance::*}; +//! Randomised property tests for value balances. + use proptest::prelude::*; +use crate::{amount::*, value_balance::*}; + proptest! { #[test] - fn test_add( + fn value_blance_add( value_balance1 in any::>(), value_balance2 in any::>()) { @@ -32,7 +35,7 @@ proptest! { } } #[test] - fn test_sub( + fn value_balance_sub( value_balance1 in any::>(), value_balance2 in any::>()) { @@ -62,7 +65,7 @@ proptest! { } #[test] - fn test_sum( + fn value_balance_sum( value_balance1 in any::>(), value_balance2 in any::>(), ) { @@ -88,4 +91,24 @@ proptest! { _ => prop_assert!(matches!(collection.iter().sum(), Err(ValueBalanceError::AmountError(_)))) } } + + #[test] + fn value_balance_serialization(value_balance in any::>()) { + zebra_test::init(); + + let bytes = value_balance.to_bytes(); + let serialized_value_balance = ValueBalance::from_bytes(bytes)?; + + prop_assert_eq!(value_balance, serialized_value_balance); + } + + #[test] + fn value_balance_deserialization(bytes in any::<[u8; 32]>()) { + zebra_test::init(); + + if let Ok(deserialized) = ValueBalance::::from_bytes(bytes) { + let bytes2 = deserialized.to_bytes(); + prop_assert_eq!(bytes, bytes2); + } + } } diff --git a/zebra-state/src/service/finalized_state/disk_format.rs b/zebra-state/src/service/finalized_state/disk_format.rs index 41b763a27..c22cf39b7 100644 --- a/zebra-state/src/service/finalized_state/disk_format.rs +++ b/zebra-state/src/service/finalized_state/disk_format.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeMap, convert::TryInto, fmt::Debug, sync::Arc}; use bincode::Options; use zebra_chain::{ + amount::NonNegative, block, block::{Block, Height}, history_tree::HistoryTree, @@ -12,6 +13,7 @@ use zebra_chain::{ sapling, serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, sprout, transaction, transparent, + value_balance::ValueBalance, }; #[derive(Debug, Clone, Copy, PartialEq)] @@ -253,6 +255,21 @@ impl IntoDisk for orchard::tree::Root { } } +impl IntoDisk for ValueBalance { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + } +} + +impl FromDisk for ValueBalance { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let array = bytes.as_ref().try_into().unwrap(); + ValueBalance::from_bytes(array).unwrap() + } +} + // The following implementations for the note commitment trees use `serde` and // `bincode` because currently the inner Merkle tree frontier (from // `incrementalmerkletree`) only supports `serde` for serialization. `bincode` @@ -526,4 +543,11 @@ mod tests { proptest!(|(val in any::())| assert_value_properties(val)); } + + #[test] + fn roundtrip_value_balance() { + zebra_test::init(); + + proptest!(|(val in any::>())| assert_value_properties(val)); + } }