Skip to content

Commit

Permalink
Add ZIP-0244 TxId Digest support
Browse files Browse the repository at this point in the history
  • Loading branch information
conradoplg committed Jun 23, 2021
1 parent 1b6688f commit cb172f5
Show file tree
Hide file tree
Showing 8 changed files with 1,760 additions and 17 deletions.
1 change: 1 addition & 0 deletions zebra-chain/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ pub use x25519_dalek as x25519;
pub use proofs::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof};

pub mod zcash_history;
mod zcash_primitives;
46 changes: 46 additions & 0 deletions zebra-chain/src/primitives/zcash_primitives.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Contains code that interfaces with the zcash_primitives crate from
//! librustzcash.
use std::{
convert::{TryFrom, TryInto},
io,
};

use crate::{serialization::ZcashSerialize, transaction::Transaction};

impl TryFrom<&Transaction> for zcash_primitives::transaction::Transaction {
type Error = io::Error;

/// Convert a Zebra transaction into a librustzcash one.
///
/// # Panics
///
/// If the transaction is not V5. (Currently there is no need for this
/// conversion for other versions.)
fn try_from(trans: &Transaction) -> Result<Self, Self::Error> {
let network_upgrade = match trans {
Transaction::V5 {
network_upgrade, ..
} => network_upgrade,
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => panic!("Zebra only uses librustzcash for V5 transactions"),
};

let serialized_tx = trans.zcash_serialize_to_vec()?;
// The `read` method currently ignores the BranchId for V5 transactions;
// but we use the correct BranchId anyway.
let branch_id: u32 = network_upgrade
.branch_id()
.expect("Network upgrade must have a Branch ID")
.into();
// We've already parsed this transaction, so its network upgrade must be valid.
let branch_id: zcash_primitives::consensus::BranchId = branch_id
.try_into()
.expect("zcash_primitives and Zebra have the same branch ids");
let alt_tx =
zcash_primitives::transaction::Transaction::read(&serialized_tx[..], branch_id)?;
Ok(alt_tx)
}
}
1 change: 1 addition & 0 deletions zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod lock_time;
mod memo;
mod serialize;
mod sighash;
mod txidhash;

#[cfg(any(test, feature = "proptest-impl"))]
pub mod arbitrary;
Expand Down
13 changes: 6 additions & 7 deletions zebra-chain/src/transaction/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use std::fmt;
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};

use crate::serialization::{sha256d, SerializationError, ZcashSerialize};
use crate::serialization::SerializationError;

use super::Transaction;
use super::{txidhash::TxIdHasher, Transaction};

/// A transaction hash.
///
Expand All @@ -19,11 +19,10 @@ pub struct Hash(pub [u8; 32]);

impl<'a> From<&'a Transaction> for Hash {
fn from(transaction: &'a Transaction) -> Self {
let mut hash_writer = sha256d::Writer::default();
transaction
.zcash_serialize(&mut hash_writer)
.expect("Transactions must serialize into the hash.");
Self(hash_writer.finish())
let hasher = TxIdHasher::new(&transaction);
hasher
.txid()
.expect("zcash_primitives and Zebra transaction formats must be compatible")
}
}

Expand Down
126 changes: 116 additions & 10 deletions zebra-chain/src/transaction/tests/vectors.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
use lazy_static::lazy_static;

use zebra_test::zip0244;

use super::super::*;

