diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index b304ce7a0..7374ffd15 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -44,27 +44,30 @@ pub fn decrypt_transaction( ) -> Vec { let mut decrypted = vec![]; - for (account, extfvk) in extfvks.iter() { - let ivk = extfvk.fvk.vk.ivk(); - let ovk = extfvk.fvk.ovk; + if let Some(bundle) = tx.sapling_bundle.as_ref() { + for (account, extfvk) in extfvks.iter() { + let ivk = extfvk.fvk.vk.ivk(); + let ovk = extfvk.fvk.ovk; - for (index, output) in tx.shielded_outputs.iter().enumerate() { - let ((note, to, memo), outgoing) = - match try_sapling_note_decryption(params, height, &ivk, output) { - Some(ret) => (ret, false), - None => match try_sapling_output_recovery(params, height, &ovk, output) { - Some(ret) => (ret, true), - None => continue, - }, - }; - decrypted.push(DecryptedOutput { - index, - note, - account: *account, - to, - memo, - outgoing, - }) + for (index, output) in bundle.shielded_outputs.iter().enumerate() { + let ((note, to, memo), outgoing) = + match try_sapling_note_decryption(params, height, &ivk, output) { + Some(ret) => (ret, false), + None => match try_sapling_output_recovery(params, height, &ovk, output) { + Some(ret) => (ret, true), + None => continue, + }, + }; + + decrypted.push(DecryptedOutput { + index, + note, + account: *account, + to, + memo, + outgoing, + }) + } } } diff --git a/zcash_client_backend/src/proto.rs b/zcash_client_backend/src/proto.rs index 49d6ebf13..e284c9981 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -9,7 +9,7 @@ use zcash_primitives::{ consensus::BlockHeight, sapling::Nullifier, transaction::{ - components::sapling::{CompactOutputDescription, OutputDescription}, + components::sapling::{self, CompactOutputDescription, OutputDescription}, TxId, }, }; @@ -113,8 +113,8 @@ impl compact_formats::CompactOutput { } } -impl From for compact_formats::CompactOutput { - fn from(out: OutputDescription) -> compact_formats::CompactOutput { +impl From> for compact_formats::CompactOutput { + fn from(out: OutputDescription) -> compact_formats::CompactOutput { let mut result = compact_formats::CompactOutput::new(); result.set_cmu(out.cmu.to_repr().to_vec()); result.set_epk(out.ephemeral_key.to_bytes().to_vec()); diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index b9629e1cf..52e0bc7bd 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -489,8 +489,10 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { // // Assumes that create_spend_to_address() will never be called in parallel, which is a // reasonable assumption for a light client such as a mobile phone. - for spend in &sent_tx.tx.shielded_spends { - wallet::mark_spent(up, tx_ref, &spend.nullifier)?; + if let Some(bundle) = sent_tx.tx.sapling_bundle.as_ref() { + for spend in &bundle.shielded_spends { + wallet::mark_spent(up, tx_ref, &spend.nullifier)?; + } } wallet::insert_sent_note( diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs index 129ff1fea..b4a737027 100644 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ b/zcash_client_sqlite/src/wallet/transact.rs @@ -631,7 +631,8 @@ mod tests { ) .unwrap(); - let output = &tx.shielded_outputs[output_index as usize]; + let output = + &tx.sapling_bundle.as_ref().unwrap().shielded_outputs[output_index as usize]; try_sapling_output_recovery( &network, diff --git a/zcash_extensions/src/consensus/transparent.rs b/zcash_extensions/src/consensus/transparent.rs index 4cb856b2a..edf80f8f2 100644 --- a/zcash_extensions/src/consensus/transparent.rs +++ b/zcash_extensions/src/consensus/transparent.rs @@ -78,8 +78,7 @@ impl<'a> demo::Context for Context<'a> { fn is_tze_only(&self) -> bool { self.tx.vin.is_empty() && self.tx.vout.is_empty() - && self.tx.shielded_spends.is_empty() - && self.tx.shielded_outputs.is_empty() + && self.tx.sapling_bundle.is_none() && self.tx.joinsplits.is_empty() && self.tx.orchard_bundle.is_none() } diff --git a/zcash_extensions/src/transparent/demo.rs b/zcash_extensions/src/transparent/demo.rs index 02787f5d6..cccc379b1 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -472,8 +472,7 @@ mod tests { extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, legacy::TransparentAddress, merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::Node, - sapling::Rseed, + sapling::{Node, Rseed}, transaction::{ builder::Builder, components::{ @@ -606,8 +605,7 @@ mod tests { fn is_tze_only(&self) -> bool { self.tx.vin.is_empty() && self.tx.vout.is_empty() - && self.tx.shielded_spends.is_empty() - && self.tx.shielded_outputs.is_empty() + && self.tx.sapling_bundle.is_none() && self.tx.joinsplits.is_empty() && self.tx.orchard_bundle.is_none() } @@ -659,7 +657,8 @@ mod tests { }; fn auth_and_freeze(tx: TransactionData) -> Transaction { - Builder::::apply_authorization( + Builder::::apply_signatures( + BranchId::ZFuture, tx, #[cfg(feature = "transparent-inputs")] None, @@ -667,7 +666,6 @@ mod tests { None, None, ) - .freeze(BranchId::ZFuture) .unwrap() } diff --git a/zcash_primitives/benches/note_decryption.rs b/zcash_primitives/benches/note_decryption.rs index 6200d5202..e4ea979f3 100644 --- a/zcash_primitives/benches/note_decryption.rs +++ b/zcash_primitives/benches/note_decryption.rs @@ -9,7 +9,10 @@ use zcash_primitives::{ util::generate_random_rseed, Diversifier, PaymentAddress, SaplingIvk, ValueCommitment, }, - transaction::components::{OutputDescription, GROTH_PROOF_SIZE}, + transaction::components::{ + sapling::{Authorized, OutputDescription}, + GROTH_PROOF_SIZE, + }, }; fn bench_note_decryption(c: &mut Criterion) { @@ -20,7 +23,7 @@ fn bench_note_decryption(c: &mut Criterion) { let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); // Construct a fake Sapling output as if we had just deserialized a transaction. - let output = { + let output: OutputDescription = { let diversifier = Diversifier([0; 11]); let pk_d = diversifier.g_d().unwrap() * valid_ivk.0; let pa = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); diff --git a/zcash_primitives/src/consensus.rs b/zcash_primitives/src/consensus.rs index e10793566..d4ebfc18b 100644 --- a/zcash_primitives/src/consensus.rs +++ b/zcash_primitives/src/consensus.rs @@ -554,6 +554,10 @@ impl BranchId { .map(|lower| (lower, None)), } } + + pub fn sprout_uses_groth_proofs(&self) -> bool { + !matches!(self, BranchId::Sprout | BranchId::Overwinter) + } } #[cfg(any(test, feature = "test-dependencies"))] diff --git a/zcash_primitives/src/merkle_tree.rs b/zcash_primitives/src/merkle_tree.rs index f01a218d8..f4a76423b 100644 --- a/zcash_primitives/src/merkle_tree.rs +++ b/zcash_primitives/src/merkle_tree.rs @@ -47,7 +47,7 @@ impl PathFiller { /// /// The depth of the Merkle tree is fixed at 32, equal to the depth of the Sapling /// commitment tree. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CommitmentTree { left: Option, right: Option, @@ -1057,3 +1057,25 @@ mod tests { } } } + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use core::fmt::Debug; + use proptest::collection::vec; + use proptest::prelude::*; + + use super::{CommitmentTree, Hashable}; + + pub fn arb_commitment_tree>( + min_size: usize, + arb_node: T, + ) -> impl Strategy> { + vec(arb_node, min_size..(min_size + 100)).prop_map(|v| { + let mut tree = CommitmentTree::empty(); + for node in v.into_iter() { + tree.append(node).unwrap(); + } + tree + }) + } +} diff --git a/zcash_primitives/src/sapling/note_encryption.rs b/zcash_primitives/src/sapling/note_encryption.rs index c2d9f85d8..cedb8c786 100644 --- a/zcash_primitives/src/sapling/note_encryption.rs +++ b/zcash_primitives/src/sapling/note_encryption.rs @@ -17,7 +17,10 @@ use crate::{ consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD}, memo::MemoBytes, sapling::{keys::OutgoingViewingKey, Diversifier, Note, PaymentAddress, Rseed, SaplingIvk}, - transaction::components::{amount::Amount, sapling::OutputDescription}, + transaction::components::{ + amount::Amount, + sapling::{self, OutputDescription}, + }, }; pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF"; @@ -383,7 +386,7 @@ pub fn try_sapling_output_recovery_with_ock( params: &P, height: BlockHeight, ock: &OutgoingCipherKey, - output: &OutputDescription, + output: &OutputDescription, ) -> Option<(Note, PaymentAddress, MemoBytes)> { let domain = SaplingDomain { params: params.clone(), @@ -405,7 +408,7 @@ pub fn try_sapling_output_recovery( params: &P, height: BlockHeight, ovk: &OutgoingViewingKey, - output: &OutputDescription, + output: &OutputDescription, ) -> Option<(Note, PaymentAddress, MemoBytes)> { let domain = SaplingDomain { params: params.clone(), @@ -450,7 +453,7 @@ mod tests { }, transaction::components::{ amount::Amount, - sapling::{CompactOutputDescription, OutputDescription}, + sapling::{self, CompactOutputDescription, OutputDescription}, GROTH_PROOF_SIZE, }, }; @@ -462,7 +465,7 @@ mod tests { OutgoingViewingKey, OutgoingCipherKey, SaplingIvk, - OutputDescription, + OutputDescription, ) { let ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); @@ -492,7 +495,11 @@ mod tests { height: BlockHeight, ivk: &SaplingIvk, mut rng: &mut R, - ) -> (OutgoingViewingKey, OutgoingCipherKey, OutputDescription) { + ) -> ( + OutgoingViewingKey, + OutgoingCipherKey, + OutputDescription, + ) { let diversifier = Diversifier([0; 11]); let pk_d = diversifier.g_d().unwrap() * ivk.0; let pa = PaymentAddress::from_parts_unchecked(diversifier, pk_d); diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index d444ab9d1..c78b4d379 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -3,6 +3,7 @@ use core::array; use std::error; use std::fmt; +use std::io; use std::sync::mpsc::Sender; use rand::{rngs::OsRng, CryptoRng, RngCore}; @@ -21,11 +22,14 @@ use crate::{ transaction::{ components::{ amount::{Amount, DEFAULT_FEE}, - sapling::builder::{self as sapling, SaplingBuilder, SaplingMetadata}, - transparent::builder::{self as transparent, TransparentBuilder}, + sapling::{ + self, + builder::{SaplingBuilder, SaplingMetadata}, + }, + transparent::{self, builder::TransparentBuilder, TxIn}, }, - signature_hash_data, Authorized, SignableInput, Transaction, TransactionData, TxVersion, - Unauthorized, SIGHASH_ALL, + signature_hash_data, SignableInput, Transaction, TransactionData, TxVersion, Unauthorized, + SIGHASH_ALL, }, zip32::ExtendedSpendingKey, }; @@ -34,17 +38,14 @@ use crate::{ use std::marker::PhantomData; #[cfg(feature = "transparent-inputs")] -use crate::{ - legacy::Script, - transaction::components::{OutPoint, TxOut}, -}; +use crate::{legacy::Script, transaction::components::transparent::TxOut}; #[cfg(feature = "zfuture")] use crate::{ extensions::transparent::{ExtensionTxBuilder, ToPayload}, transaction::components::{ - tze::builder::{self as tzebuilder, TzeBuilder, WitnessData}, - TzeOut, TzeOutPoint, + tze::builder::{TzeBuilder, WitnessData}, + tze::{self, TzeIn, TzeOut, TzeOutPoint}, }, }; @@ -58,10 +59,10 @@ pub enum Error { ChangeIsNegative(Amount), InvalidAmount, NoChangeAddress, - TransparentBuild(transparent::Error), - SaplingBuild(sapling::Error), + TransparentBuild(transparent::builder::Error), + SaplingBuild(sapling::builder::Error), #[cfg(feature = "zfuture")] - TzeBuild(tzebuilder::Error), + TzeBuild(tze::builder::Error), } impl fmt::Display for Error { @@ -175,7 +176,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA, fee: DEFAULT_FEE, transparent_builder: TransparentBuilder::empty(), - sapling_builder: SaplingBuilder::empty(params, target_height), + sapling_builder: SaplingBuilder::new(params, target_height), change_address: None, #[cfg(feature = "zfuture")] tze_builder: TzeBuilder::empty(), @@ -220,7 +221,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { pub fn add_transparent_input( &mut self, sk: secp256k1::SecretKey, - utxo: OutPoint, + utxo: transparent::OutPoint, coin: TxOut, ) -> Result<(), Error> { self.transparent_builder @@ -326,7 +327,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { let (vin, vout) = self.transparent_builder.build(); let mut ctx = prover.new_sapling_proving_context(); - let (spend_descs, output_descs, tx_metadata) = self + let (sapling_bundle, tx_metadata) = self .sapling_builder .build( prover, @@ -350,13 +351,10 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { tze_outputs, lock_time: 0, expiry_height: self.expiry_height, - value_balance: self.sapling_builder.value_balance(), - shielded_spends: spend_descs, - shielded_outputs: output_descs, joinsplits: vec![], joinsplit_pubkey: None, joinsplit_sig: None, - binding_sig: None, + sapling_bundle, orchard_bundle: None, }; @@ -373,9 +371,10 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { )); #[cfg(feature = "transparent-inputs")] - let transparent_sigs = self - .transparent_builder - .create_signatures(&unauthed_tx, consensus_branch_id); + let transparent_sigs = Some( + self.transparent_builder + .create_signatures(&unauthed_tx, consensus_branch_id), + ); let sapling_sigs = self .sapling_builder @@ -389,26 +388,25 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { .map_err(Error::TzeBuild)?, ); - let authorized_tx = Self::apply_authorization( - unauthed_tx, - #[cfg(feature = "transparent-inputs")] - Some(transparent_sigs), - sapling_sigs, - None, - #[cfg(feature = "zfuture")] - tze_witnesses, - ); - Ok(( - authorized_tx - .freeze(consensus_branch_id) - .expect("Transaction should be complete"), + Self::apply_signatures( + consensus_branch_id, + unauthed_tx, + #[cfg(feature = "transparent-inputs")] + transparent_sigs, + sapling_sigs, + None, + #[cfg(feature = "zfuture")] + tze_witnesses, + ) + .expect("An IO error occurred applying signatures."), tx_metadata, )) } - pub fn apply_authorization( - mut mtx: TransactionData, + pub fn apply_signatures( + consensus_branch_id: consensus::BranchId, + unauthed_tx: TransactionData, #[cfg(feature = "transparent-inputs")] transparent_sigs: Option>, sapling_sigs: Option<(Vec, redjubjub::Signature)>, _orchard_auth: Option<( @@ -416,46 +414,59 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { orchard::Authorized, )>, #[cfg(feature = "zfuture")] tze_witnesses: Option>, - ) -> TransactionData { - #[cfg(feature = "transparent-inputs")] - if let Some(script_sigs) = transparent_sigs { - for (i, sig) in script_sigs.into_iter().enumerate() { - mtx.vin[i].script_sig = sig; + ) -> io::Result { + let signed_sapling_bundle = match (unauthed_tx.sapling_bundle, sapling_sigs) { + (Some(bundle), Some((spend_sigs, binding_sig))) => { + Some(bundle.apply_signatures(spend_sigs, binding_sig)) } - } - - if let Some((sapling_spend_auth_sigs, sapling_binding_sig)) = sapling_sigs { - for (i, spend_auth_sig) in sapling_spend_auth_sigs.into_iter().enumerate() { - mtx.shielded_spends[i].spend_auth_sig = Some(spend_auth_sig); + (None, None) => None, + _ => { + panic!("Mismatch between sapling bundle and signatures."); } - mtx.binding_sig = Some(sapling_binding_sig); - } + }; - #[cfg(feature = "zfuture")] - if let Some(tze_witnesses) = tze_witnesses { - for (i, payload) in tze_witnesses.into_iter().enumerate() { - mtx.tze_inputs[i].witness.payload = payload.0; - } - } - - TransactionData { - version: mtx.version, - vin: mtx.vin, - vout: mtx.vout, + let mut authorized_tx = TransactionData { + version: unauthed_tx.version, + vin: unauthed_tx.vin, + vout: unauthed_tx.vout, #[cfg(feature = "zfuture")] - tze_inputs: mtx.tze_inputs, + tze_inputs: unauthed_tx.tze_inputs, #[cfg(feature = "zfuture")] - tze_outputs: mtx.tze_outputs, - lock_time: mtx.lock_time, - expiry_height: mtx.expiry_height, - value_balance: mtx.value_balance, - shielded_spends: mtx.shielded_spends, - shielded_outputs: mtx.shielded_outputs, - joinsplits: mtx.joinsplits, - joinsplit_pubkey: mtx.joinsplit_pubkey, - joinsplit_sig: mtx.joinsplit_sig, - binding_sig: mtx.binding_sig, + tze_outputs: unauthed_tx.tze_outputs, + lock_time: unauthed_tx.lock_time, + expiry_height: unauthed_tx.expiry_height, + joinsplits: unauthed_tx.joinsplits, + joinsplit_pubkey: unauthed_tx.joinsplit_pubkey, + joinsplit_sig: unauthed_tx.joinsplit_sig, + sapling_bundle: signed_sapling_bundle, orchard_bundle: None, + }; + + #[cfg(feature = "transparent-inputs")] + Self::apply_transparent_sigs(&mut authorized_tx.vin, transparent_sigs); + #[cfg(feature = "zfuture")] + Self::apply_tze_sigs(&mut authorized_tx.tze_inputs, tze_witnesses); + + authorized_tx.freeze(consensus_branch_id) + } + + #[cfg(feature = "transparent-inputs")] + pub fn apply_transparent_sigs(vin: &mut [TxIn], transparent_sigs: Option>) { + if let Some(script_sigs) = transparent_sigs { + assert!(vin.len() == script_sigs.len()); + for (i, sig) in script_sigs.into_iter().enumerate() { + vin[i].script_sig = sig; + } + } + } + + #[cfg(feature = "zfuture")] + pub fn apply_tze_sigs(vtzein: &mut [TzeIn], tze_witnesses: Option>) { + if let Some(tze_witnesses) = tze_witnesses { + assert!(vtzein.len() == tze_witnesses.len()); + for (i, payload) in tze_witnesses.into_iter().enumerate() { + vtzein[i].witness.payload = payload.0; + } } } } @@ -465,7 +476,7 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a for Builder<'a, P, R> { type BuildCtx = TransactionData; - type BuildError = tzebuilder::Error; + type BuildError = tze::builder::Error; fn add_tze_input( &mut self, @@ -475,7 +486,7 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a witness_builder: WBuilder, ) -> Result<(), Self::BuildError> where - WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result), + WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result), { self.tze_builder .add_input(extension_id, mode, prevout, witness_builder); @@ -552,7 +563,7 @@ mod tests { sapling::{prover::mock::MockTxProver, Node, Rseed}, transaction::components::{ amount::{Amount, DEFAULT_FEE}, - sapling::builder::{self as sapling}, + sapling::builder::{self as build_s}, transparent::builder::{self as transparent}, }, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, @@ -580,7 +591,7 @@ mod tests { let mut builder = Builder::new(TEST_NETWORK, sapling_activation_height); assert_eq!( builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None), - Err(Error::SaplingBuild(sapling::Error::InvalidAmount)) + Err(Error::SaplingBuild(build_s::Error::InvalidAmount)) ); } @@ -601,7 +612,7 @@ mod tests { expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA, fee: Amount::zero(), transparent_builder: TransparentBuilder::empty(), - sapling_builder: SaplingBuilder::empty(TEST_NETWORK, sapling_activation_height), + sapling_builder: SaplingBuilder::new(TEST_NETWORK, sapling_activation_height), change_address: None, #[cfg(feature = "zfuture")] tze_builder: TzeBuilder::empty(), @@ -617,7 +628,7 @@ mod tests { let (tx, _) = builder.build(&MockTxProver).unwrap(); // No binding signature, because only t input and outputs - assert!(tx.binding_sig.is_none()); + assert!(tx.sapling_bundle.is_none()); } #[test] @@ -654,7 +665,7 @@ mod tests { // that a binding signature was attempted assert_eq!( builder.build(&MockTxProver), - Err(Error::SaplingBuild(sapling::Error::BindingSig)) + Err(Error::SaplingBuild(build_s::Error::BindingSig)) ); } @@ -804,7 +815,7 @@ mod tests { .unwrap(); assert_eq!( builder.build(&MockTxProver), - Err(Error::SaplingBuild(sapling::Error::BindingSig)) + Err(Error::SaplingBuild(build_s::Error::BindingSig)) ) } } diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 9cbfce863..2e567a236 100644 --- a/zcash_primitives/src/transaction/components/sapling.rs +++ b/zcash_primitives/src/transaction/components/sapling.rs @@ -1,36 +1,113 @@ +use core::fmt::Debug; use ff::PrimeField; use group::GroupEncoding; - use std::io::{self, Read, Write}; -use zcash_note_encryption::ShieldedOutput; +use zcash_note_encryption::{ShieldedOutput, COMPACT_NOTE_SIZE}; use crate::{ consensus, sapling::{ note_encryption::SaplingDomain, - redjubjub::{PublicKey, Signature}, + redjubjub::{self, PublicKey, Signature}, Nullifier, }, }; -use zcash_note_encryption::COMPACT_NOTE_SIZE; +use super::{amount::Amount, GROTH_PROOF_SIZE}; -use super::GROTH_PROOF_SIZE; +pub type GrothProofBytes = [u8; GROTH_PROOF_SIZE]; pub mod builder; +pub trait Authorization: Debug { + type Proof: Clone + Debug; + type AuthSig: Clone + Debug; +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Unproven; + +impl Authorization for Unproven { + type Proof = (); + type AuthSig = (); +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Unauthorized; + +impl Authorization for Unauthorized { + type Proof = GrothProofBytes; + type AuthSig = (); +} + +#[derive(Debug, Copy, Clone)] +pub struct Authorized { + pub binding_sig: redjubjub::Signature, +} + +impl Authorization for Authorized { + type Proof = GrothProofBytes; + type AuthSig = redjubjub::Signature; +} + +#[derive(Debug, Clone)] +pub struct Bundle { + pub shielded_spends: Vec>, + pub shielded_outputs: Vec>, + pub value_balance: Amount, + pub authorization: A, +} + +impl Bundle { + pub fn apply_signatures( + self, + spend_auth_sigs: Vec, + binding_sig: Signature, + ) -> Bundle { + assert!(self.shielded_spends.len() == spend_auth_sigs.len()); + Bundle { + shielded_spends: self + .shielded_spends + .iter() + .zip(spend_auth_sigs.iter()) + .map(|(spend, sig)| spend.apply_signature(*sig)) + .collect(), + shielded_outputs: self + .shielded_outputs + .into_iter() + .map(|o| o.into_authorized()) + .collect(), //TODO, is there a zero-cost way to do this? + value_balance: self.value_balance, + authorization: Authorized { binding_sig }, + } + } +} + #[derive(Clone)] -pub struct SpendDescription { +pub struct SpendDescription { pub cv: jubjub::ExtendedPoint, pub anchor: bls12_381::Scalar, pub nullifier: Nullifier, pub rk: PublicKey, - pub zkproof: [u8; GROTH_PROOF_SIZE], - pub spend_auth_sig: Option, + pub zkproof: A::Proof, + pub spend_auth_sig: A::AuthSig, } -impl std::fmt::Debug for SpendDescription { +impl SpendDescription { + pub fn apply_signature(&self, spend_auth_sig: Signature) -> SpendDescription { + SpendDescription { + cv: self.cv, + anchor: self.anchor, + nullifier: self.nullifier, + rk: self.rk.clone(), + zkproof: self.zkproof, + spend_auth_sig, + } + } +} + +impl std::fmt::Debug for SpendDescription { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!( f, @@ -40,49 +117,84 @@ impl std::fmt::Debug for SpendDescription { } } -impl SpendDescription { - pub fn read(mut reader: &mut R) -> io::Result { - // Consensus rules (§4.4): - // - Canonical encoding is enforced here. - // - "Not small order" is enforced in SaplingVerificationContext::check_spend() - // (located in zcash_proofs::sapling::verifier). - let cv = { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - let cv = jubjub::ExtendedPoint::from_bytes(&bytes); - if cv.is_none().into() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid cv")); - } - cv.unwrap() - }; +/// Consensus rules (§4.4) & (§4.5): +/// - Canonical encoding is enforced here. +/// - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output()) +/// (located in zcash_proofs::sapling::verifier). +pub fn read_point(mut reader: R, field: &str) -> io::Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + let point = jubjub::ExtendedPoint::from_bytes(&bytes); - // Consensus rule (§7.3): Canonical encoding is enforced here - let anchor = { - let mut f = [0u8; 32]; - reader.read_exact(&mut f)?; - bls12_381::Scalar::from_repr(f) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "anchor not in field"))? - }; + if point.is_none().into() { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid ".to_owned() + field, + )) + } else { + Ok(point.unwrap()) + } +} +/// Consensus rules (§7.3) & (§7.4): +/// - Canonical encoding is enforced here +pub fn read_scalar(mut reader: R, field: &str) -> io::Result { + let mut f = [0u8; 32]; + reader.read_exact(&mut f)?; + bls12_381::Scalar::from_repr(f).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + field.to_owned() + " not in field", + ) + }) +} + +/// Consensus rules (§4.4) & (§4.5): +/// - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend() +/// and SaplingVerificationContext::check_output() due to the need to parse this into a +/// bellman::groth16::Proof. +/// - Proof validity is enforced in SaplingVerificationContext::check_spend() +/// and SaplingVerificationContext::check_output() +pub fn read_zkproof(mut reader: R) -> io::Result { + let mut zkproof = [0u8; GROTH_PROOF_SIZE]; + reader.read_exact(&mut zkproof)?; + Ok(zkproof) +} + +impl SpendDescription { + pub fn read_nullifier(mut reader: R) -> io::Result { let mut nullifier = Nullifier([0u8; 32]); reader.read_exact(&mut nullifier.0)?; + Ok(nullifier) + } - // Consensus rules (§4.4): + /// Consensus rules (§4.4): + /// - Canonical encoding is enforced here. + /// - "Not small order" is enforced in SaplingVerificationContext::check_spend() + pub fn read_rk(mut reader: R) -> io::Result { + PublicKey::read(&mut reader) + } + + /// Consensus rules (§4.4): + /// - Canonical encoding is enforced here. + /// - Signature validity is enforced in SaplingVerificationContext::check_spend() + pub fn read_spend_auth_sig(mut reader: R) -> io::Result { + Signature::read(&mut reader) + } + + pub fn read(mut reader: R) -> io::Result { + // Consensus rules (§4.4) & (§4.5): // - Canonical encoding is enforced here. - // - "Not small order" is enforced in SaplingVerificationContext::check_spend() - let rk = PublicKey::read(&mut reader)?; - - // Consensus rules (§4.4): - // - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend() - // due to the need to parse this into a bellman::groth16::Proof. - // - Proof validity is enforced in SaplingVerificationContext::check_spend() - let mut zkproof = [0u8; GROTH_PROOF_SIZE]; - reader.read_exact(&mut zkproof)?; - - // Consensus rules (§4.4): - // - Canonical encoding is enforced here. - // - Signature validity is enforced in SaplingVerificationContext::check_spend() - let spend_auth_sig = Some(Signature::read(&mut reader)?); + // - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output()) + // (located in zcash_proofs::sapling::verifier). + let cv = read_point(&mut reader, "cv")?; + // Consensus rules (§7.3) & (§7.4): + // - Canonical encoding is enforced here + let anchor = read_scalar(&mut reader, "anchor")?; + let nullifier = Self::read_nullifier(&mut reader)?; + let rk = Self::read_rk(&mut reader)?; + let zkproof = read_zkproof(&mut reader)?; + let spend_auth_sig = Self::read_spend_auth_sig(&mut reader)?; Ok(SpendDescription { cv, @@ -100,27 +212,23 @@ impl SpendDescription { writer.write_all(&self.nullifier.0)?; self.rk.write(&mut writer)?; writer.write_all(&self.zkproof)?; - match self.spend_auth_sig { - Some(sig) => sig.write(&mut writer), - None => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Missing spend auth signature", - )), - } + self.spend_auth_sig.write(&mut writer) } } #[derive(Clone)] -pub struct OutputDescription { +pub struct OutputDescription { pub cv: jubjub::ExtendedPoint, pub cmu: bls12_381::Scalar, pub ephemeral_key: jubjub::ExtendedPoint, pub enc_ciphertext: [u8; 580], pub out_ciphertext: [u8; 80], - pub zkproof: [u8; GROTH_PROOF_SIZE], + pub zkproof: A::Proof, } -impl ShieldedOutput> for OutputDescription { +impl ShieldedOutput> + for OutputDescription +{ fn epk(&self) -> &jubjub::ExtendedPoint { &self.ephemeral_key } @@ -134,7 +242,7 @@ impl ShieldedOutput> for OutputDescri } } -impl std::fmt::Debug for OutputDescription { +impl std::fmt::Debug for OutputDescription { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!( f, @@ -144,7 +252,7 @@ impl std::fmt::Debug for OutputDescription { } } -impl OutputDescription { +impl OutputDescription { pub fn read(reader: &mut R) -> io::Result { // Consensus rules (§4.5): // - Canonical encoding is enforced here. @@ -205,7 +313,9 @@ impl OutputDescription { zkproof, }) } +} +impl> OutputDescription { pub fn write(&self, mut writer: W) -> io::Result<()> { writer.write_all(&self.cv.to_bytes())?; writer.write_all(self.cmu.to_repr().as_ref())?; @@ -216,14 +326,27 @@ impl OutputDescription { } } +impl OutputDescription { + pub fn into_authorized(self) -> OutputDescription { + OutputDescription { + cv: self.cv, + cmu: self.cmu, + ephemeral_key: self.ephemeral_key, + enc_ciphertext: self.enc_ciphertext, + out_ciphertext: self.out_ciphertext, + zkproof: self.zkproof, + } + } +} + pub struct CompactOutputDescription { pub epk: jubjub::ExtendedPoint, pub cmu: bls12_381::Scalar, pub enc_ciphertext: Vec, } -impl From for CompactOutputDescription { - fn from(out: OutputDescription) -> CompactOutputDescription { +impl From> for CompactOutputDescription { + fn from(out: OutputDescription) -> CompactOutputDescription { CompactOutputDescription { epk: out.ephemeral_key, cmu: out.cmu, diff --git a/zcash_primitives/src/transaction/components/sapling/builder.rs b/zcash_primitives/src/transaction/components/sapling/builder.rs index 8846f82f1..fafb387b5 100644 --- a/zcash_primitives/src/transaction/components/sapling/builder.rs +++ b/zcash_primitives/src/transaction/components/sapling/builder.rs @@ -1,11 +1,10 @@ //! Types and functions for building Sapling transaction components. use std::fmt; -use std::marker::PhantomData; use std::sync::mpsc::Sender; use ff::Field; -use rand::{seq::SliceRandom, CryptoRng, RngCore}; +use rand::{seq::SliceRandom, RngCore}; use crate::{ consensus::{self, BlockHeight}, @@ -22,7 +21,10 @@ use crate::{ }, transaction::{ builder::Progress, - components::{amount::Amount, OutputDescription, SpendDescription}, + components::{ + amount::Amount, + sapling::{Bundle, OutputDescription, SpendDescription, Unauthorized}, + }, }, zip32::ExtendedSpendingKey, }; @@ -63,32 +65,19 @@ struct SpendDescriptionInfo { } #[derive(Clone)] -pub struct SaplingOutput { +struct SaplingOutput { /// `None` represents the `ovk = ⊥` case. ovk: Option, to: PaymentAddress, note: Note, memo: MemoBytes, - _params: PhantomData

, } -impl SaplingOutput

{ - pub fn new( +impl SaplingOutput { + fn new_internal( params: &P, - target_height: BlockHeight, rng: &mut R, - ovk: Option, - to: PaymentAddress, - value: Amount, - memo: Option, - ) -> Result { - Self::new_internal(params, target_height, rng, ovk, to, value, memo) - } - - fn new_internal( - params: &P, target_height: BlockHeight, - rng: &mut R, ovk: Option, to: PaymentAddress, value: Amount, @@ -113,25 +102,15 @@ impl SaplingOutput

{ to, note, memo: memo.unwrap_or_else(MemoBytes::empty), - _params: PhantomData::default(), }) } - pub fn build( + fn build( self, prover: &Pr, ctx: &mut Pr::SaplingProvingContext, rng: &mut R, - ) -> OutputDescription { - self.build_internal(prover, ctx, rng) - } - - fn build_internal( - self, - prover: &Pr, - ctx: &mut Pr::SaplingProvingContext, - rng: &mut R, - ) -> OutputDescription { + ) -> OutputDescription { let encryptor = sapling_note_encryption::( self.ovk, self.note.clone(), @@ -204,17 +183,17 @@ impl SaplingMetadata { } } -pub struct SaplingBuilder { +pub struct SaplingBuilder

{ params: P, anchor: Option, target_height: BlockHeight, value_balance: Amount, spends: Vec, - outputs: Vec>, + outputs: Vec, } impl SaplingBuilder

{ - pub fn empty(params: P, target_height: BlockHeight) -> Self { + pub fn new(params: P, target_height: BlockHeight) -> Self { SaplingBuilder { params, anchor: None, @@ -280,8 +259,8 @@ impl SaplingBuilder

{ ) -> Result<(), Error> { let output = SaplingOutput::new_internal( &self.params, - self.target_height, &mut rng, + self.target_height, ovk, to, value, @@ -311,14 +290,7 @@ impl SaplingBuilder

{ mut rng: R, target_height: BlockHeight, progress_notifier: Option<&Sender>, - ) -> Result< - ( - Vec, - Vec, - SaplingMetadata, - ), - Error, - > { + ) -> Result<(Option>, SaplingMetadata), Error> { // Record initial positions of spends and outputs let mut indexed_spends: Vec<_> = self.spends.iter().enumerate().collect(); let mut indexed_outputs: Vec<_> = self @@ -350,7 +322,7 @@ impl SaplingBuilder

{ let mut progress = 0u32; // Create Sapling SpendDescriptions - let spend_descs = if !indexed_spends.is_empty() { + let shielded_spends: Vec> = if !indexed_spends.is_empty() { let anchor = self .anchor .expect("Sapling anchor must be set if Sapling spends are present."); @@ -397,7 +369,7 @@ impl SaplingBuilder

{ nullifier, rk, zkproof, - spend_auth_sig: None, + spend_auth_sig: (), }) }) .collect::, Error>>()? @@ -406,7 +378,7 @@ impl SaplingBuilder

{ }; // Create Sapling OutputDescriptions - let output_descs = indexed_outputs + let shielded_outputs: Vec> = indexed_outputs .into_iter() .enumerate() .map(|(i, output)| { @@ -414,7 +386,7 @@ impl SaplingBuilder

{ // Record the post-randomized output location tx_metadata.output_indices[pos] = i; - output.clone().build_internal(prover, ctx, &mut rng) + output.clone().build::(prover, ctx, &mut rng) } else { // This is a dummy output let (dummy_to, dummy_note) = { @@ -491,7 +463,18 @@ impl SaplingBuilder

{ }) .collect(); - Ok((spend_descs, output_descs, tx_metadata)) + let bundle = if shielded_spends.is_empty() && shielded_outputs.is_empty() { + None + } else { + Some(Bundle { + shielded_spends, + shielded_outputs, + value_balance: self.value_balance, + authorization: Unauthorized, + }) + }; + + Ok((bundle, tx_metadata)) } pub fn create_signatures( @@ -523,10 +506,91 @@ impl SaplingBuilder

{ .unwrap_or_default(); let binding_sig = prover - .binding_sig(ctx, self.value_balance, &sighash_bytes) + .binding_sig(ctx, self.value_balance, sighash_bytes) .map_err(|_| Error::BindingSig)?; Ok(Some((spend_sigs, binding_sig))) } } } + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::collection::vec; + use proptest::prelude::*; + use rand::{rngs::StdRng, SeedableRng}; + + use crate::{ + consensus::{ + testing::{arb_branch_id, arb_height}, + TEST_NETWORK, + }, + merkle_tree::{testing::arb_commitment_tree, IncrementalWitness}, + sapling::{ + prover::{mock::MockTxProver, TxProver}, + testing::{arb_node, arb_note, arb_positive_note_value}, + Diversifier, + }, + transaction::components::{ + amount::MAX_MONEY, + sapling::{Authorized, Bundle}, + }, + zip32::testing::arb_extended_spending_key, + }; + + use super::SaplingBuilder; + + prop_compose! { + fn arb_bundle()(n_notes in 1..30usize)( + extsk in arb_extended_spending_key(), + spendable_notes in vec( + arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_flat_map(arb_note), + n_notes + ), + commitment_trees in vec( + arb_commitment_tree(n_notes, arb_node()).prop_map( + |t| IncrementalWitness::from_tree(&t).path().unwrap() + ), + n_notes + ), + diversifiers in vec(prop::array::uniform11(any::()).prop_map(Diversifier), n_notes), + target_height in arb_branch_id().prop_flat_map(|b| arb_height(b, &TEST_NETWORK)), + rng_seed in prop::array::uniform32(any::()), + fake_sighash_bytes in prop::array::uniform32(any::()), + ) -> Bundle { + let mut builder = SaplingBuilder::new(TEST_NETWORK, target_height.unwrap()); + let mut rng = StdRng::from_seed(rng_seed); + + for ((note, path), diversifier) in spendable_notes.into_iter().zip(commitment_trees.into_iter()).zip(diversifiers.into_iter()) { + builder.add_spend( + &mut rng, + extsk.clone(), + diversifier, + note, + path + ).unwrap(); + } + + let prover = MockTxProver; + let mut ctx = prover.new_sapling_proving_context(); + + let (bundle, meta) = builder.build( + &prover, + &mut ctx, + &mut rng, + target_height.unwrap(), + None + ).unwrap(); + + let (spend_auth_sigs, binding_sig) = builder.create_signatures( + &prover, + &mut ctx, + &mut rng, + &fake_sighash_bytes, + &meta + ).unwrap().unwrap(); + + bundle.unwrap().apply_signatures(spend_auth_sigs, binding_sig) + } + } +} diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index c5a661352..a8c3676bb 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -9,12 +9,14 @@ use orchard; use crate::{ consensus::{BlockHeight, BranchId}, - sapling::redjubjub::Signature, + sapling::redjubjub, serialize::Vector, }; use self::{ - components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut}, + components::{ + sapling, Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut, + }, sighash::{signature_hash_data, SignableInput, SIGHASH_ALL}, util::sha256d::{HashReader, HashWriter}, }; @@ -167,7 +169,7 @@ impl TxVersion { } } - pub fn uses_groth_proofs(&self) -> bool { + pub fn has_sapling(&self) -> bool { match self { TxVersion::Sprout(_) | TxVersion::Overwinter => false, TxVersion::Sapling => true, @@ -192,18 +194,21 @@ impl TxVersion { /// Authorization state for a bundle of transaction data. pub trait Authorization { + type SaplingAuth: sapling::Authorization; type OrchardAuth: orchard::bundle::Authorization; } pub struct Authorized; impl Authorization for Authorized { + type SaplingAuth = sapling::Authorized; type OrchardAuth = orchard::bundle::Authorized; } pub struct Unauthorized; impl Authorization for Unauthorized { + type SaplingAuth = sapling::Unauthorized; type OrchardAuth = orchard::builder::Unauthorized; } @@ -238,13 +243,10 @@ pub struct TransactionData { pub tze_outputs: Vec, pub lock_time: u32, pub expiry_height: BlockHeight, - pub value_balance: Amount, - pub shielded_spends: Vec, - pub shielded_outputs: Vec, pub joinsplits: Vec, pub joinsplit_pubkey: Option<[u8; 32]>, pub joinsplit_sig: Option<[u8; 64]>, - pub binding_sig: Option, + pub sapling_bundle: Option>, pub orchard_bundle: Option>, } @@ -282,19 +284,33 @@ impl std::fmt::Debug for TransactionData { }, self.lock_time, self.expiry_height, - self.value_balance, - self.shielded_spends, - self.shielded_outputs, + self.sapling_bundle + .as_ref() + .map_or(Amount::zero(), |b| b.value_balance), + self.sapling_bundle + .as_ref() + .map_or(&vec![], |b| &b.shielded_spends), + self.sapling_bundle + .as_ref() + .map_or(&vec![], |b| &b.shielded_outputs), self.joinsplits, self.joinsplit_pubkey, - self.binding_sig + self.sapling_bundle.as_ref().map(|b| &b.authorization) ) } } +impl TransactionData { + pub fn sapling_value_balance(&self) -> Amount { + self.sapling_bundle + .as_ref() + .map_or(Amount::zero(), |b| b.value_balance) + } +} + impl Default for TransactionData { fn default() -> Self { - TransactionData::new() + Self::new() } } @@ -310,13 +326,10 @@ impl TransactionData { tze_outputs: vec![], lock_time: 0, expiry_height: 0u32.into(), - value_balance: Amount::zero(), - shielded_spends: vec![], - shielded_outputs: vec![], joinsplits: vec![], joinsplit_pubkey: None, joinsplit_sig: None, - binding_sig: None, + sapling_bundle: None, orchard_bundle: None, } } @@ -331,13 +344,10 @@ impl TransactionData { tze_outputs: vec![], lock_time: 0, expiry_height: 0u32.into(), - value_balance: Amount::zero(), - shielded_spends: vec![], - shielded_outputs: vec![], joinsplits: vec![], joinsplit_pubkey: None, joinsplit_sig: None, - binding_sig: None, + sapling_bundle: None, orchard_bundle: None, } } @@ -365,6 +375,13 @@ impl Transaction { self.txid } + fn read_amount(mut reader: R) -> io::Result { + let mut tmp = [0; 8]; + reader.read_exact(&mut tmp)?; + Amount::from_i64_le_bytes(tmp) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range")) + } + pub fn read(reader: R) -> io::Result { let mut reader = HashReader::new(reader); @@ -396,15 +413,14 @@ impl Transaction { 0u32.into() }; - let (value_balance, shielded_spends, shielded_outputs) = if is_sapling_v4 || has_tze { - let vb = { - let mut tmp = [0; 8]; - reader.read_exact(&mut tmp)?; - Amount::from_i64_le_bytes(tmp) - } - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range"))?; - let ss = Vector::read(&mut reader, SpendDescription::read)?; - let so = Vector::read(&mut reader, OutputDescription::read)?; + let (value_balance, shielded_spends, shielded_outputs) = if version.has_sapling() { + let vb = Self::read_amount(&mut reader)?; + #[allow(clippy::redundant_closure)] + let ss: Vec> = + Vector::read(&mut reader, |r| SpendDescription::read(r))?; + #[allow(clippy::redundant_closure)] + let so: Vec> = + Vector::read(&mut reader, |r| OutputDescription::read(r))?; (vb, ss, so) } else { (Amount::zero(), vec![], vec![]) @@ -412,7 +428,7 @@ impl Transaction { let (joinsplits, joinsplit_pubkey, joinsplit_sig) = if version.has_sprout() { let jss = Vector::read(&mut reader, |r| { - JsDescription::read(r, version.uses_groth_proofs()) + JsDescription::read(r, version.has_sapling()) })?; let (pubkey, sig) = if !jss.is_empty() { let mut joinsplit_pubkey = [0; 32]; @@ -431,7 +447,7 @@ impl Transaction { let binding_sig = if (is_sapling_v4 || has_tze) && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { - Some(Signature::read(&mut reader)?) + Some(redjubjub::Signature::read(&mut reader)?) } else { None }; @@ -451,13 +467,15 @@ impl Transaction { tze_outputs, lock_time, expiry_height, - value_balance, - shielded_spends, - shielded_outputs, joinsplits, joinsplit_pubkey, joinsplit_sig, - binding_sig, + sapling_bundle: binding_sig.map(|binding_sig| sapling::Bundle { + value_balance, + shielded_spends, + shielded_outputs, + authorization: sapling::Authorized { binding_sig }, + }), orchard_bundle: None, }, }) @@ -486,10 +504,28 @@ impl Transaction { writer.write_u32::(u32::from(self.expiry_height))?; } - if is_sapling_v4 || has_tze { - writer.write_all(&self.value_balance.to_i64_le_bytes())?; - Vector::write(&mut writer, &self.shielded_spends, |w, e| e.write(w))?; - Vector::write(&mut writer, &self.shielded_outputs, |w, e| e.write(w))?; + if self.version.has_sapling() { + writer.write_all( + &self + .sapling_bundle + .as_ref() + .map_or(Amount::zero(), |b| b.value_balance) + .to_i64_le_bytes(), + )?; + Vector::write( + &mut writer, + self.sapling_bundle + .as_ref() + .map_or(&[], |b| &b.shielded_spends), + |w, e| e.write(w), + )?; + Vector::write( + &mut writer, + self.sapling_bundle + .as_ref() + .map_or(&[], |b| &b.shielded_outputs), + |w, e| e.write(w), + )?; } if self.version.has_sprout() { @@ -531,19 +567,16 @@ impl Transaction { } } - if (is_sapling_v4 || has_tze) - && !(self.shielded_spends.is_empty() && self.shielded_outputs.is_empty()) - { - match self.binding_sig { - Some(sig) => sig.write(&mut writer)?, - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Missing binding signature", - )); - } + if self.version.has_sapling() { + if let Some(bundle) = self.sapling_bundle.as_ref() { + bundle.authorization.binding_sig.write(&mut writer)?; + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Missing binding signature", + )); } - } else if self.binding_sig.is_some() { + } else { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Binding signature should not be present", @@ -686,7 +719,6 @@ pub mod testing { tze_outputs in vec(arb_tzeout(), 0..10), lock_time in any::(), expiry_height in any::(), - value_balance in arb_amount(), ) -> TransactionData { TransactionData { version, @@ -695,16 +727,10 @@ pub mod testing { tze_outputs: if branch_id == BranchId::ZFuture { tze_outputs } else { vec![] }, lock_time, expiry_height: expiry_height.into(), - value_balance: match version { - TxVersion::Sprout(_) | TxVersion::Overwinter => Amount::zero(), - _ => value_balance, - }, - shielded_spends: vec![], //FIXME - shielded_outputs: vec![], //FIXME joinsplits: vec![], //FIXME joinsplit_pubkey: None, //FIXME joinsplit_sig: None, //FIXME - binding_sig: None, //FIXME + sapling_bundle: None, //FIXME orchard_bundle: None, //FIXME } } @@ -718,23 +744,16 @@ pub mod testing { vout in vec(arb_txout(), 0..10), lock_time in any::(), expiry_height in any::(), - value_balance in arb_amount(), ) -> TransactionData { TransactionData { version, vin, vout, lock_time, expiry_height: expiry_height.into(), - value_balance: match version { - TxVersion::Sprout(_) | TxVersion::Overwinter => Amount::zero(), - _ => value_balance, - }, - shielded_spends: vec![], //FIXME - shielded_outputs: vec![], //FIXME joinsplits: vec![], //FIXME joinsplit_pubkey: None, //FIXME joinsplit_sig: None, //FIXME - binding_sig: None, //FIXME + sapling_bundle: None, //FIXME orchard_bundle: None, //FIXME } } diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs index 9be354081..3bfd2ba5f 100644 --- a/zcash_primitives/src/transaction/sighash.rs +++ b/zcash_primitives/src/transaction/sighash.rs @@ -6,7 +6,10 @@ use byteorder::{LittleEndian, WriteBytesExt}; use ff::PrimeField; use group::GroupEncoding; -use crate::{consensus, legacy::Script}; +use crate::{ + consensus::{self, BranchId}, + legacy::Script, +}; #[cfg(feature = "zfuture")] use crate::{ @@ -15,7 +18,10 @@ use crate::{ }; use super::{ - components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut}, + components::{ + sapling::{self, GrothProofBytes}, + Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut, + }, Authorization, Transaction, TransactionData, TxVersion, }; @@ -67,10 +73,6 @@ fn has_overwinter_components(version: &TxVersion) -> bool { !matches!(version, TxVersion::Sprout(_)) } -fn has_sapling_components(version: &TxVersion) -> bool { - !matches!(version, TxVersion::Sprout(_) | TxVersion::Overwinter) -} - #[cfg(feature = "zfuture")] fn has_tze_components(version: &TxVersion) -> bool { matches!(version, TxVersion::ZFuture) @@ -121,13 +123,13 @@ fn single_output_hash(tx_out: &TxOut) -> Blake2bHash { } fn joinsplits_hash( - txversion: TxVersion, + consensus_branch_id: BranchId, joinsplits: &[JsDescription], joinsplit_pubkey: &[u8; 32], ) -> Blake2bHash { let mut data = Vec::with_capacity( joinsplits.len() - * if txversion.uses_groth_proofs() { + * if consensus_branch_id.sprout_uses_groth_proofs() { 1698 // JSDescription with Groth16 proof } else { 1802 // JSDescription with PHGR13 proof @@ -143,7 +145,9 @@ fn joinsplits_hash( .hash(&data) } -fn shielded_spends_hash(shielded_spends: &[SpendDescription]) -> Blake2bHash { +fn shielded_spends_hash>( + shielded_spends: &[SpendDescription], +) -> Blake2bHash { let mut data = Vec::with_capacity(shielded_spends.len() * 384); for s_spend in shielded_spends { data.extend_from_slice(&s_spend.cv.to_bytes()); @@ -158,7 +162,9 @@ fn shielded_spends_hash(shielded_spends: &[SpendDescription]) -> Blake2bHash { .hash(&data) } -fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash { +fn shielded_outputs_hash>( + shielded_outputs: &[OutputDescription], +) -> Blake2bHash { let mut data = Vec::with_capacity(shielded_outputs.len() * 948); for s_out in shielded_outputs { s_out.write(&mut data).unwrap(); @@ -227,7 +233,10 @@ impl<'a> SignableInput<'a> { } } -pub fn signature_hash_data( +pub fn signature_hash_data< + SA: sapling::Authorization, + A: Authorization, +>( tx: &TransactionData, consensus_branch_id: consensus::BranchId, hash_type: u32, @@ -291,24 +300,44 @@ pub fn signature_hash_data( update_hash!( h, !tx.joinsplits.is_empty(), - joinsplits_hash(tx.version, &tx.joinsplits, &tx.joinsplit_pubkey.unwrap()) + joinsplits_hash( + consensus_branch_id, + &tx.joinsplits, + &tx.joinsplit_pubkey.unwrap() + ) ); - if has_sapling_components(&tx.version) { + if tx.version.has_sapling() { update_hash!( h, - !tx.shielded_spends.is_empty(), - shielded_spends_hash(&tx.shielded_spends) + !tx.sapling_bundle + .as_ref() + .map_or(true, |b| b.shielded_spends.is_empty()), + shielded_spends_hash( + tx.sapling_bundle + .as_ref() + .unwrap() + .shielded_spends + .as_slice() + ) ); update_hash!( h, - !tx.shielded_outputs.is_empty(), - shielded_outputs_hash(&tx.shielded_outputs) + !tx.sapling_bundle + .as_ref() + .map_or(true, |b| b.shielded_outputs.is_empty()), + shielded_outputs_hash( + tx.sapling_bundle + .as_ref() + .unwrap() + .shielded_outputs + .as_slice() + ) ); } update_u32!(h, tx.lock_time, tmp); update_u32!(h, tx.expiry_height.into(), tmp); - if has_sapling_components(&tx.version) { - h.update(&tx.value_balance.to_i64_le_bytes()); + if tx.version.has_sapling() { + h.update(&tx.sapling_value_balance().to_i64_le_bytes()); } update_u32!(h, hash_type, tmp); diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index 06621ece1..f55363333 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -1,41 +1,13 @@ -use ff::Field; -use rand_core::OsRng; -use std::io; - use proptest::prelude::*; -use crate::{ - consensus::{BranchId, TestNetwork}, - constants::SPENDING_KEY_GENERATOR, - sapling::redjubjub::PrivateKey, -}; - use super::{ - builder::Builder, components::Amount, sighash::{signature_hash, SignableInput}, - Transaction, TransactionData, Unauthorized, + Transaction, }; use super::testing::{arb_branch_id, arb_tx}; -fn auth_and_freeze( - txdata: TransactionData, - branch_id: BranchId, -) -> io::Result { - Builder::::apply_authorization( - txdata, - #[cfg(feature = "transparent-inputs")] - None, - None, - #[cfg(any(feature = "nu5", feature = "zfuture"))] - None, - #[cfg(feature = "zfuture")] - None, - ) - .freeze(branch_id) -} - #[test] fn tx_read_write() { let data = &self::data::tx_read_write::TX_READ_WRITE; @@ -50,49 +22,6 @@ fn tx_read_write() { assert_eq!(&data[..], &encoded[..]); } -#[test] -fn tx_write_rejects_unexpected_joinsplit_pubkey() { - // Succeeds without a JoinSplit pubkey - assert!(auth_and_freeze(TransactionData::new(), BranchId::Canopy).is_ok()); - - // Fails with an unexpected JoinSplit pubkey - { - let mut tx = TransactionData::new(); - tx.joinsplit_pubkey = Some([0; 32]); - assert!(auth_and_freeze(tx, BranchId::Canopy).is_err()); - } -} - -#[test] -fn tx_write_rejects_unexpected_joinsplit_sig() { - // Succeeds without a JoinSplit signature - assert!(auth_and_freeze(TransactionData::new(), BranchId::Canopy).is_ok()); - - // Fails with an unexpected JoinSplit signature - { - let mut tx = TransactionData::new(); - tx.joinsplit_sig = Some([0; 64]); - assert!(auth_and_freeze(tx, BranchId::Canopy).is_err()); - } -} - -#[test] -fn tx_write_rejects_unexpected_binding_sig() { - // Succeeds without a binding signature - assert!(auth_and_freeze(TransactionData::new(), BranchId::Canopy).is_ok()); - - // Fails with an unexpected binding signature - { - let mut rng = OsRng; - let sk = PrivateKey(jubjub::Fr::random(&mut rng)); - let sig = sk.sign(b"Foo bar", &mut rng, SPENDING_KEY_GENERATOR); - - let mut tx = TransactionData::new(); - tx.binding_sig = Some(sig); - assert!(auth_and_freeze(tx, BranchId::Canopy).is_err()); - } -} - proptest! { #[test] fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) { @@ -109,7 +38,7 @@ proptest! { #[cfg(feature = "zfuture")] assert_eq!(tx.tze_outputs, txo.tze_outputs); assert_eq!(tx.lock_time, txo.lock_time); - assert_eq!(tx.value_balance, txo.value_balance); + assert_eq!(tx.sapling_value_balance(), txo.sapling_value_balance()); } } @@ -156,7 +85,7 @@ fn zip_0143() { }; assert_eq!( - signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input), + signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input).as_ref(), tv.sighash ); } @@ -176,7 +105,7 @@ fn zip_0243() { }; assert_eq!( - signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input), + signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input).as_ref(), tv.sighash ); }