Add proptest generators for action and bundle types.

This commit is contained in:
Kris Nuttycombe 2021-04-27 07:49:49 -06:00
parent 75573d331a
commit 4d89d45332
11 changed files with 399 additions and 77 deletions

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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::<V>)).unzip();
let (actions, circuits): (Vec<_>, Vec<_>) = pre_actions
.into_iter()
.map(|a| a.build(&mut rng, PhantomData::<V>))
.unzip();
// Verify that bsk and bvk are consistent.
let bvk = (actions.iter().map(|a| a.cv_net()).sum::<ValueCommitment>()
@ -447,17 +449,77 @@ impl<V> Bundle<PartiallyAuthorized, V> {
}
}
/// 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<Authorized, i64> {
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<Authorized, i64> = 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))
}
}

View File

@ -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<E> {
AuthLengthMismatch(usize, usize),
}
impl<V> Bundle<Unauthorized, V> {
/// Compute the authorizing data for a bundle and apply it to the bundle, returning the
/// authorized result.
pub fn with_auth<E, F: FnOnce(&Self) -> Result<BundleAuth, E>>(
self,
f: F,
) -> Result<Bundle<Authorized, V>, BundleAuthError<E>> {
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<V> Bundle<Unauthorized, V> {
// /// Compute the authorizing data for a bundle and apply it to the bundle, returning the
// /// authorized result.
// pub fn with_auth<E, F: FnOnce(&Self) -> Result<BundleAuth, E>>(
// self,
// f: F,
// ) -> Result<Bundle<Authorized, V>, BundleAuthError<E>> {
// 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<Unauthorized, ValueSum> {
let (values, actions): (Vec<NoteValue>, Vec<Action<()>>) = 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
)
}
}
}

View File

@ -207,7 +207,7 @@ mod tests {
(
Circuit {},
Instance {
anchor: Anchor,
anchor: Anchor([0; 32]),
cv_net,
nf_old,
rk,

View File

@ -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<SpendAuth>);
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()
}
}
}

View File

@ -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,

View File

@ -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,
}
}
}
}

View File

@ -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))
}
}
}

View File

@ -18,7 +18,7 @@ impl SigType for Binding {}
/// A RedPallas signing key.
#[derive(Debug)]
pub struct SigningKey<T: SigType>(reddsa::SigningKey<T>);
pub struct SigningKey<T: SigType>(pub(crate) reddsa::SigningKey<T>);
impl<T: SigType> From<SigningKey<T>> for [u8; 32] {
fn from(sk: SigningKey<T>) -> [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<SpendAuth> {
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<Binding> {
SigningKey(sk.unwrap())
}
}
prop_compose! {
/// Generate a uniformly distributed nullifier value.
pub fn arb_spendauth_verification_key()(sk in arb_spendauth_signing_key()) -> VerificationKey<SpendAuth> {
VerificationKey::from(&sk)
}
}
prop_compose! {
/// Generate a uniformly distributed nullifier value.
pub fn arb_binding_verification_key()(sk in arb_binding_signing_key()) -> VerificationKey<Binding> {
VerificationKey::from(&sk)
}
}
}

View File

@ -66,7 +66,6 @@ impl NoteValue {
}
}
impl Sub for NoteValue {
type Output = Option<ValueSum>;
@ -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(