diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 745bfee1c..3bdd074b3 100644 --- a/zcash_primitives/src/transaction/components/sapling.rs +++ b/zcash_primitives/src/transaction/components/sapling.rs @@ -206,7 +206,7 @@ impl SpendDescription { }) } - pub fn write(&self, mut writer: W) -> io::Result<()> { + pub fn write_v4(&self, mut writer: W) -> io::Result<()> { writer.write_all(&self.cv.to_bytes())?; writer.write_all(self.anchor.to_repr().as_ref())?; writer.write_all(&self.nullifier.0)?; @@ -214,6 +214,45 @@ impl SpendDescription { writer.write_all(&self.zkproof)?; self.spend_auth_sig.write(&mut writer) } + + pub fn write_v5_without_witness_data(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.cv.to_bytes())?; + writer.write_all(&self.nullifier.0)?; + self.rk.write(&mut writer) + } +} + +#[derive(Clone)] +pub struct SpendDescriptionV5 { + pub cv: jubjub::ExtendedPoint, + pub nullifier: Nullifier, + pub rk: PublicKey, +} + +impl SpendDescriptionV5 { + pub fn read(mut reader: &mut R) -> io::Result { + let cv = read_point(&mut reader, "cv")?; + let nullifier = SpendDescription::read_nullifier(&mut reader)?; + let rk = SpendDescription::read_rk(&mut reader)?; + + Ok(SpendDescriptionV5 { cv, nullifier, rk }) + } + + pub fn into_spend_description( + self, + anchor: bls12_381::Scalar, + zkproof: GrothProofBytes, + spend_auth_sig: Signature, + ) -> SpendDescription { + SpendDescription { + cv: self.cv, + anchor, + nullifier: self.nullifier, + rk: self.rk, + zkproof, + spend_auth_sig, + } + } } #[derive(Clone)] @@ -253,56 +292,27 @@ impl std::fmt::Debug for OutputDescription { } impl OutputDescription { - pub fn read(reader: &mut R) -> io::Result { + pub fn read(mut reader: &mut R) -> io::Result { // Consensus rules (§4.5): // - Canonical encoding is enforced here. // - "Not small order" is enforced in SaplingVerificationContext::check_output() // (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() - }; + let cv = read_point(&mut reader, "cv")?; // Consensus rule (§7.4): Canonical encoding is enforced here - let cmu = { - 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, "cmu not in field"))? - }; + let cmu = read_base(&mut reader, "cmu")?; // Consensus rules (§4.5): // - Canonical encoding is enforced here. // - "Not small order" is enforced in SaplingVerificationContext::check_output() - let ephemeral_key = { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - let ephemeral_key = jubjub::ExtendedPoint::from_bytes(&bytes); - if ephemeral_key.is_none().into() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "invalid ephemeral_key", - )); - } - ephemeral_key.unwrap() - }; + let ephemeral_key = read_point(&mut reader, "ephemeral key")?; let mut enc_ciphertext = [0u8; 580]; let mut out_ciphertext = [0u8; 80]; reader.read_exact(&mut enc_ciphertext)?; reader.read_exact(&mut out_ciphertext)?; - // Consensus rules (§4.5): - // - Canonical encoding is enforced by the API of SaplingVerificationContext::check_output() - // due to the need to parse this into a bellman::groth16::Proof. - // - Proof validity is enforced in SaplingVerificationContext::check_output() - let mut zkproof = [0u8; GROTH_PROOF_SIZE]; - reader.read_exact(&mut zkproof)?; + let zkproof = read_zkproof(&mut reader)?; Ok(OutputDescription { cv, @@ -316,7 +326,7 @@ impl OutputDescription { } impl> OutputDescription { - pub fn write(&self, mut writer: W) -> io::Result<()> { + pub fn write_v4(&self, mut writer: W) -> io::Result<()> { writer.write_all(&self.cv.to_bytes())?; writer.write_all(self.cmu.to_repr().as_ref())?; writer.write_all(&self.ephemeral_key.to_bytes())?; @@ -324,6 +334,58 @@ impl> OutputDescription { writer.write_all(&self.out_ciphertext)?; writer.write_all(&self.zkproof) } + + pub fn write_v5_without_proof(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.cv.to_bytes())?; + writer.write_all(self.cmu.to_repr().as_ref())?; + writer.write_all(&self.ephemeral_key.to_bytes())?; + writer.write_all(&self.enc_ciphertext)?; + writer.write_all(&self.out_ciphertext) + } +} + +#[derive(Clone)] +pub struct OutputDescriptionV5 { + pub cv: jubjub::ExtendedPoint, + pub cmu: bls12_381::Scalar, + pub ephemeral_key: jubjub::ExtendedPoint, + pub enc_ciphertext: [u8; 580], + pub out_ciphertext: [u8; 80], +} + +impl OutputDescriptionV5 { + pub fn read(mut reader: &mut R) -> io::Result { + let cv = read_point(&mut reader, "cv")?; + let cmu = read_base(&mut reader, "cmu")?; + let ephemeral_key = read_point(&mut reader, "ephemeral key")?; + + let mut enc_ciphertext = [0u8; 580]; + let mut out_ciphertext = [0u8; 80]; + reader.read_exact(&mut enc_ciphertext)?; + reader.read_exact(&mut out_ciphertext)?; + + Ok(OutputDescriptionV5 { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + }) + } + + pub fn into_output_description( + self, + zkproof: GrothProofBytes, + ) -> OutputDescription { + OutputDescription { + cv: self.cv, + cmu: self.cmu, + ephemeral_key: self.ephemeral_key, + enc_ciphertext: self.enc_ciphertext, + out_ciphertext: self.out_ciphertext, + zkproof, + } + } } impl OutputDescription { @@ -368,3 +430,115 @@ impl ShieldedOutput> for CompactOutpu &self.enc_ciphertext } } + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use ff::Field; + use group::GroupEncoding; + use proptest::collection::vec; + use proptest::prelude::*; + use rand::{rngs::StdRng, SeedableRng}; + use std::convert::TryFrom; + + use crate::{ + constants::{SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR}, + sapling::{ + redjubjub::{PrivateKey, PublicKey}, + Nullifier, + }, + transaction::components::{amount::testing::arb_amount, GROTH_PROOF_SIZE}, + }; + + use super::{Authorized, Bundle, OutputDescription, SpendDescription}; + + prop_compose! { + /// produce a spend description with invalid data (useful only for serialization + /// roundtrip testing). + fn arb_spend_description()( + cv in prop::array::uniform32(any::()) + .prop_map(|v| jubjub::ExtendedPoint::from_bytes(&v)) + .prop_filter("Must generate valid extended points.", |v| v.is_some().unwrap_u8() == 1) + .prop_map(|v| v.unwrap()), + anchor in vec(any::(), 64) + .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap()) + .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)), + nullifier in prop::array::uniform32(any::()) + .prop_map(|v| Nullifier::from_slice(&v).unwrap()), + zkproof in vec(any::(), GROTH_PROOF_SIZE) + .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()), + rng_seed in prop::array::uniform32(prop::num::u8::ANY), + fake_sighash_bytes in prop::array::uniform32(prop::num::u8::ANY), + ) -> SpendDescription { + let mut rng = StdRng::from_seed(rng_seed); + let sk1 = PrivateKey(jubjub::Fr::random(&mut rng)); + let rk = PublicKey::from_private(&sk1, SPENDING_KEY_GENERATOR); + SpendDescription { + cv, + anchor, + nullifier, + rk, + zkproof, + spend_auth_sig: sk1.sign(&fake_sighash_bytes, &mut rng, SPENDING_KEY_GENERATOR), + } + } + } + + prop_compose! { + /// produce an output description with invalid data (useful only for serialization + /// roundtrip testing). + pub fn arb_output_description()( + cv in prop::array::uniform32(any::()) + .prop_map(|v| jubjub::ExtendedPoint::from_bytes(&v)) + .prop_filter("Must generate valid extended points.", |v| v.is_some().unwrap_u8() == 1) + .prop_map(|v| v.unwrap()), + cmu in vec(any::(), 64) + .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap()) + .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)), + enc_ciphertext in vec(any::(), 580) + .prop_map(|v| <[u8;580]>::try_from(v.as_slice()).unwrap()), + ephemeral_key in prop::array::uniform32(any::()) + .prop_map(|v| jubjub::ExtendedPoint::from_bytes(&v)) + .prop_filter("Must generate valid extended points.", |v| v.is_some().unwrap_u8() == 1) + .prop_map(|v| v.unwrap()), + out_ciphertext in vec(any::(), 80) + .prop_map(|v| <[u8;80]>::try_from(v.as_slice()).unwrap()), + zkproof in vec(any::(), GROTH_PROOF_SIZE) + .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()), + ) -> OutputDescription { + OutputDescription { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof, + } + } + } + + prop_compose! { + pub fn arb_bundle()( + shielded_spends in vec(arb_spend_description(), 0..30), + shielded_outputs in vec(arb_output_description(), 0..30), + value_balance in arb_amount(), + rng_seed in prop::array::uniform32(prop::num::u8::ANY), + fake_bvk_bytes in prop::array::uniform32(prop::num::u8::ANY), + ) -> Option> { + let mut rng = StdRng::from_seed(rng_seed); + let bsk = PrivateKey(jubjub::Fr::random(&mut rng)); + + if shielded_spends.is_empty() && shielded_outputs.is_empty() { + None + } else { + Some( + Bundle { + shielded_spends, + shielded_outputs, + value_balance, + authorization: Authorized { binding_sig: bsk.sign(&fake_bvk_bytes, &mut rng, VALUE_COMMITMENT_RANDOMNESS_GENERATOR) }, + } + ) + } + } + } +} diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index ed1285bdd..6b1fe0360 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -608,14 +608,14 @@ impl Transaction { self.sapling_bundle .as_ref() .map_or(&[], |b| &b.shielded_spends), - |w, e| e.write(w), + |w, e| e.write_v4(w), )?; Vector::write( &mut writer, self.sapling_bundle .as_ref() .map_or(&[], |b| &b.shielded_outputs), - |w, e| e.write(w), + |w, e| e.write_v4(w), )?; } else if self.sapling_bundle.is_some() { return Err(io::Error::new( diff --git a/zcash_primitives/src/transaction/sighash_v4.rs b/zcash_primitives/src/transaction/sighash_v4.rs index b106254eb..5b5cd44f1 100644 --- a/zcash_primitives/src/transaction/sighash_v4.rs +++ b/zcash_primitives/src/transaction/sighash_v4.rs @@ -129,7 +129,7 @@ fn shielded_outputs_hash>( ) -> Blake2bHash { let mut data = Vec::with_capacity(shielded_outputs.len() * 948); for s_out in shielded_outputs { - s_out.write(&mut data).unwrap(); + s_out.write_v4(&mut data).unwrap(); } Blake2bParams::new() .hash_length(32)