Skip to content
Open
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
54 changes: 51 additions & 3 deletions zebra-chain/benches/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
// Disabled due to warnings in criterion macros
#![allow(missing_docs)]

use std::io::Cursor;
use std::{io::Cursor, sync::Arc};

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};

use zebra_chain::{
block::Block,
Expand All @@ -40,6 +40,20 @@ fn first_tx_of_version(block: &Block, version: u32) -> Option<Vec<u8>> {
.map(|tx| tx.zcash_serialize_to_vec().expect("valid transaction"))
}

/// Extracts the first Orchard-only v5 transaction from a block.
fn first_v5_orchard_only_tx(block: &Block) -> Option<Arc<Transaction>> {
block
.transactions
.iter()
.find(|tx| {
tx.version() == 5
&& !tx.has_transparent_inputs_or_outputs()
&& !tx.has_sapling_shielded_data()
&& tx.has_orchard_shielded_data()
})
.cloned()
}

fn bench_transaction_deserialize(c: &mut Criterion) {
let mut group = c.benchmark_group("Transaction Deserialization");

Expand Down Expand Up @@ -114,9 +128,43 @@ fn bench_transaction_deserialize(c: &mut Criterion) {
group.finish();
}

fn bench_zip244_digests(c: &mut Criterion) {
let nu5_blocks = [
zebra_test::vectors::BLOCK_MAINNET_1687107_BYTES.as_slice(),
zebra_test::vectors::BLOCK_MAINNET_1687108_BYTES.as_slice(),
zebra_test::vectors::BLOCK_MAINNET_1687113_BYTES.as_slice(),
zebra_test::vectors::BLOCK_MAINNET_1687118_BYTES.as_slice(),
zebra_test::vectors::BLOCK_MAINNET_1687121_BYTES.as_slice(),
];
let tx = nu5_blocks
.into_iter()
.find_map(|block_bytes| {
let block = Block::zcash_deserialize(Cursor::new(block_bytes)).expect("valid block");
first_v5_orchard_only_tx(&block)
})
.expect("vectors contain an Orchard-only v5 tx");

let mut group = c.benchmark_group("ZIP-244 Digests");
group.sample_size(1000);

group.bench_function("auth_digest/orchard_only", |b| {
b.iter(|| {
black_box(&tx)
.auth_digest()
.expect("v5 tx has an auth digest")
})
});

group.bench_function("txid_and_auth_digest/orchard_only", |b| {
b.iter(|| black_box(&tx).txid_and_auth_digest())
});

group.finish();
}

criterion_group! {
name = benches;
config = Criterion::default().noise_threshold(0.1).sample_size(50);
targets = bench_transaction_deserialize
targets = bench_transaction_deserialize, bench_zip244_digests
}
criterion_main!(benches);
243 changes: 210 additions & 33 deletions zebra-chain/src/transaction/zip244.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,59 @@ const ZCASH_ORCHARD_SIGS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthOrchaHash";
const ZCASH_ORCHARD_V6_SIGS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthOrchaH_v6";
const ZCASH_IRONWOOD_SIGS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthIrnwdH_v6";

const EMPTY_TRANSPARENT_TXID_HASH: &[u8; 32] = &[
0xc3, 0x3f, 0x2e, 0x95, 0x70, 0x5f, 0xaa, 0xb3, 0x5f, 0x8d, 0x53, 0x3f, 0xa6, 0x1e, 0x95, 0xc3,
0xb7, 0xaa, 0xba, 0x07, 0x76, 0xb8, 0x74, 0xa9, 0xf7, 0x4f, 0xc1, 0x27, 0x84, 0x37, 0x6a, 0x59,
];
const EMPTY_SAPLING_TXID_HASH: &[u8; 32] = &[
0x6f, 0x2f, 0xc8, 0xf9, 0x8f, 0xea, 0xfd, 0x94, 0xe7, 0x4a, 0x0d, 0xf4, 0xbe, 0xd7, 0x43, 0x91,
0xee, 0x0b, 0x5a, 0x69, 0x94, 0x5e, 0x4c, 0xed, 0x8c, 0xa8, 0xa0, 0x95, 0x20, 0x6f, 0x00, 0xae,
];
const EMPTY_SAPLING_SPENDS_HASH: &[u8; 32] = &[
0xd7, 0x9c, 0x8d, 0xcb, 0x8a, 0xf4, 0x6b, 0xd8, 0x0f, 0xe3, 0xbc, 0xdc, 0x0e, 0x5d, 0x0e, 0xd7,
0x34, 0x4d, 0x5f, 0x48, 0xcb, 0xef, 0xf3, 0xc9, 0x4b, 0x13, 0xd4, 0x0a, 0x23, 0x05, 0xf8, 0x4d,
];
const EMPTY_SAPLING_OUTPUTS_HASH: &[u8; 32] = &[
0x9e, 0x28, 0xee, 0xf8, 0xdf, 0x6f, 0xcc, 0x96, 0x68, 0xef, 0x92, 0xfc, 0xdc, 0x41, 0x2f, 0xc5,
0xb6, 0xd7, 0x03, 0x7e, 0xf1, 0xf9, 0x63, 0x76, 0x7b, 0xd7, 0xb8, 0x01, 0x12, 0x67, 0xbf, 0x95,
];
const EMPTY_ORCHARD_V5_TXID_HASH: &[u8; 32] = &[
0x9f, 0xbe, 0x4e, 0xd1, 0x3b, 0x0c, 0x08, 0xe6, 0x71, 0xc1, 0x1a, 0x34, 0x07, 0xd8, 0x4e, 0x11,
0x17, 0xcd, 0x45, 0x02, 0x8a, 0x2e, 0xee, 0x1b, 0x9f, 0xea, 0xe7, 0x8b, 0x48, 0xa6, 0xe2, 0xc1,
];
const EMPTY_ORCHARD_V6_TXID_HASH: &[u8; 32] = &[
0xa3, 0x36, 0x7d, 0x2f, 0xde, 0xa2, 0x91, 0x01, 0x59, 0xfc, 0x50, 0x26, 0xe9, 0xbf, 0x1f, 0xcc,
0xd3, 0xe2, 0x8c, 0xe5, 0xe6, 0xde, 0x46, 0xbf, 0xb7, 0x15, 0x87, 0x23, 0x0e, 0xea, 0x95, 0x15,
];
const EMPTY_IRONWOOD_TXID_HASH: &[u8; 32] = &[
0xb9, 0xcf, 0xe6, 0x43, 0xce, 0x45, 0xb2, 0x8c, 0x33, 0x19, 0x0f, 0x0d, 0x52, 0x23, 0xe4, 0x75,
0x97, 0x2f, 0x2a, 0x14, 0x9d, 0xc5, 0x44, 0x04, 0xfd, 0x83, 0x65, 0x52, 0x1f, 0x84, 0x16, 0xc5,
];
const EMPTY_TRANSPARENT_AUTH_HASH: &[u8; 32] = &[
0xe9, 0x88, 0x2b, 0xce, 0x1c, 0xf1, 0x35, 0x69, 0x02, 0xc6, 0xe2, 0x58, 0xc5, 0x67, 0xeb, 0xc0,
0xd9, 0x92, 0x88, 0x67, 0xde, 0x9a, 0x35, 0x89, 0xbb, 0xbd, 0x31, 0x0e, 0xb6, 0x89, 0x04, 0xe5,
];
const EMPTY_SAPLING_V5_AUTH_HASH: &[u8; 32] = &[
0xd2, 0x25, 0x67, 0x30, 0x66, 0xb0, 0xcd, 0x76, 0xa7, 0x71, 0x51, 0xbf, 0x05, 0x6d, 0x57, 0x77,
0x92, 0xf3, 0x57, 0x73, 0x91, 0x20, 0x8d, 0x4c, 0xec, 0x25, 0x31, 0x8a, 0x8d, 0x5c, 0xd9, 0x6f,
];
const EMPTY_SAPLING_V6_AUTH_HASH: &[u8; 32] = &[
0x0e, 0xe4, 0xb9, 0x56, 0x0b, 0xae, 0x42, 0x30, 0xa9, 0x9b, 0xfa, 0x52, 0x8e, 0x0a, 0x6a, 0xab,
0xc7, 0xe2, 0x53, 0x86, 0xf3, 0x66, 0x59, 0x97, 0x67, 0xe7, 0xca, 0x10, 0x7b, 0x8c, 0x17, 0x46,
];
const EMPTY_ORCHARD_V5_AUTH_HASH: &[u8; 32] = &[
0x14, 0xed, 0xaa, 0x1e, 0x66, 0x9a, 0x63, 0xa8, 0x00, 0xbf, 0xe0, 0xb8, 0xfc, 0xd3, 0xd1, 0x0e,
0x36, 0x81, 0x11, 0x5b, 0xee, 0x03, 0x25, 0x3d, 0xa0, 0x2e, 0x09, 0x80, 0x42, 0xd9, 0xff, 0x90,
];
const EMPTY_ORCHARD_V6_AUTH_HASH: &[u8; 32] = &[
0x79, 0x8e, 0x7f, 0xcd, 0x05, 0xbb, 0x7b, 0x7e, 0x9e, 0x49, 0x42, 0x4b, 0xdd, 0xe7, 0x68, 0xa5,
0x00, 0xb6, 0x6f, 0xf2, 0x3d, 0x75, 0x9b, 0x87, 0x7d, 0x07, 0x54, 0xe3, 0xff, 0x79, 0x7d, 0x91,
];
const EMPTY_IRONWOOD_AUTH_HASH: &[u8; 32] = &[
0xec, 0x97, 0x68, 0xfd, 0xaa, 0x11, 0xf1, 0x2c, 0xdb, 0x13, 0xf5, 0x66, 0xb5, 0x95, 0x84, 0x3f,
0x3d, 0x0e, 0x92, 0xb6, 0x70, 0x3e, 0xaf, 0xff, 0x17, 0x2e, 0x21, 0x34, 0x5b, 0x58, 0x61, 0x33,
];

/// A new BLAKE2b-256 state with the given 16-byte personalization.
fn hasher(personal: &[u8; 16]) -> State {
Params::new().hash_length(32).personal(personal).to_state()
Expand Down Expand Up @@ -160,6 +213,13 @@ impl Zip244Version {
}
}

fn empty_sapling_auth_hash(self) -> &'static [u8; 32] {
match self {
Self::V5 => EMPTY_SAPLING_V5_AUTH_HASH,
Self::V6 => EMPTY_SAPLING_V6_AUTH_HASH,
}
}

fn sapling_auth_includes_anchor(self) -> bool {
match self {
Self::V5 => false,
Expand Down Expand Up @@ -244,6 +304,22 @@ impl BundleCommitmentFormat {
Self::OrchardV6 | Self::IronwoodV6 => true,
}
}

fn empty_txid_hash(self) -> &'static [u8; 32] {
match self {
Self::OrchardV5 => EMPTY_ORCHARD_V5_TXID_HASH,
Self::OrchardV6 => EMPTY_ORCHARD_V6_TXID_HASH,
Self::IronwoodV6 => EMPTY_IRONWOOD_TXID_HASH,
}
}

fn empty_auth_hash(self) -> &'static [u8; 32] {
match self {
Self::OrchardV5 => EMPTY_ORCHARD_V5_AUTH_HASH,
Self::OrchardV6 => EMPTY_ORCHARD_V6_AUTH_HASH,
Self::IronwoodV6 => EMPTY_IRONWOOD_AUTH_HASH,
}
}
}

