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:
parent
76591ceeed
commit
eadca72e75
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue