diff --git a/Cargo.toml b/Cargo.toml index f3f0139a..d0ff6f78 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" @@ -50,6 +51,9 @@ 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..69543d12 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! { + /// Generates an arbitrary payment address. + 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 88a86868..0ea87a63 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,5 +1,6 @@ //! Logic for building Orchard components of transactions. +use std::convert::TryFrom; use std::iter; use ff::Field; @@ -8,23 +9,29 @@ use pasta_curves::pallas; use rand::RngCore; use crate::{ + address::Address, bundle::{Action, Authorization, Authorized, Bundle, Flags}, circuit::{Circuit, Proof, ProvingKey}, keys::{ FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey, }, + note::{Note, TransmittedNoteCiphertext}, primitives::redpallas::{self, Binding, SpendAuth}, tree::{Anchor, MerklePath}, - value::{self, NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, - Address, EncryptedNote, Note, + value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum}, }; const MIN_ACTIONS: usize = 2; +/// An error type for the kinds of errors that can occur during bundle construction. #[derive(Debug)] pub enum Error { + /// A bundle could not be built because required signatures were missing. MissingSignatures, + /// An error occurred in the process of producing a proof for a bundle. Proof(halo2::plonk::Error), + /// An overflow error occurred while attempting to construct the value + /// for a bundle. ValueSum(value::OverflowError), } @@ -110,7 +117,7 @@ impl ActionInfo { } /// Returns the value sum for this action. - fn value_sum(&self) -> Result { + fn value_sum(&self) -> Option { self.spend.note.value() - self.output.value } @@ -137,7 +144,11 @@ impl ActionInfo { let cm_new = note.commitment(); // TODO: Note encryption - let encrypted_note = EncryptedNote; + let encrypted_note = TransmittedNoteCiphertext { + epk_bytes: [0u8; 32], + enc_ciphertext: [0u8; 580], + out_ciphertext: [0u8; 80], + }; ( Action::from_parts( @@ -158,6 +169,7 @@ impl ActionInfo { /// A builder that constructs a [`Bundle`] from a set of notes to be spent, and recipients /// to receive funds. +#[derive(Debug)] pub struct Builder { spends: Vec, recipients: Vec, @@ -166,6 +178,7 @@ pub struct Builder { } impl Builder { + /// Constructs a new empty builder for an Orchard bundle. pub fn new(flags: Flags, anchor: Anchor) -> Self { Builder { spends: vec![], @@ -233,11 +246,11 @@ impl Builder { /// /// This API assumes that none of the notes being spent are controlled by (threshold) /// multisignatures, and immediately constructs the bundle proof. - fn build( + fn build>( mut self, mut rng: impl RngCore, pk: &ProvingKey, - ) -> Result, Error> { + ) -> Result, Error> { // Pair up the spends and recipients, extending with dummy values as necessary. // // TODO: Do we want to shuffle the order like we do for Sapling? And if we do, do @@ -272,11 +285,16 @@ impl Builder { let anchor = self.anchor; // Determine the value balance for this bundle, ensuring it is valid. - let value_balance: ValueSum = pre_actions + let value_balance = pre_actions .iter() - .fold(Ok(ValueSum::zero()), |acc, action| { + .fold(Some(ValueSum::zero()), |acc, action| { acc? + action.value_sum()? - })?; + }) + .ok_or(OverflowError)?; + + let result_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)))?; // Compute the transaction binding signing key. let bsk = pre_actions @@ -305,7 +323,7 @@ impl Builder { Ok(Bundle::from_parts( NonEmpty::from_vec(actions).unwrap(), flags, - value_balance, + result_value_balance, anchor, Unauthorized { proof, bsk }, )) @@ -349,7 +367,7 @@ impl Authorization for PartiallyAuthorized { type SpendAuth = (Option>, SpendValidatingKey); } -impl Bundle { +impl Bundle { /// Loads the sighash into this bundle, preparing it for signing. /// /// This API ensures that all signatures are created over the same sighash. @@ -357,7 +375,7 @@ impl Bundle { self, mut rng: R, sighash: [u8; 32], - ) -> Bundle { + ) -> Bundle { self.authorize( &mut rng, |rng, _, SigningMetadata { dummy_ask, ak }| { @@ -381,7 +399,7 @@ impl Bundle { mut rng: R, sighash: [u8; 32], signing_keys: &[SpendAuthorizingKey], - ) -> Result, Error> { + ) -> Result, Error> { signing_keys .iter() .fold(self.prepare(&mut rng, sighash), |partial, ask| { @@ -391,7 +409,7 @@ impl Bundle { } } -impl Bundle { +impl Bundle { /// Signs this bundle with the given [`SpendAuthorizingKey`]. /// /// This will apply signatures for all notes controlled by this spending key. @@ -422,7 +440,7 @@ impl Bundle { /// Finalizes this bundle, enabling it to be included in a transaction. /// /// Returns an error if any signatures are missing. - pub fn finalize(self) -> Result, Error> { + pub fn finalize(self) -> Result, Error> { self.try_authorize( &mut (), |_, _, (sig, _)| match sig { @@ -439,17 +457,139 @@ impl Bundle { } } +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use rand::{rngs::StdRng, CryptoRng, SeedableRng}; + use std::convert::TryFrom; + use std::fmt::Debug; + + use proptest::collection::vec; + use proptest::prelude::*; + + use crate::{ + address::testing::arb_address, + bundle::{Authorized, Bundle, Flags}, + circuit::ProvingKey, + keys::{ + testing::arb_spending_key, FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, + SpendingKey, + }, + note::testing::arb_note, + tree::{Anchor, MerklePath}, + value::{testing::arb_positive_note_value, NoteValue, MAX_NOTE_VALUE}, + Address, Note, + }; + + 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 { + rng: R, + rng_7: R7, + sk: SpendingKey, + anchor: Anchor, + notes: Vec, + recipient_amounts: Vec<(Address, NoteValue)>, + } + + impl ArbitraryBundleInputs { + /// Create a bundle from the set of arbitrary bundle inputs. + fn into_bundle>(mut self) -> Bundle { + 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 pk = ProvingKey::build(); + builder + .build(&mut self.rng, &pk) + .unwrap() + .prepare(&mut self.rng_7, [0; 32]) + .sign(&mut self.rng_7, &SpendAuthorizingKey::from(&self.sk)) + .finalize() + .unwrap() + } + } + + prop_compose! { + /// Produce a random valid Orchard bundle. + fn arb_bundle_inputs(sk: SpendingKey) + ( + n_notes in 1..30, + n_recipients in 1..30, + ) + ( + anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor), + // generate note values that we're certain won't exceed MAX_NOTE_VALUE in total + notes in vec( + arb_positive_note_value(MAX_NOTE_VALUE / n_notes as u64).prop_flat_map(arb_note), + n_notes as usize + ), + recipient_amounts in vec( + arb_address().prop_flat_map(move |a| { + arb_positive_note_value(MAX_NOTE_VALUE / n_recipients as u64) + .prop_map(move |v| (a.clone(), v)) + }), + n_recipients as usize + ), + rng_seed in prop::array::uniform32(prop::num::u8::ANY) + ) -> ArbitraryBundleInputs { + ArbitraryBundleInputs { + rng: StdRng::from_seed(rng_seed), + rng_7: ::from_seed(rng_seed), + sk: sk.clone(), + anchor, + notes, + recipient_amounts + } + } + } + + /// Produce an arbitrary valid Orchard bundle using a random spending key. + pub fn arb_bundle + Debug>() -> impl Strategy> { + arb_spending_key() + .prop_flat_map(arb_bundle_inputs) + .prop_map(|inputs| inputs.into_bundle::()) + } + + /// Produce an arbitrary valid Orchard bundle using a specified spending key. + pub fn arb_bundle_with_key + Debug>( + k: SpendingKey, + ) -> impl Strategy> { + arb_bundle_inputs(k).prop_map(|inputs| inputs.into_bundle::()) + } +} + #[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] @@ -461,16 +601,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 37254e3b..a0c717de 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -4,10 +4,10 @@ use nonempty::NonEmpty; use crate::{ circuit::{Instance, Proof}, - note::{EncryptedNote, ExtractedNoteCommitment, Nullifier}, + note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext}, primitives::redpallas::{self, Binding, SpendAuth}, tree::Anchor, - value::{ValueCommitment, ValueSum}, + value::ValueCommitment, }; /// An action applied to the global ledger. @@ -19,19 +19,19 @@ use crate::{ /// Internally, this may both consume a note and create a note, or it may do only one of /// the two. TODO: Determine which is more efficient (circuit size vs bundle size). #[derive(Debug)] -pub struct Action { +pub struct Action { /// The nullifier of the note being spent. nf: Nullifier, /// The randomized verification key for the note being spent. rk: redpallas::VerificationKey, /// A commitment to the new note being created. cmx: ExtractedNoteCommitment, - /// The encrypted output note. - encrypted_note: EncryptedNote, + /// The transmitted note ciphertext. + encrypted_note: TransmittedNoteCiphertext, /// A commitment to the net value created or consumed by this action. cv_net: ValueCommitment, /// The authorization for this action. - authorization: T, + authorization: A, } impl Action { @@ -40,7 +40,7 @@ impl Action { nf: Nullifier, rk: redpallas::VerificationKey, cmx: ExtractedNoteCommitment, - encrypted_note: EncryptedNote, + encrypted_note: TransmittedNoteCiphertext, cv_net: ValueCommitment, authorization: T, ) -> Self { @@ -70,7 +70,7 @@ impl Action { } /// Returns the encrypted note ciphertext. - pub fn encrypted_note(&self) -> &EncryptedNote { + pub fn encrypted_note(&self) -> &TransmittedNoteCiphertext { &self.encrypted_note } @@ -174,7 +174,7 @@ pub trait Authorization { /// A bundle of actions to be applied to the ledger. #[derive(Debug)] -pub struct Bundle { +pub struct Bundle { /// The list of actions that make up this bundle. actions: NonEmpty>, /// Orchard-specific transaction-level flags for this bundle. @@ -182,19 +182,19 @@ pub struct Bundle { /// The net value moved out of the Orchard shielded pool. /// /// This is the sum of Orchard spends minus the sum of Orchard outputs. - value_balance: ValueSum, + value_balance: V, /// The root of the Orchard commitment tree that this bundle commits to. anchor: Anchor, /// The authorization for this bundle. authorization: T, } -impl Bundle { +impl Bundle { /// Constructs a `Bundle` from its constituent parts. pub fn from_parts( actions: NonEmpty>, flags: Flags, - value_balance: ValueSum, + value_balance: V, anchor: Anchor, authorization: T, ) -> Self { @@ -220,7 +220,7 @@ impl Bundle { /// Returns the net value moved into or out of the Orchard shielded pool. /// /// This is the sum of Orchard spends minus the sum Orchard outputs. - pub fn value_balance(&self) -> &ValueSum { + pub fn value_balance(&self) -> &V { &self.value_balance } @@ -242,13 +242,28 @@ impl Bundle { todo!() } + /// Construct a new bundle by applying a transformation that might fail + /// to the value balance. + pub fn try_map_value_balance Result>( + self, + f: F, + ) -> Result, E> { + Ok(Bundle { + actions: self.actions, + flags: self.flags, + value_balance: f(self.value_balance)?, + anchor: self.anchor, + authorization: self.authorization, + }) + } + /// Transitions this bundle from one authorization state to another. pub fn authorize( self, context: &mut R, mut spend_auth: impl FnMut(&mut R, &T, T::SpendAuth) -> U::SpendAuth, step: impl FnOnce(&mut R, T) -> U, - ) -> Bundle { + ) -> Bundle { let authorization = self.authorization; Bundle { actions: self @@ -267,7 +282,7 @@ impl Bundle { context: &mut R, mut spend_auth: impl FnMut(&mut R, &T, T::SpendAuth) -> Result, step: impl FnOnce(&mut R, T) -> Result, - ) -> Result, E> { + ) -> Result, E> { let authorization = self.authorization; let new_actions = self .actions @@ -304,9 +319,19 @@ impl Authorized { 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 { + &self.binding_signature + } } -impl Bundle { +impl Bundle { /// Computes a commitment to the authorizing data within for this bundle. /// /// This together with `Bundle::commitment` bind the entire bundle. @@ -325,3 +350,224 @@ 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 rand_7::{rngs::StdRng, SeedableRng}; + use reddsa::orchard::SpendAuth; + + use proptest::collection::vec; + use proptest::prelude::*; + + use crate::{ + circuit::Proof, + note::{ + commitment::ExtractedNoteCommitment, nullifier::testing::arb_nullifier, + testing::arb_note, TransmittedNoteCiphertext, + }, + primitives::redpallas::{ + self, + testing::{ + arb_binding_signing_key, arb_spendauth_signing_key, arb_spendauth_verification_key, + }, + }, + value::{ + testing::arb_note_value_bounded, NoteValue, ValueCommitTrapdoor, ValueCommitment, + ValueSum, MAX_NOTE_VALUE, + }, + Anchor, + }; + + use super::{Action, Authorization, Authorized, 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(spend_value: NoteValue, output_value: NoteValue)( + nf in arb_nullifier(), + rk in arb_spendauth_verification_key(), + note in arb_note(output_value), + ) -> Action<()> { + let cmx = ExtractedNoteCommitment::from(note.commitment()); + let cv_net = ValueCommitment::derive( + (spend_value - output_value).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: () + } + } + } + + /// Generate an unauthorized action having spend and output values less than MAX_NOTE_VALUE / n_actions. + pub fn arb_unauthorized_action_n( + n_actions: usize, + flags: Flags, + ) -> impl Strategy)> { + let spend_value_gen = if flags.spends_enabled { + Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64)) + } else { + Strategy::boxed(Just(NoteValue::zero())) + }; + + spend_value_gen.prop_flat_map(move |spend_value| { + let output_value_gen = if flags.outputs_enabled { + Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64)) + } else { + Strategy::boxed(Just(NoteValue::zero())) + }; + + output_value_gen.prop_flat_map(move |output_value| { + arb_unauthorized_action(spend_value, output_value) + .prop_map(move |a| ((spend_value - output_value).unwrap(), a)) + }) + }) + } + + prop_compose! { + /// Generate an action with invalid (random) authorization data. + pub fn arb_action(spend_value: NoteValue, output_value: NoteValue)( + nf in arb_nullifier(), + sk in arb_spendauth_signing_key(), + note in arb_note(output_value), + rng_seed in prop::array::uniform32(prop::num::u8::ANY), + fake_sighash in prop::array::uniform32(prop::num::u8::ANY), + ) -> Action> { + let cmx = ExtractedNoteCommitment::from(note.commitment()); + let cv_net = ValueCommitment::derive( + (spend_value - output_value).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] + }; + + let rng = StdRng::from_seed(rng_seed); + + Action { + nf, + rk: redpallas::VerificationKey::from(&sk), + cmx, + encrypted_note, + cv_net, + authorization: sk.sign(rng, &fake_sighash), + } + } + } + + /// Generate an authorized action having spend and output values less than MAX_NOTE_VALUE / n_actions. + pub fn arb_action_n( + n_actions: usize, + flags: Flags, + ) -> impl Strategy>)> { + let spend_value_gen = if flags.spends_enabled { + Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64)) + } else { + Strategy::boxed(Just(NoteValue::zero())) + }; + + spend_value_gen.prop_flat_map(move |spend_value| { + let output_value_gen = if flags.outputs_enabled { + Strategy::boxed(arb_note_value_bounded(MAX_NOTE_VALUE / n_actions as u64)) + } else { + Strategy::boxed(Just(NoteValue::zero())) + }; + + output_value_gen.prop_flat_map(move |output_value| { + arb_action(spend_value, output_value) + .prop_map(move |a| ((spend_value - output_value).unwrap(), a)) + }) + }) + } + + 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() + ( + n_actions in 1usize..100, + flags in arb_flags(), + ) + ( + acts in vec(arb_unauthorized_action_n(n_actions, flags), n_actions), + anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor), + flags in Just(flags) + ) -> Bundle { + let (balances, actions): (Vec, Vec>) = acts.into_iter().unzip(); + + Bundle::from_parts( + NonEmpty::from_vec(actions).unwrap(), + flags, + balances.into_iter().sum::>().unwrap(), + anchor, + Unauthorized + ) + } + } + + prop_compose! { + /// Generate an arbitrary bundle with fake authorization data. This bundle does not + /// necessarily respect consensus rules; for that use + /// [`crate::builder::testing::arb_bundle`] + pub fn arb_bundle() + ( + n_actions in 1usize..100, + flags in arb_flags(), + ) + ( + acts in vec(arb_action_n(n_actions, flags), n_actions), + anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor), + sk in arb_binding_signing_key(), + rng_seed in prop::array::uniform32(prop::num::u8::ANY), + fake_proof in vec(prop::num::u8::ANY, 1973), + fake_sighash in prop::array::uniform32(prop::num::u8::ANY), + flags in Just(flags) + ) -> Bundle { + let (balances, actions): (Vec, Vec>) = acts.into_iter().unzip(); + let rng = StdRng::from_seed(rng_seed); + + Bundle::from_parts( + NonEmpty::from_vec(actions).unwrap(), + flags, + balances.into_iter().sum::>().unwrap(), + anchor, + Authorized { + proof: Proof::new(fake_proof), + binding_signature: sk.sign(rng, &fake_sighash), + } + ) + } + } +} diff --git a/src/circuit.rs b/src/circuit.rs index 2bf53bc2..af2a6d78 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -7,11 +7,10 @@ use halo2::{ use pasta_curves::{pallas, vesta}; use crate::{ - note::ExtractedNoteCommitment, + note::{nullifier::Nullifier, ExtractedNoteCommitment}, primitives::redpallas::{SpendAuth, VerificationKey}, tree::Anchor, value::ValueCommitment, - Nullifier, }; pub(crate) mod gadget; @@ -120,6 +119,12 @@ impl Instance { #[derive(Debug)] pub struct Proof(Vec); +impl AsRef<[u8]> for Proof { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl Proof { /// Creates a proof for the given circuits and instances. pub fn create( @@ -156,6 +161,11 @@ impl Proof { Err(plonk::Error::ConstraintSystemFailure) } } + + /// Constructs a new Proof value. + pub fn new(bytes: Vec) -> Self { + Proof(bytes) + } } #[cfg(test)] @@ -174,6 +184,7 @@ mod tests { value::{ValueCommitTrapdoor, ValueCommitment}, }; + // TODO: recast as a proptest #[test] fn round_trip() { let mut rng = OsRng; @@ -195,7 +206,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..58116f0e 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -25,7 +25,7 @@ use crate::{ /// 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 SpendingKey([u8; 32]); impl SpendingKey { @@ -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 0d56315c..eb3890e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,16 +17,19 @@ #![deny(unsafe_code)] mod address; -mod builder; +pub mod builder; pub mod bundle; mod circuit; mod constants; pub mod keys; -mod note; +pub mod note; pub mod primitives; mod spec; mod tree; pub mod value; pub use address::Address; -pub use note::{EncryptedNote, Note, NoteCommitment, Nullifier}; +pub use bundle::Bundle; +pub use circuit::Proof; +pub use note::Note; +pub use tree::Anchor; diff --git a/src/note.rs b/src/note.rs index 0294a079..84e9e400 100644 --- a/src/note.rs +++ b/src/note.rs @@ -1,3 +1,4 @@ +//! Data structures used for note construction. use group::GroupEncoding; use pasta_curves::pallas; use rand::RngCore; @@ -9,10 +10,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. @@ -135,4 +136,47 @@ impl Note { /// An encrypted note. #[derive(Debug)] -pub struct EncryptedNote; +pub struct TransmittedNoteCiphertext { + /// The serialization of the ephemeral public key + pub epk_bytes: [u8; 32], + /// The encrypted note ciphertext + pub enc_ciphertext: [u8; 580], + /// An encrypted value that allows the holder of the outgoing cipher + /// 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/commitment.rs b/src/note/commitment.rs index 00c6d853..784024c7 100644 --- a/src/note/commitment.rs +++ b/src/note/commitment.rs @@ -2,7 +2,7 @@ use std::iter; use bitvec::{array::BitArray, order::Lsb0}; use ff::PrimeField; -use pasta_curves::pallas; +use pasta_curves::{arithmetic::FieldExt, pallas}; use subtle::CtOption; use crate::{ @@ -59,6 +59,18 @@ impl NoteCommitment { #[derive(Clone, Debug)] pub struct ExtractedNoteCommitment(pub(super) pallas::Base); +impl ExtractedNoteCommitment { + /// Deserialize the extracted note commitment from a byte array. + pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { + pallas::Base::from_bytes(bytes).map(ExtractedNoteCommitment) + } + + /// Serialize the value commitment to its canonical byte representation. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } +} + impl From for ExtractedNoteCommitment { fn from(cm: NoteCommitment) -> Self { ExtractedNoteCommitment(extract_p(&cm.0)) diff --git a/src/note/nullifier.rs b/src/note/nullifier.rs index 65f55afd..bc0dd6e3 100644 --- a/src/note/nullifier.rs +++ b/src/note/nullifier.rs @@ -1,7 +1,8 @@ use group::Group; use halo2::arithmetic::CurveExt; -use pasta_curves::pallas; +use pasta_curves::{arithmetic::FieldExt, pallas}; use rand::RngCore; +use subtle::CtOption; use super::NoteCommitment; use crate::{ @@ -11,7 +12,7 @@ use crate::{ /// A unique nullifier for a note. #[derive(Clone, Debug)] -pub struct Nullifier(pub(super) pallas::Base); +pub struct Nullifier(pub(crate) pallas::Base); impl Nullifier { /// Generates a dummy nullifier for use as $\rho$ in dummy spent notes. @@ -30,6 +31,16 @@ impl Nullifier { Nullifier(extract_p(&pallas::Point::random(rng))) } + /// Deserialize the nullifier from a byte array. + pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { + pallas::Base::from_bytes(bytes).map(Nullifier) + } + + /// Serialize the nullifier to its canonical byte representation. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + /// $DeriveNullifier$. /// /// Defined in [Zcash Protocol Spec § 4.16: Note Commitments and Nullifiers][commitmentsandnullifiers]. @@ -46,3 +57,27 @@ 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 group::GroupEncoding; + use pasta_curves::pallas; + + use super::Nullifier; + use crate::spec::extract_p; + + prop_compose! { + /// Generate a uniformly distributed nullifier value. + pub fn arb_nullifier()( + coord in prop::array::uniform32(any::()).prop_map(|b| pallas::Point::from_bytes(&b)).prop_filter( + "Must generate a valid Pallas point", + |p| p.is_some().into() + ) + ) -> Nullifier { + Nullifier(extract_p(&coord.unwrap())) + } + } +} diff --git a/src/primitives/redpallas.rs b/src/primitives/redpallas.rs index 7db402ec..0a8ba3d7 100644 --- a/src/primitives/redpallas.rs +++ b/src/primitives/redpallas.rs @@ -96,6 +96,18 @@ impl VerificationKey { #[derive(Debug)] pub struct Signature(reddsa::Signature); +impl From<[u8; 64]> for Signature { + fn from(bytes: [u8; 64]) -> Self { + Signature(bytes.into()) + } +} + +impl From<&Signature> for [u8; 64] { + fn from(sig: &Signature) -> Self { + sig.0.into() + } +} + pub(crate) mod private { use super::{Binding, SpendAuth}; @@ -105,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 RedDSA spend authorization signing key. + 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 RedDSA binding signing key. + 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 RedDSA spend authorization verification key. + pub fn arb_spendauth_verification_key()(sk in arb_spendauth_signing_key()) -> VerificationKey { + VerificationKey::from(&sk) + } + } + + prop_compose! { + /// Generate a uniformly distributed RedDSA binding verification key. + pub fn arb_binding_verification_key()(sk in arb_binding_signing_key()) -> VerificationKey { + VerificationKey::from(&sk) + } + } +} diff --git a/src/tree.rs b/src/tree.rs index 63089e31..e0d14582 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -2,7 +2,7 @@ use rand::RngCore; /// The root of an Orchard commitment tree. #[derive(Clone, Debug)] -pub struct Anchor; +pub struct Anchor(pub [u8; 32]); #[derive(Debug)] pub struct MerklePath; @@ -10,7 +10,6 @@ pub struct MerklePath; impl MerklePath { /// Generates a dummy Merkle path for use in dummy spent notes. pub(crate) fn dummy(_rng: &mut impl RngCore) -> Self { - // TODO MerklePath } } diff --git a/src/value.rs b/src/value.rs index c636d173..379bb599 100644 --- a/src/value.rs +++ b/src/value.rs @@ -14,8 +14,8 @@ //! [`Action`]: crate::bundle::Action //! [`Bundle`]: crate::bundle::Bundle -use std::convert::TryInto; -use std::fmt; +use std::convert::{TryFrom, TryInto}; +use std::fmt::{self, Debug}; use std::iter::Sum; use std::ops::{Add, Sub}; @@ -27,9 +27,23 @@ use pasta_curves::{ pallas, }; use rand::RngCore; +use subtle::CtOption; use crate::primitives::redpallas::{self, Binding}; +use std::ops::RangeInclusive; + +/// Maximum note value. +pub const MAX_NOTE_VALUE: u64 = u64::MAX; + +/// The valid range of the scalar multiplication used in ValueCommit^Orchard. +/// +/// Defined in a note in [Zcash Protocol Spec § 4.17.4: Action Statement (Orchard)][actionstatement]. +/// +/// [actionstatement]: https://zips.z.cash/protocol/nu5.pdf#actionstatement +pub const VALUE_SUM_RANGE: RangeInclusive = + -(MAX_NOTE_VALUE as i128)..=MAX_NOTE_VALUE as i128; + /// A value operation overflowed. #[derive(Debug)] pub struct OverflowError; @@ -66,18 +80,21 @@ impl NoteValue { } impl Sub for NoteValue { - type Output = Result; + type Output = Option; + #[allow(clippy::suspicious_arithmetic_impl)] fn sub(self, rhs: Self) -> Self::Output { - let a: i64 = self.0.try_into().map_err(|_| OverflowError)?; - let b: i64 = rhs.0.try_into().map_err(|_| OverflowError)?; - Ok(ValueSum(a - b)) + let a = self.0 as i128; + let b = rhs.0 as i128; + a.checked_sub(b) + .filter(|v| VALUE_SUM_RANGE.contains(v)) + .map(ValueSum) } } -/// A sum of Orchard note values. +/// A sum of Orchard note values #[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct ValueSum(i64); +pub struct ValueSum(i128); impl ValueSum { pub(crate) fn zero() -> Self { @@ -90,21 +107,39 @@ impl ValueSum { /// This only enforces that the value is a signed 63-bit integer. Callers should /// enforce any additional constraints on the value's valid range themselves. pub fn from_raw(value: i64) -> Self { - ValueSum(value) + ValueSum(value as i128) } } impl Add for ValueSum { - type Output = Result; + type Output = Option; + #[allow(clippy::suspicious_arithmetic_impl)] fn add(self, rhs: Self) -> Self::Output { - self.0.checked_add(rhs.0).map(ValueSum).ok_or(OverflowError) + self.0 + .checked_add(rhs.0) + .filter(|v| VALUE_SUM_RANGE.contains(v)) + .map(ValueSum) } } impl<'a> Sum<&'a ValueSum> for Result { fn sum>(iter: I) -> Self { - iter.fold(Ok(ValueSum(0)), |acc, cv| acc? + *cv) + iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + *v).ok_or(OverflowError)) + } +} + +impl Sum for Result { + fn sum>(iter: I) -> Self { + iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + v).ok_or(OverflowError)) + } +} + +impl TryFrom for i64 { + type Error = OverflowError; + + fn try_from(v: ValueSum) -> Result { + i64::try_from(v.0).map_err(|_| OverflowError) } } @@ -190,11 +225,12 @@ impl ValueCommitment { let hasher = pallas::Point::hash_to_curve("z.cash:Orchard-cv"); let V = hasher(b"v"); let R = hasher(b"r"); + let abs_value = u64::try_from(value.0.abs()).expect("value must be in valid range"); let value = if value.0.is_negative() { - -pallas::Scalar::from_u64((-value.0) as u64) + -pallas::Scalar::from_u64(abs_value) } else { - pallas::Scalar::from_u64(value.0 as u64) + pallas::Scalar::from_u64(abs_value) }; ValueCommitment(V * value + R * rcv.0) @@ -204,22 +240,29 @@ impl ValueCommitment { // TODO: impl From for redpallas::VerificationKey. self.0.to_bytes().try_into().unwrap() } + + /// Deserialize a value commitment from its byte representation + pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { + pallas::Point::from_bytes(bytes).map(ValueCommitment) + } + + /// Serialize this value commitment to its canonical byte representation. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } } -#[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; - - /// 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; + use super::{NoteValue, ValueCommitTrapdoor, ValueSum, MAX_NOTE_VALUE, VALUE_SUM_RANGE}; 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); @@ -228,21 +271,68 @@ 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()(value in VALUE_SUM_RANGE) -> ValueSum { + ValueSum(value as i128) } } prop_compose! { - fn arb_trapdoor()(rcv in arb_scalar()) -> ValueCommitTrapdoor { + /// Generate an arbitrary [`ValueSum`] in the range of valid Zcash values. + pub fn arb_value_sum_bounded(bound: NoteValue)(value in -(bound.0 as i128)..=(bound.0 as i128)) -> ValueSum { + ValueSum(value as i128) + } + } + + prop_compose! { + /// 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_NOTE_VALUE) -> 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_note_value_bounded(max: u64)(value in 0u64..max) -> 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_note_value_bounded, arb_trapdoor, arb_value_sum_bounded}, + OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum, MAX_NOTE_VALUE, + }; + use crate::primitives::redpallas; + proptest! { #[test] fn bsk_consistent_with_bvk( - values in prop::collection::vec((arb_value_sum(MAX_MONEY), arb_trapdoor()), 1..10), + values in (1usize..10).prop_flat_map(|n_values| + arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound| + prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor()), n_values) + ) + ) ) { let value_balance = values .iter()