/// The fields of a v5/v6 transaction needed to compute its digests.
Expand Down Expand Up @@ -443,16 +519,24 @@ fn hash_sapling_txid(
sapling: Option<&sapling::ShieldedData<sapling::SharedAnchor>>,
version: Zip244Version,
) -> Blake2bHash {
let Some(sapling) = sapling else {
return hasher(ZCASH_SAPLING_HASH_PERSONALIZATION).finalize();
};

let mut h = hasher(ZCASH_SAPLING_HASH_PERSONALIZATION);
if let Some(sapling) = sapling {
// `ShieldedData` only exists with at least one spend or output, so this
// matches librustzcash's "non-empty bundle" branch.
if sapling.spends().next().is_some() || sapling.outputs().next().is_some() {
h.update(hash_sapling_spends(sapling, version).as_bytes());
h.update(hash_sapling_outputs(sapling).as_bytes());
h.update(&sapling.value_balance.zatoshis().to_le_bytes());
}
// `ShieldedData` only exists with at least one spend or output, so this
// matches librustzcash's "non-empty bundle" branch.
if sapling.spends().next().is_some() {
h.update(hash_sapling_spends(sapling, version).as_bytes());
} else {
h.update(EMPTY_SAPLING_SPENDS_HASH);
}
if sapling.outputs().next().is_some() {
h.update(hash_sapling_outputs(sapling).as_bytes());
} else {
h.update(EMPTY_SAPLING_OUTPUTS_HASH);
}
h.update(&sapling.value_balance.zatoshis().to_le_bytes());
h.finalize()
}

Expand Down Expand Up @@ -500,25 +584,38 @@ fn hash_bundle_txid(
/// https://github.com/zcash/librustzcash/blob/4367ba26ed57624544e2350f055a5df89079474a/zcash_primitives/src/transaction/txid.rs#L426
fn txid_inner(parts: &Zip244Parts) -> Hash {
let header = hash_header(parts);
let transparent = hash_transparent_txid(parts.inputs, parts.outputs);
let sapling = hash_sapling_txid(parts.sapling, parts.version);
let orchard = hash_bundle_txid(parts.orchard, parts.version.orchard_format());
let ironwood = parts
.version
.has_ironwood()
.then(|| hash_bundle_txid(parts.ironwood, BundleCommitmentFormat::IronwoodV6));

let mut personal = [0u8; 16];
personal[..12].copy_from_slice(ZCASH_TX_PERSONALIZATION_PREFIX);
personal[12..].copy_from_slice(&consensus_branch_id(parts).to_le_bytes());

let mut h = hasher(&personal);
h.update(header.as_bytes());
h.update(transparent.as_bytes());
h.update(sapling.as_bytes());
h.update(orchard.as_bytes());
if let Some(ironwood) = ironwood {
h.update(ironwood.as_bytes());
if parts.inputs.is_empty() && parts.outputs.is_empty() {
h.update(EMPTY_TRANSPARENT_TXID_HASH);
} else {
h.update(hash_transparent_txid(parts.inputs, parts.outputs).as_bytes());
}
if parts.sapling.is_some() {
h.update(hash_sapling_txid(parts.sapling, parts.version).as_bytes());
} else {
h.update(EMPTY_SAPLING_TXID_HASH);
}

let orchard_format = parts.version.orchard_format();
if parts.orchard.is_some() {
h.update(hash_bundle_txid(parts.orchard, orchard_format).as_bytes());
} else {
h.update(orchard_format.empty_txid_hash());
}
if parts.version.has_ironwood() {
if parts.ironwood.is_some() {
h.update(
hash_bundle_txid(parts.ironwood, BundleCommitmentFormat::IronwoodV6).as_bytes(),
);
} else {
h.update(BundleCommitmentFormat::IronwoodV6.empty_txid_hash());
}
}

Hash(
Expand Down Expand Up @@ -617,24 +714,36 @@ fn hash_bundle_auth(
/// Reference implementation:
/// https://github.com/zcash/librustzcash/blob/4367ba26ed57624544e2350f055a5df89079474a/zcash_primitives/src/transaction/txid.rs#L426-L448
fn auth_digest_inner(parts: &Zip244Parts) -> AuthDigest {
let transparent = hash_transparent_auth(parts.inputs, parts.outputs);
let sapling = hash_sapling_auth(parts.sapling, parts.version);
let orchard = hash_bundle_auth(parts.orchard, parts.version.orchard_format());
let ironwood = parts
.version
.has_ironwood()
.then(|| hash_bundle_auth(parts.ironwood, BundleCommitmentFormat::IronwoodV6));

let mut personal = [0u8; 16];
personal[..12].copy_from_slice(ZCASH_AUTH_PERSONALIZATION_PREFIX);
personal[12..].copy_from_slice(&consensus_branch_id(parts).to_le_bytes());

let mut h = hasher(&personal);
h.update(transparent.as_bytes());
h.update(sapling.as_bytes());
h.update(orchard.as_bytes());
if let Some(ironwood) = ironwood {
h.update(ironwood.as_bytes());
if parts.inputs.is_empty() && parts.outputs.is_empty() {
h.update(EMPTY_TRANSPARENT_AUTH_HASH);
} else {
h.update(hash_transparent_auth(parts.inputs, parts.outputs).as_bytes());
}
if parts.sapling.is_some() {
h.update(hash_sapling_auth(parts.sapling, parts.version).as_bytes());
} else {
h.update(parts.version.empty_sapling_auth_hash());
}

let orchard_format = parts.version.orchard_format();
if parts.orchard.is_some() {
h.update(hash_bundle_auth(parts.orchard, orchard_format).as_bytes());
} else {
h.update(orchard_format.empty_auth_hash());
}
if parts.version.has_ironwood() {
if parts.ironwood.is_some() {
h.update(
hash_bundle_auth(parts.ironwood, BundleCommitmentFormat::IronwoodV6).as_bytes(),
);
} else {
h.update(BundleCommitmentFormat::IronwoodV6.empty_auth_hash());
}
}

AuthDigest(
Expand Down Expand Up @@ -665,3 +774,71 @@ pub(crate) fn txid_and_auth_digest(tx: &Transaction) -> Option<(Hash, AuthDigest
let parts = zip244_parts(tx)?;
Some((txid_inner(&parts), auth_digest_inner(&parts)))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn empty_hash_constants_match_personalizations() {
let empty_hashes = [
(
ZCASH_TRANSPARENT_HASH_PERSONALIZATION,
EMPTY_TRANSPARENT_TXID_HASH,
),
(ZCASH_SAPLING_HASH_PERSONALIZATION, EMPTY_SAPLING_TXID_HASH),
(
ZCASH_SAPLING_SPENDS_HASH_PERSONALIZATION,
EMPTY_SAPLING_SPENDS_HASH,
),
(
ZCASH_SAPLING_OUTPUTS_HASH_PERSONALIZATION,
EMPTY_SAPLING_OUTPUTS_HASH,
),
(
ZCASH_ORCHARD_HASH_PERSONALIZATION,
EMPTY_ORCHARD_V5_TXID_HASH,
),
(
ZCASH_ORCHARD_V6_HASH_PERSONALIZATION,
EMPTY_ORCHARD_V6_TXID_HASH,
),
(
ZCASH_IRONWOOD_HASH_PERSONALIZATION,
EMPTY_IRONWOOD_TXID_HASH,
),
(
ZCASH_TRANSPARENT_SCRIPTS_HASH_PERSONALIZATION,
EMPTY_TRANSPARENT_AUTH_HASH,
),
(
ZCASH_SAPLING_SIGS_HASH_PERSONALIZATION,
EMPTY_SAPLING_V5_AUTH_HASH,
),
(
ZCASH_SAPLING_V6_SIGS_HASH_PERSONALIZATION,
EMPTY_SAPLING_V6_AUTH_HASH,
),
(
ZCASH_ORCHARD_SIGS_HASH_PERSONALIZATION,
EMPTY_ORCHARD_V5_AUTH_HASH,
),
(
ZCASH_ORCHARD_V6_SIGS_HASH_PERSONALIZATION,
EMPTY_ORCHARD_V6_AUTH_HASH,
),
(
ZCASH_IRONWOOD_SIGS_HASH_PERSONALIZATION,
EMPTY_IRONWOOD_AUTH_HASH,
),
];

for (personalization, expected_hash) in empty_hashes {
assert_eq!(
hasher(personalization).finalize().as_bytes(),
expected_hash,
"empty hash must match {personalization:?}",
);
}
}
}