Implement ZIP-244 authorizing data commitment (auth_digest) (#2547)

* Implement ZIP-244 authorizing data commitment (auth_digest)

* s/Merke/Merkle/

* Apply suggestions from code review

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>

* Fix AuthDataRoot computation to use padded leaves; add tests

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>
This commit is contained in:
Conrado Gouvea 2021-08-13 13:58:04 -03:00 committed by GitHub
parent 76591ceeed
commit eadca72e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 229 additions and 3 deletions

View File

@ -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::<AuthDataRoot>()
}
}
impl<'a> From<&'a Block> for Hash {

View File

@ -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<T> std::iter::FromIterator<T> for Root
where
T: std::convert::AsRef<Transaction>,
@ -99,7 +122,6 @@ impl std::iter::FromIterator<transaction::Hash> for Root {
I: IntoIterator<Item = transaction::Hash>,
{
let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::<Vec<_>>();
while hashes.len() > 1 {
hashes = hashes
.chunks(2)
@ -110,6 +132,76 @@ impl std::iter::FromIterator<transaction::Hash> 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<T> std::iter::FromIterator<T> for AuthDataRoot
where
T: std::convert::AsRef<Transaction>,
{
fn from_iter<I>(transactions: I) -> Self
where
I: IntoIterator<Item = T>,
{
// > 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<transaction::AuthDigest> for AuthDataRoot {
fn from_iter<I>(hashes: I) -> Self
where
I: IntoIterator<Item = transaction::AuthDigest>,
{
let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::<Vec<_>>();
// > 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<transaction::Hash> 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::<AuthDataRoot>();
// 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::<AuthDataRoot>();
// 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::<AuthDataRoot>();
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::<AuthDataRoot>();
// 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::<AuthDataRoot>();
assert_eq!(auth_root, expect_auth_root);
}
}

View File

@ -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)
}

View File

@ -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<AuthDigest> {
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?

View File

@ -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)
}
}

View File

@ -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::<Transaction>()?;
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();