Validate nConsensusBranchId (#2100)

* validate nConsensusBranchId
* add tests

* fix bug in transaction_to_fake_v5

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2021-05-09 22:31:45 -03:00 committed by GitHub
parent f222bf94ea
commit 29893f2b9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 18 deletions

View File

@ -6,7 +6,7 @@ use proptest::{arbitrary::any, array, collection::vec, option, prelude::*};
use crate::{
amount::Amount,
block,
parameters::NetworkUpgrade,
parameters::{Network, NetworkUpgrade},
primitives::{Bctv14Proof, Groth16Proof, ZkSnarkProof},
sapling, sprout, transparent, LedgerState,
};
@ -349,21 +349,24 @@ impl Arbitrary for Transaction {
// Utility functions
/// The network upgrade for any fake transactions we will create.
const FAKE_NETWORK_UPGRADE: NetworkUpgrade = NetworkUpgrade::Nu5;
/// Convert `trans` into a fake v5 transaction,
/// converting sapling shielded data from v4 to v5 if possible.
pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
pub fn transaction_to_fake_v5(
trans: &Transaction,
network: Network,
height: block::Height,
) -> Transaction {
use Transaction::*;
let block_nu = NetworkUpgrade::current(network, height);
match trans {
V1 {
inputs,
outputs,
lock_time,
} => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE,
network_upgrade: block_nu,
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
@ -376,7 +379,7 @@ pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
lock_time,
..
} => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE,
network_upgrade: block_nu,
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
@ -390,7 +393,7 @@ pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
expiry_height,
..
} => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE,
network_upgrade: block_nu,
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
@ -405,7 +408,7 @@ pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
sapling_shielded_data,
..
} => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE,
network_upgrade: block_nu,
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,

View File

@ -1,7 +1,8 @@
use super::super::*;
use crate::{
block::{Block, MAX_BLOCK_BYTES},
block::{Block, Height, MAX_BLOCK_BYTES},
parameters::{Network, NetworkUpgrade},
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
};
@ -164,11 +165,31 @@ fn empty_v4_round_trip() {
fn fake_v5_round_trip() {
zebra_test::init();
for original_bytes in zebra_test::vectors::BLOCKS.iter() {
fake_v5_round_trip_for_network(Network::Mainnet);
fake_v5_round_trip_for_network(Network::Testnet);
}
fn fake_v5_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;
}
// skip this block if it only contains v5 transactions,
// the block round-trip test covers it already
if original_block
@ -184,7 +205,7 @@ fn fake_v5_round_trip() {
.transactions
.iter()
.map(AsRef::as_ref)
.map(arbitrary::transaction_to_fake_v5)
.map(|t| arbitrary::transaction_to_fake_v5(t, network, Height(*height)))
.map(Into::into)
.collect();

View File

@ -161,7 +161,7 @@ where
.map(|t| t.hash())
.collect::<Vec<_>>();
check::merkle_root_validity(&block, &transaction_hashes)?;
check::merkle_root_validity(network, &block, &transaction_hashes)?;
// Since errors cause an early exit, try to do the
// quick checks first.

View File

@ -168,10 +168,48 @@ pub fn time_is_valid_at(
/// Check Merkle root validity.
///
/// `transaction_hashes` is a precomputed list of transaction hashes.
///
/// # Consensus rules:
///
/// - The nConsensusBranchId field MUST match the consensus branch ID used for
/// SIGHASH transaction hashes, as specifed in [ZIP-244] ([7.1]).
/// - A SHA-256d hash in internal byte order. The merkle root is derived from the
/// hashes of all transactions included in this block, ensuring that none of
/// those transactions can be modified without modifying the header. [7.6]
///
/// # Panics
///
/// - If block does not have a coinbase transaction.
///
/// [ZIP-244]: https://zips.z.cash/zip-0244
/// [7.1]: https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus
/// [7.6]: https://zips.z.cash/protocol/nu5.pdf#blockheader
pub fn merkle_root_validity(
network: Network,
block: &Block,
transaction_hashes: &[transaction::Hash],
) -> Result<(), BlockError> {
use transaction::Transaction;
let block_nu =
NetworkUpgrade::current(network, block.coinbase_height().expect("a valid height"));
if block
.transactions
.iter()
.filter_map(|trans| match *trans.as_ref() {
Transaction::V5 {
network_upgrade, ..
} => Some(network_upgrade),
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => None,
})
.any(|trans_nu| trans_nu != block_nu)
{
return Err(BlockError::WrongTransactionConsensusBranchId);
}
let merkle_root = transaction_hashes.iter().cloned().collect();
if block.header.merkle_root != merkle_root {

View File

@ -13,8 +13,9 @@ use tower::buffer::Buffer;
use zebra_chain::{
block::{self, Block, Height},
parameters::Network,
parameters::{Network, NetworkUpgrade},
serialization::{ZcashDeserialize, ZcashDeserializeInto},
transaction::{arbitrary::transaction_to_fake_v5, Transaction},
work::difficulty::{ExpandedDifficulty, INVALID_COMPACT_DIFFICULTY},
};
use zebra_test::transcript::{TransError, Transcript};
@ -435,3 +436,92 @@ fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
Ok(())
}
#[test]
fn merkle_root_is_valid() -> Result<(), Report> {
zebra_test::init();
// test all original blocks available, all blocks validate
merkle_root_is_valid_for_network(Network::Mainnet)?;
merkle_root_is_valid_for_network(Network::Testnet)?;
// create and test fake blocks with v5 transactions, all blocks fail validation
merkle_root_fake_v5_for_network(Network::Mainnet)?;
merkle_root_fake_v5_for_network(Network::Testnet)?;
Ok(())
}
fn merkle_root_is_valid_for_network(network: Network) -> Result<(), Report> {
let block_iter = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
};
for (_height, block) in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
let transaction_hashes = block
.transactions
.iter()
.map(|tx| tx.hash())
.collect::<Vec<_>>();
check::merkle_root_validity(network, &block, &transaction_hashes)
.expect("merkle root should be valid for this block");
}
Ok(())
}
fn merkle_root_fake_v5_for_network(network: Network) -> Result<(), Report> {
let block_iter = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
};
for (height, block) in block_iter {
let mut block = block
.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 overwinter activation height")
.0
{
continue;
}
// convert all transactions from the block to V5
let transactions: Vec<Arc<Transaction>> = block
.transactions
.iter()
.map(AsRef::as_ref)
.map(|t| transaction_to_fake_v5(t, network, Height(*height)))
.map(Into::into)
.collect();
block.transactions = transactions;
let transaction_hashes = block
.transactions
.iter()
.map(|tx| tx.hash())
.collect::<Vec<_>>();
// Replace the merkle root so that it matches the modified transactions.
// This test provides some transaction id and merkle root coverage,
// but we also need to test against zcashd test vectors.
block.header.merkle_root = transaction_hashes.iter().cloned().collect();
check::merkle_root_validity(network, &block, &transaction_hashes)
.expect("merkle root should be valid for this block");
}
Ok(())
}

View File

@ -464,7 +464,7 @@ where
.map(|tx| tx.hash())
.collect::<Vec<_>>();
crate::block::check::merkle_root_validity(&block, &transaction_hashes)?;
crate::block::check::merkle_root_validity(self.network, &block, &transaction_hashes)?;
Ok(height)
}

View File

@ -146,4 +146,7 @@ pub enum BlockError {
zebra_chain::work::difficulty::ExpandedDifficulty,
zebra_chain::parameters::Network,
),
#[error("transaction has wrong consensus branch id for block network upgrade")]
WrongTransactionConsensusBranchId,
}

View File

@ -1,5 +1,6 @@
use zebra_chain::{
block::Block,
block::{Block, Height},
parameters::Network,
serialization::ZcashDeserializeInto,
transaction::{arbitrary::transaction_to_fake_v5, Transaction},
};
@ -11,8 +12,22 @@ use color_eyre::eyre::Report;
fn v5_fake_transactions() -> Result<(), Report> {
zebra_test::init();
v5_fake_transactions_for_network(Network::Mainnet)?;
v5_fake_transactions_for_network(Network::Testnet)?;
Ok(())
}
fn v5_fake_transactions_for_network(network: Network) -> Result<(), Report> {
zebra_test::init();
// get all the blocks we have available
for original_bytes in zebra_test::vectors::BLOCKS.iter() {
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");
@ -22,7 +37,7 @@ fn v5_fake_transactions() -> Result<(), Report> {
.transactions
.iter()
.map(AsRef::as_ref)
.map(transaction_to_fake_v5)
.map(|t| transaction_to_fake_v5(t, network, Height(*height)))
.map(Into::into)
.collect();