From 9c20a0bfb73937488a19b0f7491d058e0e1b485c Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 18 Dec 2023 15:48:43 -0700 Subject: [PATCH] Add a Sapling bundle builder function. This factors out the essential operation of the Sapling builder into a pure function. --- CHANGELOG.md | 9 +- src/builder.rs | 446 ++++++++++++++++++++++++++++--------------------- src/tree.rs | 5 + 3 files changed, 265 insertions(+), 195 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f2784..613fd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,17 +14,18 @@ The entries below are relative to the `zcash_primitives::sapling` module as of - `sapling_crypto::SaplingVerificationContext` (moved from `zcash_proofs::sapling`). - `sapling_crypto::builder` (moved from - `zcash_primitives::transaction::components::sapling::builder`). Additional + `zcash_primitives::transaction::components::sapling::builder`). Further additions to this module: - `UnauthorizedBundle` - `InProgress` - `{InProgressProofs, Unproven, Proven}` - `{InProgressSignatures, Unsigned, PartiallyAuthorized}` - `{MaybeSigned, SigningParts}` - - `SpendDescriptionInfo::value` - - `SaplingOutputInfo` + - `SpendInfo` + - `OutputInfo` - `ProverProgress` - `BundleType` + - `bundle` bundle builder function. - `sapling_crypto::bundle` module: - The following types moved from `zcash_primitives::transaction::components::sapling`: @@ -168,6 +169,8 @@ The entries below are relative to the `zcash_primitives::sapling` module as of - `OutputDescription::read` - `OutputDescription::{write_v4, write_v5_without_proof}` - `OutputDescriptionV5::read` +- `sapling_crypto::builder`: + - `SpendDescriptionInfo` - `sapling_crypto::note_encryption::SaplingDomain::for_height` (use `SaplingDomain::new` instead). - `sapling_crypto::redjubjub` module (use the `redjubjub` crate instead). diff --git a/src/builder.rs b/src/builder.rs index 716b418..be0f579 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,7 +1,7 @@ //! Types and functions for building Sapling transaction components. use core::fmt; -use std::marker::PhantomData; +use std::{iter, marker::PhantomData}; use group::ff::Field; use rand::{seq::SliceRandom, RngCore}; @@ -101,46 +101,71 @@ impl fmt::Display for Error { } } +/// A struct containing the information necessary to add a spend to a bundle. #[derive(Debug, Clone)] -pub struct SpendDescriptionInfo { +pub struct SpendInfo { proof_generation_key: ProofGenerationKey, note: Note, - alpha: jubjub::Fr, merkle_path: MerklePath, - rcv: ValueCommitTrapdoor, } -impl SpendDescriptionInfo { - fn new_internal( - mut rng: &mut R, - extsk: &ExtendedSpendingKey, +impl SpendInfo { + /// Constructs a [`SpendInfo`] from its constituent parts. + pub fn new( + proof_generation_key: ProofGenerationKey, note: Note, merkle_path: MerklePath, ) -> Self { - SpendDescriptionInfo { - proof_generation_key: extsk.expsk.proof_generation_key(), + Self { + proof_generation_key, note, - alpha: jubjub::Fr::random(&mut rng), merkle_path, - rcv: ValueCommitTrapdoor::random(rng), } } + /// Returns the value of the note to be spent. pub fn value(&self) -> NoteValue { self.note.value() } - fn build( + fn prepare(self, rng: R) -> PreparedSpendInfo { + PreparedSpendInfo { + proof_generation_key: self.proof_generation_key, + note: self.note, + merkle_path: self.merkle_path, + rcv: ValueCommitTrapdoor::random(rng), + } + } +} + +#[derive(Debug, Clone)] +struct PreparedSpendInfo { + proof_generation_key: ProofGenerationKey, + note: Note, + merkle_path: MerklePath, + rcv: ValueCommitTrapdoor, +} + +impl PreparedSpendInfo { + fn anchor(&self) -> jubjub::Base { + let node = Node::from_cmu(&self.note.cmu()); + self.merkle_path.root(node).into() + } + + fn build( self, - anchor: bls12_381::Scalar, + mut rng: R, ) -> Result>, Error> { // Construct the value commitment. + let alpha = jubjub::Fr::random(&mut rng); let cv = ValueCommitment::derive(self.note.value(), self.rcv.clone()); + let node = Node::from_cmu(&self.note.cmu()); + let anchor = *self.merkle_path.root(node).inner(); let ak = self.proof_generation_key.ak.clone(); // This is the result of the re-randomization, we compute it for the caller - let rk = ak.randomize(&self.alpha); + let rk = ak.randomize(&alpha); let nullifier = self.note.nf( &self.proof_generation_key.to_viewing_key().nk, @@ -153,7 +178,7 @@ impl SpendDescriptionInfo { *self.note.recipient().diversifier(), *self.note.rseed(), self.note.value(), - self.alpha, + alpha, self.rcv, anchor, self.merkle_path.clone(), @@ -166,10 +191,7 @@ impl SpendDescriptionInfo { nullifier, rk, zkproof, - SigningParts { - ak, - alpha: self.alpha, - }, + SigningParts { ak, alpha }, )) } } @@ -177,16 +199,46 @@ impl SpendDescriptionInfo { /// A struct containing the information required in order to construct a /// Sapling output to a transaction. #[derive(Clone)] -pub struct SaplingOutputInfo { +pub struct OutputInfo { /// `None` represents the `ovk = ⊥` case. ovk: Option, - note: Note, - memo: Option<[u8; 512]>, - rcv: ValueCommitTrapdoor, + to: PaymentAddress, + value: NoteValue, + memo: [u8; 512], } -impl SaplingOutputInfo { - fn dummy(mut rng: &mut R, zip212_enforcement: Zip212Enforcement) -> Self { +impl OutputInfo { + /// Constructs a new [`OutputInfo`] from its constituent parts. + pub fn new( + ovk: Option, + to: PaymentAddress, + value: NoteValue, + memo: Option<[u8; 512]>, + ) -> Self { + Self { + ovk, + to, + value, + memo: memo.unwrap_or_else(|| { + let mut memo = [0; 512]; + memo[0] = 0xf6; + memo + }), + } + } + + /// Returns the recipient of the new output. + pub fn recipient(&self) -> PaymentAddress { + self.to + } + + /// Returns the value of the output. + pub fn value(&self) -> NoteValue { + self.value + } + + /// Constructs a new dummy Sapling output. + pub fn dummy(mut rng: &mut R) -> Self { // This is a dummy output let dummy_to = { let mut diversifier = Diversifier([0; 11]); @@ -199,50 +251,41 @@ impl SaplingOutputInfo { } }; - Self::new_internal( - rng, - None, - dummy_to, - NoteValue::from_raw(0), - None, - zip212_enforcement, - ) + Self::new(None, dummy_to, NoteValue::from_raw(0), None) } - fn new_internal( + fn prepare( + self, rng: &mut R, - ovk: Option, - to: PaymentAddress, - value: NoteValue, - memo: Option<[u8; 512]>, zip212_enforcement: Zip212Enforcement, - ) -> Self { + ) -> PreparedOutputInfo { let rseed = generate_random_rseed_internal(zip212_enforcement, rng); - let note = Note::from_parts(to, value, rseed); + let note = Note::from_parts(self.to, self.value, rseed); - SaplingOutputInfo { - ovk, + PreparedOutputInfo { + ovk: self.ovk, note, - memo, + memo: self.memo, rcv: ValueCommitTrapdoor::random(rng), } } +} +struct PreparedOutputInfo { + /// `None` represents the `ovk = ⊥` case. + ovk: Option, + note: Note, + memo: [u8; 512], + rcv: ValueCommitTrapdoor, +} + +impl PreparedOutputInfo { fn build( self, rng: &mut R, ) -> OutputDescription { - let encryptor = sapling_note_encryption::( - self.ovk, - self.note.clone(), - self.memo.unwrap_or_else(|| { - let mut memo = [0; 512]; - memo[0] = 0xf6; - memo - }), - rng, - ); + let encryptor = sapling_note_encryption::(self.ovk, self.note.clone(), self.memo, rng); // Construct the value commitment. let cv = ValueCommitment::derive(self.note.value(), self.rcv.clone()); @@ -272,14 +315,6 @@ impl SaplingOutputInfo { zkproof, ) } - - pub fn recipient(&self) -> PaymentAddress { - self.note.recipient() - } - - pub fn value(&self) -> NoteValue { - self.note.value() - } } /// Metadata about a transaction created by a [`SaplingBuilder`]. @@ -320,33 +355,167 @@ impl SaplingMetadata { } } +/// Constructs a new Sapling transaction bundle of the given type from the specified set of spends +/// and outputs. +pub fn bundle>( + mut rng: R, + spends: Vec, + outputs: Vec, + bundle_type: BundleType, + zip212_enforcement: Zip212Enforcement, +) -> Result, SaplingMetadata)>, Error> { + let requested_output_count = outputs.len(); + let bundle_output_count = bundle_type + .num_outputs(spends.len(), requested_output_count) + .map_err(|_| Error::BundleTypeNotSatisfiable)?; + + // Record initial positions of spends and outputs + let mut indexed_spends: Vec<_> = spends + .into_iter() + .enumerate() + .map(|(i, s)| (i, s.prepare(&mut rng))) + .collect(); + + // Verify anchor consistency + let mut anchor = None; + for (_, prepared) in &indexed_spends { + if let Some(anchor) = &anchor { + if anchor != &prepared.anchor() { + return Err(Error::AnchorMismatch); + } + } else { + anchor = Some(prepared.anchor()); + } + } + + let padding_outputs = iter::repeat_with(|| OutputInfo::dummy(&mut rng)) + .take(bundle_output_count - requested_output_count) + .collect::>(); + let mut indexed_outputs: Vec<_> = outputs + .into_iter() + .chain(padding_outputs.into_iter()) + .enumerate() + .map(|(i, o)| (i, o.prepare(&mut rng, zip212_enforcement))) + .collect(); + + // Set up the transaction metadata that will be used to record how + // inputs and outputs are shuffled. + let mut tx_metadata = SaplingMetadata::empty(); + tx_metadata.spend_indices.resize(indexed_spends.len(), 0); + tx_metadata.output_indices.resize(indexed_outputs.len(), 0); + + // Randomize order of inputs and outputs + indexed_spends.shuffle(&mut rng); + indexed_outputs.shuffle(&mut rng); + + // Record the transaction metadata and create dummy outputs. + let spend_infos = indexed_spends + .into_iter() + .enumerate() + .map(|(i, (pos, spend))| { + // Record the post-randomized spend location + tx_metadata.spend_indices[pos] = i; + + spend + }) + .collect::>(); + let output_infos = indexed_outputs + .into_iter() + .enumerate() + .map(|(i, (pos, output))| { + // Record the post-randomized output location + tx_metadata.output_indices[pos] = i; + + output + }) + .collect::>(); + + // Compute the transaction binding signing key. + let bsk = { + let spends: TrapdoorSum = spend_infos.iter().map(|spend| &spend.rcv).sum(); + let outputs: TrapdoorSum = output_infos.iter().map(|output| &output.rcv).sum(); + (spends - outputs).into_bsk() + }; + + // Compute the Sapling value balance of the bundle for comparison to `bvk` and `bsk` + let input_total = spend_infos + .iter() + .try_fold(ValueSum::zero(), |balance, spend| { + (balance + spend.note.value()).ok_or(Error::InvalidAmount) + })?; + let value_balance = output_infos + .iter() + .try_fold(input_total, |balance, output| { + (balance - output.note.value()).ok_or(Error::InvalidAmount) + })?; + + // Create the unauthorized Spend and Output descriptions. + let shielded_spends = spend_infos + .into_iter() + .map(|a| a.build::(&mut rng)) + .collect::, _>>()?; + let shielded_outputs = output_infos + .into_iter() + .map(|a| a.build::(&mut rng)) + .collect::>(); + + // Verify that bsk and bvk are consistent. + let bvk = { + let spends = shielded_spends + .iter() + .map(|spend| spend.cv()) + .sum::(); + let outputs = shielded_outputs + .iter() + .map(|output| output.cv()) + .sum::(); + (spends - outputs).into_bvk(i64::try_from(value_balance).map_err(|_| Error::InvalidAmount)?) + }; + assert_eq!(redjubjub::VerificationKey::from(&bsk), bvk); + + Ok(Bundle::from_parts( + shielded_spends, + shielded_outputs, + i64::try_from(value_balance) + .map_err(|_| Error::InvalidAmount) + .and_then(|i| V::try_from(i).map_err(|_| Error::InvalidAmount))?, + InProgress { + sigs: Unsigned { bsk }, + _proof_state: PhantomData::default(), + }, + ) + .map(|b| (b, tx_metadata))) +} + +/// A mutable builder type for constructing Sapling bundles. pub struct SaplingBuilder { anchor: Option, value_balance: ValueSum, - spends: Vec, - outputs: Vec, + spends: Vec, + outputs: Vec, zip212_enforcement: Zip212Enforcement, + bundle_type: BundleType, } impl SaplingBuilder { - pub fn new(zip212_enforcement: Zip212Enforcement) -> Self { + pub fn new(zip212_enforcement: Zip212Enforcement, bundle_type: BundleType) -> Self { SaplingBuilder { anchor: None, value_balance: ValueSum::zero(), spends: vec![], outputs: vec![], zip212_enforcement, + bundle_type, } } - /// Returns the list of Sapling inputs that will be consumed by the transaction being - /// constructed. - pub fn inputs(&self) -> &[SpendDescriptionInfo] { + /// Returns the list of Sapling inputs that have been added to the builder. + pub fn inputs(&self) -> &[SpendInfo] { &self.spends } - /// Returns the Sapling outputs that will be produced by the transaction being constructed - pub fn outputs(&self) -> &[SaplingOutputInfo] { + /// Returns the Sapling outputs that have been added to the builder. + pub fn outputs(&self) -> &[OutputInfo] { &self.outputs } @@ -371,9 +540,8 @@ impl SaplingBuilder { /// /// Returns an error if the given Merkle path does not have the same anchor as the /// paths for previous Sapling notes. - pub fn add_spend( + pub fn add_spend( &mut self, - mut rng: R, extsk: &ExtendedSpendingKey, note: Note, merkle_path: MerklePath, @@ -392,7 +560,7 @@ impl SaplingBuilder { self.value_balance = (self.value_balance + note.value()).ok_or(Error::InvalidAmount)?; self.try_value_balance::()?; - let spend = SpendDescriptionInfo::new_internal(&mut rng, extsk, note, merkle_path); + let spend = SpendInfo::new(extsk.expsk.proof_generation_key(), note, merkle_path); self.spends.push(spend); @@ -403,20 +571,12 @@ impl SaplingBuilder { #[allow(clippy::too_many_arguments)] pub fn add_output( &mut self, - mut rng: R, ovk: Option, to: PaymentAddress, value: NoteValue, memo: Option<[u8; 512]>, ) -> Result<(), Error> { - let output = SaplingOutputInfo::new_internal( - &mut rng, - ovk, - to, - value, - memo, - self.zip212_enforcement, - ); + let output = OutputInfo::new(ovk, to, value, memo); self.value_balance = (self.value_balance - value).ok_or(Error::InvalidAddress)?; self.try_value_balance::()?; @@ -426,114 +586,18 @@ impl SaplingBuilder { Ok(()) } + /// Constructs the Sapling bundle from the builder's accumulated state. pub fn build>( self, - mut rng: R, - bundle_type: &BundleType, + rng: R, ) -> Result, SaplingMetadata)>, Error> { - let value_balance = self.try_value_balance()?; - let bundle_output_count = bundle_type - .num_outputs(self.spends.len(), self.outputs.len()) - .map_err(|_| Error::BundleTypeNotSatisfiable)?; - - // Record initial positions of spends and outputs - let mut indexed_spends: Vec<_> = self.spends.into_iter().enumerate().collect(); - let mut indexed_outputs: Vec<_> = self - .outputs - .into_iter() - .enumerate() - .map(|(i, o)| Some((i, o))) - .collect(); - - // Set up the transaction metadata that will be used to record how - // inputs and outputs are shuffled. - let mut tx_metadata = SaplingMetadata::empty(); - tx_metadata.spend_indices.resize(indexed_spends.len(), 0); - tx_metadata.output_indices.resize(indexed_outputs.len(), 0); - - // Pad Sapling outputs - while indexed_outputs.len() < bundle_output_count { - indexed_outputs.push(None); - } - - // Randomize order of inputs and outputs - indexed_spends.shuffle(&mut rng); - indexed_outputs.shuffle(&mut rng); - - // Record the transaction metadata and create dummy outputs. - let spend_infos = indexed_spends - .into_iter() - .enumerate() - .map(|(i, (pos, spend))| { - // Record the post-randomized spend location - tx_metadata.spend_indices[pos] = i; - - spend - }) - .collect::>(); - let output_infos = indexed_outputs - .into_iter() - .enumerate() - .map(|(i, output)| { - if let Some((pos, output)) = output { - // Record the post-randomized output location - tx_metadata.output_indices[pos] = i; - - output - } else { - // This is a dummy output - SaplingOutputInfo::dummy(&mut rng, self.zip212_enforcement) - } - }) - .collect::>(); - - // Compute the transaction binding signing key. - let bsk = { - let spends: TrapdoorSum = spend_infos.iter().map(|spend| &spend.rcv).sum(); - let outputs: TrapdoorSum = output_infos.iter().map(|output| &output.rcv).sum(); - (spends - outputs).into_bsk() - }; - - // Create the unauthorized Spend and Output descriptions. - let shielded_spends = spend_infos - .into_iter() - .map(|a| { - a.build::( - self.anchor - .expect("Sapling anchor must be set if Sapling spends are present."), - ) - }) - .collect::, _>>()?; - let shielded_outputs = output_infos - .into_iter() - .map(|a| a.build::(&mut rng)) - .collect::>(); - - // Verify that bsk and bvk are consistent. - let bvk = { - let spends = shielded_spends - .iter() - .map(|spend| spend.cv()) - .sum::(); - let outputs = shielded_outputs - .iter() - .map(|output| output.cv()) - .sum::(); - (spends - outputs) - .into_bvk(i64::try_from(self.value_balance).map_err(|_| Error::InvalidAmount)?) - }; - assert_eq!(redjubjub::VerificationKey::from(&bsk), bvk); - - Ok(Bundle::from_parts( - shielded_spends, - shielded_outputs, - value_balance, - InProgress { - sigs: Unsigned { bsk }, - _proof_state: PhantomData::default(), - }, + bundle::( + rng, + self.spends, + self.outputs, + self.bundle_type, + self.zip212_enforcement, ) - .map(|b| (b, tx_metadata))) } } @@ -933,21 +997,19 @@ pub mod testing { }) .prop_map( move |(extsk, spendable_notes, commitment_trees, rng_seed, fake_sighash_bytes)| { - let mut builder = SaplingBuilder::new(zip212_enforcement); + let mut builder = + SaplingBuilder::new(zip212_enforcement, BundleType::Transactional); let mut rng = StdRng::from_seed(rng_seed); for (note, path) in spendable_notes .into_iter() .zip(commitment_trees.into_iter()) { - builder.add_spend(&mut rng, &extsk, note, path).unwrap(); + builder.add_spend(&extsk, note, path).unwrap(); } let (bundle, _) = builder - .build::( - &mut rng, - &BundleType::Transactional, - ) + .build::(&mut rng) .unwrap() .unwrap(); diff --git a/src/tree.rs b/src/tree.rs index ed82d9d..f98f499 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -101,6 +101,11 @@ impl Node { pub fn to_bytes(&self) -> [u8; 32] { self.0.to_repr() } + + /// Returns the wrapped value + pub(crate) fn inner(&self) -> &jubjub::Base { + &self.0 + } } impl Hashable for Node {