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::{
|
use crate::{
|
||||||
amount::NegativeAllowed,
|
amount::NegativeAllowed,
|
||||||
|
block::merkle::AuthDataRoot,
|
||||||
fmt::DisplayToDebug,
|
fmt::DisplayToDebug,
|
||||||
orchard,
|
orchard,
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
|
@ -198,6 +199,14 @@ impl Block {
|
||||||
|
|
||||||
Ok(transaction_value_balance_total.neg())
|
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 {
|
impl<'a> From<&'a Block> for Hash {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
//! The Bitcoin-inherited Merkle tree of transactions.
|
//! The Bitcoin-inherited Merkle tree of transactions.
|
||||||
#![allow(clippy::unit_arg)]
|
#![allow(clippy::unit_arg)]
|
||||||
|
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::iter;
|
||||||
use std::{fmt, io::Write};
|
use std::{fmt, io::Write};
|
||||||
|
|
||||||
#[cfg(any(any(test, feature = "proptest-impl"), feature = "proptest-impl"))]
|
#[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
|
/// The root of the Bitcoin-inherited transaction Merkle tree, binding the
|
||||||
/// block header to the transactions in the block.
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
/// data on disk, and because it does not permanently fail blocks or use an
|
||||||
/// aggressive anti-DoS mechanism.
|
/// aggressive anti-DoS mechanism.
|
||||||
|
///
|
||||||
|
/// [ZIP-244]: https://zips.z.cash/zip-0244
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
|
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
|
||||||
pub struct Root(pub [u8; 32]);
|
pub struct Root(pub [u8; 32]);
|
||||||
|
@ -78,6 +85,22 @@ fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] {
|
||||||
w.finish()
|
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
|
impl<T> std::iter::FromIterator<T> for Root
|
||||||
where
|
where
|
||||||
T: std::convert::AsRef<Transaction>,
|
T: std::convert::AsRef<Transaction>,
|
||||||
|
@ -99,7 +122,6 @@ impl std::iter::FromIterator<transaction::Hash> for Root {
|
||||||
I: IntoIterator<Item = transaction::Hash>,
|
I: IntoIterator<Item = transaction::Hash>,
|
||||||
{
|
{
|
||||||
let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::<Vec<_>>();
|
let mut hashes = hashes.into_iter().map(|hash| hash.0).collect::<Vec<_>>();
|
||||||
|
|
||||||
while hashes.len() > 1 {
|
while hashes.len() > 1 {
|
||||||
hashes = hashes
|
hashes = hashes
|
||||||
.chunks(2)
|
.chunks(2)
|
||||||
|
@ -110,6 +132,76 @@ impl std::iter::FromIterator<transaction::Hash> for Root {
|
||||||
})
|
})
|
||||||
.collect();
|
.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])
|
Self(hashes[0])
|
||||||
}
|
}
|
||||||
|
@ -119,7 +211,7 @@ impl std::iter::FromIterator<transaction::Hash> for Root {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::{block::Block, serialization::ZcashDeserialize};
|
use crate::{block::Block, serialization::ZcashDeserialize, transaction::AuthDigest};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn block_test_vectors() {
|
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},
|
amount::{Amount, NonNegative},
|
||||||
parameters::NetworkUpgrade,
|
parameters::NetworkUpgrade,
|
||||||
serialization::ZcashSerialize,
|
serialization::ZcashSerialize,
|
||||||
transaction::{HashType, SigHash, Transaction},
|
transaction::{AuthDigest, HashType, SigHash, Transaction},
|
||||||
transparent::{self, Script},
|
transparent::{self, Script},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,3 +124,25 @@ pub(crate) fn sighash(
|
||||||
.as_ref(),
|
.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 halo2::pasta::pallas;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod auth_digest;
|
||||||
mod hash;
|
mod hash;
|
||||||
mod joinsplit;
|
mod joinsplit;
|
||||||
mod lock_time;
|
mod lock_time;
|
||||||
|
@ -16,6 +17,7 @@ pub mod arbitrary;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
pub use auth_digest::AuthDigest;
|
||||||
pub use hash::Hash;
|
pub use hash::Hash;
|
||||||
pub use joinsplit::JoinSplitData;
|
pub use joinsplit::JoinSplitData;
|
||||||
pub use lock_time::LockTime;
|
pub use lock_time::LockTime;
|
||||||
|
@ -160,6 +162,22 @@ impl Transaction {
|
||||||
sighash::SigHasher::new(self, hash_type, network_upgrade, input).sighash()
|
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
|
// other properties
|
||||||
|
|
||||||
/// Does this transaction have transparent or shielded inputs?
|
/// 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(())
|
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]
|
#[test]
|
||||||
fn test_vec143_1() -> Result<()> {
|
fn test_vec143_1() -> Result<()> {
|
||||||
zebra_test::init();
|
zebra_test::init();
|
||||||
|
|
Loading…
Reference in New Issue