From 2ae55b414558878e6de8222f1c738dc7f0a60006 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 12 May 2021 14:12:39 -0600 Subject: [PATCH 01/26] Add more flexibility to vector serialization. --- zcash_primitives/src/serialize.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/zcash_primitives/src/serialize.rs b/zcash_primitives/src/serialize.rs index e09fc44d12..3c9f34b165 100644 --- a/zcash_primitives/src/serialize.rs +++ b/zcash_primitives/src/serialize.rs @@ -70,6 +70,13 @@ impl Vector { F: Fn(&mut R) -> io::Result, { let count = CompactSize::read(&mut reader)?; + Self::read_count(reader, count, func) + } + + pub fn read_count(mut reader: R, count: usize, func: F) -> io::Result> + where + F: Fn(&mut R) -> io::Result, + { (0..count).map(|_| func(&mut reader)).collect() } @@ -80,6 +87,17 @@ impl Vector { CompactSize::write(&mut writer, vec.len())?; vec.iter().try_for_each(|e| func(&mut writer, e)) } + + pub fn write_items, F>( + mut writer: W, + vec: I, + func: F, + ) -> io::Result<()> + where + F: Fn(&mut W, &E) -> io::Result<()>, + { + vec.into_iter().try_for_each(|e| func(&mut writer, &e)) + } } pub struct Optional; From 4bcad97ba11f67efd01455c6a8e9ad5e6b44f2f0 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 12 May 2021 14:20:04 -0600 Subject: [PATCH 02/26] Add amount conversion for Orchard values. --- zcash_primitives/src/transaction/components/amount.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/zcash_primitives/src/transaction/components/amount.rs b/zcash_primitives/src/transaction/components/amount.rs index 1da978d76e..3ea1c9bab8 100644 --- a/zcash_primitives/src/transaction/components/amount.rs +++ b/zcash_primitives/src/transaction/components/amount.rs @@ -2,6 +2,8 @@ use std::convert::TryFrom; use std::iter::Sum; use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; +use orchard::value as orchard; + pub const COIN: i64 = 1_0000_0000; pub const MAX_MONEY: i64 = 21_000_000 * COIN; @@ -180,6 +182,14 @@ impl Neg for Amount { } } +impl TryFrom for Amount { + type Error = (); + + fn try_from(v: orchard::ValueSum) -> Result { + i64::try_from(v).map_err(|_| ()).and_then(Amount::try_from) + } +} + #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::prelude::prop_compose; From fd1790fec21f20132d8bb8c5ea3cf3d4d2728005 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 12 May 2021 15:29:54 -0600 Subject: [PATCH 03/26] Move sighash.rs -> sighash_v4.rs --- .../src/transaction/components/transparent/builder.rs | 2 +- zcash_primitives/src/transaction/mod.rs | 4 ++-- .../src/transaction/{sighash.rs => sighash_v4.rs} | 0 zcash_primitives/src/transaction/tests.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename zcash_primitives/src/transaction/{sighash.rs => sighash_v4.rs} (100%) diff --git a/zcash_primitives/src/transaction/components/transparent/builder.rs b/zcash_primitives/src/transaction/components/transparent/builder.rs index ffbb177df3..2e34c88626 100644 --- a/zcash_primitives/src/transaction/components/transparent/builder.rs +++ b/zcash_primitives/src/transaction/components/transparent/builder.rs @@ -15,7 +15,7 @@ use crate::{ consensus::{self}, legacy::Script, transaction::{ - components::OutPoint, sighash::signature_hash_data, SignableInput, TransactionData, + components::OutPoint, sighash_v4::signature_hash_data, SignableInput, TransactionData, Unauthorized, SIGHASH_ALL, }, }; diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index 0d505e3807..7ca2c02195 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -18,7 +18,7 @@ use self::{ sprout::{self, JsDescription}, transparent::{self, TxIn, TxOut}, }, - sighash::{signature_hash_data, SignableInput, SIGHASH_ALL}, + sighash_v4::{signature_hash_data, SignableInput, SIGHASH_ALL}, util::sha256d::{HashReader, HashWriter}, }; @@ -27,7 +27,7 @@ use self::components::tze; pub mod builder; pub mod components; -pub mod sighash; +pub mod sighash_v4; pub mod util; #[cfg(test)] diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash_v4.rs similarity index 100% rename from zcash_primitives/src/transaction/sighash.rs rename to zcash_primitives/src/transaction/sighash_v4.rs diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index c870e964b2..b2451b4e7c 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -2,7 +2,7 @@ use proptest::prelude::*; use super::{ components::Amount, - sighash::{signature_hash, SignableInput}, + sighash_v4::{signature_hash, SignableInput}, Transaction, }; From 1138343c892f1b76f801de0f3ee7b1737110ffb7 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 12 May 2021 15:54:21 -0600 Subject: [PATCH 04/26] Add data structures for transaction digests. --- zcash_primitives/src/transaction/mod.rs | 76 +++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index 7ca2c02195..bce3ec4ea1 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -590,6 +590,82 @@ impl Transaction { } } +#[derive(Clone)] +pub struct TransparentDigests { + pub prevout_digest: A, + pub sequence_digest: A, + pub outputs_digest: A, + pub per_input_digest: Option, +} + +#[derive(Clone)] +pub struct TzeDigests { + pub inputs_digest: A, + pub outputs_digest: A, + pub per_input_digest: Option, +} + +#[derive(Clone)] +pub struct TxDigests { + pub header_digest: A, + pub transparent_digests: Option>, + pub sapling_digest: A, + pub orchard_digest: A, + #[cfg(feature = "zfuture")] + pub tze_digests: Option>, +} + +pub(crate) trait TransactionDigest { + type HeaderDigest; + type TransparentDigest; + type SaplingDigest; + type OrchardDigest; + + #[cfg(feature = "zfuture")] + type TzeDigest; + + type Digest; + + fn digest_header( + &self, + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + ) -> Self::HeaderDigest; + + fn digest_transparent( + &self, + transparent_bundle: Option<&transparent::Bundle>, + ) -> Self::TransparentDigest; + + fn digest_sapling( + &self, + sapling_bundle: Option<&sapling::Bundle>, + ) -> Self::SaplingDigest; + + fn digest_orchard( + &self, + orchard_bundle: Option<&orchard::Bundle>, + ) -> Self::OrchardDigest; + + #[cfg(feature = "zfuture")] + fn digest_tze(&self, tze_bundle: Option<&tze::Bundle>) -> Self::TzeDigest; + + fn combine( + &self, + header_digest: Self::HeaderDigest, + transparent_digest: Self::TransparentDigest, + sapling_digest: Self::SaplingDigest, + orchard_digest: Self::OrchardDigest, + #[cfg(feature = "zfuture")] tze_digest: Self::TzeDigest, + ) -> Self::Digest; +} + +pub enum DigestError { + NotSigned, +} + #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::prelude::*; From 55d1090f7080e6e91c495738a090cffcb5834df2 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 12 May 2021 17:01:17 -0600 Subject: [PATCH 05/26] Add v5 txid & signature hashing. --- zcash_client_backend/src/data_api/wallet.rs | 3 +- zcash_client_backend/src/decrypt.rs | 2 +- zcash_client_sqlite/src/lib.rs | 2 +- zcash_client_sqlite/src/wallet.rs | 4 +- zcash_client_sqlite/src/wallet/transact.rs | 12 +- zcash_extensions/src/consensus/transparent.rs | 10 +- zcash_extensions/src/transparent/demo.rs | 93 +-- zcash_primitives/src/transaction/builder.rs | 24 +- .../components/transparent/builder.rs | 35 +- zcash_primitives/src/transaction/mod.rs | 240 ++++--- zcash_primitives/src/transaction/sighash.rs | 136 ++++ .../src/transaction/sighash_v4.rs | 136 +--- .../src/transaction/sighash_v5.rs | 182 ++++++ zcash_primitives/src/transaction/tests.rs | 18 +- zcash_primitives/src/transaction/txid.rs | 585 ++++++++++++++++++ 15 files changed, 1196 insertions(+), 286 deletions(-) create mode 100644 zcash_primitives/src/transaction/sighash.rs create mode 100644 zcash_primitives/src/transaction/sighash_v5.rs create mode 100644 zcash_primitives/src/transaction/txid.rs diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index c8d598875d..ea3c05f13e 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -238,8 +238,7 @@ where }, RecipientAddress::Transparent(addr) => { let script = addr.script(); - tx.transparent_bundle - .as_ref() + tx.transparent_bundle() .and_then(|b| { b.vout .iter() diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index 7374ffd159..7cce1e80ac 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -44,7 +44,7 @@ pub fn decrypt_transaction( ) -> Vec { let mut decrypted = vec![]; - if let Some(bundle) = tx.sapling_bundle.as_ref() { + 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; diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 52e0bc7bd9..fb8fdc7a65 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -489,7 +489,7 @@ 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. - if let Some(bundle) = sent_tx.tx.sapling_bundle.as_ref() { + 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)?; } diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 73e7ffd236..402b917847 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -650,14 +650,14 @@ pub fn put_tx_data<'a, P>( if stmts .stmt_update_tx_data - .execute(params![u32::from(tx.expiry_height), raw_tx, txid,])? + .execute(params![u32::from(tx.expiry_height()), raw_tx, txid,])? == 0 { // It isn't there, so insert our transaction into the database. stmts.stmt_insert_tx_data.execute(params![ txid, created_at, - u32::from(tx.expiry_height), + u32::from(tx.expiry_height()), raw_tx ])?; diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs index b4a7370271..62071d37be 100644 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ b/zcash_client_sqlite/src/wallet/transact.rs @@ -68,9 +68,9 @@ pub fn get_spendable_notes

