From 55d1090f7080e6e91c495738a090cffcb5834df2 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 12 May 2021 17:01:17 -0600 Subject: [PATCH] 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 | 138 +---- .../src/transaction/sighash_v5.rs | 182 ++++++ zcash_primitives/src/transaction/tests.rs | 18 +- zcash_primitives/src/transaction/txid.rs | 585 ++++++++++++++++++ 15 files changed, 1197 insertions(+), 287 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 c8d598875..ea3c05f13 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 7374ffd15..7cce1e80a 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 52e0bc7bd..fb8fdc7a6 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 73e7ffd23..402b91784 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 b4a737027..62071d37b 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 c9bde5d21..7b975dc1c 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 fcd493b6b..b98342353 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 b7987d203..eacdb1fb4 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 2e34c8862..3fbb3e111 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 bce3ec4ea..ed1285bdd 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 000000000..864ccb0dc --- /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 fa6ceedf4..b106254eb 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::Shielded => (), + #[cfg(feature = "zfuture")] + SignableInput::Tze(_) => { + panic!("A request has been made to sign a TZE input, but the transaction version is not ZFuture"); + } } - 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 000000000..54984fdbc --- /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 b2451b4e7..2642051d9 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 000000000..498e0bb79 --- /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() + } +}