Use builder to generate "valid" bundles via proptest.

This commit is contained in:
Kris Nuttycombe 2021-04-27 13:56:36 -06:00
parent 4d89d45332
commit f91088d35b
8 changed files with 123 additions and 124 deletions

View File

@ -46,6 +46,7 @@ rev = "f1e76dbc9abf2b68cc609e874fe39f2a15b75b12"
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
hex = "0.4" hex = "0.4"
proptest = "1.0.0"
[lib] [lib]
bench = false bench = false

View File

@ -49,7 +49,7 @@ pub mod testing {
use super::Address; use super::Address;
prop_compose! { prop_compose! {
/// Generate an arbitrary random seed /// Generates an arbitrary payment address.
pub(crate) fn arb_address()(sk in arb_spending_key()) -> Address { pub(crate) fn arb_address()(sk in arb_spending_key()) -> Address {
let fvk = FullViewingKey::from(&sk); let fvk = FullViewingKey::from(&sk);
fvk.default_address() fvk.default_address()

View File

@ -2,7 +2,6 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use std::iter; use std::iter;
use std::marker::PhantomData;
use ff::Field; use ff::Field;
use nonempty::NonEmpty; use nonempty::NonEmpty;
@ -17,16 +16,21 @@ use crate::{
}, },
primitives::redpallas::{self, Binding, SpendAuth}, primitives::redpallas::{self, Binding, SpendAuth},
tree::{Anchor, MerklePath}, tree::{Anchor, MerklePath},
value::{self, NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
Address, Note, TransmittedNoteCiphertext, Address, Note, TransmittedNoteCiphertext,
}; };
const MIN_ACTIONS: usize = 2; const MIN_ACTIONS: usize = 2;
/// An error type for the kinds of errors that can occur during bundle construction.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// A bundle could not be built because required signatures were missing.
MissingSignatures, MissingSignatures,
/// An error occurred in the process of producing a proof for a bundle.
Proof(halo2::plonk::Error), Proof(halo2::plonk::Error),
/// An overflow error occurred while attempting to construct the value
/// for a bundle.
ValueSum(value::OverflowError), ValueSum(value::OverflowError),
} }
@ -121,7 +125,7 @@ impl ActionInfo {
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
/// ///
/// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend
fn build<V: TryFrom<i64>(self, mut rng: impl RngCore) -> (Action<SigningMetadata>, Circuit) { fn build(self, mut rng: impl RngCore) -> (Action<SigningMetadata>, Circuit) {
let v_net = self.value_sum().expect("already checked this"); let v_net = self.value_sum().expect("already checked this");
let cv_net = ValueCommitment::derive(v_net, self.rcv); let cv_net = ValueCommitment::derive(v_net, self.rcv);
@ -164,6 +168,7 @@ impl ActionInfo {
/// A builder that constructs a [`Bundle`] from a set of notes to be spent, and recipients /// A builder that constructs a [`Bundle`] from a set of notes to be spent, and recipients
/// to receive funds. /// to receive funds.
#[derive(Debug)]
pub struct Builder { pub struct Builder {
spends: Vec<SpendInfo>, spends: Vec<SpendInfo>,
recipients: Vec<RecipientInfo>, recipients: Vec<RecipientInfo>,
@ -172,6 +177,7 @@ pub struct Builder {
} }
impl Builder { impl Builder {
/// Construct a new empty builder for an Orchard bundle.
pub fn new(flags: Flags, anchor: Anchor) -> Self { pub fn new(flags: Flags, anchor: Anchor) -> Self {
Builder { Builder {
spends: vec![], spends: vec![],
@ -282,7 +288,8 @@ impl Builder {
.iter() .iter()
.fold(Some(ValueSum::zero()), |acc, action| { .fold(Some(ValueSum::zero()), |acc, action| {
acc? + action.value_sum()? acc? + action.value_sum()?
}).ok_or(Error::ValueSum(value::OverflowError))?; })
.ok_or(OverflowError)?;
// Compute the transaction binding signing key. // Compute the transaction binding signing key.
let bsk = pre_actions let bsk = pre_actions
@ -292,10 +299,8 @@ impl Builder {
.into_bsk(); .into_bsk();
// Create the actions. // Create the actions.
let (actions, circuits): (Vec<_>, Vec<_>) = pre_actions let (actions, circuits): (Vec<_>, Vec<_>) =
.into_iter() pre_actions.into_iter().map(|a| a.build(&mut rng)).unzip();
.map(|a| a.build(&mut rng, PhantomData::<V>))
.unzip();
// Verify that bsk and bvk are consistent. // Verify that bsk and bvk are consistent.
let bvk = (actions.iter().map(|a| a.cv_net()).sum::<ValueCommitment>() let bvk = (actions.iter().map(|a| a.cv_net()).sum::<ValueCommitment>()
@ -310,7 +315,9 @@ impl Builder {
.collect(); .collect();
let proof = Proof::create(pk, &circuits, &instances)?; let proof = Proof::create(pk, &circuits, &instances)?;
let value_balance: V = i64::try_from(value_balance).map_err(Error::ValueSum).and_then(|i| V::try_from(i).map_err(|_| Error::ValueSum(value::OverflowError)))?; let value_balance: V = i64::try_from(value_balance)
.map_err(Error::ValueSum)
.and_then(|i| V::try_from(i).map_err(|_| Error::ValueSum(value::OverflowError)))?;
Ok(Bundle::from_parts( Ok(Bundle::from_parts(
NonEmpty::from_vec(actions).unwrap(), NonEmpty::from_vec(actions).unwrap(),
@ -453,27 +460,80 @@ impl<V> Bundle<PartiallyAuthorized, V> {
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use rand::rngs::OsRng; use rand::rngs::OsRng;
use std::convert::TryFrom;
use std::fmt::Debug;
use proptest::collection::vec; use proptest::collection::vec;
use proptest::prelude::*; use proptest::prelude::*;
//use pasta_curves::{pallas};
use crate::{ use crate::{
address::testing::arb_address, address::testing::arb_address,
bundle::{Authorized, Bundle, Flags}, bundle::{Authorized, Bundle, Flags},
circuit::ProvingKey, circuit::ProvingKey,
keys::{FullViewingKey, OutgoingViewingKey, SpendingKey}, keys::{
testing::arb_spending_key, FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey,
SpendingKey,
},
note::testing::arb_note, note::testing::arb_note,
tree::{Anchor, MerklePath}, tree::{Anchor, MerklePath},
value::testing::{arb_positive_note_value, MAX_MONEY}, value::{
testing::{arb_positive_note_value, MAX_MONEY},
NoteValue,
},
Address, Note,
}; };
use super::Builder; use super::Builder;
/// An intermediate type used for construction of arbitrary
/// bundle values. This type is required because of a limitation
/// of the proptest prop_compose! macro which does not correctly
/// handle polymorphic generator functions. Instead of generating
/// a bundle directly, we generate the bundle inputs, and then
/// are able to use the `build` function to construct the bundle
/// from these inputs, but using a `ValueBalance` implementation that
/// is defined by the end user.
#[derive(Debug)]
struct ArbitraryBundleInputs {
sk: SpendingKey,
anchor: Anchor,
notes: Vec<Note>,
recipient_amounts: Vec<(Address, NoteValue)>,
}
impl ArbitraryBundleInputs {
/// Create a bundle from the set of arbitrary bundle inputs.
fn into_bundle<V: TryFrom<i64>>(self) -> Bundle<Authorized, V> {
let fvk = FullViewingKey::from(&self.sk);
let ovk = OutgoingViewingKey::from(&fvk);
let flags = Flags::from_parts(true, true);
let mut builder = Builder::new(flags, self.anchor);
for note in self.notes.into_iter() {
builder.add_spend(fvk.clone(), note, MerklePath).unwrap();
}
for (addr, value) in self.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])
.sign(rand_7::rngs::OsRng, &SpendAuthorizingKey::from(&self.sk))
.finalize()
.unwrap()
}
}
prop_compose! { prop_compose! {
/// Produce a random valid Orchard bundle. /// Produce a random valid Orchard bundle.
pub fn arb_bundle(sk: SpendingKey)( fn arb_bundle_inputs(sk: SpendingKey)(
anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor), 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 // 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), notes in vec(arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_flat_map(arb_note), 1..30),
@ -483,30 +543,29 @@ pub mod testing {
), ),
1..30 1..30
), ),
) -> Bundle<Authorized, i64> { ) -> ArbitraryBundleInputs {
let fvk = FullViewingKey::from(&sk); ArbitraryBundleInputs {
let ovk = OutgoingViewingKey::from(&fvk); sk: sk.clone(),
let flags = Flags::from_parts(true, true); anchor,
let mut builder = Builder::new(flags, anchor); notes,
recipient_amounts
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()
} }
} }
/// Produce an arbitrary valid Orchard bundle using a random spending key.
pub fn arb_bundle<V: TryFrom<i64> + Debug>() -> impl Strategy<Value = Bundle<Authorized, V>> {
arb_spending_key()
.prop_flat_map(arb_bundle_inputs)
.prop_map(|inputs| inputs.into_bundle::<V>())
}
/// Produce an arbitrary valid Orchard bundle using a specified spending key.
pub fn arb_bundle_with_key<V: TryFrom<i64> + Debug>(
k: SpendingKey,
) -> impl Strategy<Value = Bundle<Authorized, V>> {
arb_bundle_inputs(k).prop_map(|inputs| inputs.into_bundle::<V>())
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -172,37 +172,6 @@ pub trait Authorization {
type SpendAuth; type SpendAuth;
} }
/// Authorizing data for a bundle of actions, ready to be committed to the ledger.
#[derive(Debug)]
pub struct Authorized {
proof: Proof,
binding_signature: redpallas::Signature<Binding>,
}
impl Authorized {
/// Construct a new value with authorizing data.
pub fn new(proof: Proof, binding_signature: redpallas::Signature<Binding>) -> Self {
Authorized {
proof,
binding_signature,
}
}
/// Return the proof component of the authorizing data.
pub fn proof(&self) -> &Proof {
&self.proof
}
/// Return the binding signature.
pub fn binding_signature(&self) -> &redpallas::Signature<Binding> {
&self.binding_signature
}
}
impl Authorization for Authorized {
type SpendAuth = redpallas::Signature<SpendAuth>;
}
/// A bundle of actions to be applied to the ledger. /// A bundle of actions to be applied to the ledger.
#[derive(Debug)] #[derive(Debug)]
pub struct Bundle<T: Authorization, V> { pub struct Bundle<T: Authorization, V> {
@ -318,58 +287,15 @@ impl<T: Authorization, V> Bundle<T, V> {
/// Authorizing data for a bundle of actions, ready to be committed to the ledger. /// Authorizing data for a bundle of actions, ready to be committed to the ledger.
#[derive(Debug)] #[derive(Debug)]
pub struct BundleAuth { pub struct Authorized {
/// The authorizing data for the actions in a bundle proof: Proof,
pub action_authorizations: NonEmpty<<Authorized as Authorization>::SpendAuth>, binding_signature: redpallas::Signature<Binding>,
/// The authorizing data that covers the bundle as a whole
pub authorization: Authorized,
} }
/// Errors that may be generated in the process of constructing bundle authorizing data. impl Authorization for Authorized {
#[derive(Debug)] type SpendAuth = redpallas::Signature<SpendAuth>;
pub enum BundleAuthError<E> {
/// An error produced by the underlying computation of authorizing data for a bundle
Wrapped(E),
/// Authorizing data for the bundle could not be matched to bundle contents.
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 Authorized { impl Authorized {
/// Constructs the authorizing data for a bundle of actions from its constituent parts. /// Constructs the authorizing data for a bundle of actions from its constituent parts.
pub fn from_parts(proof: Proof, binding_signature: redpallas::Signature<Binding>) -> Self { pub fn from_parts(proof: Proof, binding_signature: redpallas::Signature<Binding>) -> Self {
@ -378,6 +304,16 @@ impl Authorized {
binding_signature, binding_signature,
} }
} }
/// Return the proof component of the authorizing data.
pub fn proof(&self) -> &Proof {
&self.proof
}
/// Return the binding signature.
pub fn binding_signature(&self) -> &redpallas::Signature<Binding> {
&self.binding_signature
}
} }
impl<V> Bundle<Authorized, V> { impl<V> Bundle<Authorized, V> {
@ -417,8 +353,7 @@ pub mod testing {
}, },
primitives::redpallas::testing::arb_spendauth_verification_key, primitives::redpallas::testing::arb_spendauth_verification_key,
value::{ value::{
testing::{arb_note_value}, testing::arb_note_value, NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum,
NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum,
}, },
Anchor, Anchor,
}; };
@ -489,7 +424,7 @@ pub mod testing {
NonEmpty::from_vec(actions).unwrap(), NonEmpty::from_vec(actions).unwrap(),
flags, flags,
values.into_iter().fold( values.into_iter().fold(
ValueSum::zero(), ValueSum::zero(),
|acc, cv| (acc + (cv - NoteValue::zero()).unwrap()).unwrap() |acc, cv| (acc + (cv - NoteValue::zero()).unwrap()).unwrap()
), ),
anchor, anchor,

View File

@ -25,7 +25,7 @@ use crate::{
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
/// ///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct SpendingKey([u8; 32]); pub struct SpendingKey([u8; 32]);
impl SpendingKey { impl SpendingKey {

View File

@ -17,7 +17,7 @@
#![deny(unsafe_code)] #![deny(unsafe_code)]
mod address; mod address;
mod builder; pub mod builder;
pub mod bundle; pub mod bundle;
mod circuit; mod circuit;
mod constants; mod constants;

View File

@ -10,7 +10,6 @@ pub struct MerklePath;
impl MerklePath { impl MerklePath {
/// Generates a dummy Merkle path for use in dummy spent notes. /// Generates a dummy Merkle path for use in dummy spent notes.
pub(crate) fn dummy(_rng: &mut impl RngCore) -> Self { pub(crate) fn dummy(_rng: &mut impl RngCore) -> Self {
// TODO
MerklePath MerklePath
} }
} }

View File

@ -72,7 +72,9 @@ impl Sub for NoteValue {
fn sub(self, rhs: Self) -> Self::Output { fn sub(self, rhs: Self) -> Self::Output {
let a = self.0 as i128; let a = self.0 as i128;
let b = rhs.0 as i128; let b = rhs.0 as i128;
a.checked_sub(b).filter(|v| v > &(-(std::u64::MAX as i128))).map(ValueSum) a.checked_sub(b)
.filter(|v| v > &(-(std::u64::MAX as i128)))
.map(ValueSum)
} }
} }
@ -99,7 +101,10 @@ impl Add for ValueSum {
type Output = Option<ValueSum>; type Output = Option<ValueSum>;
fn add(self, rhs: Self) -> Self::Output { fn add(self, rhs: Self) -> Self::Output {
self.0.checked_add(rhs.0).filter(|v| v < &(std::u64::MAX as i128)).map(ValueSum) self.0
.checked_add(rhs.0)
.filter(|v| v < &(std::u64::MAX as i128))
.map(ValueSum)
} }
} }
@ -284,7 +289,7 @@ mod tests {
use super::{ use super::{
testing::{arb_trapdoor, arb_value_sum, MAX_MONEY}, testing::{arb_trapdoor, arb_value_sum, MAX_MONEY},
OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum,
}; };
use crate::primitives::redpallas; use crate::primitives::redpallas;