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::{ use crate::{
amount::Amount, amount::Amount,
block, block,
parameters::NetworkUpgrade, parameters::{Network, NetworkUpgrade},
primitives::{Bctv14Proof, Groth16Proof, ZkSnarkProof}, primitives::{Bctv14Proof, Groth16Proof, ZkSnarkProof},
sapling, sprout, transparent, LedgerState, sapling, sprout, transparent, LedgerState,
}; };
@ -349,21 +349,24 @@ impl Arbitrary for Transaction {
// Utility functions // 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, /// Convert `trans` into a fake v5 transaction,
/// converting sapling shielded data from v4 to v5 if possible. /// 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::*; use Transaction::*;
let block_nu = NetworkUpgrade::current(network, height);
match trans { match trans {
V1 { V1 {
inputs, inputs,
outputs, outputs,
lock_time, lock_time,
} => V5 { } => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE, network_upgrade: block_nu,
inputs: inputs.to_vec(), inputs: inputs.to_vec(),
outputs: outputs.to_vec(), outputs: outputs.to_vec(),
lock_time: *lock_time, lock_time: *lock_time,
@ -376,7 +379,7 @@ pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
lock_time, lock_time,
.. ..
} => V5 { } => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE, network_upgrade: block_nu,
inputs: inputs.to_vec(), inputs: inputs.to_vec(),
outputs: outputs.to_vec(), outputs: outputs.to_vec(),
lock_time: *lock_time, lock_time: *lock_time,
@ -390,7 +393,7 @@ pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
expiry_height, expiry_height,
.. ..
} => V5 { } => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE, network_upgrade: block_nu,
inputs: inputs.to_vec(), inputs: inputs.to_vec(),
outputs: outputs.to_vec(), outputs: outputs.to_vec(),
lock_time: *lock_time, lock_time: *lock_time,
@ -405,7 +408,7 @@ pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
sapling_shielded_data, sapling_shielded_data,
.. ..
} => V5 { } => V5 {
network_upgrade: FAKE_NETWORK_UPGRADE, network_upgrade: block_nu,
inputs: inputs.to_vec(), inputs: inputs.to_vec(),
outputs: outputs.to_vec(), outputs: outputs.to_vec(),
lock_time: *lock_time, lock_time: *lock_time,

View File

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

View File

@ -161,7 +161,7 @@ where
.map(|t| t.hash()) .map(|t| t.hash())
.collect::<Vec<_>>(); .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 // Since errors cause an early exit, try to do the
// quick checks first. // quick checks first.

View File

@ -168,10 +168,48 @@ pub fn time_is_valid_at(
/// Check Merkle root validity. /// Check Merkle root validity.
/// ///
/// `transaction_hashes` is a precomputed list of transaction hashes. /// `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( pub fn merkle_root_validity(
network: Network,
block: &Block, block: &Block,
transaction_hashes: &[transaction::Hash], transaction_hashes: &[transaction::Hash],
) -> Result<(), BlockError> { ) -> 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(); let merkle_root = transaction_hashes.iter().cloned().collect();
if block.header.merkle_root != merkle_root { if block.header.merkle_root != merkle_root {

View File

@ -13,8 +13,9 @@ use tower::buffer::Buffer;
use zebra_chain::{ use zebra_chain::{
block::{self, Block, Height}, block::{self, Block, Height},
parameters::Network, parameters::{Network, NetworkUpgrade},
serialization::{ZcashDeserialize, ZcashDeserializeInto}, serialization::{ZcashDeserialize, ZcashDeserializeInto},
transaction::{arbitrary::transaction_to_fake_v5, Transaction},
work::difficulty::{ExpandedDifficulty, INVALID_COMPACT_DIFFICULTY}, work::difficulty::{ExpandedDifficulty, INVALID_COMPACT_DIFFICULTY},
}; };
use zebra_test::transcript::{TransError, Transcript}; use zebra_test::transcript::{TransError, Transcript};
@ -435,3 +436,92 @@ fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
Ok(()) 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()) .map(|tx| tx.hash())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
crate::block::check::merkle_root_validity(&block, &transaction_hashes)?; crate::block::check::merkle_root_validity(self.network, &block, &transaction_hashes)?;
Ok(height) Ok(height)
} }

View File

@ -146,4 +146,7 @@ pub enum BlockError {
zebra_chain::work::difficulty::ExpandedDifficulty, zebra_chain::work::difficulty::ExpandedDifficulty,
zebra_chain::parameters::Network, 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::{ use zebra_chain::{
block::Block, block::{Block, Height},
parameters::Network,
serialization::ZcashDeserializeInto, serialization::ZcashDeserializeInto,
transaction::{arbitrary::transaction_to_fake_v5, Transaction}, transaction::{arbitrary::transaction_to_fake_v5, Transaction},
}; };
@ -11,8 +12,22 @@ use color_eyre::eyre::Report;
fn v5_fake_transactions() -> Result<(), Report> { fn v5_fake_transactions() -> Result<(), Report> {
zebra_test::init(); 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 // 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 let original_block = original_bytes
.zcash_deserialize_into::<Block>() .zcash_deserialize_into::<Block>()
.expect("block is structurally valid"); .expect("block is structurally valid");
@ -22,7 +37,7 @@ fn v5_fake_transactions() -> Result<(), Report> {
.transactions .transactions
.iter() .iter()
.map(AsRef::as_ref) .map(AsRef::as_ref)
.map(transaction_to_fake_v5) .map(|t| transaction_to_fake_v5(t, network, Height(*height)))
.map(Into::into) .map(Into::into)
.collect(); .collect();