( "SELECT diversifier, value, rcm, witness FROM received_notes INNER JOIN transactions ON transactions.id_tx = received_notes.tx - INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note - WHERE account = :account - AND spent IS NULL + INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note + WHERE account = :account + AND spent IS NULL AND transactions.block <= :anchor_height AND sapling_witnesses.block = :anchor_height", )?; @@ -153,7 +153,7 @@ mod tests { use zcash_primitives::{ block::BlockHash, - consensus::BlockHeight, + consensus::{BlockHeight, BranchId}, legacy::TransparentAddress, sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver}, transaction::{components::Amount, Transaction}, @@ -617,7 +617,7 @@ mod tests { |row| row.get(0), ) .unwrap(); - let tx = Transaction::read(&raw_tx[..]).unwrap(); + let tx = Transaction::read(&raw_tx[..], BranchId::Canopy).unwrap(); // Fetch the output index from the database let output_index: i64 = db_write @@ -632,7 +632,7 @@ mod tests { .unwrap(); let output = - &tx.sapling_bundle.as_ref().unwrap().shielded_outputs[output_index as usize]; + &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 c9bde5d219..7b975dc1c9 100644 --- a/zcash_extensions/src/consensus/transparent.rs +++ b/zcash_extensions/src/consensus/transparent.rs @@ -78,14 +78,14 @@ pub trait Epoch { /// by the context. impl<'a> demo::Context for Context<'a> { fn is_tze_only(&self) -> bool { - self.tx.transparent_bundle.is_none() - && self.tx.sapling_bundle.is_none() - && self.tx.sprout_bundle.is_none() - && self.tx.orchard_bundle.is_none() + self.tx.transparent_bundle().is_none() + && self.tx.sapling_bundle().is_none() + && self.tx.sprout_bundle().is_none() + && self.tx.orchard_bundle().is_none() } fn tx_tze_outputs(&self) -> &[TzeOut] { - if let Some(bundle) = &self.tx.tze_bundle { + if let Some(bundle) = &self.tx.tze_bundle() { &bundle.vout } else { &[] diff --git a/zcash_extensions/src/transparent/demo.rs b/zcash_extensions/src/transparent/demo.rs index fcd493b6b0..b983423530 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -621,14 +621,14 @@ mod tests { /// by the context. impl<'a> Context for Ctx<'a> { fn is_tze_only(&self) -> bool { - self.tx.transparent_bundle.is_none() - && self.tx.sprout_bundle.is_none() - && self.tx.sapling_bundle.is_none() - && self.tx.orchard_bundle.is_none() + self.tx.transparent_bundle().is_none() + && self.tx.sapling_bundle().is_none() + && self.tx.sprout_bundle().is_none() + && self.tx.orchard_bundle().is_none() } fn tx_tze_outputs(&self) -> &[TzeOut] { - match &self.tx.tze_bundle { + match self.tx.tze_bundle().as_ref() { Some(b) => &b.vout, None => &[], } @@ -683,20 +683,21 @@ mod tests { precondition: tze::Precondition::from(0, &Precondition::open(hash_1)), }; - let tx_a = TransactionData { - version: TxVersion::ZFuture, - lock_time: 0, - expiry_height: 0u32.into(), - transparent_bundle: None, - sprout_bundle: None, - sapling_bundle: None, - orchard_bundle: None, - tze_bundle: Some(Bundle { + let tx_a = TransactionData::from_parts( + TxVersion::ZFuture, + BranchId::ZFuture, + 0, + 0u32.into(), + None, + None, + None, + None, + Some(Bundle { vin: vec![], vout: vec![out_a], }), - } - .freeze(BranchId::ZFuture) + ) + .freeze() .unwrap(); // @@ -712,20 +713,21 @@ mod tests { precondition: tze::Precondition::from(0, &Precondition::close(hash_2)), }; - let tx_b = TransactionData { - version: TxVersion::ZFuture, - lock_time: 0, - expiry_height: 0u32.into(), - transparent_bundle: None, - sprout_bundle: None, - sapling_bundle: None, - orchard_bundle: None, - tze_bundle: Some(Bundle { + let tx_b = TransactionData::from_parts( + TxVersion::ZFuture, + BranchId::ZFuture, + 0, + 0u32.into(), + None, + None, + None, + None, + Some(Bundle { vin: vec![in_b], vout: vec![out_b], }), - } - .freeze(BranchId::ZFuture) + ) + .freeze() .unwrap(); // @@ -737,20 +739,21 @@ mod tests { witness: tze::Witness::from(0, &Witness::close(preimage_2)), }; - let tx_c = TransactionData { - version: TxVersion::ZFuture, - lock_time: 0, - expiry_height: 0u32.into(), - transparent_bundle: None, - sprout_bundle: None, - sapling_bundle: None, - orchard_bundle: None, - tze_bundle: Some(Bundle { + let tx_c = TransactionData::from_parts( + TxVersion::ZFuture, + BranchId::ZFuture, + 0, + 0u32.into(), + None, + None, + None, + None, + Some(Bundle { vin: vec![in_c], vout: vec![], }), - } - .freeze(BranchId::ZFuture) + ) + .freeze() .unwrap(); // Verify tx_b @@ -758,8 +761,8 @@ mod tests { let ctx = Ctx { tx: &tx_b }; assert_eq!( Program.verify( - &tx_a.tze_bundle.as_ref().unwrap().vout[0].precondition, - &tx_b.tze_bundle.as_ref().unwrap().vin[0].witness, + &tx_a.tze_bundle().as_ref().unwrap().vout[0].precondition, + &tx_b.tze_bundle().as_ref().unwrap().vin[0].witness, &ctx ), Ok(()) @@ -771,8 +774,8 @@ mod tests { let ctx = Ctx { tx: &tx_c }; assert_eq!( Program.verify( - &tx_b.tze_bundle.as_ref().unwrap().vout[0].precondition, - &tx_c.tze_bundle.as_ref().unwrap().vin[0].witness, + &tx_b.tze_bundle().as_ref().unwrap().vout[0].precondition, + &tx_c.tze_bundle().as_ref().unwrap().vin[0].witness, &ctx ), Ok(()) @@ -830,7 +833,7 @@ mod tests { .build(&prover) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); - let tze_a = tx_a.tze_bundle.as_ref().unwrap(); + let tze_a = tx_a.tze_bundle().unwrap(); // // Transfer @@ -848,7 +851,7 @@ mod tests { .build(&prover) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); - let tze_b = tx_b.tze_bundle.as_ref().unwrap(); + let tze_b = tx_b.tze_bundle().unwrap(); // // Closing transaction @@ -873,7 +876,7 @@ mod tests { .build(&prover) .map_err(|e| format!("build failure: {:?}", e)) .unwrap(); - let tze_c = tx_c.tze_bundle.as_ref().unwrap(); + let tze_c = tx_c.tze_bundle().unwrap(); // Verify tx_b let ctx0 = Ctx { tx: &tx_b }; diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index b7987d2039..eacdb1fb4e 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -27,8 +27,9 @@ use crate::{ }, transparent::{self, builder::TransparentBuilder}, }, - signature_hash_data, SignableInput, Transaction, TransactionData, TxVersion, Unauthorized, - SIGHASH_ALL, + sighash::{SignableInput, SIGHASH_ALL}, + sighash_v4::v4_signature_hash, + Transaction, TransactionData, TxVersion, Unauthorized, }, zip32::ExtendedSpendingKey, }; @@ -342,6 +343,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { let unauthed_tx = TransactionData { version, + consensus_branch_id, lock_time: 0, expiry_height: self.expiry_height, transparent_bundle, @@ -357,17 +359,12 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { // let mut sighash = [0u8; 32]; - sighash.copy_from_slice(&signature_hash_data( - &unauthed_tx, - consensus_branch_id, - SIGHASH_ALL, - SignableInput::Shielded, - )); + sighash.copy_from_slice( + &v4_signature_hash(&unauthed_tx, SignableInput::Shielded, SIGHASH_ALL).as_ref(), + ); #[cfg(feature = "transparent-inputs")] - let transparent_sigs = self - .transparent_builder - .create_signatures(&unauthed_tx, consensus_branch_id); + let transparent_sigs = self.transparent_builder.create_signatures(&unauthed_tx); let sapling_sigs = self .sapling_builder @@ -382,7 +379,6 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { Ok(( Self::apply_signatures( - consensus_branch_id, unauthed_tx, #[cfg(feature = "transparent-inputs")] transparent_sigs, @@ -397,7 +393,6 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { } fn apply_signatures( - consensus_branch_id: consensus::BranchId, unauthed_tx: TransactionData, #[cfg(feature = "transparent-inputs")] transparent_sigs: Option>, sapling_sigs: Option<(Vec, redjubjub::Signature)>, @@ -448,6 +443,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { let authorized_tx = TransactionData { version: unauthed_tx.version, + consensus_branch_id: unauthed_tx.consensus_branch_id, lock_time: unauthed_tx.lock_time, expiry_height: unauthed_tx.expiry_height, transparent_bundle, @@ -458,7 +454,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { tze_bundle: signed_tze_bundle, }; - authorized_tx.freeze(consensus_branch_id) + authorized_tx.freeze() } } diff --git a/zcash_primitives/src/transaction/components/transparent/builder.rs b/zcash_primitives/src/transaction/components/transparent/builder.rs index 2e34c88626..3fbb3e111b 100644 --- a/zcash_primitives/src/transaction/components/transparent/builder.rs +++ b/zcash_primitives/src/transaction/components/transparent/builder.rs @@ -12,11 +12,12 @@ use crate::{ #[cfg(feature = "transparent-inputs")] use crate::{ - consensus::{self}, legacy::Script, transaction::{ - components::OutPoint, sighash_v4::signature_hash_data, SignableInput, TransactionData, - Unauthorized, SIGHASH_ALL, + components::OutPoint, + sighash::{SignableInput, SIGHASH_ALL}, + sighash_v4::v4_signature_hash, + TransactionData, Unauthorized, }, }; @@ -153,11 +154,7 @@ impl TransparentBuilder { } #[cfg(feature = "transparent-inputs")] - pub fn create_signatures( - self, - mtx: &TransactionData, - consensus_branch_id: consensus::BranchId, - ) -> Option> { + pub fn create_signatures(self, mtx: &TransactionData) -> Option> { if self.inputs.is_empty() && self.vout.is_empty() { None } else { @@ -167,16 +164,18 @@ impl TransparentBuilder { .enumerate() .map(|(i, info)| { let mut sighash = [0u8; 32]; - sighash.copy_from_slice(&signature_hash_data( - mtx, - consensus_branch_id, - SIGHASH_ALL, - SignableInput::transparent( - i, - &info.coin.script_pubkey, - info.coin.value, - ), - )); + sighash.copy_from_slice( + &v4_signature_hash( + mtx, + SignableInput::transparent( + i, + &info.coin.script_pubkey, + info.coin.value, + ), + SIGHASH_ALL, + ) + .as_ref(), + ); let msg = secp256k1::Message::from_slice(sighash.as_ref()).expect("32 bytes"); diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index bce3ec4ea1..ed1285bddc 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -1,4 +1,14 @@ //! Structs and methods for handling Zcash transactions. +pub mod builder; +pub mod components; +pub mod sighash; +pub mod sighash_v4; +pub mod sighash_v5; +pub mod txid; +pub mod util; + +#[cfg(test)] +mod tests; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use std::fmt; @@ -18,21 +28,12 @@ use self::{ sprout::{self, JsDescription}, transparent::{self, TxIn, TxOut}, }, - sighash_v4::{signature_hash_data, SignableInput, SIGHASH_ALL}, util::sha256d::{HashReader, HashWriter}, }; #[cfg(feature = "zfuture")] use self::components::tze; -pub mod builder; -pub mod components; -pub mod sighash_v4; -pub mod util; - -#[cfg(test)] -mod tests; - const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270; const OVERWINTER_TX_VERSION: u32 = 3; const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085; @@ -113,6 +114,7 @@ impl TxVersion { match (version, reader.read_u32::()?) { (OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID) => Ok(TxVersion::Overwinter), (SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID) => Ok(TxVersion::Sapling), + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::ZcashTxV5), #[cfg(feature = "zfuture")] (ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID) => Ok(TxVersion::ZFuture), _ => Err(io::Error::new( @@ -177,6 +179,10 @@ impl TxVersion { } } + pub fn has_overwinter(&self) -> bool { + !matches!(self, TxVersion::Sprout(_)) + } + pub fn has_sapling(&self) -> bool { match self { TxVersion::Sprout(_) | TxVersion::Overwinter => false, @@ -187,6 +193,20 @@ impl TxVersion { } } + pub fn has_orchard(&self) -> bool { + match self { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => false, + TxVersion::ZcashTxV5 => true, + #[cfg(feature = "zfuture")] + TxVersion::ZFuture => true, + } + } + + #[cfg(feature = "zfuture")] + pub fn has_tze(&self) -> bool { + matches!(self, TxVersion::ZFuture) + } + pub fn suggested_for_branch(consensus_branch_id: BranchId) -> Self { match consensus_branch_id { BranchId::Sprout => TxVersion::Sprout(2), @@ -255,15 +275,93 @@ impl PartialEq for Transaction { } pub struct TransactionData { - pub version: TxVersion, - pub lock_time: u32, - pub expiry_height: BlockHeight, - pub transparent_bundle: Option>, - pub sprout_bundle: Option, - pub sapling_bundle: Option>, - pub orchard_bundle: Option>, + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + transparent_bundle: Option>, + sprout_bundle: Option, + sapling_bundle: Option>, + orchard_bundle: Option>, #[cfg(feature = "zfuture")] - pub tze_bundle: Option>, + tze_bundle: Option>, +} + +impl TransactionData { + #[allow(clippy::too_many_arguments)] + pub fn from_parts( + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + transparent_bundle: Option>, + sprout_bundle: Option, + sapling_bundle: Option>, + orchard_bundle: Option>, + #[cfg(feature = "zfuture")] tze_bundle: Option>, + ) -> Self { + TransactionData { + version, + consensus_branch_id, + lock_time, + expiry_height, + transparent_bundle, + sprout_bundle, + sapling_bundle, + orchard_bundle, + #[cfg(feature = "zfuture")] + tze_bundle, + } + } + + pub fn version(&self) -> TxVersion { + self.version + } + + pub fn consensus_branch_id(&self) -> BranchId { + self.consensus_branch_id + } + + pub fn expiry_height(&self) -> BlockHeight { + self.expiry_height + } + + pub fn transparent_bundle(&self) -> Option<&transparent::Bundle> { + self.transparent_bundle.as_ref() + } + + pub fn sprout_bundle(&self) -> Option<&sprout::Bundle> { + self.sprout_bundle.as_ref() + } + + pub fn sapling_bundle(&self) -> Option<&sapling::Bundle> { + self.sapling_bundle.as_ref() + } + + pub fn orchard_bundle(&self) -> Option<&orchard::Bundle> { + self.orchard_bundle.as_ref() + } + + #[cfg(feature = "zfuture")] + pub fn tze_bundle(&self) -> Option<&tze::Bundle> { + self.tze_bundle.as_ref() + } + + pub fn digest>(&self, digester: D) -> D::Digest { + digester.combine( + digester.digest_header( + self.version, + self.consensus_branch_id, + self.lock_time, + self.expiry_height, + ), + digester.digest_transparent(self.transparent_bundle.as_ref()), + digester.digest_sapling(self.sapling_bundle.as_ref()), + digester.digest_orchard(self.orchard_bundle.as_ref()), + #[cfg(feature = "zfuture")] + digester.digest_tze(self.tze_bundle.as_ref()), + ) + } } impl std::fmt::Debug for TransactionData { @@ -272,17 +370,25 @@ impl std::fmt::Debug for TransactionData { f, "TransactionData( version = {:?}, + consensus_branch_id = {:?}, lock_time = {:?}, expiry_height = {:?}, - {}{}{}{}", + transparent_fields = {{{}}} + sprout = {{{}}}, + sapling = {{{}}}, + orchard = {{{}}}, + tze = {{{}}} + )", self.version, + self.consensus_branch_id, self.lock_time, self.expiry_height, if let Some(b) = &self.transparent_bundle { format!( " - vin = {:?}, - vout = {:?},", + vin = {:?}, + vout = {:?}, + ", b.vin, b.vout ) } else { @@ -291,8 +397,9 @@ impl std::fmt::Debug for TransactionData { if let Some(b) = &self.sprout_bundle { format!( " - joinsplits = {:?}, - joinsplit_pubkey = {:?},", + joinsplits = {:?}, + joinsplit_pubkey = {:?}, + ", b.joinsplits, b.joinsplit_pubkey ) } else { @@ -301,22 +408,36 @@ impl std::fmt::Debug for TransactionData { if let Some(b) = &self.sapling_bundle { format!( " - value_balance = {:?}, - shielded_spends = {:?}, - shielded_outputs = {:?}, - binding_sig = {:?},", + value_balance = {:?}, + shielded_spends = {:?}, + shielded_outputs = {:?}, + binding_sig = {:?}, + ", b.value_balance, b.shielded_spends, b.shielded_outputs, b.authorization ) } else { "".to_string() }, + if let Some(b) = &self.orchard_bundle { + format!( + " + value_balance = {:?}, + actions = {:?}, + ", + b.value_balance(), + b.actions().len() + ) + } else { + "".to_string() + }, { #[cfg(feature = "zfuture")] if let Some(b) = &self.tze_bundle { format!( " - tze_inputs = {:?}, - tze_outputs = {:?},", + tze_inputs = {:?}, + tze_outputs = {:?}, + ", b.vin, b.vout ) } else { @@ -337,53 +458,14 @@ impl TransactionData { } } -impl Default for TransactionData { - fn default() -> Self { - Self::new() - } -} - -impl TransactionData { - pub fn new() -> Self { - TransactionData { - version: TxVersion::Sapling, - lock_time: 0, - expiry_height: 0u32.into(), - transparent_bundle: None, - sprout_bundle: None, - sapling_bundle: None, - orchard_bundle: None, - #[cfg(feature = "zfuture")] - tze_bundle: None, - } - } - - #[cfg(feature = "zfuture")] - pub fn zfuture() -> Self { - TransactionData { - version: TxVersion::ZFuture, - lock_time: 0, - expiry_height: 0u32.into(), - transparent_bundle: None, - sprout_bundle: None, - sapling_bundle: None, - orchard_bundle: None, - tze_bundle: None, - } - } -} - impl TransactionData { - pub fn freeze(self, consensus_branch_id: BranchId) -> io::Result { - Transaction::from_data(self, consensus_branch_id) + pub fn freeze(self) -> io::Result { + Transaction::from_data(self) } } impl Transaction { - fn from_data( - data: TransactionData, - _consensus_branch_id: BranchId, - ) -> io::Result { + fn from_data(data: TransactionData) -> io::Result { let mut tx = Transaction { txid: TxId([0; 32]), data, @@ -398,7 +480,8 @@ impl Transaction { self.txid } - pub fn read(reader: R) -> io::Result { + #[allow(clippy::redundant_closure)] + pub fn read(reader: R, consensus_branch_id: BranchId) -> io::Result { let mut reader = HashReader::new(reader); let version = TxVersion::read(&mut reader)?; @@ -462,6 +545,7 @@ impl Transaction { txid: TxId(txid), data: TransactionData { version, + consensus_branch_id, lock_time, expiry_height, transparent_bundle, @@ -615,7 +699,7 @@ pub struct TxDigests { pub tze_digests: Option>, } -pub(crate) trait TransactionDigest { +pub trait TransactionDigest { type HeaderDigest; type TransparentDigest; type SaplingDigest; @@ -707,14 +791,15 @@ pub mod testing { #[cfg(not(feature = "zfuture"))] prop_compose! { - pub fn arb_txdata(branch_id: BranchId)( - version in arb_tx_version(branch_id), + pub fn arb_txdata(consensus_branch_id: BranchId)( + version in arb_tx_version(consensus_branch_id), lock_time in any::(), expiry_height in any::(), transparent_bundle in transparent::arb_bundle(), ) -> TransactionData { TransactionData { version, + consensus_branch_id, lock_time, expiry_height: expiry_height.into(), transparent_bundle, @@ -727,8 +812,8 @@ pub mod testing { #[cfg(feature = "zfuture")] prop_compose! { - pub fn arb_txdata(branch_id: BranchId)( - version in arb_tx_version(branch_id), + pub fn arb_txdata(consensus_branch_id: BranchId)( + version in arb_tx_version(consensus_branch_id), lock_time in any::(), expiry_height in any::(), transparent_bundle in transparent::arb_bundle(), @@ -736,6 +821,7 @@ pub mod testing { ) -> TransactionData { TransactionData { version, + consensus_branch_id, lock_time, expiry_height: expiry_height.into(), transparent_bundle, @@ -749,7 +835,7 @@ pub mod testing { prop_compose! { pub fn arb_tx(branch_id: BranchId)(tx_data in arb_txdata(branch_id)) -> Transaction { - Transaction::from_data(tx_data, branch_id).unwrap() + Transaction::from_data(tx_data).unwrap() } } } diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs new file mode 100644 index 0000000000..864ccb0dc2 --- /dev/null +++ b/zcash_primitives/src/transaction/sighash.rs @@ -0,0 +1,136 @@ +use crate::legacy::Script; +use blake2b_simd::Hash as Blake2bHash; +use std::convert::TryInto; + +use super::{ + components::{ + sapling::{self, GrothProofBytes}, + Amount, + }, + sighash_v4::v4_signature_hash, + sighash_v5::v5_signature_hash, + Authorization, TransactionData, TxDigests, TxVersion, +}; + +#[cfg(feature = "zfuture")] +use crate::extensions::transparent::Precondition; + +pub const SIGHASH_ALL: u32 = 1; +pub const SIGHASH_NONE: u32 = 2; +pub const SIGHASH_SINGLE: u32 = 3; +pub const SIGHASH_MASK: u32 = 0x1f; +pub const SIGHASH_ANYONECANPAY: u32 = 0x80; + +pub struct TransparentInput<'a> { + index: usize, + script_code: &'a Script, + value: Amount, +} + +impl<'a> TransparentInput<'a> { + pub fn new(index: usize, script_code: &'a Script, value: Amount) -> Self { + TransparentInput { + index, + script_code, + value, + } + } + + pub fn index(&self) -> usize { + self.index + } + + pub fn script_code(&self) -> &'a Script { + self.script_code + } + + pub fn value(&self) -> Amount { + self.value + } +} + +#[cfg(feature = "zfuture")] +pub struct TzeInput<'a> { + index: usize, + precondition: &'a Precondition, + value: Amount, +} + +#[cfg(feature = "zfuture")] +impl<'a> TzeInput<'a> { + pub fn new(index: usize, precondition: &'a Precondition, value: Amount) -> Self { + TzeInput { + index, + precondition, + value, + } + } + + pub fn index(&self) -> usize { + self.index + } + + pub fn precondition(&self) -> &'a Precondition { + self.precondition + } + + pub fn value(&self) -> Amount { + self.value + } +} + +pub enum SignableInput<'a> { + Shielded, + Transparent(TransparentInput<'a>), + #[cfg(feature = "zfuture")] + Tze(TzeInput<'a>), +} + +impl<'a> SignableInput<'a> { + pub fn transparent(index: usize, script_code: &'a Script, value: Amount) -> Self { + SignableInput::Transparent(TransparentInput { + index, + script_code, + value, + }) + } + + #[cfg(feature = "zfuture")] + pub fn tze(index: usize, precondition: &'a Precondition, value: Amount) -> Self { + SignableInput::Tze(TzeInput { + index, + precondition, + value, + }) + } +} + +pub struct SignatureHash(Blake2bHash); + +impl AsRef<[u8; 32]> for SignatureHash { + fn as_ref(&self) -> &[u8; 32] { + self.0.as_ref().try_into().unwrap() + } +} + +pub fn signature_hash< + 'a, + SA: sapling::Authorization, + A: Authorization, +>( + tx: &TransactionData, + signable_input: SignableInput<'a>, + txid_parts: &TxDigests, + hash_type: u32, +) -> SignatureHash { + SignatureHash(match tx.version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + v4_signature_hash(tx, signable_input, hash_type) + } + + TxVersion::ZcashTxV5 => v5_signature_hash(tx, txid_parts, signable_input, hash_type), + + #[cfg(feature = "zfuture")] + TxVersion::ZFuture => v5_signature_hash(tx, txid_parts, signable_input, hash_type), + }) +} diff --git a/zcash_primitives/src/transaction/sighash_v4.rs b/zcash_primitives/src/transaction/sighash_v4.rs index fa6ceedf45..b106254eb4 100644 --- a/zcash_primitives/src/transaction/sighash_v4.rs +++ b/zcash_primitives/src/transaction/sighash_v4.rs @@ -3,22 +3,16 @@ use byteorder::{LittleEndian, WriteBytesExt}; use ff::PrimeField; use group::GroupEncoding; -use crate::{ - consensus::{self, BranchId}, - legacy::Script, -}; - -#[cfg(feature = "zfuture")] -use crate::extensions::transparent::Precondition; +use crate::consensus::BranchId; use super::{ components::{ - amount::Amount, sapling::{self, GrothProofBytes, OutputDescription, SpendDescription}, sprout::JsDescription, transparent::{self, TxIn, TxOut}, }, - Authorization, Transaction, TransactionData, TxVersion, + sighash::{SignableInput, SIGHASH_ANYONECANPAY, SIGHASH_MASK, SIGHASH_NONE, SIGHASH_SINGLE}, + Authorization, TransactionData, }; const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash"; @@ -29,12 +23,6 @@ const ZCASH_JOINSPLITS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashJSplitsHash"; const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash"; const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash"; -pub const SIGHASH_ALL: u32 = 1; -const SIGHASH_NONE: u32 = 2; -const SIGHASH_SINGLE: u32 = 3; -const SIGHASH_MASK: u32 = 0x1f; -const SIGHASH_ANYONECANPAY: u32 = 0x80; - macro_rules! update_u32 { ($h:expr, $value:expr, $tmp:expr) => { (&mut $tmp[..4]).write_u32::($value).unwrap(); @@ -52,10 +40,6 @@ macro_rules! update_hash { }; } -fn has_overwinter_components(version: &TxVersion) -> bool { - !matches!(version, TxVersion::Sprout(_)) -} - fn prevout_hash(vin: &[TxIn]) -> Blake2bHash { let mut data = Vec::with_capacity(vin.len() * 36); for t_in in vin { @@ -110,7 +94,7 @@ fn joinsplits_hash( * if consensus_branch_id.sprout_uses_groth_proofs() { 1698 // JSDescription with Groth16 proof } else { - 1802 // JSDescription with PHGR13 proof + 1802 // JsDescription with PHGR13 proof }, ); for js in joinsplits { @@ -130,7 +114,7 @@ fn shielded_spends_hash>( for s_spend in shielded_spends { data.extend_from_slice(&s_spend.cv.to_bytes()); data.extend_from_slice(s_spend.anchor.to_repr().as_ref()); - data.extend_from_slice(&s_spend.nullifier.0); + data.extend_from_slice(&s_spend.nullifier.as_ref()); s_spend.rk.write(&mut data).unwrap(); data.extend_from_slice(&s_spend.zkproof); } @@ -153,55 +137,19 @@ fn shielded_outputs_hash>( .hash(&data) } -pub enum SignableInput<'a> { - Shielded, - Transparent { - index: usize, - script_code: &'a Script, - value: Amount, - }, - #[cfg(feature = "zfuture")] - Tze { - index: usize, - precondition: &'a Precondition, - value: Amount, - }, -} - -impl<'a> SignableInput<'a> { - pub fn transparent(index: usize, script_code: &'a Script, value: Amount) -> Self { - SignableInput::Transparent { - index, - script_code, - value, - } - } - - #[cfg(feature = "zfuture")] - pub fn tze(index: usize, precondition: &'a Precondition, value: Amount) -> Self { - SignableInput::Tze { - index, - precondition, - value, - } - } -} - -pub fn signature_hash_data< - TA: transparent::Authorization, +pub fn v4_signature_hash< SA: sapling::Authorization, - A: Authorization, + A: Authorization, >( tx: &TransactionData, - consensus_branch_id: consensus::BranchId, - hash_type: u32, signable_input: SignableInput<'_>, -) -> Vec { - if has_overwinter_components(&tx.version) { + hash_type: u32, +) -> Blake2bHash { + if tx.version.has_overwinter() { let mut personal = [0; 16]; (&mut personal[..12]).copy_from_slice(ZCASH_SIGHASH_PERSONALIZATION_PREFIX); (&mut personal[12..]) - .write_u32::(consensus_branch_id.into()) + .write_u32::(tx.consensus_branch_id.into()) .unwrap(); let mut h = Blake2bParams::new() @@ -223,7 +171,7 @@ pub fn signature_hash_data< ); update_hash!( h, - hash_type & SIGHASH_ANYONECANPAY == 0 + (hash_type & SIGHASH_ANYONECANPAY) == 0 && (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE && (hash_type & SIGHASH_MASK) != SIGHASH_NONE, sequence_hash( @@ -246,14 +194,15 @@ pub fn signature_hash_data< ); } else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE { match (tx.transparent_bundle.as_ref(), &signable_input) { - (Some(b), SignableInput::Transparent { index, .. }) if *index < b.vout.len() => { - h.update(single_output_hash(&b.vout[*index]).as_bytes()) + (Some(b), SignableInput::Transparent(input)) if input.index() < b.vout.len() => { + h.update(single_output_hash(&b.vout[input.index()]).as_bytes()) } _ => h.update(&[0; 32]), }; } else { h.update(&[0; 32]); }; + update_hash!( h, !tx.sprout_bundle @@ -262,38 +211,27 @@ pub fn signature_hash_data< { let bundle = tx.sprout_bundle.as_ref().unwrap(); joinsplits_hash( - consensus_branch_id, + tx.consensus_branch_id, &bundle.joinsplits, &bundle.joinsplit_pubkey, ) } ); + if tx.version.has_sapling() { update_hash!( h, !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() - ) + shielded_spends_hash(&tx.sapling_bundle.as_ref().unwrap().shielded_spends) ); update_hash!( h, !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() - ) + shielded_outputs_hash(&tx.sapling_bundle.as_ref().unwrap().shielded_outputs) ); } update_u32!(h, tx.lock_time, tmp); @@ -304,19 +242,15 @@ pub fn signature_hash_data< update_u32!(h, hash_type, tmp); match signable_input { - SignableInput::Transparent { - index, - script_code, - value, - } => { + SignableInput::Shielded => (), + SignableInput::Transparent(input) => { if let Some(bundle) = tx.transparent_bundle.as_ref() { let mut data = vec![]; - - bundle.vin[index].prevout.write(&mut data).unwrap(); - script_code.write(&mut data).unwrap(); - data.extend_from_slice(&value.to_i64_le_bytes()); + bundle.vin[input.index()].prevout.write(&mut data).unwrap(); + input.script_code().write(&mut data).unwrap(); + data.extend_from_slice(&input.value().to_i64_le_bytes()); (&mut data) - .write_u32::(bundle.vin[index].sequence) + .write_u32::(bundle.vin[input.index()].sequence) .unwrap(); h.update(&data); } else { @@ -325,25 +259,15 @@ pub fn signature_hash_data< ); } } + #[cfg(feature = "zfuture")] - SignableInput::Tze { .. } => { - panic!("A request has been made to sign a TZE input in a V4 transaction."); + SignableInput::Tze(_) => { + panic!("A request has been made to sign a TZE input, but the transaction version is not ZFuture"); } - - SignableInput::Shielded => (), } - h.finalize().as_ref().to_vec() + h.finalize() } else { - unimplemented!() + panic!("Signature hashing for pre-overwinter transactions is not supported.") } } - -pub fn signature_hash( - tx: &Transaction, - consensus_branch_id: consensus::BranchId, - hash_type: u32, - signable_input: SignableInput<'_>, -) -> Vec { - signature_hash_data(tx, consensus_branch_id, hash_type, signable_input) -} diff --git a/zcash_primitives/src/transaction/sighash_v5.rs b/zcash_primitives/src/transaction/sighash_v5.rs new file mode 100644 index 0000000000..54984fdbc4 --- /dev/null +++ b/zcash_primitives/src/transaction/sighash_v5.rs @@ -0,0 +1,182 @@ +use std::io::Write; + +use blake2b_simd::{Hash as Blake2bHash, Params, State}; +use byteorder::{LittleEndian, WriteBytesExt}; + +use crate::transaction::{ + components::transparent::{self, TxOut}, + sighash::{ + SignableInput, TransparentInput, SIGHASH_ANYONECANPAY, SIGHASH_MASK, SIGHASH_NONE, + SIGHASH_SINGLE, + }, + txid::{ + to_hash, transparent_outputs_hash, transparent_prevout_hash, transparent_sequence_hash, + }, + Authorization, TransactionData, TransparentDigests, TxDigests, +}; + +#[cfg(feature = "zfuture")] +use std::convert::TryInto; + +#[cfg(feature = "zfuture")] +use crate::{ + serialize::{CompactSize, Vector}, + transaction::{components::tze, sighash::TzeInput, TzeDigests}, +}; + +const ZCASH_TRANSPARENT_INPUT_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash___TxInHash"; + +#[cfg(feature = "zfuture")] +const ZCASH_TZE_INPUT_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash__TzeInHash"; + +fn hasher(personal: &[u8; 16]) -> State { + Params::new().hash_length(32).personal(personal).to_state() +} + +fn transparent_input_sigdigests( + bundle: &transparent::Bundle, + input: &TransparentInput<'_>, + txid_digests: &TransparentDigests, + hash_type: u32, +) -> TransparentDigests { + let flag_anyonecanpay = hash_type & SIGHASH_ANYONECANPAY != 0; + let flag_single = hash_type & SIGHASH_MASK == SIGHASH_SINGLE; + let flag_none = hash_type & SIGHASH_MASK == SIGHASH_NONE; + + let prevout_digest = if flag_anyonecanpay { + transparent_prevout_hash::(&[]) + } else { + txid_digests.prevout_digest + }; + + let sequence_digest = if flag_anyonecanpay || flag_single || flag_none { + transparent_sequence_hash::(&[]) + } else { + txid_digests.sequence_digest + }; + + let outputs_digest = if flag_single { + if input.index() < bundle.vout.len() { + transparent_outputs_hash(&[&bundle.vout[input.index()]]) + } else { + transparent_outputs_hash::(&[]) + } + } else if flag_none { + transparent_outputs_hash::(&[]) + } else { + txid_digests.outputs_digest + }; + + // If we are serializing an input (i.e. this is not a JoinSplit signature hash): + // a. outpoint (32-byte hash + 4-byte little endian) + // b. scriptCode of the input (serialized as scripts inside CTxOuts) + // c. value of the output spent by this input (8-byte little endian) + // d. nSequence of the input (4-byte little endian) + let mut ch = hasher(ZCASH_TRANSPARENT_INPUT_HASH_PERSONALIZATION); + let txin = &bundle.vin[input.index()]; + txin.prevout.write(&mut ch).unwrap(); + input.script_code().write(&mut ch).unwrap(); + ch.write_all(&input.value().to_i64_le_bytes()).unwrap(); + ch.write_u32::(txin.sequence).unwrap(); + let per_input_digest = ch.finalize(); + + TransparentDigests { + prevout_digest, + sequence_digest, + outputs_digest, + per_input_digest: Some(per_input_digest), + } +} + +#[cfg(feature = "zfuture")] +fn tze_input_sigdigests( + bundle: &tze::Bundle, + input: &TzeInput<'_>, + txid_digests: &TzeDigests, +) -> TzeDigests { + let mut ch = hasher(ZCASH_TZE_INPUT_HASH_PERSONALIZATION); + let tzein = &bundle.vin[input.index()]; + tzein.prevout.write(&mut ch).unwrap(); + CompactSize::write( + &mut ch, + input.precondition().extension_id.try_into().unwrap(), + ) + .unwrap(); + CompactSize::write(&mut ch, input.precondition().mode.try_into().unwrap()).unwrap(); + Vector::write(&mut ch, &input.precondition().payload, |w, e| { + w.write_u8(*e) + }) + .unwrap(); + ch.write_all(&input.value().to_i64_le_bytes()).unwrap(); + let per_input_digest = ch.finalize(); + + TzeDigests { + inputs_digest: txid_digests.inputs_digest, + outputs_digest: txid_digests.outputs_digest, + per_input_digest: Some(per_input_digest), + } +} + +pub fn v5_signature_hash( + tx: &TransactionData, + txid_parts: &TxDigests, + signable_input: SignableInput<'_>, + hash_type: u32, +) -> Blake2bHash { + match signable_input { + SignableInput::Shielded => to_hash( + tx.version, + tx.consensus_branch_id, + txid_parts.header_digest, + txid_parts.transparent_digests.as_ref(), + txid_parts.sapling_digest, + txid_parts.orchard_digest, + #[cfg(feature = "zfuture")] + txid_parts.tze_digests.as_ref(), + ), + SignableInput::Transparent(input) => { + if let Some((bundle, txid_digests)) = tx + .transparent_bundle + .as_ref() + .zip(txid_parts.transparent_digests.as_ref()) + { + to_hash( + tx.version, + tx.consensus_branch_id, + txid_parts.header_digest, + Some(&transparent_input_sigdigests( + bundle, + &input, + txid_digests, + hash_type, + )), + txid_parts.sapling_digest, + txid_parts.orchard_digest, + #[cfg(feature = "zfuture")] + txid_parts.tze_digests.as_ref(), + ) + } else { + panic!("It is not possible to sign a transparent input with missing bundle data.") + } + } + #[cfg(feature = "zfuture")] + SignableInput::Tze(input) => { + if let Some((bundle, txid_digests)) = + tx.tze_bundle.as_ref().zip(txid_parts.tze_digests.as_ref()) + { + to_hash( + tx.version, + tx.consensus_branch_id, + txid_parts.header_digest, + txid_parts.transparent_digests.as_ref(), + txid_parts.sapling_digest, + txid_parts.orchard_digest, + #[cfg(feature = "zfuture")] + Some(&tze_input_sigdigests(bundle, &input, txid_digests)), + ) + } else { + panic!("It is not possible to sign a tze input with missing bundle data.") + } + } + } +} diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index b2451b4e7c..2642051d9b 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -1,9 +1,9 @@ use proptest::prelude::*; +use crate::consensus::BranchId; + use super::{ - components::Amount, - sighash_v4::{signature_hash, SignableInput}, - Transaction, + components::Amount, sighash::SignableInput, sighash_v4::v4_signature_hash, Transaction, }; use super::testing::{arb_branch_id, arb_tx}; @@ -11,7 +11,7 @@ use super::testing::{arb_branch_id, arb_tx}; #[test] fn tx_read_write() { let data = &self::data::tx_read_write::TX_READ_WRITE; - let tx = Transaction::read(&data[..]).unwrap(); + let tx = Transaction::read(&data[..], BranchId::Canopy).unwrap(); assert_eq!( format!("{}", tx.txid()), "64f0bd7fe30ce23753358fe3a2dc835b8fba9c0274c4e2c54a6f73114cb55639" @@ -28,7 +28,7 @@ proptest! { let mut txn_bytes = vec![]; tx.write(&mut txn_bytes).unwrap(); - let txo = Transaction::read(&txn_bytes[..]).unwrap(); + let txo = Transaction::read(&txn_bytes[..], BranchId::Canopy).unwrap(); prop_assert_eq!(tx.version, txo.version); prop_assert_eq!(tx.lock_time, txo.lock_time); @@ -43,7 +43,7 @@ mod data; #[test] fn zip_0143() { for tv in self::data::zip_0143::make_test_vectors() { - let tx = Transaction::read(&tv.tx[..]).unwrap(); + let tx = Transaction::read(&tv.tx[..], tv.consensus_branch_id).unwrap(); let signable_input = match tv.transparent_input { Some(n) => SignableInput::transparent( n as usize, @@ -54,7 +54,7 @@ fn zip_0143() { }; assert_eq!( - signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input).as_ref(), + v4_signature_hash(&tx, signable_input, tv.hash_type).as_ref(), tv.sighash ); } @@ -63,7 +63,7 @@ fn zip_0143() { #[test] fn zip_0243() { for tv in self::data::zip_0243::make_test_vectors() { - let tx = Transaction::read(&tv.tx[..]).unwrap(); + let tx = Transaction::read(&tv.tx[..], tv.consensus_branch_id).unwrap(); let signable_input = match tv.transparent_input { Some(n) => SignableInput::transparent( n as usize, @@ -74,7 +74,7 @@ fn zip_0243() { }; assert_eq!( - signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input).as_ref(), + v4_signature_hash(&tx, signable_input, tv.hash_type).as_ref(), tv.sighash ); } diff --git a/zcash_primitives/src/transaction/txid.rs b/zcash_primitives/src/transaction/txid.rs new file mode 100644 index 0000000000..498e0bb790 --- /dev/null +++ b/zcash_primitives/src/transaction/txid.rs @@ -0,0 +1,585 @@ +use std::borrow::Borrow; +use std::convert::TryFrom; +use std::io::Write; + +use blake2b_simd::{Hash as Blake2bHash, Params, State}; +use byteorder::{LittleEndian, WriteBytesExt}; +use ff::PrimeField; +use group::GroupEncoding; +use orchard::bundle::{self as orchard}; + +use crate::consensus::{BlockHeight, BranchId}; + +use super::{ + components::{ + amount::Amount, + orchard as ser_orch, + sapling::{self, OutputDescription, SpendDescription}, + transparent::{self, TxIn, TxOut}, + }, + Authorization, Authorized, TransactionDigest, TransparentDigests, TxDigests, TxId, TxVersion, +}; + +#[cfg(feature = "zfuture")] +use super::{ + components::tze::{self, TzeIn, TzeOut}, + TzeDigests, +}; + +/// TxId tree root personalization +const ZCASH_TX_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashTxHash_"; + +// TxId level 1 node personalization +const ZCASH_HEADERS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdHeadersHash"; +const ZCASH_TRANSPARENT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdTranspaHash"; +const ZCASH_SAPLING_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSaplingHash"; +const ZCASH_ORCHARD_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrchardHash"; +#[cfg(feature = "zfuture")] +const ZCASH_TZE_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdTZE____Hash"; + +// TxId transparent level 2 node personalization +const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdPrevoutHash"; +const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSequencHash"; +const ZCASH_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOutputsHash"; + +// TxId tze level 2 node personalization +#[cfg(feature = "zfuture")] +const ZCASH_TZE_INPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdTZEIns_Hash"; +#[cfg(feature = "zfuture")] +const ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdTZEOutsHash"; + +// TxId sapling level 2 node personalization +const ZCASH_SAPLING_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSSpendsHash"; +const ZCASH_SAPLING_SPENDS_COMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSSpendCHash"; +const ZCASH_SAPLING_SPENDS_NONCOMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSSpendNHash"; + +const ZCASH_SAPLING_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSOutputHash"; +const ZCASH_SAPLING_OUTPUTS_COMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSOutC__Hash"; +const ZCASH_SAPLING_OUTPUTS_MEMOS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSOutM__Hash"; +const ZCASH_SAPLING_OUTPUTS_NONCOMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdSOutN__Hash"; + +const ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActCHash"; +const ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActMHash"; +const ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActNHash"; + +const ZCASH_AUTH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZTxAuthHash_"; +const ZCASH_TRANSPARENT_SCRIPTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthTransHash"; +const ZCASH_SAPLING_SIGS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthSapliHash"; +const ZCASH_ORCHARD_SIGS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthOrchaHash"; +#[cfg(feature = "zfuture")] +const ZCASH_TZE_WITNESSES_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthTZE__Hash"; + +fn hasher(personal: &[u8; 16]) -> State { + Params::new().hash_length(32).personal(personal).to_state() +} + +/// Sequentially append the serialized value of each transparent input +/// to a hash personalized by ZCASH_PREVOUTS_HASH_PERSONALIZATION. +/// In the case that no inputs are provided, this produces a default +/// hash from just the personalization string. +pub(crate) fn transparent_prevout_hash( + vin: &[TxIn], +) -> Blake2bHash { + let mut h = hasher(ZCASH_PREVOUTS_HASH_PERSONALIZATION); + for t_in in vin { + t_in.prevout.write(&mut h).unwrap(); + } + h.finalize() +} + +/// Hash of the little-endian u32 interpretation of the +/// `sequence` values for each TxIn record passed in vin. +pub(crate) fn transparent_sequence_hash( + vin: &[TxIn], +) -> Blake2bHash { + let mut h = hasher(ZCASH_SEQUENCE_HASH_PERSONALIZATION); + for t_in in vin { + (&mut h).write_u32::(t_in.sequence).unwrap(); + } + h.finalize() +} + +/// Sequentially append the full serialized value of each transparent output +/// to a hash personalized by ZCASH_OUTPUTS_HASH_PERSONALIZATION. +/// In the case that no outputs are provided, this produces a default +/// hash from just the personalization string. +pub(crate) fn transparent_outputs_hash>(vout: &[T]) -> Blake2bHash { + let mut h = hasher(ZCASH_OUTPUTS_HASH_PERSONALIZATION); + for t_out in vout { + t_out.borrow().write(&mut h).unwrap(); + } + h.finalize() +} + +/// Sequentially append the serialized value of each TZE input, excluding +/// witness data, to a hash personalized by ZCASH_TZE_INPUTS_HASH_PERSONALIZATION. +/// In the case that no inputs are provided, this produces a default +/// hash from just the personalization string. +#[cfg(feature = "zfuture")] +pub(crate) fn hash_tze_inputs(tze_inputs: &[TzeIn]) -> Blake2bHash { + let mut h = hasher(ZCASH_TZE_INPUTS_HASH_PERSONALIZATION); + for tzein in tze_inputs { + tzein.write_without_witness(&mut h).unwrap(); + } + h.finalize() +} + +/// Sequentially append the full serialized value of each TZE output +/// to a hash personalized by ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION. +/// In the case that no outputs are provided, this produces a default +/// hash from just the personalization string. +#[cfg(feature = "zfuture")] +pub(crate) fn hash_tze_outputs(tze_outputs: &[TzeOut]) -> Blake2bHash { + let mut h = hasher(ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION); + for tzeout in tze_outputs { + tzeout.write(&mut h).unwrap(); + } + h.finalize() +} + +/// Write disjoint parts of each Sapling shielded spend to a pair of hashes: +/// * [nullifier*] - personalized with ZCASH_SAPLING_SPENDS_COMPACT_HASH_PERSONALIZATION +/// * [(cv, anchor, rk, zkproof)*] - personalized with ZCASH_SAPLING_SPENDS_NONCOMPACT_HASH_PERSONALIZATION +/// +/// Then, hash these together personalized by ZCASH_SAPLING_SPENDS_HASH_PERSONALIZATION +pub(crate) fn hash_sapling_spends( + shielded_spends: &[SpendDescription], +) -> Blake2bHash { + let mut ch = hasher(ZCASH_SAPLING_SPENDS_COMPACT_HASH_PERSONALIZATION); + let mut nh = hasher(ZCASH_SAPLING_SPENDS_NONCOMPACT_HASH_PERSONALIZATION); + for s_spend in shielded_spends { + // we build the hash of nullifiers separately for compact blocks. + ch.write_all(&s_spend.nullifier.as_ref()).unwrap(); + + nh.write_all(&s_spend.cv.to_bytes()).unwrap(); + nh.write_all(&s_spend.anchor.to_repr()).unwrap(); + s_spend.rk.write(&mut nh).unwrap(); + } + + let mut h = hasher(ZCASH_SAPLING_SPENDS_HASH_PERSONALIZATION); + h.write_all(&ch.finalize().as_bytes()).unwrap(); + h.write_all(&nh.finalize().as_bytes()).unwrap(); + h.finalize() +} + +/// Write disjoint parts of each Sapling shielded output as 3 separate hashes: +/// * [(cmu, epk, enc_ciphertext[..52])*] personalized with ZCASH_SAPLING_OUTPUTS_COMPACT_HASH_PERSONALIZATION +/// * [enc_ciphertext[52..564]*] (memo ciphertexts) personalized with ZCASH_SAPLING_OUTPUTS_MEMOS_HASH_PERSONALIZATION +/// * [(cv, enc_ciphertext[564..], out_ciphertext, zkproof)*] personalized with ZCASH_SAPLING_OUTPUTS_NONCOMPACT_HASH_PERSONALIZATION +/// +/// Then, hash these together personalized with ZCASH_SAPLING_OUTPUTS_HASH_PERSONALIZATION +pub(crate) fn hash_sapling_outputs( + shielded_outputs: &[OutputDescription], +) -> Blake2bHash { + let mut ch = hasher(ZCASH_SAPLING_OUTPUTS_COMPACT_HASH_PERSONALIZATION); + let mut mh = hasher(ZCASH_SAPLING_OUTPUTS_MEMOS_HASH_PERSONALIZATION); + let mut nh = hasher(ZCASH_SAPLING_OUTPUTS_NONCOMPACT_HASH_PERSONALIZATION); + for s_out in shielded_outputs { + ch.write_all(&s_out.cmu.to_repr().as_ref()).unwrap(); + ch.write_all(&s_out.ephemeral_key.to_bytes()).unwrap(); + ch.write_all(&s_out.enc_ciphertext[..52]).unwrap(); + + mh.write_all(&s_out.enc_ciphertext[52..564]).unwrap(); + + nh.write_all(&s_out.cv.to_bytes()).unwrap(); + nh.write_all(&s_out.enc_ciphertext[564..]).unwrap(); + nh.write_all(&s_out.out_ciphertext).unwrap(); + } + + let mut h = hasher(ZCASH_SAPLING_OUTPUTS_HASH_PERSONALIZATION); + h.write_all(&ch.finalize().as_bytes()).unwrap(); + h.write_all(&mh.finalize().as_bytes()).unwrap(); + h.write_all(&nh.finalize().as_bytes()).unwrap(); + h.finalize() +} + +/// The txid commits to the hash of all transparent outputs. The +/// prevout and sequence_hash components of txid +fn transparent_digests( + bundle: &transparent::Bundle, +) -> TransparentDigests { + TransparentDigests { + prevout_digest: transparent_prevout_hash(&bundle.vin), + sequence_digest: transparent_sequence_hash(&bundle.vin), + outputs_digest: transparent_outputs_hash(&bundle.vout), + per_input_digest: None, + } +} + +#[cfg(feature = "zfuture")] +fn tze_digests(bundle: &tze::Bundle) -> TzeDigests { + // The txid commits to the hash for all outputs. + TzeDigests { + inputs_digest: hash_tze_inputs(&bundle.vin), + outputs_digest: hash_tze_outputs(&bundle.vout), + per_input_digest: None, + } +} + +fn hash_header_txid_data( + version: TxVersion, + // we commit to the consensus branch ID with the header + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, +) -> Blake2bHash { + let mut h = hasher(ZCASH_HEADERS_HASH_PERSONALIZATION); + + (&mut h) + .write_u32::(version.header()) + .unwrap(); + (&mut h) + .write_u32::(version.version_group_id()) + .unwrap(); + (&mut h) + .write_u32::(consensus_branch_id.into()) + .unwrap(); + (&mut h).write_u32::(lock_time).unwrap(); + (&mut h) + .write_u32::(expiry_height.into()) + .unwrap(); + + h.finalize() +} + +fn hash_transparent_txid_data(t_digests: Option<&TransparentDigests>) -> Blake2bHash { + let mut h = hasher(ZCASH_TRANSPARENT_HASH_PERSONALIZATION); + if let Some(d) = t_digests { + h.write_all(d.prevout_digest.as_bytes()).unwrap(); + h.write_all(d.sequence_digest.as_bytes()).unwrap(); + h.write_all(d.outputs_digest.as_bytes()).unwrap(); + if let Some(s) = d.per_input_digest { + h.write_all(s.as_bytes()).unwrap(); + }; + } + h.finalize() +} + +fn hash_sapling_txid_data( + sapling_bundle: Option<&sapling::Bundle>, +) -> Blake2bHash { + let mut h = hasher(ZCASH_SAPLING_HASH_PERSONALIZATION); + if let Some(bundle) = sapling_bundle { + if !bundle.shielded_spends.is_empty() { + h.write_all(hash_sapling_spends(&bundle.shielded_spends).as_bytes()) + .unwrap(); + } + + if !bundle.shielded_outputs.is_empty() { + h.write_all(hash_sapling_outputs(&bundle.shielded_outputs).as_bytes()) + .unwrap(); + } + + h.write_all(&bundle.value_balance.to_i64_le_bytes()) + .unwrap(); + } + h.finalize() +} + +/// Write disjoint parts of each Orchard shielded action as 3 separate hashes: +/// * [(nullifier, cmx, ephemeral_key, enc_ciphertext[..52])*] personalized +/// with ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION +/// * [enc_ciphertext[52..564]*] (memo ciphertexts) personalized +/// with ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION +/// * [(cv, rk, enc_ciphertext[564..], out_ciphertext)*] personalized +/// with ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION +/// +/// Then, hash these together along with (flags, value_balance_orchard, anchor_orchard), +/// personalized with ZCASH_ORCHARD_ACTIONS_HASH_PERSONALIZATION +fn hash_orchard_txid_data( + orchard_bundle: Option<&orchard::Bundle>, +) -> Blake2bHash { + let mut h = hasher(ZCASH_ORCHARD_HASH_PERSONALIZATION); + if let Some(bundle) = orchard_bundle { + let mut ch = hasher(ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION); + let mut mh = hasher(ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION); + let mut nh = hasher(ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION); + + for action in bundle.actions().iter() { + ch.write_all(&action.nullifier().to_bytes()).unwrap(); + ch.write_all(&action.cmx().to_bytes()).unwrap(); + ch.write_all(&action.encrypted_note().epk_bytes).unwrap(); + ch.write_all(&action.encrypted_note().enc_ciphertext[..52]) + .unwrap(); + + mh.write_all(&action.encrypted_note().enc_ciphertext[52..564]) + .unwrap(); + + nh.write_all(&action.cv_net().to_bytes()).unwrap(); + nh.write_all(&<[u8; 32]>::from(action.rk())).unwrap(); + nh.write_all(&action.encrypted_note().enc_ciphertext[564..]) + .unwrap(); + nh.write_all(&action.encrypted_note().out_ciphertext) + .unwrap(); + } + + h.write_all(&ch.finalize().as_bytes()).unwrap(); + h.write_all(&mh.finalize().as_bytes()).unwrap(); + h.write_all(&nh.finalize().as_bytes()).unwrap(); + ser_orch::write_flags(&mut h, bundle.flags()).unwrap(); + h.write_all(&bundle.value_balance().to_i64_le_bytes()) + .unwrap(); + ser_orch::write_anchor(&mut h, bundle.anchor()).unwrap(); + } + h.finalize() +} + +#[cfg(feature = "zfuture")] +fn hash_tze_txid_data(tze_digests: Option<&TzeDigests>) -> Blake2bHash { + let mut h = hasher(ZCASH_TZE_HASH_PERSONALIZATION); + if let Some(d) = tze_digests { + h.write_all(d.inputs_digest.as_bytes()).unwrap(); + h.write_all(d.outputs_digest.as_bytes()).unwrap(); + if let Some(s) = d.per_input_digest { + h.write_all(s.as_bytes()).unwrap(); + } + } + h.finalize() +} + +pub struct TxIdDigester; + +// A TransactionDigest implementation that commits to all of the effecting +// data of a transaction to produce a nonmalleable transaction identifier. +// +// This expects and relies upon the existence of canonical encodings for +// each effecting component of a transaction. +impl TransactionDigest for TxIdDigester { + type HeaderDigest = Blake2bHash; + type TransparentDigest = Option>; + type SaplingDigest = Blake2bHash; + type OrchardDigest = Blake2bHash; + + #[cfg(feature = "zfuture")] + type TzeDigest = Option>; + + type Digest = TxDigests; + + fn digest_header( + &self, + version: TxVersion, + consensus_branch_id: BranchId, + lock_time: u32, + expiry_height: BlockHeight, + ) -> Self::HeaderDigest { + hash_header_txid_data(version, consensus_branch_id, lock_time, expiry_height) + } + + fn digest_transparent( + &self, + transparent_bundle: Option<&transparent::Bundle>, + ) -> Self::TransparentDigest { + transparent_bundle.map(transparent_digests) + } + + fn digest_sapling( + &self, + sapling_bundle: Option<&sapling::Bundle>, + ) -> Self::SaplingDigest { + hash_sapling_txid_data(sapling_bundle) + } + + fn digest_orchard( + &self, + orchard_bundle: Option<&orchard::Bundle>, + ) -> Self::OrchardDigest { + hash_orchard_txid_data(orchard_bundle) + } + + #[cfg(feature = "zfuture")] + fn digest_tze(&self, tze_bundle: Option<&tze::Bundle>) -> Self::TzeDigest { + tze_bundle.map(tze_digests) + } + + fn combine( + &self, + header_digest: Self::HeaderDigest, + transparent_digests: Self::TransparentDigest, + sapling_digest: Self::SaplingDigest, + orchard_digest: Self::OrchardDigest, + #[cfg(feature = "zfuture")] tze_digests: Self::TzeDigest, + ) -> Self::Digest { + TxDigests { + header_digest, + transparent_digests, + sapling_digest, + orchard_digest, + #[cfg(feature = "zfuture")] + tze_digests, + } + } +} + +pub fn to_hash( + _txversion: TxVersion, + consensus_branch_id: BranchId, + header_digest: Blake2bHash, + transparent_digests: Option<&TransparentDigests>, + sapling_digest: Blake2bHash, + orchard_digest: Blake2bHash, + #[cfg(feature = "zfuture")] tze_digests: Option<&TzeDigests>, +) -> Blake2bHash { + let mut personal = [0; 16]; + (&mut personal[..12]).copy_from_slice(ZCASH_TX_PERSONALIZATION_PREFIX); + (&mut personal[12..]) + .write_u32::(consensus_branch_id.into()) + .unwrap(); + + let mut h = hasher(&personal); + h.write_all(header_digest.as_bytes()).unwrap(); + h.write_all(hash_transparent_txid_data(transparent_digests).as_bytes()) + .unwrap(); + h.write_all(sapling_digest.as_bytes()).unwrap(); + h.write_all(orchard_digest.as_bytes()).unwrap(); + + #[cfg(feature = "zfuture")] + if _txversion.has_tze() { + h.write_all(hash_tze_txid_data(tze_digests).as_bytes()) + .unwrap(); + } + + h.finalize() +} + +pub fn to_txid( + txversion: TxVersion, + consensus_branch_id: BranchId, + digests: &TxDigests, +) -> TxId { + let txid_digest = to_hash( + txversion, + consensus_branch_id, + digests.header_digest, + digests.transparent_digests.as_ref(), + digests.sapling_digest, + digests.orchard_digest, + #[cfg(feature = "zfuture")] + digests.tze_digests.as_ref(), + ); + + TxId(<[u8; 32]>::try_from(txid_digest.as_bytes()).unwrap()) +} + +/// Digester which constructs a digest of only the witness data. +/// This does not internally commit to the txid, so if that is +/// desired it should be done using the result of this digest +/// function. +pub struct BlockTxCommitmentDigester; + +impl TransactionDigest for BlockTxCommitmentDigester { + /// We use the header digest to pass the transaction ID into + /// where it needs to be used for personalization string construction. + type HeaderDigest = BranchId; + type TransparentDigest = Blake2bHash; + type SaplingDigest = Blake2bHash; + type OrchardDigest = Blake2bHash; + + #[cfg(feature = "zfuture")] + type TzeDigest = Blake2bHash; + + type Digest = Blake2bHash; + + fn digest_header( + &self, + _version: TxVersion, + consensus_branch_id: BranchId, + _lock_time: u32, + _expiry_height: BlockHeight, + ) -> Self::HeaderDigest { + consensus_branch_id + } + + fn digest_transparent( + &self, + transparent_bundle: Option<&transparent::Bundle>, + ) -> Blake2bHash { + let mut h = hasher(ZCASH_TRANSPARENT_SCRIPTS_HASH_PERSONALIZATION); + if let Some(bundle) = transparent_bundle { + for txin in &bundle.vin { + h.write_all(&txin.script_sig.0).unwrap(); + } + } + h.finalize() + } + + fn digest_sapling( + &self, + sapling_bundle: Option<&sapling::Bundle>, + ) -> Blake2bHash { + let mut h = hasher(ZCASH_SAPLING_SIGS_HASH_PERSONALIZATION); + if let Some(bundle) = sapling_bundle { + for spend in &bundle.shielded_spends { + h.write_all(&spend.zkproof).unwrap(); + spend.spend_auth_sig.write(&mut h).unwrap(); + } + + for output in &bundle.shielded_outputs { + h.write_all(&output.zkproof).unwrap(); + } + + bundle.authorization.binding_sig.write(&mut h).unwrap(); + } + h.finalize() + } + + fn digest_orchard( + &self, + orchard_bundle: Option<&orchard::Bundle>, + ) -> Self::OrchardDigest { + let mut h = hasher(ZCASH_ORCHARD_SIGS_HASH_PERSONALIZATION); + if let Some(bundle) = orchard_bundle { + h.write_all(bundle.authorization().proof().as_ref()) + .unwrap(); + for action in bundle.actions().iter() { + h.write_all(&<[u8; 64]>::from(action.authorization())) + .unwrap(); + } + h.write_all(&<[u8; 64]>::from( + bundle.authorization().binding_signature(), + )) + .unwrap(); + } + h.finalize() + } + + #[cfg(feature = "zfuture")] + fn digest_tze(&self, tze_bundle: Option<&tze::Bundle>) -> Blake2bHash { + let mut h = hasher(ZCASH_TZE_WITNESSES_HASH_PERSONALIZATION); + if let Some(bundle) = tze_bundle { + for tzein in &bundle.vin { + h.write_all(&tzein.witness.payload.0).unwrap(); + } + } + h.finalize() + } + + fn combine( + &self, + consensus_branch_id: Self::HeaderDigest, + transparent_digest: Self::TransparentDigest, + sapling_digest: Self::SaplingDigest, + orchard_digest: Self::OrchardDigest, + #[cfg(feature = "zfuture")] tze_digest: Self::TzeDigest, + ) -> Self::Digest { + let digests = [ + transparent_digest, + sapling_digest, + orchard_digest, + #[cfg(feature = "zfuture")] + tze_digest, + ]; + + let mut personal = [0; 16]; + (&mut personal[..12]).copy_from_slice(ZCASH_AUTH_PERSONALIZATION_PREFIX); + (&mut personal[12..]) + .write_u32::(consensus_branch_id.into()) + .unwrap(); + + let mut h = hasher(&personal); + for digest in &digests { + h.write_all(digest.as_bytes()).unwrap(); + } + + h.finalize() + } +} From 1a5aad723bb6b0387ae321db6d7dc4c3d320cee5 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 13 May 2021 00:16:40 -0600 Subject: [PATCH 06/26] Use generalized signature_hash for transaction builder. --- zcash_client_backend/src/address.rs | 1 + zcash_client_backend/src/decrypt.rs | 2 +- zcash_client_sqlite/src/lib.rs | 2 +- zcash_client_sqlite/src/wallet/transact.rs | 3 +- zcash_extensions/Cargo.toml | 2 +- zcash_extensions/src/consensus/transparent.rs | 2 +- zcash_primitives/src/transaction/builder.rs | 77 +++++++++---------- .../components/transparent/builder.rs | 35 +++++---- 8 files changed, 63 insertions(+), 61 deletions(-) diff --git a/zcash_client_backend/src/address.rs b/zcash_client_backend/src/address.rs index 8b144a3b71..f605d46371 100644 --- a/zcash_client_backend/src/address.rs +++ b/zcash_client_backend/src/address.rs @@ -8,6 +8,7 @@ use crate::encoding::{ }; /// An address that funds can be sent to. +// TODO: rename to ParsedAddress #[derive(Debug, PartialEq, Clone)] pub enum RecipientAddress { Shielded(PaymentAddress), diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index 7cce1e80ac..418db67080 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -44,7 +44,7 @@ pub fn decrypt_transaction( ) -> Vec { let mut decrypted = vec![]; - if let Some(bundle) = tx.sapling_bundle().as_ref() { + if let Some(bundle) = tx.sapling_bundle() { for (account, extfvk) in extfvks.iter() { let ivk = extfvk.fvk.vk.ivk(); let ovk = extfvk.fvk.ovk; diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index fb8fdc7a65..e7fdafc63c 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -489,7 +489,7 @@ 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. - if let Some(bundle) = sent_tx.tx.sapling_bundle().as_ref() { + if let Some(bundle) = sent_tx.tx.sapling_bundle() { for spend in &bundle.shielded_spends { wallet::mark_spent(up, tx_ref, &spend.nullifier)?; } diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs index 62071d37be..ac9dfcdebc 100644 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ b/zcash_client_sqlite/src/wallet/transact.rs @@ -631,8 +631,7 @@ mod tests { ) .unwrap(); - let output = - &tx.sapling_bundle().as_ref().unwrap().shielded_outputs[output_index as usize]; + let output = &tx.sapling_bundle().unwrap().shielded_outputs[output_index as usize]; try_sapling_output_recovery( &network, diff --git a/zcash_extensions/Cargo.toml b/zcash_extensions/Cargo.toml index 4280a8efce..5d4e777046 100644 --- a/zcash_extensions/Cargo.toml +++ b/zcash_extensions/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" [dependencies] blake2b_simd = "0.5" -zcash_primitives = { version = "0.5", path = "../zcash_primitives", features = ["zfuture"] } +zcash_primitives = { version = "0.5", path = "../zcash_primitives", features = ["zfuture", "test-dependencies"] } [dev-dependencies] ff = "0.10" diff --git a/zcash_extensions/src/consensus/transparent.rs b/zcash_extensions/src/consensus/transparent.rs index 7b975dc1c9..6c5a4b4554 100644 --- a/zcash_extensions/src/consensus/transparent.rs +++ b/zcash_extensions/src/consensus/transparent.rs @@ -85,7 +85,7 @@ impl<'a> demo::Context for Context<'a> { } fn tx_tze_outputs(&self) -> &[TzeOut] { - if let Some(bundle) = &self.tx.tze_bundle() { + if let Some(bundle) = self.tx.tze_bundle() { &bundle.vout } else { &[] diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index eacdb1fb4e..fa41125b17 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -1,13 +1,15 @@ //! Structs for building transactions. -use rand::{rngs::OsRng, CryptoRng, RngCore}; use std::array; use std::error; use std::fmt; use std::io; use std::sync::mpsc::Sender; -use orchard::bundle::{self as orchard}; +#[cfg(not(feature = "zfuture"))] +use std::marker::PhantomData; + +use rand::{rngs::OsRng, CryptoRng, RngCore}; use crate::{ consensus::{self, BlockHeight, BranchId}, @@ -27,16 +29,13 @@ use crate::{ }, transparent::{self, builder::TransparentBuilder}, }, - sighash::{SignableInput, SIGHASH_ALL}, - sighash_v4::v4_signature_hash, + sighash::{signature_hash, SignableInput, SIGHASH_ALL}, + txid::TxIdDigester, Transaction, TransactionData, TxVersion, Unauthorized, }, zip32::ExtendedSpendingKey, }; -#[cfg(not(feature = "zfuture"))] -use std::marker::PhantomData; - #[cfg(feature = "transparent-inputs")] use crate::{legacy::Script, transaction::components::transparent::TxOut}; @@ -280,11 +279,6 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { /// /// Upon success, returns a tuple containing the final transaction, and the /// [`SaplingMetadata`] generated during the build process. - /// - /// `consensus_branch_id` must be valid for the block height that this transaction is - /// targeting. An invalid `consensus_branch_id` will *not* result in an error from - /// this function, and instead will generate a transaction that will be rejected by - /// the network. pub fn build( mut self, prover: &impl TxProver, @@ -343,7 +337,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { let unauthed_tx = TransactionData { version, - consensus_branch_id, + consensus_branch_id: BranchId::for_height(&self.params, self.target_height), lock_time: 0, expiry_height: self.expiry_height, transparent_bundle, @@ -357,18 +351,32 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { // // Signatures -- everything but the signatures must already have been added. // - - let mut sighash = [0u8; 32]; - sighash.copy_from_slice( - &v4_signature_hash(&unauthed_tx, SignableInput::Shielded, SIGHASH_ALL).as_ref(), - ); + let txid_parts = unauthed_tx.digest(TxIdDigester); #[cfg(feature = "transparent-inputs")] - let transparent_sigs = self.transparent_builder.create_signatures(&unauthed_tx); + let transparent_sigs = self + .transparent_builder + .create_signatures(&unauthed_tx, &txid_parts); + + // the commitment being signed is shared across all Sapling inputs; once + // V4 transactions are deprecated this should just be the txid, but + // for now we need to continue to compute it here. + let shielded_sig_commitment = signature_hash( + &unauthed_tx, + SignableInput::Shielded, + &txid_parts, + SIGHASH_ALL, + ); let sapling_sigs = self .sapling_builder - .create_signatures(prover, &mut ctx, &mut self.rng, &sighash, &tx_metadata) + .create_signatures( + prover, + &mut ctx, + &mut self.rng, + shielded_sig_commitment.as_ref(), + &tx_metadata, + ) .map_err(Error::SaplingBuild)?; #[cfg(feature = "zfuture")] @@ -380,10 +388,9 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { Ok(( Self::apply_signatures( unauthed_tx, + sapling_sigs, #[cfg(feature = "transparent-inputs")] transparent_sigs, - sapling_sigs, - None, #[cfg(feature = "zfuture")] tze_witnesses, ) @@ -392,26 +399,18 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { )) } - fn apply_signatures( + pub fn apply_signatures( unauthed_tx: TransactionData, - #[cfg(feature = "transparent-inputs")] transparent_sigs: Option>, sapling_sigs: Option<(Vec, redjubjub::Signature)>, - _orchard_auth: Option<( - Vec<::SpendAuth>, - orchard::Authorized, - )>, + #[cfg(feature = "transparent-inputs")] transparent_sigs: Option>, #[cfg(feature = "zfuture")] tze_witnesses: Option>, ) -> io::Result { #[cfg(feature = "transparent-inputs")] let transparent_bundle = match (unauthed_tx.transparent_bundle, transparent_sigs) { (Some(bundle), Some(script_sigs)) => Some(bundle.apply_signatures(script_sigs)), (None, None) => None, - (b, s) => { - panic!( - "Mismatch between transparent bundle ({}) and signatures ({}).", - b.is_some(), - s.is_some() - ); + _ => { + panic!("Mismatch between transparent bundle and signatures."); } }; @@ -526,19 +525,19 @@ mod tests { transaction::components::{ amount::{Amount, DEFAULT_FEE}, sapling::builder::{self as build_s}, - transparent::builder::{self as transparent}, + transparent::builder::{self as build_t}, }, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, }; use super::{Builder, Error, SaplingBuilder, DEFAULT_TX_EXPIRY_DELTA}; - #[cfg(not(feature = "zfuture"))] - use std::marker::PhantomData; - #[cfg(feature = "zfuture")] use super::TzeBuilder; + #[cfg(not(feature = "zfuture"))] + use std::marker::PhantomData; + #[test] fn fails_on_negative_output() { let extsk = ExtendedSpendingKey::master(&[]); @@ -642,7 +641,7 @@ mod tests { &TransparentAddress::PublicKey([0; 20]), Amount::from_i64(-1).unwrap(), ), - Err(Error::TransparentBuild(transparent::Error::InvalidAmount)) + Err(Error::TransparentBuild(build_t::Error::InvalidAmount)) ); } diff --git a/zcash_primitives/src/transaction/components/transparent/builder.rs b/zcash_primitives/src/transaction/components/transparent/builder.rs index 3fbb3e111b..fee107e2a2 100644 --- a/zcash_primitives/src/transaction/components/transparent/builder.rs +++ b/zcash_primitives/src/transaction/components/transparent/builder.rs @@ -2,6 +2,9 @@ use std::fmt; +#[cfg(feature = "transparent-inputs")] +use blake2b_simd::Hash as Blake2bHash; + use crate::{ legacy::TransparentAddress, transaction::components::{ @@ -15,9 +18,8 @@ use crate::{ legacy::Script, transaction::{ components::OutPoint, - sighash::{SignableInput, SIGHASH_ALL}, - sighash_v4::v4_signature_hash, - TransactionData, Unauthorized, + sighash::{signature_hash, SignableInput, SIGHASH_ALL}, + TransactionData, TxDigests, Unauthorized, }, }; @@ -154,7 +156,11 @@ impl TransparentBuilder { } #[cfg(feature = "transparent-inputs")] - pub fn create_signatures(self, mtx: &TransactionData) -> Option> { + pub fn create_signatures( + self, + mtx: &TransactionData, + txid_parts_cache: &TxDigests, + ) -> Option> { if self.inputs.is_empty() && self.vout.is_empty() { None } else { @@ -163,18 +169,15 @@ impl TransparentBuilder { .iter() .enumerate() .map(|(i, info)| { - let mut sighash = [0u8; 32]; - sighash.copy_from_slice( - &v4_signature_hash( - mtx, - SignableInput::transparent( - i, - &info.coin.script_pubkey, - info.coin.value, - ), - SIGHASH_ALL, - ) - .as_ref(), + let sighash = signature_hash( + mtx, + SignableInput::transparent( + i, + &info.coin.script_pubkey, + info.coin.value, + ), + txid_parts_cache, + SIGHASH_ALL, ); let msg = From e828dbf5d0792f54c805a6f76f392c262e7af806 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 13 May 2021 12:45:21 -0600 Subject: [PATCH 07/26] Add v5 parsing and serialization for Sapling components. --- .../src/transaction/components/sapling.rs | 246 +++++++++++++++--- zcash_primitives/src/transaction/mod.rs | 4 +- .../src/transaction/sighash_v4.rs | 2 +- 3 files changed, 213 insertions(+), 39 deletions(-) diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 745bfee1c6..3bdd074b34 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 ed1285bddc..6b1fe0360e 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 b106254eb4..5b5cd44f1b 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) From 38b864c1004ded43616ec88aa4c2eb34c114bd2b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 13 May 2021 14:11:20 -0600 Subject: [PATCH 08/26] Implement V5 transaction serialization & roundtrip property tests. --- .../src/transaction/components/orchard.rs | 36 ++ .../src/transaction/components/sapling.rs | 21 +- zcash_primitives/src/transaction/mod.rs | 434 +++++++++++++++--- zcash_primitives/src/transaction/sighash.rs | 2 +- zcash_primitives/src/transaction/tests.rs | 104 ++++- .../src/transaction/util/sha256d.rs | 4 + 6 files changed, 525 insertions(+), 76 deletions(-) diff --git a/zcash_primitives/src/transaction/components/orchard.rs b/zcash_primitives/src/transaction/components/orchard.rs index 9b7357f626..a6d274f055 100644 --- a/zcash_primitives/src/transaction/components/orchard.rs +++ b/zcash_primitives/src/transaction/components/orchard.rs @@ -179,3 +179,39 @@ pub fn write_flags(mut writer: W, flags: &Flags) -> io::Result<()> { pub fn write_anchor(mut writer: W, anchor: &Anchor) -> io::Result<()> { writer.write_all(&anchor.0) } + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::*; + + use orchard::bundle::{ + testing::{self as t_orch}, + Authorized, Bundle, + }; + + use crate::transaction::{ + components::amount::{testing::arb_amount, Amount}, + TxVersion, + }; + + prop_compose! { + pub fn arb_bundle()( + orchard_value_balance in arb_amount(), + bundle in t_orch::arb_bundle() + ) -> Bundle { + // overwrite the value balance, as we can't guarantee that the + // value doesn't exceed the MAX_MONEY bounds. + bundle.try_map_value_balance::<_, (), _>(|_| Ok(orchard_value_balance)).unwrap() + } + } + + pub fn arb_bundle_for_version( + v: TxVersion, + ) -> impl Strategy>> { + if v.has_orchard() { + Strategy::boxed(prop::option::of(arb_bundle())) + } else { + Strategy::boxed(Just(None)) + } + } +} diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 3bdd074b34..9433da4bd2 100644 --- a/zcash_primitives/src/transaction/components/sapling.rs +++ b/zcash_primitives/src/transaction/components/sapling.rs @@ -446,7 +446,10 @@ pub mod testing { redjubjub::{PrivateKey, PublicKey}, Nullifier, }, - transaction::components::{amount::testing::arb_amount, GROTH_PROOF_SIZE}, + transaction::{ + components::{amount::testing::arb_amount, GROTH_PROOF_SIZE}, + TxVersion, + }, }; use super::{Authorized, Bundle, OutputDescription, SpendDescription}; @@ -524,12 +527,12 @@ pub mod testing { 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 { + let mut rng = StdRng::from_seed(rng_seed); + let bsk = PrivateKey(jubjub::Fr::random(&mut rng)); + Some( Bundle { shielded_spends, @@ -541,4 +544,14 @@ pub mod testing { } } } + + pub fn arb_bundle_for_version( + v: TxVersion, + ) -> impl Strategy>> { + if v.has_sapling() { + Strategy::boxed(arb_bundle()) + } else { + Strategy::boxed(Just(None)) + } + } } diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index 6b1fe0360e..eb1a5fb39f 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -10,11 +10,18 @@ pub mod util; #[cfg(test)] mod tests; +use blake2b_simd::Hash as Blake2bHash; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use ff::PrimeField; +use nonempty::NonEmpty; +use std::convert::TryFrom; use std::fmt; +use std::fmt::Debug; use std::io::{self, Read, Write}; use std::ops::Deref; +use orchard::{self, primitives::redpallas}; + use crate::{ consensus::{BlockHeight, BranchId}, sapling::redjubjub, @@ -24,15 +31,19 @@ use crate::{ use self::{ components::{ amount::Amount, - sapling::{self, OutputDescription, SpendDescription}, + orchard as orchard_serialization, + sapling::{ + self, OutputDescription, OutputDescriptionV5, SpendDescription, SpendDescriptionV5, + }, sprout::{self, JsDescription}, transparent::{self, TxIn, TxOut}, }, + txid::{to_txid, BlockTxCommitmentDigester, TxIdDigester}, util::sha256d::{HashReader, HashWriter}, }; #[cfg(feature = "zfuture")] -use self::components::tze; +use self::components::tze::{self, TzeIn, TzeOut}; const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270; const OVERWINTER_TX_VERSION: u32 = 3; @@ -99,7 +110,7 @@ pub enum TxVersion { Sprout(u32), Overwinter, Sapling, - ZcashTxV5, + Zip225, #[cfg(feature = "zfuture")] ZFuture, } @@ -114,7 +125,7 @@ impl TxVersion { match (version, reader.read_u32::()?) { (OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID) => Ok(TxVersion::Overwinter), (SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID) => Ok(TxVersion::Sapling), - (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::ZcashTxV5), + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), #[cfg(feature = "zfuture")] (ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID) => Ok(TxVersion::ZFuture), _ => Err(io::Error::new( @@ -144,7 +155,7 @@ impl TxVersion { TxVersion::Sprout(v) => *v, TxVersion::Overwinter => OVERWINTER_TX_VERSION, TxVersion::Sapling => SAPLING_TX_VERSION, - TxVersion::ZcashTxV5 => V5_TX_VERSION, + TxVersion::Zip225 => V5_TX_VERSION, #[cfg(feature = "zfuture")] TxVersion::ZFuture => ZFUTURE_TX_VERSION, } @@ -155,7 +166,7 @@ impl TxVersion { TxVersion::Sprout(_) => 0, TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID, TxVersion::Sapling => SAPLING_VERSION_GROUP_ID, - TxVersion::ZcashTxV5 => V5_VERSION_GROUP_ID, + TxVersion::Zip225 => V5_VERSION_GROUP_ID, #[cfg(feature = "zfuture")] TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID, } @@ -173,7 +184,7 @@ impl TxVersion { match self { TxVersion::Sprout(v) => *v >= 2u32, TxVersion::Overwinter | TxVersion::Sapling => true, - TxVersion::ZcashTxV5 => false, + TxVersion::Zip225 => false, #[cfg(feature = "zfuture")] TxVersion::ZFuture => true, } @@ -187,7 +198,7 @@ impl TxVersion { match self { TxVersion::Sprout(_) | TxVersion::Overwinter => false, TxVersion::Sapling => true, - TxVersion::ZcashTxV5 => true, + TxVersion::Zip225 => true, #[cfg(feature = "zfuture")] TxVersion::ZFuture => true, } @@ -196,7 +207,7 @@ impl TxVersion { pub fn has_orchard(&self) -> bool { match self { TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => false, - TxVersion::ZcashTxV5 => true, + TxVersion::Zip225 => true, #[cfg(feature = "zfuture")] TxVersion::ZFuture => true, } @@ -214,7 +225,7 @@ impl TxVersion { BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => { TxVersion::Sapling } - BranchId::Nu5 => TxVersion::Sapling, //TEMPORARY WORKAROUND + BranchId::Nu5 => TxVersion::Zip225, //TEMPORARY WORKAROUND #[cfg(feature = "zfuture")] BranchId::ZFuture => TxVersion::ZFuture, } @@ -466,6 +477,17 @@ impl TransactionData { impl Transaction { fn from_data(data: TransactionData) -> io::Result { + match data.version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + Self::from_data_v4(data) + } + TxVersion::Zip225 => Ok(Self::from_data_v5(data)), + #[cfg(feature = "zfuture")] + TxVersion::ZFuture => Ok(Self::from_data_v5(data)), + } + } + + fn from_data_v4(data: TransactionData) -> io::Result { let mut tx = Transaction { txid: TxId([0; 32]), data, @@ -476,22 +498,44 @@ impl Transaction { Ok(tx) } + fn from_data_v5(data: TransactionData) -> Self { + let txid = to_txid( + data.version, + data.consensus_branch_id, + &data.digest(TxIdDigester), + ); + + Transaction { txid, data } + } + pub fn txid(&self) -> TxId { self.txid } - #[allow(clippy::redundant_closure)] pub fn read(reader: R, consensus_branch_id: BranchId) -> io::Result { let mut reader = HashReader::new(reader); let version = TxVersion::read(&mut reader)?; - let is_overwinter_v3 = version == TxVersion::Overwinter; - let is_sapling_v4 = version == TxVersion::Sapling; + match version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + Self::read_v4(reader, version, consensus_branch_id) + } + TxVersion::Zip225 => Self::read_v5(reader.into_base_reader(), version), + #[cfg(feature = "zfuture")] + TxVersion::ZFuture => Self::read_v5(reader.into_base_reader(), version), + } + } + #[allow(clippy::redundant_closure)] + fn read_v4( + mut reader: HashReader, + version: TxVersion, + consensus_branch_id: BranchId, + ) -> io::Result { let transparent_bundle = Self::read_transparent(&mut reader)?; let lock_time = reader.read_u32::()?; - let expiry_height: BlockHeight = if is_overwinter_v3 || is_sapling_v4 { + let expiry_height: BlockHeight = if version.has_overwinter() { reader.read_u32::()?.into() } else { 0u32.into() @@ -531,15 +575,17 @@ impl Transaction { None }; - let binding_sig = - if is_sapling_v4 && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { - Some(redjubjub::Signature::read(&mut reader)?) - } else { - None - }; + let binding_sig = if version.has_sapling() + && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) + { + Some(redjubjub::Signature::read(&mut reader)?) + } else { + None + }; let mut txid = [0; 32]; - txid.copy_from_slice(&reader.into_hash()); + let hash_bytes = reader.into_hash(); + txid.copy_from_slice(&hash_bytes); Ok(Transaction { txid: TxId(txid), @@ -582,16 +628,190 @@ impl Transaction { .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range")) } - pub fn write(&self, mut writer: W) -> io::Result<()> { - self.version.write(&mut writer)?; + fn read_v5(mut reader: R, version: TxVersion) -> io::Result { + let (consensus_branch_id, lock_time, expiry_height) = + Self::read_v5_header_fragment(&mut reader)?; + let transparent_bundle = Self::read_transparent(&mut reader)?; + let sapling_bundle = Self::read_v5_sapling(&mut reader)?; + let orchard_bundle = Self::read_v5_orchard(&mut reader)?; - let is_overwinter_v3 = self.version == TxVersion::Overwinter; - let is_sapling_v4 = self.version == TxVersion::Sapling; + #[cfg(feature = "zfuture")] + let tze_bundle = Self::read_tze(&mut reader)?; - self.write_transparent(&mut writer)?; + let data = TransactionData { + version, + consensus_branch_id, + lock_time, + expiry_height, + transparent_bundle, + sprout_bundle: None, + sapling_bundle, + orchard_bundle, + #[cfg(feature = "zfuture")] + tze_bundle, + }; + + Ok(Self::from_data_v5(data)) + } + + fn read_v5_header_fragment(mut reader: R) -> io::Result<(BranchId, u32, BlockHeight)> { + let consensus_branch_id = reader.read_u32::().and_then(|value| { + BranchId::try_from(value).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + "invalid consensus branch id: ".to_owned() + e, + ) + }) + })?; + let lock_time = reader.read_u32::()?; + let expiry_height: BlockHeight = reader.read_u32::()?.into(); + Ok((consensus_branch_id, lock_time, expiry_height)) + } + + #[allow(clippy::redundant_closure)] + fn read_v5_sapling( + mut reader: R, + ) -> io::Result>> { + let n_spends = CompactSize::read(&mut reader)?; + let sd_v5s = Vector::read_count(&mut reader, n_spends, SpendDescriptionV5::read)?; + let n_outputs = CompactSize::read(&mut reader)?; + let od_v5s = Vector::read_count(&mut reader, n_outputs, OutputDescriptionV5::read)?; + let value_balance = if n_spends > 0 || n_outputs > 0 { + Self::read_amount(&mut reader)? + } else { + Amount::zero() + }; + + let anchor = if n_spends > 0 { + Some(sapling::read_base(&mut reader, "anchor")?) + } else { + None + }; + + let v_spend_proofs = + Vector::read_count(&mut reader, n_spends, |r| sapling::read_zkproof(r))?; + let v_spend_auth_sigs = Vector::read_count(&mut reader, n_spends, |r| { + SpendDescription::read_spend_auth_sig(r) + })?; + let v_output_proofs = + Vector::read_count(&mut reader, n_outputs, |r| sapling::read_zkproof(r))?; + let binding_sig = if n_spends > 0 || n_outputs > 0 { + Some(redjubjub::Signature::read(&mut reader)?) + } else { + None + }; + + let shielded_spends = sd_v5s + .into_iter() + .zip( + v_spend_proofs + .into_iter() + .zip(v_spend_auth_sigs.into_iter()), + ) + .map(|(sd_5, (zkproof, spend_auth_sig))| { + // the following `unwrap` is safe because we know n_spends > 0. + sd_5.into_spend_description(anchor.unwrap(), zkproof, spend_auth_sig) + }) + .collect(); + + let shielded_outputs = od_v5s + .into_iter() + .zip(v_output_proofs.into_iter()) + .map(|(od_5, zkproof)| od_5.into_output_description(zkproof)) + .collect(); + + Ok(binding_sig.map(|binding_sig| sapling::Bundle { + value_balance, + shielded_spends, + shielded_outputs, + authorization: sapling::Authorized { binding_sig }, + })) + } + + fn read_v5_orchard( + mut reader: R, + ) -> io::Result>> { + let n_actions = CompactSize::read(&mut reader)?; + if n_actions == 0 { + Ok(None) + } else { + let actions_without_auth = Vector::read_count(&mut reader, n_actions, |r| { + orchard_serialization::read_action_without_auth(r) + })?; + let flags = orchard_serialization::read_flags(&mut reader)?; + let value_balance = Self::read_amount(&mut reader)?; + let anchor = orchard_serialization::read_anchor(&mut reader)?; + let proof_size = CompactSize::read(&mut reader)?; + let mut proof_bytes = vec![0u8; proof_size]; + reader.read_exact(&mut proof_bytes)?; + let spend_sigs = Vector::read_count(&mut reader, n_actions, |r| { + orchard_serialization::read_signature::<_, redpallas::SpendAuth>(r) + })?; + let binding_signature = + orchard_serialization::read_signature::<_, redpallas::Binding>(&mut reader) + .map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "An error occurred deserializing the Orchard binding signature: {}", + e + ), + ) + })?; + + let actions = NonEmpty::from_vec( + actions_without_auth + .into_iter() + .zip(spend_sigs.into_iter()) + .map(|(act, sig)| act.map(|_| sig)) + .collect(), + ) + .expect("A nonzero number of actions was read from the transaction data."); + + let authorization = orchard::bundle::Authorized::from_parts( + orchard::Proof::new(proof_bytes), + binding_signature, + ); + + Ok(Some(orchard::Bundle::from_parts( + actions, + flags, + value_balance, + anchor, + authorization, + ))) + } + } + + #[cfg(feature = "zfuture")] + fn read_tze(mut reader: &mut R) -> io::Result>> { + let vin = Vector::read(&mut reader, TzeIn::read)?; + let vout = Vector::read(&mut reader, TzeOut::read)?; + Ok(if vin.is_empty() && vout.is_empty() { + None + } else { + Some(tze::Bundle { vin, vout }) + }) + } + + pub fn write(&self, writer: W) -> io::Result<()> { + match self.version { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + self.write_v4(writer) + } + TxVersion::Zip225 => self.write_v5(writer), + #[cfg(feature = "zfuture")] + TxVersion::ZFuture => self.write_v5(writer), + } + } + + pub fn write_v4(&self, mut writer: W) -> io::Result<()> { + self.version.write(&mut writer)?; + + self.write_transparent(&mut writer)?; writer.write_u32::(self.lock_time)?; - if is_overwinter_v3 || is_sapling_v4 { + if self.version.has_overwinter() { writer.write_u32::(u32::from(self.expiry_height))?; } @@ -638,10 +858,12 @@ impl Transaction { if let Some(bundle) = self.sapling_bundle.as_ref() { bundle.authorization.binding_sig.write(&mut writer)?; } - } else if self.sapling_bundle.is_some() { + } + + if self.orchard_bundle.is_some() { return Err(io::Error::new( io::ErrorKind::InvalidInput, - "Sapling components may not be present if Sapling is not active.", + "Orchard components may not be present when serializing to the V4 transaction format." )); } @@ -660,6 +882,101 @@ impl Transaction { Ok(()) } + pub fn write_v5(&self, mut writer: W) -> io::Result<()> { + self.write_v5_header(&mut writer)?; + self.write_transparent(&mut writer)?; + self.write_v5_sapling(&mut writer)?; + self.write_v5_orchard(&mut writer)?; + #[cfg(feature = "zfuture")] + self.write_tze(&mut writer)?; + Ok(()) + } + + pub fn write_v5_header(&self, mut writer: W) -> io::Result<()> { + self.version.write(&mut writer)?; + writer.write_u32::(u32::from(self.consensus_branch_id))?; + writer.write_u32::(self.lock_time)?; + writer.write_u32::(u32::from(self.expiry_height))?; + Ok(()) + } + + pub fn write_v5_sapling(&self, mut writer: W) -> io::Result<()> { + if let Some(bundle) = &self.sapling_bundle { + CompactSize::write(&mut writer, bundle.shielded_spends.len())?; + Vector::write_items(&mut writer, &bundle.shielded_spends, |w, e| { + e.write_v5_without_witness_data(w) + })?; + + CompactSize::write(&mut writer, bundle.shielded_outputs.len())?; + Vector::write_items(&mut writer, &bundle.shielded_outputs, |w, e| { + e.write_v5_without_proof(w) + })?; + + if !(bundle.shielded_spends.is_empty() && bundle.shielded_outputs.is_empty()) { + writer.write_all(&bundle.value_balance.to_i64_le_bytes())?; + } + if !bundle.shielded_spends.is_empty() { + writer.write_all(bundle.shielded_spends[0].anchor.to_repr().as_ref())?; + } + + Vector::write_items( + &mut writer, + bundle.shielded_spends.iter().map(|s| s.zkproof), + |w, e| w.write_all(e), + )?; + Vector::write_items( + &mut writer, + bundle.shielded_spends.iter().map(|s| s.spend_auth_sig), + |w, e| e.write(w), + )?; + + Vector::write_items( + &mut writer, + bundle.shielded_outputs.iter().map(|s| s.zkproof), + |w, e| w.write_all(e), + )?; + + if !bundle.shielded_spends.is_empty() || !bundle.shielded_outputs.is_empty() { + bundle.authorization.binding_sig.write(&mut writer)?; + } + } else { + CompactSize::write(&mut writer, 0)?; + CompactSize::write(&mut writer, 0)?; + } + + Ok(()) + } + + pub fn write_v5_orchard(&self, mut writer: W) -> io::Result<()> { + if let Some(bundle) = &self.orchard_bundle { + CompactSize::write(&mut writer, bundle.actions().len())?; + Vector::write_items(&mut writer, bundle.actions().iter(), |w, a| { + orchard_serialization::write_action_without_auth(w, a) + })?; + + if !bundle.actions().is_empty() { + orchard_serialization::write_flags(&mut writer, &bundle.flags())?; + writer.write_all(&bundle.value_balance().to_i64_le_bytes())?; + orchard_serialization::write_anchor(&mut writer, bundle.anchor())?; + let proof_bytes: &[u8] = bundle.authorization().proof().as_ref(); + CompactSize::write(&mut writer, proof_bytes.len())?; + writer.write_all(&proof_bytes)?; + Vector::write_items( + &mut writer, + bundle.actions().iter().map(|a| a.authorization()), + |w, auth| w.write_all(&<[u8; 64]>::from(*auth)), + )?; + writer.write_all(&<[u8; 64]>::from( + bundle.authorization().binding_signature(), + ))?; + } + } else { + CompactSize::write(&mut writer, 0)?; + } + + Ok(()) + } + #[cfg(feature = "zfuture")] pub fn write_tze(&self, mut writer: W) -> io::Result<()> { if let Some(bundle) = &self.tze_bundle { @@ -672,6 +989,11 @@ impl Transaction { Ok(()) } + + // TODO: should this be moved to `from_data` and stored? + pub fn auth_commitment(&self) -> Result { + Ok(self.data.digest(BlockTxCommitmentDigester)) + } } #[derive(Clone)] @@ -753,39 +1075,35 @@ pub enum DigestError { #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::prelude::*; - use proptest::sample::select; use crate::consensus::BranchId; use super::{ - components::transparent::testing as transparent, Authorized, Transaction, TransactionData, - TxId, TxVersion, + components::{ + orchard::testing::{self as orchard}, + sapling::testing::{self as sapling}, + transparent::testing::{self as transparent}, + }, + Authorized, Transaction, TransactionData, TxId, TxVersion, }; + #[cfg(feature = "zfuture")] + use super::components::tze::testing::{self as tze}; + pub fn arb_txid() -> impl Strategy { prop::array::uniform32(any::()).prop_map(TxId::from_bytes) } - pub fn arb_branch_id() -> impl Strategy { - select(vec![ - BranchId::Sprout, - BranchId::Overwinter, - BranchId::Sapling, - BranchId::Blossom, - BranchId::Heartwood, - BranchId::Canopy, - #[cfg(feature = "zfuture")] - BranchId::ZFuture, - ]) - } - pub fn arb_tx_version(branch_id: BranchId) -> impl Strategy { match branch_id { BranchId::Sprout => (1..=2u32).prop_map(TxVersion::Sprout).boxed(), BranchId::Overwinter => Just(TxVersion::Overwinter).boxed(), - //#[cfg(feature = "zfuture")] - //BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(), - _otherwise => Just(TxVersion::Sapling).boxed(), + BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => { + Just(TxVersion::Sapling).boxed() + } + BranchId::Nu5 => Just(TxVersion::Zip225).boxed(), + #[cfg(feature = "zfuture")] + BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(), } } @@ -793,9 +1111,13 @@ pub mod testing { prop_compose! { pub fn arb_txdata(consensus_branch_id: BranchId)( version in arb_tx_version(consensus_branch_id), + )( lock_time in any::(), expiry_height in any::(), transparent_bundle in transparent::arb_bundle(), + sapling_bundle in sapling::arb_bundle_for_version(version), + orchard_bundle in orchard::arb_bundle_for_version(version), + version in Just(version) ) -> TransactionData { TransactionData { version, @@ -804,8 +1126,8 @@ pub mod testing { expiry_height: expiry_height.into(), transparent_bundle, sprout_bundle: None, - sapling_bundle: None, //FIXME - orchard_bundle: None, //FIXME + sapling_bundle, + orchard_bundle } } } @@ -814,10 +1136,14 @@ pub mod testing { prop_compose! { pub fn arb_txdata(consensus_branch_id: BranchId)( version in arb_tx_version(consensus_branch_id), + )( lock_time in any::(), expiry_height in any::(), transparent_bundle in transparent::arb_bundle(), - //tze_bundle in tze::arb_bundle(branch_id), + sapling_bundle in sapling::arb_bundle_for_version(version), + orchard_bundle in orchard::arb_bundle_for_version(version), + tze_bundle in tze::arb_bundle(consensus_branch_id), + version in Just(version) ) -> TransactionData { TransactionData { version, @@ -826,9 +1152,9 @@ pub mod testing { expiry_height: expiry_height.into(), transparent_bundle, sprout_bundle: None, - sapling_bundle: None, //FIXME - orchard_bundle: None, //FIXME - tze_bundle: None + sapling_bundle, + orchard_bundle, + tze_bundle } } } diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs index 864ccb0dc2..2ec210e6d8 100644 --- a/zcash_primitives/src/transaction/sighash.rs +++ b/zcash_primitives/src/transaction/sighash.rs @@ -128,7 +128,7 @@ pub fn signature_hash< v4_signature_hash(tx, signable_input, hash_type) } - TxVersion::ZcashTxV5 => v5_signature_hash(tx, txid_parts, signable_input, hash_type), + TxVersion::Zip225 => v5_signature_hash(tx, txid_parts, signable_input, hash_type), #[cfg(feature = "zfuture")] TxVersion::ZFuture => v5_signature_hash(tx, txid_parts, signable_input, hash_type), diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index 2642051d9b..2352487014 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -1,13 +1,14 @@ +use std::ops::Deref; + use proptest::prelude::*; use crate::consensus::BranchId; use super::{ - components::Amount, sighash::SignableInput, sighash_v4::v4_signature_hash, Transaction, + components::Amount, sighash::SignableInput, sighash_v4::v4_signature_hash, testing::arb_tx, + Transaction, }; -use super::testing::{arb_branch_id, arb_tx}; - #[test] fn tx_read_write() { let data = &self::data::tx_read_write::TX_READ_WRITE; @@ -22,20 +23,89 @@ fn tx_read_write() { assert_eq!(&data[..], &encoded[..]); } +fn check_roundtrip(branch_id: BranchId, tx: Transaction) -> Result<(), TestCaseError> { + let mut txn_bytes = vec![]; + tx.write(&mut txn_bytes).unwrap(); + let txo = Transaction::read(&txn_bytes[..], branch_id).unwrap(); + + prop_assert_eq!(tx.version, txo.version); + #[cfg(feature = "zfuture")] + prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref()); + prop_assert_eq!(tx.lock_time, txo.lock_time); + prop_assert_eq!( + tx.transparent_bundle.as_ref(), + txo.transparent_bundle.as_ref() + ); + prop_assert_eq!(tx.sapling_value_balance(), txo.sapling_value_balance()); + prop_assert_eq!( + tx.orchard_bundle.as_ref().map(|v| *v.value_balance()), + txo.orchard_bundle.as_ref().map(|v| *v.value_balance()) + ); + Ok(()) +} + +proptest! { + #[test] + #[ignore] + fn tx_serialization_roundtrip_sprout(tx in arb_tx(BranchId::Sprout)) { + check_roundtrip(BranchId::Sprout, tx)?; + } +} + +proptest! { + #[test] + #[ignore] + fn tx_serialization_roundtrip_overwinter(tx in arb_tx(BranchId::Overwinter)) { + check_roundtrip(BranchId::Overwinter, tx)?; + } +} + +proptest! { + #[test] + #[ignore] + fn tx_serialization_roundtrip_sapling(tx in arb_tx(BranchId::Sapling)) { + check_roundtrip(BranchId::Sapling, tx)?; + } +} + +proptest! { + #[test] + #[ignore] + fn tx_serialization_roundtrip_blossom(tx in arb_tx(BranchId::Blossom)) { + check_roundtrip(BranchId::Blossom, tx)?; + } +} + +proptest! { + #[test] + #[ignore] + fn tx_serialization_roundtrip_heartwood(tx in arb_tx(BranchId::Heartwood)) { + check_roundtrip(BranchId::Heartwood, tx)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(20))] + #[test] + fn tx_serialization_roundtrip_canopy(tx in arb_tx(BranchId::Canopy)) { + check_roundtrip(BranchId::Canopy, tx)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(20))] + #[test] + fn tx_serialization_roundtrip_nu5(tx in arb_tx(BranchId::Nu5)) { + check_roundtrip(BranchId::Nu5, tx)?; + } +} + +#[cfg(feature = "zfuture")] proptest! { #[test] - fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) { - let mut txn_bytes = vec![]; - tx.write(&mut txn_bytes).unwrap(); - - let txo = Transaction::read(&txn_bytes[..], BranchId::Canopy).unwrap(); - - prop_assert_eq!(tx.version, txo.version); - prop_assert_eq!(tx.lock_time, txo.lock_time); - prop_assert_eq!(tx.transparent_bundle.as_ref(), txo.transparent_bundle.as_ref()); - prop_assert_eq!(tx.sapling_value_balance(), txo.sapling_value_balance()); - #[cfg(feature = "zfuture")] - prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref()); + #[ignore] + fn tx_serialization_roundtrip_future(tx in arb_tx(BranchId::ZFuture)) { + check_roundtrip(BranchId::ZFuture, tx)?; } } @@ -54,7 +124,7 @@ fn zip_0143() { }; assert_eq!( - v4_signature_hash(&tx, signable_input, tv.hash_type).as_ref(), + v4_signature_hash(tx.deref(), signable_input, tv.hash_type).as_ref(), tv.sighash ); } @@ -74,7 +144,7 @@ fn zip_0243() { }; assert_eq!( - v4_signature_hash(&tx, signable_input, tv.hash_type).as_ref(), + v4_signature_hash(tx.deref(), signable_input, tv.hash_type).as_ref(), tv.sighash ); } diff --git a/zcash_primitives/src/transaction/util/sha256d.rs b/zcash_primitives/src/transaction/util/sha256d.rs index bc23db4f85..0852b3d6c2 100644 --- a/zcash_primitives/src/transaction/util/sha256d.rs +++ b/zcash_primitives/src/transaction/util/sha256d.rs @@ -16,6 +16,10 @@ impl HashReader { } } + pub fn into_base_reader(self) -> R { + self.reader + } + /// Destroy this reader and return the hash of what was read. pub fn into_hash(self) -> Output { Sha256::digest(&self.hasher.finalize()) From dac68ce2aae92ad529e1b482db84e808081a28f4 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 18 May 2021 15:52:42 -0600 Subject: [PATCH 09/26] Drop proptest space size to reduce test runtime. --- zcash_primitives/src/transaction/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index 2352487014..b361fe50f6 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -85,7 +85,7 @@ proptest! { } proptest! { - #![proptest_config(ProptestConfig::with_cases(20))] + #![proptest_config(ProptestConfig::with_cases(10))] #[test] fn tx_serialization_roundtrip_canopy(tx in arb_tx(BranchId::Canopy)) { check_roundtrip(BranchId::Canopy, tx)?; @@ -93,7 +93,7 @@ proptest! { } proptest! { - #![proptest_config(ProptestConfig::with_cases(20))] + #![proptest_config(ProptestConfig::with_cases(10))] #[test] fn tx_serialization_roundtrip_nu5(tx in arb_tx(BranchId::Nu5)) { check_roundtrip(BranchId::Nu5, tx)?; From ab1b31ebf6a990895fe70daa3f0aadc466116c55 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 3 Jun 2021 17:27:15 -0600 Subject: [PATCH 10/26] Store partial authorizing data for transparent txs in transparent authorization. --- zcash_primitives/src/transaction/builder.rs | 33 ++--- .../src/transaction/components/transparent.rs | 41 +----- .../components/transparent/builder.rs | 127 ++++++++++++------ zcash_primitives/src/transaction/mod.rs | 8 +- 4 files changed, 104 insertions(+), 105 deletions(-) diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index fa41125b17..149828cab0 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -37,7 +37,7 @@ use crate::{ }; #[cfg(feature = "transparent-inputs")] -use crate::{legacy::Script, transaction::components::transparent::TxOut}; +use crate::transaction::components::transparent::TxOut; #[cfg(feature = "zfuture")] use crate::{ @@ -353,10 +353,14 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { // let txid_parts = unauthed_tx.digest(TxIdDigester); - #[cfg(feature = "transparent-inputs")] - let transparent_sigs = self - .transparent_builder - .create_signatures(&unauthed_tx, &txid_parts); + let transparent_bundle = unauthed_tx.transparent_bundle.clone().map(|b| { + b.apply_signatures( + #[cfg(feature = "transparent-inputs")] + &unauthed_tx, + #[cfg(feature = "transparent-inputs")] + &txid_parts, + ) + }); // the commitment being signed is shared across all Sapling inputs; once // V4 transactions are deprecated this should just be the txid, but @@ -389,8 +393,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { Self::apply_signatures( unauthed_tx, sapling_sigs, - #[cfg(feature = "transparent-inputs")] - transparent_sigs, + transparent_bundle, #[cfg(feature = "zfuture")] tze_witnesses, ) @@ -402,23 +405,9 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> { pub fn apply_signatures( unauthed_tx: TransactionData, sapling_sigs: Option<(Vec, redjubjub::Signature)>, - #[cfg(feature = "transparent-inputs")] transparent_sigs: Option>, + transparent_bundle: Option>, #[cfg(feature = "zfuture")] tze_witnesses: Option>, ) -> io::Result { - #[cfg(feature = "transparent-inputs")] - let transparent_bundle = match (unauthed_tx.transparent_bundle, transparent_sigs) { - (Some(bundle), Some(script_sigs)) => Some(bundle.apply_signatures(script_sigs)), - (None, None) => None, - _ => { - panic!("Mismatch between transparent bundle and signatures."); - } - }; - - #[cfg(not(feature = "transparent-inputs"))] - let transparent_bundle = unauthed_tx - .transparent_bundle - .map(|b| b.apply_signatures(vec![])); - 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)) diff --git a/zcash_primitives/src/transaction/components/transparent.rs b/zcash_primitives/src/transaction/components/transparent.rs index 6b1261be78..59059dbb96 100644 --- a/zcash_primitives/src/transaction/components/transparent.rs +++ b/zcash_primitives/src/transaction/components/transparent.rs @@ -15,13 +15,6 @@ pub trait Authorization: Debug { type ScriptSig: Debug + Clone + PartialEq; } -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Unauthorized; - -impl Authorization for Unauthorized { - type ScriptSig = (); -} - #[derive(Debug, Copy, Clone, PartialEq)] pub struct Authorized; @@ -33,25 +26,7 @@ impl Authorization for Authorized { pub struct Bundle { pub vin: Vec>, pub vout: Vec, -} - -impl Bundle { - pub fn apply_signatures(self, script_sigs: Vec