diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index b3a4b753e34..73f3553d5be 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -29,6 +29,7 @@ use serde::{Deserialize, Serialize}; use crate::{ amount::NegativeAllowed, + block::merkle::AuthDataRoot, fmt::DisplayToDebug, orchard, parameters::{Network, NetworkUpgrade}, @@ -198,6 +199,14 @@ impl Block { Ok(transaction_value_balance_total.neg()) } + + /// Compute the root of the authorizing data Merkle tree, + /// as defined in [ZIP-244]. + /// + /// [ZIP-244]: https://zips.z.cash/zip-0244 + pub fn auth_data_root(&self) -> AuthDataRoot { + self.transactions.iter().collect::() + } } impl<'a> From<&'a Block> for Hash { diff --git a/zebra-chain/src/block/merkle.rs b/zebra-chain/src/block/merkle.rs index 28467ea41ab..cf95737eb5f 100644 --- a/zebra-chain/src/block/merkle.rs +++ b/zebra-chain/src/block/merkle.rs @@ -1,6 +1,8 @@ //! The Bitcoin-inherited Merkle tree of transactions. #![allow(clippy::unit_arg)] +use std::convert::TryInto; +use std::iter; use std::{fmt, io::Write}; #[cfg(any(any(test, feature = "proptest-impl"), feature = "proptest-impl"))] @@ -12,6 +14,9 @@ use crate::transaction::{self, Transaction}; /// The root of the Bitcoin-inherited transaction Merkle tree, binding the /// block header to the transactions in the block. /// +/// Note: for V5-onward transactions it does not bind to authorizing data +/// (signature and proofs) which makes it non-malleable [ZIP-244]. +/// /// Note that because of a flaw in Bitcoin's design, the `merkle_root` does /// not always precisely bind the contents of the block (CVE-2012-2459). It /// is sometimes possible for an attacker to create multiple distinct sets of @@ -61,6 +66,8 @@ use crate::transaction::{self, Transaction}; /// This vulnerability does not apply to Zebra, because it does not store invalid /// data on disk, and because it does not permanently fail blocks or use an /// aggressive anti-DoS mechanism. +/// +/// [ZIP-244]: https://zips.z.cash/zip-0244 #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub struct Root(pub [u8; 32]); @@ -78,6 +85,22 @@ fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] { w.finish() } +fn auth_data_hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] { + // > Non-leaf hashes in this tree are BLAKE2b-256 hashes personalized by + // > the string "ZcashAuthDatHash". + // https://zips.z.cash/zip-0244#block-header-changes + blake2b_simd::Params::new() + .hash_length(32) + .personal(b"ZcashAuthDatHash") + .to_state() + .update(h1) + .update(h2) + .finalize() + .as_bytes() + .try_into() + .expect("32 byte array") +} + impl std::iter::FromIterator for Root where T: std::convert::AsRef, @@ -99,7 +122,6 @@ impl std::iter::FromIterator for Root { I: IntoIterator, { let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::>(); - while hashes.len() > 1 { hashes = hashes .chunks(2) @@ -110,6 +132,76 @@ impl std::iter::FromIterator for Root { }) .collect(); } + Self(hashes[0]) + } +} + +/// The root of the authorizing data Merkle tree, binding the +/// block header to the authorizing data of the block (signatures, proofs) +/// as defined in [ZIP-244]. +/// +/// See [`Root`] for an important disclaimer. +/// +/// [ZIP-244]: https://zips.z.cash/zip-0244 +#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +pub struct AuthDataRoot(pub(crate) [u8; 32]); + +impl fmt::Debug for AuthDataRoot { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("AuthRoot") + .field(&hex::encode(&self.0)) + .finish() + } +} + +impl std::iter::FromIterator for AuthDataRoot +where + T: std::convert::AsRef, +{ + fn from_iter(transactions: I) -> Self + where + I: IntoIterator, + { + // > For transaction versions before v5, a placeholder value consisting + // > of 32 bytes of 0xFF is used in place of the authorizing data commitment. + // > This is only used in the tree committed to by hashAuthDataRoot. + // https://zips.z.cash/zip-0244#authorizing-data-commitment + transactions + .into_iter() + .map(|tx| { + tx.as_ref() + .auth_digest() + .unwrap_or_else(|| transaction::AuthDigest([0xFF; 32])) + }) + .collect() + } +} + +impl std::iter::FromIterator for AuthDataRoot { + fn from_iter(hashes: I) -> Self + where + I: IntoIterator, + { + let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::>(); + // > This new commitment is named hashAuthDataRoot and is the root of a + // > binary Merkle tree of transaction authorizing data commitments [...] + // > padded with leaves having the "null" hash value [0u8; 32]. + // https://zips.z.cash/zip-0244#block-header-changes + // Pad with enough leaves to make the tree full (a power of 2). + let pad_count = hashes.len().next_power_of_two() - hashes.len(); + hashes.extend(iter::repeat([0u8; 32]).take(pad_count)); + assert!(hashes.len().is_power_of_two()); + + while hashes.len() > 1 { + hashes = hashes + .chunks(2) + .map(|chunk| match chunk { + [h1, h2] => auth_data_hash(h1, h2), + _ => unreachable!("number of nodes is always even since tree is full"), + }) + .collect(); + } Self(hashes[0]) } @@ -119,7 +211,7 @@ impl std::iter::FromIterator for Root { mod tests { use super::*; - use crate::{block::Block, serialization::ZcashDeserialize}; + use crate::{block::Block, serialization::ZcashDeserialize, transaction::AuthDigest}; #[test] fn block_test_vectors() { @@ -140,4 +232,51 @@ mod tests { ); } } + + #[test] + fn auth_digest() { + for block_bytes in zebra_test::vectors::BLOCKS.iter() { + let block = Block::zcash_deserialize(&**block_bytes).unwrap(); + let _auth_root = block.transactions.iter().collect::(); + // No test vectors for now, so just check it computes without panicking + } + } + + #[test] + fn auth_data_padding() { + // Compute the root of a 3-leaf tree with arbitrary leaves + let mut v = vec![ + AuthDigest([0x42; 32]), + AuthDigest([0xAA; 32]), + AuthDigest([0x77; 32]), + ]; + let root_3 = v.iter().copied().collect::(); + + // Compute the root a 4-leaf tree with the same leaves as before and + // an additional all-zeroes leaf. + // Since this is the same leaf used as padding in the previous tree, + // then both trees must have the same root. + v.push(AuthDigest([0x00; 32])); + let root_4 = v.iter().copied().collect::(); + + assert_eq!(root_3, root_4); + } + + #[test] + fn auth_data_pre_v5() { + // Compute the AuthDataRoot for a single transaction of an arbitrary pre-V5 block + let block = + Block::zcash_deserialize(&**zebra_test::vectors::BLOCK_MAINNET_1046400_BYTES).unwrap(); + let auth_root = block.transactions.iter().take(1).collect::(); + + // Compute the AuthDataRoot with a single [0xFF; 32] digest. + // Since ZIP-244 specifies that this value must be used as the auth digest of + // pre-V5 transactions, then the roots must match. + let expect_auth_root = vec![AuthDigest([0xFF; 32])] + .iter() + .copied() + .collect::(); + + assert_eq!(auth_root, expect_auth_root); + } } diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index d1f1a6a8781..2c22404825c 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -11,7 +11,7 @@ use crate::{ amount::{Amount, NonNegative}, parameters::NetworkUpgrade, serialization::ZcashSerialize, - transaction::{HashType, SigHash, Transaction}, + transaction::{AuthDigest, HashType, SigHash, Transaction}, transparent::{self, Script}, }; @@ -124,3 +124,25 @@ pub(crate) fn sighash( .as_ref(), ) } + +/// Compute the authorizing data commitment of this transaction as specified +/// in [ZIP-244]. +/// +/// # Panics +/// +/// If passed a pre-v5 transaction. +/// +/// [ZIP-244]: https://zips.z.cash/zip-0244. +pub(crate) fn auth_digest(trans: &Transaction) -> AuthDigest { + let alt_tx: zcash_primitives::transaction::Transaction = trans + .try_into() + .expect("zcash_primitives and Zebra transaction formats must be compatible"); + + let digest_bytes: [u8; 32] = alt_tx + .auth_commitment() + .as_ref() + .try_into() + .expect("digest has the correct size"); + + AuthDigest(digest_bytes) +} diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 3b84fcf224b..97d738fc052 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -3,6 +3,7 @@ use halo2::pasta::pallas; use serde::{Deserialize, Serialize}; +mod auth_digest; mod hash; mod joinsplit; mod lock_time; @@ -16,6 +17,7 @@ pub mod arbitrary; #[cfg(test)] mod tests; +pub use auth_digest::AuthDigest; pub use hash::Hash; pub use joinsplit::JoinSplitData; pub use lock_time::LockTime; @@ -160,6 +162,22 @@ impl Transaction { sighash::SigHasher::new(self, hash_type, network_upgrade, input).sighash() } + /// Compute the authorizing data commitment of this transaction as specified + /// in [ZIP-244]. + /// + /// Returns None for pre-v5 transactions. + /// + /// [ZIP-244]: https://zips.z.cash/zip-0244. + pub fn auth_digest(&self) -> Option { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + Transaction::V5 { .. } => Some(AuthDigest::from(self)), + } + } + // other properties /// Does this transaction have transparent or shielded inputs? diff --git a/zebra-chain/src/transaction/auth_digest.rs b/zebra-chain/src/transaction/auth_digest.rs new file mode 100644 index 00000000000..878d7d6d32f --- /dev/null +++ b/zebra-chain/src/transaction/auth_digest.rs @@ -0,0 +1,20 @@ +use crate::primitives::zcash_primitives::auth_digest; + +use super::Transaction; + +/// An authorizing data commitment hash as specified in [ZIP-244]. +/// +/// [ZIP-244]: https://zips.z.cash/zip-0244.. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct AuthDigest(pub(crate) [u8; 32]); + +impl<'a> From<&'a Transaction> for AuthDigest { + /// Computes the authorizing data commitment for a transaction. + /// + /// # Panics + /// + /// If passed a pre-v5 transaction. + fn from(transaction: &'a Transaction) -> Self { + auth_digest(transaction) + } +} diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index c011008396b..021d7976d34 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -467,6 +467,24 @@ fn zip244_txid() -> Result<()> { Ok(()) } +#[test] +fn zip244_auth_digest() -> Result<()> { + zebra_test::init(); + + for test in zip0244::TEST_VECTORS.iter() { + let transaction = test.tx.zcash_deserialize_into::()?; + let auth_digest = transaction.auth_digest(); + assert_eq!( + auth_digest + .expect("must have auth_digest since it must be a V5 transaction") + .0, + test.auth_digest + ); + } + + Ok(()) +} + #[test] fn test_vec143_1() -> Result<()> { zebra_test::init();