Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions zebra-chain/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use serde::{Deserialize, Serialize};

use crate::{
amount::NegativeAllowed,
block::merkle::AuthDataRoot,
fmt::DisplayToDebug,
orchard,
parameters::{Network, NetworkUpgrade},
Expand Down Expand Up @@ -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>()
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl<'a> From<&'a Block> for Hash {
Expand Down
143 changes: 141 additions & 2 deletions zebra-chain/src/block/merkle.rs
Original file line number Diff line number Diff line change
@@ -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"))]
Expand All @@ -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].
///
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
/// 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
Expand Down Expand Up @@ -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]);
Expand All @@ -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>,
Expand All @@ -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)
Expand All @@ -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].
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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.
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
// 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).
dconnolly marked this conversation as resolved.
Show resolved Hide resolved
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])
}
Expand All @@ -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() {
Expand All @@ -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);
}
}
24 changes: 23 additions & 1 deletion zebra-chain/src/primitives/zcash_primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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.
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
///
/// [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
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
.auth_commitment()
.as_ref()
.try_into()
.expect("digest has the correct size");

AuthDigest(digest_bytes)
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
}
18 changes: 18 additions & 0 deletions zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use halo2::pasta::pallas;
use serde::{Deserialize, Serialize};

mod auth_digest;
mod hash;
mod joinsplit;
mod lock_time;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
dconnolly marked this conversation as resolved.
Show resolved Hide resolved
Transaction::V5 { .. } => Some(AuthDigest::from(self)),
}
}

// other properties

/// Does this transaction have transparent or shielded inputs?
Expand Down
20 changes: 20 additions & 0 deletions zebra-chain/src/transaction/auth_digest.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
18 changes: 18 additions & 0 deletions zebra-chain/src/transaction/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
conradoplg marked this conversation as resolved.
Show resolved Hide resolved

#[test]
fn test_vec143_1() -> Result<()> {
zebra_test::init();
Expand Down