diff --git a/Cargo.toml b/Cargo.toml index f3f0139a..53baa37b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ blake2b_simd = "0.5" ff = "0.9" fpe = "0.4" group = "0.9" +proptest = { version = "1.0.0", optional = true } rand = "0.8" rand_7 = { package = "rand", version = "0.7" } nonempty = "0.6" @@ -45,11 +46,13 @@ rev = "f1e76dbc9abf2b68cc609e874fe39f2a15b75b12" [dev-dependencies] criterion = "0.3" hex = "0.4" -proptest = "1.0.0" [lib] bench = false +[features] +test-dependencies = ["proptest"] + [[bench]] name = "small" harness = false diff --git a/src/address.rs b/src/address.rs index 96328985..ffefc5af 100644 --- a/src/address.rs +++ b/src/address.rs @@ -15,7 +15,7 @@ use crate::{ /// let sk = SpendingKey::from_bytes([7; 32]).unwrap(); /// let address = FullViewingKey::from(&sk).default_address(); /// ``` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Address { d: Diversifier, pk_d: DiversifiedTransmissionKey, @@ -38,3 +38,21 @@ impl Address { &self.pk_d } } + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use crate::keys::{testing::arb_spending_key, FullViewingKey}; + + use super::Address; + + prop_compose! { + /// Generate an arbitrary random seed + pub(crate) fn arb_address()(sk in arb_spending_key()) -> Address { + let fvk = FullViewingKey::from(&sk); + fvk.default_address() + } + } +} diff --git a/src/builder.rs b/src/builder.rs index 74dc67d2..a497bdcd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,7 @@ use crate::{ primitives::redpallas::{self, Binding, SpendAuth}, tree::{Anchor, MerklePath}, value::{self, NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, - Address, TransmittedNoteCiphertext, Note, + Address, Note, TransmittedNoteCiphertext, }; const MIN_ACTIONS: usize = 2; @@ -292,8 +292,10 @@ impl Builder { .into_bsk(); // Create the actions. - let (actions, circuits): (Vec<_>, Vec<_>) = - pre_actions.into_iter().map(|a| a.build(&mut rng, PhantomData::)).unzip(); + let (actions, circuits): (Vec<_>, Vec<_>) = pre_actions + .into_iter() + .map(|a| a.build(&mut rng, PhantomData::)) + .unzip(); // Verify that bsk and bvk are consistent. let bvk = (actions.iter().map(|a| a.cv_net()).sum::() @@ -447,17 +449,77 @@ impl Bundle { } } +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use rand::rngs::OsRng; + + use proptest::collection::vec; + use proptest::prelude::*; + + //use pasta_curves::{pallas}; + + use crate::{ + address::testing::arb_address, + bundle::{Authorized, Bundle, Flags}, + circuit::ProvingKey, + keys::{FullViewingKey, OutgoingViewingKey, SpendingKey}, + note::testing::arb_note, + tree::{Anchor, MerklePath}, + value::testing::{arb_positive_note_value, MAX_MONEY}, + }; + + use super::Builder; + + prop_compose! { + /// Produce a random valid Orchard bundle. + pub fn arb_bundle(sk: SpendingKey)( + anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor), + // generate note values that we're certain won't exceed MAX_MONEY in total + notes in vec(arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_flat_map(arb_note), 1..30), + recipient_amounts in vec( + arb_address().prop_flat_map( + |a| arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_map(move |v| (a.clone(), v)) + ), + 1..30 + ), + ) -> Bundle { + let fvk = FullViewingKey::from(&sk); + let ovk = OutgoingViewingKey::from(&fvk); + let flags = Flags::from_parts(true, true); + let mut builder = Builder::new(flags, anchor); + + for note in notes.into_iter() { + builder.add_spend(fvk.clone(), note, MerklePath).unwrap(); + } + + for (addr, value) in recipient_amounts.into_iter() { + builder.add_recipient(Some(ovk.clone()), addr, value, None).unwrap(); + } + + let mut rng = OsRng; + let pk = ProvingKey::build(); + builder + .build(&mut rng, &pk) + .unwrap() + .prepare(rand_7::rngs::OsRng, [0; 32]) + .finalize() + .unwrap() + } + } +} + #[cfg(test)] mod tests { use rand::rngs::OsRng; use super::Builder; use crate::{ - bundle::Flags, + bundle::{Authorized, Bundle, Flags}, circuit::ProvingKey, keys::{FullViewingKey, SpendingKey}, tree::Anchor, - value::{NoteValue, ValueSum}, + value::NoteValue, }; #[test] @@ -469,16 +531,16 @@ mod tests { let fvk = FullViewingKey::from(&sk); let recipient = fvk.default_address(); - let mut builder = Builder::new(Flags::from_parts(true, true), Anchor); + let mut builder = Builder::new(Flags::from_parts(true, true), Anchor([0; 32])); builder .add_recipient(None, recipient, NoteValue::from_raw(5000), None) .unwrap(); - let bundle = dbg!(builder + let bundle: Bundle = dbg!(builder .build(&mut rng, &pk) .unwrap() .prepare(rand_7::rngs::OsRng, [0; 32])) .finalize() .unwrap(); - assert_eq!(bundle.value_balance(), &ValueSum::from_raw(-5000)) + assert_eq!(bundle.value_balance(), &(-5000)) } } diff --git a/src/bundle.rs b/src/bundle.rs index 55834c45..02ea5480 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -172,14 +172,6 @@ pub trait Authorization { type SpendAuth; } -/// Marker for an unauthorized bundle with no proofs or signatures. -#[derive(Debug)] -pub struct Unauthorized {} - -impl Authorization for Unauthorized { - type SpendAuth = (); -} - /// Authorizing data for a bundle of actions, ready to be committed to the ledger. #[derive(Debug)] pub struct Authorized { @@ -342,41 +334,41 @@ pub enum BundleAuthError { AuthLengthMismatch(usize, usize), } -impl Bundle { - /// Compute the authorizing data for a bundle and apply it to the bundle, returning the - /// authorized result. - pub fn with_auth Result>( - self, - f: F, - ) -> Result, BundleAuthError> { - let auth = f(&self).map_err(BundleAuthError::Wrapped)?; - let actions_len = self.actions.len(); - - if actions_len != auth.action_authorizations.len() { - Err(BundleAuthError::AuthLengthMismatch( - actions_len, - auth.action_authorizations.len(), - )) - } else { - let actions = NonEmpty::from_vec( - self.actions - .into_iter() - .zip(auth.action_authorizations.into_iter()) - .map(|(act, a)| act.map(|_| a)) - .collect(), - ) - .ok_or(BundleAuthError::AuthLengthMismatch(actions_len, 0))?; - - Ok(Bundle { - actions, - flags: self.flags, - value_balance: self.value_balance, - anchor: self.anchor, - authorization: auth.authorization, - }) - } - } -} +//impl Bundle { +// /// Compute the authorizing data for a bundle and apply it to the bundle, returning the +// /// authorized result. +// pub fn with_auth Result>( +// self, +// f: F, +// ) -> Result, BundleAuthError> { +// let auth = f(&self).map_err(BundleAuthError::Wrapped)?; +// let actions_len = self.actions.len(); +// +// if actions_len != auth.action_authorizations.len() { +// Err(BundleAuthError::AuthLengthMismatch( +// actions_len, +// auth.action_authorizations.len(), +// )) +// } else { +// let actions = NonEmpty::from_vec( +// self.actions +// .into_iter() +// .zip(auth.action_authorizations.into_iter()) +// .map(|(act, a)| act.map(|_| a)) +// .collect(), +// ) +// .ok_or(BundleAuthError::AuthLengthMismatch(actions_len, 0))?; +// +// Ok(Bundle { +// actions, +// flags: self.flags, +// value_balance: self.value_balance, +// anchor: self.anchor, +// authorization: auth.authorization, +// }) +// } +// } +//} impl Authorized { /// Constructs the authorizing data for a bundle of actions from its constituent parts. @@ -407,3 +399,102 @@ pub struct BundleCommitment; /// A commitment to the authorizing data within a bundle of actions. #[derive(Debug)] pub struct BundleAuthorizingCommitment; + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use nonempty::NonEmpty; + + use proptest::collection::vec; + use proptest::prelude::*; + + //use pasta_curves::{pallas}; + + use crate::{ + note::{ + commitment::ExtractedNoteCommitment, nullifier::testing::arb_nullifier, + testing::arb_note, TransmittedNoteCiphertext, + }, + primitives::redpallas::testing::arb_spendauth_verification_key, + value::{ + testing::{arb_note_value}, + NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum, + }, + Anchor, + }; + + use super::{Action, Authorization, Bundle, Flags}; + + /// Marker for an unauthorized bundle with no proofs or signatures. + #[derive(Debug)] + pub struct Unauthorized; + + impl Authorization for Unauthorized { + type SpendAuth = (); + } + + prop_compose! { + /// Generate an action without authorization data. + pub fn arb_unauthorized_action(value: NoteValue)( + nf in arb_nullifier(), + rk in arb_spendauth_verification_key(), + note in arb_note(value), + ) -> Action<()> { + let cmx = ExtractedNoteCommitment::from(note.commitment()); + let cv_net = ValueCommitment::derive( + (note.value() - NoteValue::zero()).unwrap(), + ValueCommitTrapdoor::zero() + ); + // FIXME: make a real one from the note. + let encrypted_note = TransmittedNoteCiphertext { + epk_bytes: [0u8; 32], + enc_ciphertext: [0u8; 580], + out_ciphertext: [0u8; 80] + }; + Action { + nf, + rk, + cmx, + encrypted_note, + cv_net, + authorization: () + } + } + } + + prop_compose! { + /// Create an arbitrary set of flags. + pub fn arb_flags()(spends_enabled in prop::bool::ANY, outputs_enabled in prop::bool::ANY) -> Flags { + Flags::from_parts(spends_enabled, outputs_enabled) + } + } + + prop_compose! { + /// Generate an arbitrary unauthorized bundle. This bundle does not + /// necessarily respect consensus rules; for that use + /// [`crate::builder::testing::arb_bundle`] + pub fn arb_unauthorized_bundle()( + acts in vec( + arb_note_value().prop_flat_map(|v| + arb_unauthorized_action(v).prop_map(move |a| (v, a)) + ), + 1..10 + ), + flags in arb_flags(), + anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor) + ) -> Bundle { + let (values, actions): (Vec, Vec>) = acts.into_iter().unzip(); + + Bundle::from_parts( + NonEmpty::from_vec(actions).unwrap(), + flags, + values.into_iter().fold( + ValueSum::zero(), + |acc, cv| (acc + (cv - NoteValue::zero()).unwrap()).unwrap() + ), + anchor, + Unauthorized + ) + } + } +} diff --git a/src/circuit.rs b/src/circuit.rs index ed1d9086..0d0d5281 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -207,7 +207,7 @@ mod tests { ( Circuit {}, Instance { - anchor: Anchor, + anchor: Anchor([0; 32]), cv_net, nf_old, rk, diff --git a/src/keys.rs b/src/keys.rs index 52074693..8bc584c9 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -109,7 +109,7 @@ impl From<&SpendingKey> for SpendAuthorizingKey { /// $\mathsf{ak}$ but stored here as a RedPallas verification key. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SpendValidatingKey(redpallas::VerificationKey); impl From<&SpendAuthorizingKey> for SpendValidatingKey { @@ -138,7 +138,7 @@ impl SpendValidatingKey { /// [`Nullifier`]: crate::note::Nullifier /// [`Note`]: crate::note::Note /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct NullifierDerivingKey(pallas::Base); impl From<&SpendingKey> for NullifierDerivingKey { @@ -158,7 +158,7 @@ impl NullifierDerivingKey { /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] struct CommitIvkRandomness(pallas::Scalar); impl From<&SpendingKey> for CommitIvkRandomness { @@ -175,7 +175,7 @@ impl From<&SpendingKey> for CommitIvkRandomness { /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FullViewingKey { ak: SpendValidatingKey, nk: NullifierDerivingKey, @@ -287,7 +287,7 @@ impl DiversifierKey { /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Diversifier([u8; 11]); impl Diversifier { @@ -343,7 +343,7 @@ impl IncomingViewingKey { /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct OutgoingViewingKey([u8; 32]); impl From<&FullViewingKey> for OutgoingViewingKey { @@ -357,7 +357,7 @@ impl From<&FullViewingKey> for OutgoingViewingKey { /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct DiversifiedTransmissionKey(pallas::Point); impl DiversifiedTransmissionKey { @@ -374,3 +374,25 @@ impl DiversifiedTransmissionKey { self.0.to_bytes() } } + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use super::SpendingKey; + + prop_compose! { + /// Generate a uniformly distributed fake note commitment value. + pub fn arb_spending_key()( + key in prop::array::uniform32(prop::num::u8::ANY). + prop_map(SpendingKey::from_bytes). + prop_filter( + "Values must correspond to valid Orchard spending keys.", + |opt| bool::from(opt.is_some()) + ) + ) -> SpendingKey { + key.unwrap() + } + } +} diff --git a/src/lib.rs b/src/lib.rs index edd54100..972e02fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ mod tree; pub mod value; pub use address::Address; -pub use bundle::{Action, Authorization, Authorized, Bundle, Unauthorized}; +pub use bundle::{Action, Authorization, Authorized, Bundle}; pub use circuit::Proof; pub use note::{ ExtractedNoteCommitment, Note, NoteCommitment, Nullifier, TransmittedNoteCiphertext, diff --git a/src/note.rs b/src/note.rs index 00aca21b..3870737c 100644 --- a/src/note.rs +++ b/src/note.rs @@ -9,10 +9,10 @@ use crate::{ Address, }; -mod commitment; +pub(crate) mod commitment; pub use self::commitment::{ExtractedNoteCommitment, NoteCommitment}; -mod nullifier; +pub(crate) mod nullifier; pub use self::nullifier::Nullifier; /// The ZIP 212 seed randomness for a note. @@ -144,3 +144,38 @@ pub struct TransmittedNoteCiphertext { /// key for the note to recover the note plaintext. pub out_ciphertext: [u8; 80], } + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use crate::{ + address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue, + }; + + use super::{Note, RandomSeed}; + + prop_compose! { + /// Generate an arbitrary random seed + pub(crate) fn arb_rseed()(elems in prop::array::uniform32(prop::num::u8::ANY)) -> RandomSeed { + RandomSeed(elems) + } + } + + prop_compose! { + /// Generate an action without authorization data. + pub fn arb_note(value: NoteValue)( + recipient in arb_address(), + rho in arb_nullifier(), + rseed in arb_rseed(), + ) -> Note { + Note { + recipient, + value, + rho, + rseed, + } + } + } +} diff --git a/src/note/nullifier.rs b/src/note/nullifier.rs index 76ba392a..a9a517c0 100644 --- a/src/note/nullifier.rs +++ b/src/note/nullifier.rs @@ -11,8 +11,8 @@ use crate::{ }; /// A unique nullifier for a note. -#[derive(Clone, Debug)] -pub struct Nullifier(pub(super) pallas::Base); +#[derive(Debug, Clone)] +pub struct Nullifier(pub(crate) pallas::Base); impl Nullifier { /// Generates a dummy nullifier for use as $\rho$ in dummy spent notes. @@ -57,3 +57,20 @@ impl Nullifier { Nullifier(extract_p(&(k * mod_r_p(nk.prf_nf(rho) + psi) + cm.0))) } } + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use pasta_curves::pallas; + + use super::Nullifier; + + prop_compose! { + /// Generate a uniformly distributed nullifier value. + pub fn arb_nullifier()(elems in prop::array::uniform4(prop::num::u64::ANY)) -> Nullifier { + Nullifier(pallas::Base::from_raw(elems)) + } + } +} diff --git a/src/primitives/redpallas.rs b/src/primitives/redpallas.rs index 505a05b1..c8fc0f7a 100644 --- a/src/primitives/redpallas.rs +++ b/src/primitives/redpallas.rs @@ -18,7 +18,7 @@ impl SigType for Binding {} /// A RedPallas signing key. #[derive(Debug)] -pub struct SigningKey(reddsa::SigningKey); +pub struct SigningKey(pub(crate) reddsa::SigningKey); impl From> for [u8; 32] { fn from(sk: SigningKey) -> [u8; 32] { @@ -117,3 +117,49 @@ pub(crate) mod private { impl Sealed for Binding {} } + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use std::convert::TryFrom; + + use proptest::prelude::*; + + use super::{Binding, SigningKey, SpendAuth, VerificationKey}; + + prop_compose! { + /// Generate a uniformly distributed nullifier value. + pub fn arb_spendauth_signing_key()( + sk in prop::array::uniform32(prop::num::u8::ANY) + .prop_map(reddsa::SigningKey::try_from) + .prop_filter("Values must be parseable as valid signing keys", |r| r.is_ok()) + ) -> SigningKey { + SigningKey(sk.unwrap()) + } + } + + prop_compose! { + /// Generate a uniformly distributed nullifier value. + pub fn arb_binding_signing_key()( + sk in prop::array::uniform32(prop::num::u8::ANY) + .prop_map(reddsa::SigningKey::try_from) + .prop_filter("Values must be parseable as valid signing keys", |r| r.is_ok()) + ) -> SigningKey { + SigningKey(sk.unwrap()) + } + } + + prop_compose! { + /// Generate a uniformly distributed nullifier value. + pub fn arb_spendauth_verification_key()(sk in arb_spendauth_signing_key()) -> VerificationKey { + VerificationKey::from(&sk) + } + } + + prop_compose! { + /// Generate a uniformly distributed nullifier value. + pub fn arb_binding_verification_key()(sk in arb_binding_signing_key()) -> VerificationKey { + VerificationKey::from(&sk) + } + } +} diff --git a/src/value.rs b/src/value.rs index 20b9f1c8..8b912484 100644 --- a/src/value.rs +++ b/src/value.rs @@ -66,7 +66,6 @@ impl NoteValue { } } - impl Sub for NoteValue { type Output = Option; @@ -227,20 +226,21 @@ impl ValueCommitment { } } -#[cfg(test)] -mod tests { +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { use pasta_curves::{arithmetic::FieldExt, pallas}; use proptest::prelude::*; - use super::{OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum}; - use crate::primitives::redpallas; + use super::{NoteValue, ValueCommitTrapdoor, ValueSum}; /// Zcash's maximum money amount. Used as a bound in proptests so we don't artifically /// overflow `ValueSum`'s size. - const MAX_MONEY: i64 = 21_000_000 * 1_0000_0000; + pub const MAX_MONEY: i64 = 21_000_000 * 1_0000_0000; prop_compose! { - fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar { + /// Generate an arbitrary Pallas scalar. + pub fn arb_scalar()(bytes in prop::array::uniform32(0u8..)) -> pallas::Scalar { // Instead of rejecting out-of-range bytes, let's reduce them. let mut buf = [0; 64]; buf[..32].copy_from_slice(&bytes); @@ -249,17 +249,45 @@ mod tests { } prop_compose! { - fn arb_value_sum(bound: i64)(value in -bound..bound) -> ValueSum { - ValueSum(value) + /// Generate an arbitrary [`ValueSum`] in the range of valid Zcash values. + pub fn arb_value_sum(bound: i64)(value in -bound..bound) -> ValueSum { + ValueSum(value as i128) } } prop_compose! { - fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor { + /// Generate an arbitrary ValueCommitTrapdoor + pub fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor { ValueCommitTrapdoor(rcv) } } + prop_compose! { + /// Generate an arbitrary value in the range of valid nonnegative Zcash amounts. + pub fn arb_note_value()(value in 0u64..(MAX_MONEY as u64)) -> NoteValue { + NoteValue(value) + } + } + + prop_compose! { + /// Generate an arbitrary value in the range of valid positive Zcash amounts + /// less than a specified value. + pub fn arb_positive_note_value(max: u64)(value in 1u64..max) -> NoteValue { + NoteValue(value) + } + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::{ + testing::{arb_trapdoor, arb_value_sum, MAX_MONEY}, + OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum + }; + use crate::primitives::redpallas; + proptest! { #[test] fn bsk_consistent_with_bvk(