diff --git a/.gitignore b/.gitignore index 57ee1a9ad..d7f36bd31 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ Cargo.lock .vscode .idea +action-circuit-layout.png +proptest-regressions/*.txt diff --git a/benches/circuit.rs b/benches/circuit.rs index 3140a90a4..6400513e2 100644 --- a/benches/circuit.rs +++ b/benches/circuit.rs @@ -6,6 +6,7 @@ use criterion::{BenchmarkId, Criterion}; #[cfg(unix)] use pprof::criterion::{Output, PProfProfiler}; +use orchard::note::NoteType; use orchard::{ builder::Builder, bundle::Flags, @@ -32,7 +33,13 @@ fn criterion_benchmark(c: &mut Criterion) { ); for _ in 0..num_recipients { builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(10), + NoteType::native(), + None, + ) .unwrap(); } let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); diff --git a/benches/note_decryption.rs b/benches/note_decryption.rs index cab432fe3..73d4fcaea 100644 --- a/benches/note_decryption.rs +++ b/benches/note_decryption.rs @@ -4,6 +4,7 @@ use orchard::{ bundle::Flags, circuit::ProvingKey, keys::{FullViewingKey, Scope, SpendingKey}, + note::NoteType, note_encryption::{CompactAction, OrchardDomain}, value::NoteValue, Anchor, Bundle, @@ -51,10 +52,22 @@ fn bench_note_decryption(c: &mut Criterion) { // The builder pads to two actions, and shuffles their order. Add two recipients // so the first action is always decryptable. builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(10), + NoteType::native(), + None, + ) .unwrap(); builder - .add_recipient(None, recipient, NoteValue::from_raw(10), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(10), + NoteType::native(), + None, + ) .unwrap(); let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); bundle diff --git a/src/action.rs b/src/action.rs index b6396ed91..b1eda4819 100644 --- a/src/action.rs +++ b/src/action.rs @@ -126,7 +126,7 @@ pub(crate) mod testing { use proptest::prelude::*; - use crate::note::NoteType; + use crate::note::note_type::testing::arb_note_type; use crate::{ note::{ commitment::ExtractedNoteCommitment, nullifier::testing::arb_nullifier, @@ -147,12 +147,13 @@ pub(crate) mod testing { nf in arb_nullifier(), rk in arb_spendauth_verification_key(), note in arb_note(output_value), + note_type in arb_note_type() ) -> Action<()> { let cmx = ExtractedNoteCommitment::from(note.commitment()); let cv_net = ValueCommitment::derive( spend_value - output_value, ValueCommitTrapdoor::zero(), - NoteType::native() + note_type ); // FIXME: make a real one from the note. let encrypted_note = TransmittedNoteCiphertext { @@ -179,12 +180,13 @@ pub(crate) mod testing { 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), + note_type in arb_note_type() ) -> Action> { let cmx = ExtractedNoteCommitment::from(note.commitment()); let cv_net = ValueCommitment::derive( spend_value - output_value, ValueCommitTrapdoor::zero(), - NoteType::native() + note_type ); // FIXME: make a real one from the note. diff --git a/src/builder.rs b/src/builder.rs index 7e3f00de1..0ce8a06cc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,6 +2,7 @@ use core::fmt; use core::iter; +use std::collections::HashMap; use ff::Field; use halo2_proofs::circuit::Value; @@ -58,21 +59,23 @@ impl From for Error { } /// Information about a specific note to be spent in an [`Action`]. -#[derive(Debug)] +#[derive(Debug, Clone)] struct SpendInfo { dummy_sk: Option, fvk: FullViewingKey, scope: Scope, note: Note, merkle_path: MerklePath, + // a flag to indicate whether the value of the note will be counted in the value sum of the action. + split_flag: bool, } impl SpendInfo { /// Defined in [Zcash Protocol Spec ยง 4.8.3: Dummy Notes (Orchard)][orcharddummynotes]. /// /// [orcharddummynotes]: https://zips.z.cash/protocol/nu5.pdf#orcharddummynotes - fn dummy(rng: &mut impl RngCore) -> Self { - let (sk, fvk, note) = Note::dummy(rng, None); + fn dummy(note_type: NoteType, rng: &mut impl RngCore) -> Self { + let (sk, fvk, note) = Note::dummy(rng, None, note_type); let merkle_path = MerklePath::dummy(rng); SpendInfo { @@ -83,16 +86,25 @@ impl SpendInfo { scope: Scope::External, note, merkle_path, + split_flag: false, } } + + /// Duplicates the spend info and set the split flag to `true`. + fn create_split_spend(&self) -> Self { + let mut split_spend = self.clone(); + split_spend.split_flag = true; + split_spend + } } /// Information about a specific recipient to receive funds in an [`Action`]. -#[derive(Debug)] +#[derive(Debug, Clone)] struct RecipientInfo { ovk: Option, recipient: Address, value: NoteValue, + note_type: NoteType, memo: Option<[u8; 512]>, } @@ -108,6 +120,7 @@ impl RecipientInfo { ovk: None, recipient, value: NoteValue::zero(), + note_type: NoteType::native(), memo: None, } } @@ -131,7 +144,17 @@ impl ActionInfo { } /// Returns the value sum for this action. + /// Split notes does not contribute to the value sum. fn value_sum(&self) -> ValueSum { + // TODO: Aurel, uncomment when circuit for split flag is implemented. + // let spent_value = self + // .spend + // .split_flag + // .then(|| self.spend.note.value()) + // .unwrap_or_else(NoteValue::zero); + // + // spent_value - self.output.value + self.spend.note.value() - self.output.value } @@ -141,8 +164,15 @@ impl ActionInfo { /// /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend fn build(self, mut rng: impl RngCore) -> (Action, Circuit) { + assert_eq!( + self.output.note_type, + self.spend.note.note_type(), + "spend and recipient note types must be equal" + ); + let v_net = self.value_sum(); - let cv_net = ValueCommitment::derive(v_net, self.rcv, NoteType::native()); + let note_type = self.output.note_type; + let cv_net = ValueCommitment::derive(v_net, self.rcv, note_type); let nf_old = self.spend.note.nullifier(&self.spend.fvk); let sender_address = self.spend.note.recipient(); @@ -202,6 +232,7 @@ impl ActionInfo { g_d_old: Value::known(sender_address.g_d()), pk_d_old: Value::known(*sender_address.pk_d()), v_old: Value::known(self.spend.note.value()), + // split_flag: Value::known(self.spend.split_flag), rho_old: Value::known(rho_old), psi_old: Value::known(psi_old), rcm_old: Value::known(rcm_old), @@ -283,6 +314,7 @@ impl Builder { scope, note, merkle_path, + split_flag: false, }); Ok(()) @@ -294,6 +326,7 @@ impl Builder { ovk: Option, recipient: Address, value: NoteValue, + note_type: NoteType, memo: Option<[u8; 512]>, ) -> Result<(), &'static str> { if !self.flags.outputs_enabled() { @@ -304,6 +337,7 @@ impl Builder { ovk, recipient, value, + note_type, memo, }); @@ -315,23 +349,31 @@ impl Builder { /// The returned bundle will have no proof or signatures; these can be applied with /// [`Bundle::create_proof`] and [`Bundle::apply_signatures`] respectively. pub fn build>( - mut self, + self, mut rng: impl RngCore, ) -> Result, V>, Error> { + let mut pre_actions: Vec<_> = Vec::new(); + // Pair up the spends and recipients, extending with dummy values as necessary. - let pre_actions: Vec<_> = { - let num_spends = self.spends.len(); - let num_recipients = self.recipients.len(); + for (note_type, (mut spends, mut recipients)) in partition(&self.spends, &self.recipients) { + let num_spends = spends.len(); + let num_recipients = recipients.len(); let num_actions = [num_spends, num_recipients, MIN_ACTIONS] .iter() .max() .cloned() .unwrap(); - self.spends.extend( - iter::repeat_with(|| SpendInfo::dummy(&mut rng)).take(num_actions - num_spends), + // use the first spend to create split spend(s) or create a dummy if empty. + let dummy_spend = spends.first().map_or_else( + || SpendInfo::dummy(note_type, &mut rng), + |s| s.create_split_spend(), ); - self.recipients.extend( + + // Extend the spends and recipients with dummy values. + spends.extend(iter::repeat_with(|| dummy_spend.clone()).take(num_actions - num_spends)); + + recipients.extend( iter::repeat_with(|| RecipientInfo::dummy(&mut rng)) .take(num_actions - num_recipients), ); @@ -339,15 +381,16 @@ impl Builder { // Shuffle the spends and recipients, so that learning the position of a // specific spent note or output note doesn't reveal anything on its own about // the meaning of that note in the transaction context. - self.spends.shuffle(&mut rng); - self.recipients.shuffle(&mut rng); - - self.spends - .into_iter() - .zip(self.recipients.into_iter()) - .map(|(spend, recipient)| ActionInfo::new(spend, recipient, &mut rng)) - .collect() - }; + spends.shuffle(&mut rng); + recipients.shuffle(&mut rng); + + pre_actions.extend( + spends + .into_iter() + .zip(recipients.into_iter()) + .map(|(spend, recipient)| ActionInfo::new(spend, recipient, &mut rng)), + ); + } // Move some things out of self that we will need. let flags = self.flags; @@ -399,6 +442,30 @@ impl Builder { } } +/// partition a list of spends and recipients by note types. +fn partition( + spends: &[SpendInfo], + recipients: &[RecipientInfo], +) -> HashMap, Vec)> { + let mut hm = HashMap::new(); + + for s in spends.iter() { + hm.entry(s.note.note_type()) + .or_insert((vec![], vec![])) + .0 + .push(s.clone()); + } + + for r in recipients.iter() { + hm.entry(r.note_type) + .or_insert((vec![], vec![])) + .1 + .push(r.clone()) + } + + hm +} + /// Marker trait representing bundle signatures in the process of being created. pub trait InProgressSignatures: fmt::Debug { /// The authorization type of an Orchard action in the process of being authorized. @@ -663,6 +730,7 @@ pub mod testing { use proptest::collection::vec; use proptest::prelude::*; + use crate::note::NoteType; use crate::{ address::testing::arb_address, bundle::{Authorized, Bundle, Flags}, @@ -690,7 +758,7 @@ pub mod testing { sk: SpendingKey, anchor: Anchor, notes: Vec<(Note, MerklePath)>, - recipient_amounts: Vec<(Address, NoteValue)>, + recipient_amounts: Vec<(Address, NoteValue, NoteType)>, } impl ArbitraryBundleInputs { @@ -704,12 +772,12 @@ pub mod testing { builder.add_spend(fvk.clone(), note, path).unwrap(); } - for (addr, value) in self.recipient_amounts.into_iter() { + for (addr, value, note_type) in self.recipient_amounts.into_iter() { let scope = fvk.scope_for_address(&addr).unwrap(); let ovk = fvk.to_ovk(scope); builder - .add_recipient(Some(ovk.clone()), addr, value, None) + .add_recipient(Some(ovk.clone()), addr, value, note_type, None) .unwrap(); } @@ -743,9 +811,11 @@ pub mod testing { 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, v)) + .prop_map(move |v| { + (a,v, NoteType::native()) + }) }), - n_recipients as usize + n_recipients as usize, ), rng_seed in prop::array::uniform32(prop::num::u8::ANY) ) -> ArbitraryBundleInputs { @@ -793,6 +863,8 @@ mod tests { use rand::rngs::OsRng; use super::Builder; + // use crate::keys::{IssuerAuthorizingKey, IssuerValidatingKey}; + use crate::note::NoteType; use crate::{ bundle::{Authorized, Bundle, Flags}, circuit::ProvingKey, @@ -817,8 +889,15 @@ mod tests { ); builder - .add_recipient(None, recipient, NoteValue::from_raw(5000), None) + .add_recipient( + None, + recipient, + NoteValue::from_raw(5000), + NoteType::native(), + None, + ) .unwrap(); + let bundle: Bundle = builder .build(&mut rng) .unwrap() diff --git a/src/circuit.rs b/src/circuit.rs index b9f64175d..346ae9b33 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -909,7 +909,7 @@ mod tests { }; fn generate_circuit_instance(mut rng: R) -> (Circuit, Instance) { - let (_, fvk, spent_note) = Note::dummy(&mut rng, None); + let (_, fvk, spent_note) = Note::dummy(&mut rng, None, NoteType::native()); let sender_address = spent_note.recipient(); let nk = *fvk.nk(); @@ -919,7 +919,7 @@ mod tests { let alpha = pallas::Scalar::random(&mut rng); let rk = ak.randomize(&alpha); - let (_, _, output_note) = Note::dummy(&mut rng, Some(nf_old)); + let (_, _, output_note) = Note::dummy(&mut rng, Some(nf_old), NoteType::native()); let cmx = output_note.commitment().into(); let value = spent_note.value() - output_note.value(); diff --git a/src/note.rs b/src/note.rs index 514836e50..9e56e31d0 100644 --- a/src/note.rs +++ b/src/note.rs @@ -163,6 +163,7 @@ impl Note { pub(crate) fn dummy( rng: &mut impl RngCore, rho: Option, + note_type: NoteType, ) -> (SpendingKey, FullViewingKey, Self) { let sk = SpendingKey::random(rng); let fvk: FullViewingKey = (&sk).into(); @@ -171,7 +172,7 @@ impl Note { let note = Note::new( recipient, NoteValue::zero(), - NoteType::native(), + note_type, rho.unwrap_or_else(|| Nullifier::dummy(rng)), rng, ); @@ -189,7 +190,7 @@ impl Note { self.value } - /// Returns the note type + /// Returns the note type of this note. pub fn note_type(&self) -> NoteType { self.note_type } @@ -296,7 +297,7 @@ pub mod testing { } prop_compose! { - /// Generate an action without authorization data. + /// Generate an arbitrary note pub fn arb_note(value: NoteValue)( recipient in arb_address(), rho in arb_nullifier(), diff --git a/src/note/note_type.rs b/src/note/note_type.rs index 6075259e3..8ac04b00b 100644 --- a/src/note/note_type.rs +++ b/src/note/note_type.rs @@ -1,16 +1,18 @@ use group::GroupEncoding; use halo2_proofs::arithmetic::CurveExt; use pasta_curves::pallas; +use std::hash::{Hash, Hasher}; + use subtle::{Choice, ConstantTimeEq, CtOption}; use crate::constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_V_BYTES}; use crate::keys::IssuerValidatingKey; /// Note type identifier. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct NoteType(pub(crate) pallas::Point); +#[derive(Clone, Copy, Debug, Eq)] +pub struct NoteType(pallas::Point); -const MAX_ASSET_DESCRIPTION_SIZE: usize = 512; +pub const MAX_ASSET_DESCRIPTION_SIZE: usize = 512; // the hasher used to derive the assetID #[allow(non_snake_case)] @@ -62,16 +64,29 @@ impl NoteType { } } +impl Hash for NoteType { + fn hash(&self, h: &mut H) { + h.write(&self.to_bytes()); + h.finish(); + } +} + +impl PartialEq for NoteType { + fn eq(&self, other: &Self) -> bool { + bool::from(self.0.ct_eq(&other.0)) + } +} + /// Generators for property testing. #[cfg(any(test, feature = "test-dependencies"))] #[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))] pub mod testing { + use super::NoteType; + use proptest::prelude::*; use crate::keys::{testing::arb_spending_key, IssuerAuthorizingKey, IssuerValidatingKey}; - use super::NoteType; - prop_compose! { /// Generate a uniformly distributed note type pub fn arb_note_type()( @@ -89,4 +104,12 @@ pub mod testing { } } } + + prop_compose! { + /// Generate the native note type + pub fn native_note_type()(_i in 0..10) -> NoteType { + // TODO: remove _i + NoteType::native() + } + } } diff --git a/src/note_encryption.rs b/src/note_encryption.rs index 721185831..eeeacf575 100644 --- a/src/note_encryption.rs +++ b/src/note_encryption.rs @@ -394,6 +394,7 @@ mod tests { EphemeralKeyBytes, }; + use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption}; use crate::note::NoteType; use crate::{ action::Action, @@ -411,7 +412,6 @@ mod tests { }; use super::{get_note_version, orchard_parse_note_plaintext_without_memo}; - use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption}; proptest! { #[test] @@ -438,6 +438,7 @@ mod tests { // Check. assert_eq!(parsed_note, note); assert_eq!(parsed_recipient, note.recipient()); + if parsed_note.note_type().is_native().into() { assert_eq!(parsed_version, 0x02); assert_eq!(&parsed_memo, memo); diff --git a/src/tree.rs b/src/tree.rs index c2854200b..7e9601312 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -77,7 +77,7 @@ impl Anchor { /// The Merkle path from a leaf of the note commitment tree /// to its anchor. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MerklePath { position: u32, auth_path: [MerkleHashOrchard; MERKLE_DEPTH_ORCHARD], diff --git a/src/value.rs b/src/value.rs index 8db1839aa..a3a2e7a8a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -38,6 +38,7 @@ use core::fmt::{self, Debug}; use core::iter::Sum; use core::ops::{Add, RangeInclusive, Sub}; +use std::ops::Neg; use bitvec::{array::BitArray, order::Lsb0}; use ff::{Field, PrimeField}; @@ -187,6 +188,18 @@ impl Add for ValueSum { } } +impl Neg for ValueSum { + type Output = Option; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn neg(self) -> Self::Output { + self.0 + .checked_neg() + .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, v| (acc? + *v).ok_or(OverflowError)) @@ -407,7 +420,8 @@ pub mod testing { #[cfg(test)] mod tests { - use crate::note::note_type::testing::arb_note_type; + use crate::note::note_type::testing::{arb_note_type, native_note_type}; + use crate::note::NoteType; use proptest::prelude::*; @@ -417,43 +431,77 @@ mod tests { }; use crate::primitives::redpallas; - fn _bsk_consistent_with_bvk(values: &[(ValueSum, ValueCommitTrapdoor)], note_type: NoteType) { - let value_balance = values + fn _bsk_consistent_with_bvk( + native_values: &[(ValueSum, ValueCommitTrapdoor, NoteType)], + arb_values: &[(ValueSum, ValueCommitTrapdoor, NoteType)], + neg_trapdoors: &[ValueCommitTrapdoor], + ) { + // for each arb value, create a negative value with a different trapdoor + let neg_arb_values: Vec<_> = arb_values + .iter() + .cloned() + .zip(neg_trapdoors.iter().cloned()) + .map(|((value, _, note_type), rcv)| ((-value).unwrap(), rcv, note_type)) + .collect(); + + let native_value_balance = native_values .iter() - .map(|(value, _)| value) + .map(|(value, _, _)| value) .sum::>() .expect("we generate values that won't overflow"); + let values = [native_values, arb_values, &neg_arb_values].concat(); + let bsk = values .iter() - .map(|(_, rcv)| rcv) + .map(|(_, rcv, _)| rcv) .sum::() .into_bsk(); let bvk = (values .iter() - .map(|(value, rcv)| ValueCommitment::derive(*value, *rcv, note_type)) + .map(|(value, rcv, note_type)| ValueCommitment::derive(*value, *rcv, *note_type)) .sum::() - - ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero(), note_type)) + - ValueCommitment::derive( + native_value_balance, + ValueCommitTrapdoor::zero(), + NoteType::native(), + )) .into_bvk(); assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); } + proptest! { + #[test] + fn bsk_consistent_with_bvk_native_only( + native_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(), native_note_type()), n_values) + ) + ), + ) { + // Test with native note type (zec) only + _bsk_consistent_with_bvk(&native_values, &[], &[]); + } + } + proptest! { #[test] fn bsk_consistent_with_bvk( - values in (1usize..10).prop_flat_map(|n_values| + native_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) + prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor(), native_note_type()), n_values) ) ), - arb_note_type in arb_note_type(), + (arb_values,neg_trapdoors) 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(), arb_note_type()), n_values) + ), prop::collection::vec(arb_trapdoor(), n_values)) + ), ) { // Test with native note type (zec) - _bsk_consistent_with_bvk(&values, NoteType::native()); - // Test with arbitrary note type - _bsk_consistent_with_bvk(&values, arb_note_type); + _bsk_consistent_with_bvk(&native_values, &arb_values, &neg_trapdoors); } } } diff --git a/tests/builder.rs b/tests/builder.rs index 35c53976d..4329efc7a 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -1,4 +1,5 @@ use incrementalmerkletree::{bridgetree::BridgeTree, Hashable, Tree}; +use orchard::note::NoteType; use orchard::{ builder::Builder, bundle::{Authorized, Flags}, @@ -43,7 +44,13 @@ fn bundle_chain() { let mut builder = Builder::new(Flags::from_parts(false, true), anchor); assert_eq!( - builder.add_recipient(None, recipient, NoteValue::from_raw(5000), None), + builder.add_recipient( + None, + recipient, + NoteValue::from_raw(5000), + NoteType::native(), + None + ), Ok(()) ); let unauthorized = builder.build(&mut rng).unwrap(); @@ -85,7 +92,13 @@ fn bundle_chain() { let mut builder = Builder::new(Flags::from_parts(true, true), anchor); assert_eq!(builder.add_spend(fvk, note, merkle_path), Ok(())); assert_eq!( - builder.add_recipient(None, recipient, NoteValue::from_raw(5000), None), + builder.add_recipient( + None, + recipient, + NoteValue::from_raw(5000), + NoteType::native(), + None + ), Ok(()) ); let unauthorized = builder.build(&mut rng).unwrap();