use crate::{
block::{Block, Height, MAX_BLOCK_BYTES},
parameters::{Network, NetworkUpgrade},
serialization::{SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
transaction::txidhash::TxIdHasher,
};

use color_eyre::eyre::Result;

use std::convert::TryInto;

lazy_static! {
pub static ref EMPTY_V5_TX: Transaction = Transaction::V5 {
network_upgrade: NetworkUpgrade::Nu5,
lock_time: LockTime::min_lock_time(),
expiry_height: block::Height(0),
inputs: Vec::new(),
outputs: Vec::new(),
sapling_shielded_data: None,
orchard_shielded_data: None,
};
}

#[test]
fn librustzcash_tx_deserialize_and_round_trip() {
zebra_test::init();
Expand Down Expand Up @@ -133,18 +152,10 @@ fn zip243_deserialize_and_round_trip() {
fn empty_v5_round_trip() {
zebra_test::init();

let tx = Transaction::V5 {
network_upgrade: NetworkUpgrade::Nu5,
lock_time: LockTime::min_lock_time(),
expiry_height: block::Height(0),
inputs: Vec::new(),
outputs: Vec::new(),
sapling_shielded_data: None,
orchard_shielded_data: None,
};
let tx: &Transaction = &*EMPTY_V5_TX;

let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx2 = data
let tx2: &Transaction = &data
.zcash_deserialize_into()
.expect("tx should deserialize");

Expand Down Expand Up @@ -188,6 +199,17 @@ fn empty_v4_round_trip() {
assert_eq!(data, data2, "data must be equal if structs are equal");
}

/// Check if an empty V5 transaction can be deserialized by librustzcash too.
#[test]
fn empty_v5_librustzcash_round_trip() {
zebra_test::init();

let tx: &Transaction = &*EMPTY_V5_TX;
let _alt_tx: zcash_primitives::transaction::Transaction = tx
.try_into()
.expect("librustzcash deserialization might work for empty zebra serialized transactions. Hint: if empty transactions fail, but other transactions work, delete this test");
}

/// Do a round-trip test on fake v5 transactions created from v4 transactions
/// in the block test vectors.
///
Expand Down Expand Up @@ -341,3 +363,87 @@ fn invalid_orchard_nullifier() {
SerializationError::Parse("Invalid pallas::Base value for orchard Nullifier").to_string()
);
}

/// Do a round-trip test via librustzcash on fake v5 transactions created from v4 transactions
/// in the block test vectors.
/// Makes sure that zebra-serialized transactions can be deserialized by librustzcash.
#[test]
fn fake_v5_librustzcash_round_trip() {
zebra_test::init();

fake_v5_librustzcash_round_trip_for_network(Network::Mainnet);
fake_v5_librustzcash_round_trip_for_network(Network::Testnet);
}

fn fake_v5_librustzcash_round_trip_for_network(network: Network) {
let block_iter = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
};

for (height, original_bytes) in block_iter {
let original_block = original_bytes
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");

// skip blocks that are before overwinter as they will not have a valid consensus branch id
if *height
< NetworkUpgrade::Overwinter
.activation_height(network)
.expect("a valid height")
.0
{
continue;
}

let mut fake_block = original_block.clone();
fake_block.transactions = fake_block
.transactions
.iter()
.map(AsRef::as_ref)
.map(|t| arbitrary::transaction_to_fake_v5(t, network, Height(*height)))
.map(Into::into)
.collect();

// test each transaction
for (original_tx, fake_tx) in original_block
.transactions
.iter()
.zip(fake_block.transactions.iter())
{
assert_ne!(
&original_tx, &fake_tx,
"v1-v4 transactions must change when converted to fake v5"
);

let fake_bytes = fake_tx
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");

assert_ne!(
&original_bytes[..],
fake_bytes,
"v1-v4 transaction data must change when converted to fake v5"
);

let _alt_tx: zcash_primitives::transaction::Transaction = fake_tx
.as_ref()
.try_into()
.expect("librustzcash deserialization must work for zebra serialized transactions");
}
}
}

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

for test in zip0244::TEST_VECTORS.iter() {
let transaction = test.tx.zcash_deserialize_into::<Transaction>()?;
let hasher = TxIdHasher::new(&transaction);
let txid = hasher.txid()?;
assert_eq!(txid.0, test.txid);
}

Ok(())
}
54 changes: 54 additions & 0 deletions zebra-chain/src/transaction/txidhash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Transaction ID hashing. Contains code for generating the Transaction ID
//! from the transaction, using hashing.
use std::{convert::TryInto, io};

use super::Transaction;

use crate::serialization::{sha256d, ZcashSerialize};

use super::Hash;

/// A Transaction ID hasher. It computes the transaction ID by hashing
/// different parts of the transaction, depending on the transaction version.
/// For V5 transactions, it follows ZIP-244 and ZIP-225.
pub(super) struct TxIdHasher<'a> {
trans: &'a Transaction,
}

impl<'a> TxIdHasher<'a> {
/// Return a new TxIdHasher for the given transaction.
pub fn new(trans: &'a Transaction) -> Self {
TxIdHasher { trans }
}

/// Compute the Transaction ID for the previously specified transaction.
pub(super) fn txid(self) -> Result<Hash, io::Error> {
match self.trans {
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => self.txid_v1_to_v4(),
Transaction::V5 { .. } => self.txid_v5(),
}
}

/// Compute the Transaction ID for transactions V1 to V4.
/// In these cases it's simply the hash of the serialized transaction.
fn txid_v1_to_v4(self) -> Result<Hash, io::Error> {
let mut hash_writer = sha256d::Writer::default();
self.trans
.zcash_serialize(&mut hash_writer)
.expect("Transactions must serialize into the hash.");
Ok(Hash(hash_writer.finish()))
}

/// Compute the Transaction ID for a V5 transaction in the given network upgrade.
/// In this case it's the hash of a tree of hashes of specific parts of the
/// transaction, as specified in ZIP-244 and ZIP-225.
fn txid_v5(self) -> Result<Hash, io::Error> {
// The v5 txid (from ZIP-244) is computed using librustzcash. Convert the zebra
// transaction to a librustzcash transaction.
let alt_tx: zcash_primitives::transaction::Transaction = self.trans.try_into()?;
Ok(Hash(*alt_tx.txid().as_ref()))
}
}
1 change: 1 addition & 0 deletions zebra-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod net;
pub mod prelude;
pub mod transcript;
pub mod vectors;
pub mod zip0244;

static INIT: Once = Once::new();

Expand Down
Loading

0 comments on commit cb172f5

Please sign in to comment.