From 3552b2e44d8dbae729a7a762466460d3087bf6fd Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Mon, 9 Aug 2021 17:53:57 -0300 Subject: [PATCH 1/4] Implement ZIP-244 authorizing data commitment (auth_digest) --- zebra-chain/src/block.rs | 9 ++ zebra-chain/src/block/merkle.rs | 92 +++++++++++++++++-- .../src/primitives/zcash_primitives.rs | 22 ++++- zebra-chain/src/transaction.rs | 18 ++++ zebra-chain/src/transaction/auth_digest.rs | 20 ++++ zebra-chain/src/transaction/tests/vectors.rs | 18 ++++ 6 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 zebra-chain/src/transaction/auth_digest.rs 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..1f394823152 100644 --- a/zebra-chain/src/block/merkle.rs +++ b/zebra-chain/src/block/merkle.rs @@ -12,6 +12,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 +64,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 +83,23 @@ fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] { w.finish() } +/// Compute the root of a Merke tree as used in Bitcoin. +/// `hashes` must contain the hashes of the tree leaves. +/// The root is written to the the first element of the input vector. +/// See [`Root`] for an important disclaimer. +fn root(hashes: &mut Vec<[u8; 32]>) { + while hashes.len() > 1 { + *hashes = hashes + .chunks(2) + .map(|chunk| match chunk { + [h1, h2] => hash(h1, h2), + [h1] => hash(h1, h1), + _ => unreachable!("chunks(2)"), + }) + .collect(); + } +} + impl std::iter::FromIterator for Root where T: std::convert::AsRef, @@ -99,18 +121,59 @@ impl std::iter::FromIterator for Root { I: IntoIterator, { let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::>(); + root(&mut hashes); + Self(hashes[0]) + } +} - while hashes.len() > 1 { - hashes = hashes - .chunks(2) - .map(|chunk| match chunk { - [h1, h2] => hash(h1, h2), - [h1] => hash(h1, h1), - _ => unreachable!("chunks(2)"), - }) - .collect(); - } +/// 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 [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. + 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::>(); + root(&mut hashes); Self(hashes[0]) } } @@ -140,4 +203,13 @@ mod tests { ); } } + + #[test] + fn auth_digest() { + for block_bytes in zebra_test::vectors::BLOCKS.iter() { + let block = Block::zcash_deserialize(&**block_bytes).unwrap(); + let _merkle_root = block.transactions.iter().collect::(); + // No test vectors for now, so just check it works + } + } } diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index d1f1a6a8781..76352932b64 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,23 @@ 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..533b84f41a1 --- /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 [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(); From 5abb7d0ec2161a810656e9fce2808b9cf6dd513a Mon Sep 17 00:00:00 2001 From: Deirdre Connolly Date: Wed, 11 Aug 2021 22:09:18 -0400 Subject: [PATCH 2/4] s/Merke/Merkle/ --- zebra-chain/src/block/merkle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebra-chain/src/block/merkle.rs b/zebra-chain/src/block/merkle.rs index 1f394823152..cc0dea63083 100644 --- a/zebra-chain/src/block/merkle.rs +++ b/zebra-chain/src/block/merkle.rs @@ -83,7 +83,7 @@ fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] { w.finish() } -/// Compute the root of a Merke tree as used in Bitcoin. +/// Compute the root of a Merkle tree as used in Bitcoin. /// `hashes` must contain the hashes of the tree leaves. /// The root is written to the the first element of the input vector. /// See [`Root`] for an important disclaimer. From 5cd6df2797e2bd5b07200165331a31dbc8b1f7bd Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Thu, 12 Aug 2021 13:58:56 -0300 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Deirdre Connolly --- zebra-chain/src/block/merkle.rs | 10 +++++----- zebra-chain/src/primitives/zcash_primitives.rs | 2 ++ zebra-chain/src/transaction/auth_digest.rs | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/zebra-chain/src/block/merkle.rs b/zebra-chain/src/block/merkle.rs index cc0dea63083..694d1f2ec68 100644 --- a/zebra-chain/src/block/merkle.rs +++ b/zebra-chain/src/block/merkle.rs @@ -83,8 +83,8 @@ fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] { w.finish() } -/// Compute the root of a Merkle tree as used in Bitcoin. -/// `hashes` must contain the hashes of the tree leaves. +/// Compute the root of a Merkle tree of transactions as used in Bitcoin. +/// `hashes` must contain the hashes of the tree leaves (transactions) in some form. /// The root is written to the the first element of the input vector. /// See [`Root`] for an important disclaimer. fn root(hashes: &mut Vec<[u8; 32]>) { @@ -135,7 +135,7 @@ impl std::iter::FromIterator for Root { /// [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 [u8; 32]); +pub struct AuthDataRoot(pub(crate) [u8; 32]); impl fmt::Debug for AuthDataRoot { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -208,8 +208,8 @@ mod tests { fn auth_digest() { for block_bytes in zebra_test::vectors::BLOCKS.iter() { let block = Block::zcash_deserialize(&**block_bytes).unwrap(); - let _merkle_root = block.transactions.iter().collect::(); - // No test vectors for now, so just check it works + let _auth_digest = block.transactions.iter().collect::(); + // No test vectors for now, so just check it computes without panicking } } } diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index 76352932b64..2c22404825c 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -137,10 +137,12 @@ 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/auth_digest.rs b/zebra-chain/src/transaction/auth_digest.rs index 533b84f41a1..878d7d6d32f 100644 --- a/zebra-chain/src/transaction/auth_digest.rs +++ b/zebra-chain/src/transaction/auth_digest.rs @@ -6,7 +6,7 @@ use super::Transaction; /// /// [ZIP-244]: https://zips.z.cash/zip-0244.. #[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub struct AuthDigest(pub [u8; 32]); +pub struct AuthDigest(pub(crate) [u8; 32]); impl<'a> From<&'a Transaction> for AuthDigest { /// Computes the authorizing data commitment for a transaction. From 9cb90c84e98c3f6177c9f0bf3b30e9e4ada190a8 Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Thu, 12 Aug 2021 15:04:56 -0300 Subject: [PATCH 4/4] Fix AuthDataRoot computation to use padded leaves; add tests --- zebra-chain/src/block/merkle.rs | 105 ++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 19 deletions(-) diff --git a/zebra-chain/src/block/merkle.rs b/zebra-chain/src/block/merkle.rs index 694d1f2ec68..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"))] @@ -83,21 +85,20 @@ fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] { w.finish() } -/// Compute the root of a Merkle tree of transactions as used in Bitcoin. -/// `hashes` must contain the hashes of the tree leaves (transactions) in some form. -/// The root is written to the the first element of the input vector. -/// See [`Root`] for an important disclaimer. -fn root(hashes: &mut Vec<[u8; 32]>) { - while hashes.len() > 1 { - *hashes = hashes - .chunks(2) - .map(|chunk| match chunk { - [h1, h2] => hash(h1, h2), - [h1] => hash(h1, h1), - _ => unreachable!("chunks(2)"), - }) - .collect(); - } +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 @@ -121,7 +122,16 @@ impl std::iter::FromIterator for Root { I: IntoIterator, { let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::>(); - root(&mut hashes); + while hashes.len() > 1 { + hashes = hashes + .chunks(2) + .map(|chunk| match chunk { + [h1, h2] => hash(h1, h2), + [h1] => hash(h1, h1), + _ => unreachable!("chunks(2)"), + }) + .collect(); + } Self(hashes[0]) } } @@ -156,6 +166,7 @@ where // > 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| { @@ -173,7 +184,25 @@ impl std::iter::FromIterator for AuthDataRoot { I: IntoIterator, { let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::>(); - root(&mut hashes); + // > 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]) } } @@ -182,7 +211,7 @@ impl std::iter::FromIterator for AuthDataRoot { mod tests { use super::*; - use crate::{block::Block, serialization::ZcashDeserialize}; + use crate::{block::Block, serialization::ZcashDeserialize, transaction::AuthDigest}; #[test] fn block_test_vectors() { @@ -208,8 +237,46 @@ mod tests { fn auth_digest() { for block_bytes in zebra_test::vectors::BLOCKS.iter() { let block = Block::zcash_deserialize(&**block_bytes).unwrap(); - let _auth_digest = block.transactions.iter().collect::(); + 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); + } }