consensus: check Merkle roots
As a side effect of computing Merkle roots, we build a list of transaction hashes. Instead of discarding these, add them to PreparedBlock and FinalizedBlock so that they can be reused rather than recomputed. This commit adds Merkle root validation to: 1. the block verifier; 2. the checkpoint verifier. In the first case, Bitcoin Merkle tree malleability has no effect, because only a single Merkle tree in each malleablity set is valid (the others have duplicate transactions). In the second case, we need to check that the Merkle tree does not contain any duplicate transactions. Closes #1385 Closes #906
This commit is contained in:
parent
738b5b0f1b
commit
7c08c0c315
|
@ -25,12 +25,12 @@ use tracing::Instrument;
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
parameters::Network,
|
parameters::Network,
|
||||||
transparent,
|
transaction, transparent,
|
||||||
work::equihash,
|
work::equihash,
|
||||||
};
|
};
|
||||||
use zebra_state as zs;
|
use zebra_state as zs;
|
||||||
|
|
||||||
use crate::{error::*, transaction};
|
use crate::{error::*, transaction as tx};
|
||||||
use crate::{script, BoxError};
|
use crate::{script, BoxError};
|
||||||
|
|
||||||
mod check;
|
mod check;
|
||||||
|
@ -44,7 +44,7 @@ pub struct BlockVerifier<S> {
|
||||||
/// The network to be verified.
|
/// The network to be verified.
|
||||||
network: Network,
|
network: Network,
|
||||||
state_service: S,
|
state_service: S,
|
||||||
transaction_verifier: transaction::Verifier<S>,
|
transaction_verifier: tx::Verifier<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: dedupe with crate::error::BlockError
|
// TODO: dedupe with crate::error::BlockError
|
||||||
|
@ -83,7 +83,7 @@ where
|
||||||
{
|
{
|
||||||
pub fn new(network: Network, state_service: S) -> Self {
|
pub fn new(network: Network, state_service: S) -> Self {
|
||||||
let transaction_verifier =
|
let transaction_verifier =
|
||||||
transaction::Verifier::new(network, script::Verifier::new(state_service.clone()));
|
tx::Verifier::new(network, script::Verifier::new(state_service.clone()));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
network,
|
network,
|
||||||
|
@ -161,17 +161,24 @@ where
|
||||||
check::coinbase_is_first(&block)?;
|
check::coinbase_is_first(&block)?;
|
||||||
check::subsidy_is_valid(&block, network)?;
|
check::subsidy_is_valid(&block, network)?;
|
||||||
|
|
||||||
// TODO: context-free header verification: merkle root
|
// Precomputing this avoids duplicating transaction hash computations.
|
||||||
|
let transaction_hashes = block
|
||||||
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.hash())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
check::merkle_root_validity(&block, &transaction_hashes)?;
|
||||||
|
|
||||||
let mut async_checks = FuturesUnordered::new();
|
let mut async_checks = FuturesUnordered::new();
|
||||||
|
|
||||||
let known_utxos = new_outputs(&block);
|
let known_utxos = new_outputs(&block, &transaction_hashes);
|
||||||
for transaction in &block.transactions {
|
for transaction in &block.transactions {
|
||||||
let rsp = transaction_verifier
|
let rsp = transaction_verifier
|
||||||
.ready_and()
|
.ready_and()
|
||||||
.await
|
.await
|
||||||
.expect("transaction verifier is always ready")
|
.expect("transaction verifier is always ready")
|
||||||
.call(transaction::Request::Block {
|
.call(tx::Request::Block {
|
||||||
transaction: transaction.clone(),
|
transaction: transaction.clone(),
|
||||||
known_utxos: known_utxos.clone(),
|
known_utxos: known_utxos.clone(),
|
||||||
height,
|
height,
|
||||||
|
@ -199,6 +206,7 @@ where
|
||||||
hash,
|
hash,
|
||||||
height,
|
height,
|
||||||
new_outputs,
|
new_outputs,
|
||||||
|
transaction_hashes,
|
||||||
};
|
};
|
||||||
match state_service
|
match state_service
|
||||||
.ready_and()
|
.ready_and()
|
||||||
|
@ -220,11 +228,19 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_outputs(block: &Block) -> Arc<HashMap<transparent::OutPoint, zs::Utxo>> {
|
/// Compute an index of newly created transparent outputs, given a block and a
|
||||||
|
/// list of precomputed transaction hashes.
|
||||||
|
fn new_outputs(
|
||||||
|
block: &Block,
|
||||||
|
transaction_hashes: &[transaction::Hash],
|
||||||
|
) -> Arc<HashMap<transparent::OutPoint, zs::Utxo>> {
|
||||||
let mut new_outputs = HashMap::default();
|
let mut new_outputs = HashMap::default();
|
||||||
let height = block.coinbase_height().expect("block has coinbase height");
|
let height = block.coinbase_height().expect("block has coinbase height");
|
||||||
for transaction in &block.transactions {
|
for (transaction, hash) in block
|
||||||
let hash = transaction.hash();
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.zip(transaction_hashes.iter().cloned())
|
||||||
|
{
|
||||||
let from_coinbase = transaction.is_coinbase();
|
let from_coinbase = transaction.is_coinbase();
|
||||||
for (index, output) in transaction.outputs().iter().cloned().enumerate() {
|
for (index, output) in transaction.outputs().iter().cloned().enumerate() {
|
||||||
let index = index as u32;
|
let index = index as u32;
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::Hash,
|
block::{Block, Hash, Header, Height},
|
||||||
block::Height,
|
|
||||||
block::{Block, Header},
|
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
|
transaction,
|
||||||
work::{difficulty::ExpandedDifficulty, equihash},
|
work::{difficulty::ExpandedDifficulty, equihash},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -165,3 +164,21 @@ pub fn time_is_valid_at(
|
||||||
) -> Result<(), zebra_chain::block::BlockTimeError> {
|
) -> Result<(), zebra_chain::block::BlockTimeError> {
|
||||||
header.time_is_valid_at(now, height, hash)
|
header.time_is_valid_at(now, height, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check Merkle root validity.
|
||||||
|
///
|
||||||
|
/// `transaction_hashes` is a precomputed list of transaction hashes.
|
||||||
|
pub fn merkle_root_validity(
|
||||||
|
block: &Block,
|
||||||
|
transaction_hashes: &[transaction::Hash],
|
||||||
|
) -> Result<(), BlockError> {
|
||||||
|
let merkle_root = transaction_hashes.iter().cloned().collect();
|
||||||
|
if block.header.merkle_root == merkle_root {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(BlockError::BadMerkleRoot {
|
||||||
|
actual: merkle_root,
|
||||||
|
expected: block.header.merkle_root,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
//! block for the configured network.
|
//! block for the configured network.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::{BTreeMap, HashSet},
|
||||||
future::Future,
|
future::Future,
|
||||||
ops::{Bound, Bound::*},
|
ops::{Bound, Bound::*},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
|
@ -459,6 +459,33 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check for a valid Merkle root. To prevent malleability (CVE-2012-2459),
|
||||||
|
// we also need to check whether the transaction hashes are unique.
|
||||||
|
|
||||||
|
let transaction_hashes = block
|
||||||
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.map(|tx| tx.hash())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let merkle_root = transaction_hashes.iter().cloned().collect();
|
||||||
|
|
||||||
|
if block.header.merkle_root != merkle_root {
|
||||||
|
tx.send(Err(VerifyCheckpointError::BadMerkleRoot {
|
||||||
|
expected: block.header.merkle_root,
|
||||||
|
actual: merkle_root,
|
||||||
|
}))
|
||||||
|
.expect("rx has not been dropped yet");
|
||||||
|
return rx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collecting into a HashSet deduplicates, so this checks that there
|
||||||
|
// are no duplicate transaction hashes, preventing Merkle root malleability.
|
||||||
|
if transaction_hashes.len() != transaction_hashes.iter().collect::<HashSet<_>>().len() {
|
||||||
|
tx.send(Err(VerifyCheckpointError::DuplicateTransaction))
|
||||||
|
.expect("rx has not been dropped yet");
|
||||||
|
return rx;
|
||||||
|
}
|
||||||
|
|
||||||
// Since we're using Arc<Block>, each entry is a single pointer to the
|
// Since we're using Arc<Block>, each entry is a single pointer to the
|
||||||
// Arc. But there are a lot of QueuedBlockLists in the queue, so we keep
|
// Arc. But there are a lot of QueuedBlockLists in the queue, so we keep
|
||||||
// allocations as small as possible.
|
// allocations as small as possible.
|
||||||
|
@ -779,6 +806,13 @@ pub enum VerifyCheckpointError {
|
||||||
},
|
},
|
||||||
#[error("the block {hash:?} does not have a coinbase height")]
|
#[error("the block {hash:?} does not have a coinbase height")]
|
||||||
CoinbaseHeight { hash: block::Hash },
|
CoinbaseHeight { hash: block::Hash },
|
||||||
|
#[error("merkle root {actual:?} does not match expected {expected:?}")]
|
||||||
|
BadMerkleRoot {
|
||||||
|
actual: block::merkle::Root,
|
||||||
|
expected: block::merkle::Root,
|
||||||
|
},
|
||||||
|
#[error("duplicate transactions in block")]
|
||||||
|
DuplicateTransaction,
|
||||||
#[error("checkpoint verifier was dropped")]
|
#[error("checkpoint verifier was dropped")]
|
||||||
Dropped,
|
Dropped,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use zebra_chain::primitives::ed25519;
|
use zebra_chain::{block, primitives::ed25519};
|
||||||
|
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
|
@ -99,6 +99,12 @@ pub enum BlockError {
|
||||||
#[error("block has no transactions")]
|
#[error("block has no transactions")]
|
||||||
NoTransactions,
|
NoTransactions,
|
||||||
|
|
||||||
|
#[error("block has mismatched merkle root")]
|
||||||
|
BadMerkleRoot {
|
||||||
|
actual: block::merkle::Root,
|
||||||
|
expected: block::merkle::Root,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("block {0:?} is already in the chain at depth {1:?}")]
|
#[error("block {0:?} is already in the chain at depth {1:?}")]
|
||||||
AlreadyInChain(zebra_chain::block::Hash, u32),
|
AlreadyInChain(zebra_chain::block::Hash, u32),
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,8 @@ pub struct PreparedBlock {
|
||||||
/// be unspent, since a later transaction in a block can spend outputs of an
|
/// be unspent, since a later transaction in a block can spend outputs of an
|
||||||
/// earlier transaction.
|
/// earlier transaction.
|
||||||
pub new_outputs: HashMap<transparent::OutPoint, Utxo>,
|
pub new_outputs: HashMap<transparent::OutPoint, Utxo>,
|
||||||
|
/// A precomputed list of the hashes of the transactions in this block.
|
||||||
|
pub transaction_hashes: Vec<transaction::Hash>,
|
||||||
// TODO: add these parameters when we can compute anchors.
|
// TODO: add these parameters when we can compute anchors.
|
||||||
// sprout_anchor: sprout::tree::Root,
|
// sprout_anchor: sprout::tree::Root,
|
||||||
// sapling_anchor: sapling::tree::Root,
|
// sapling_anchor: sapling::tree::Root,
|
||||||
|
@ -97,6 +99,8 @@ pub struct FinalizedBlock {
|
||||||
/// be unspent, since a later transaction in a block can spend outputs of an
|
/// be unspent, since a later transaction in a block can spend outputs of an
|
||||||
/// earlier transaction.
|
/// earlier transaction.
|
||||||
pub(crate) new_outputs: HashMap<transparent::OutPoint, Utxo>,
|
pub(crate) new_outputs: HashMap<transparent::OutPoint, Utxo>,
|
||||||
|
/// A precomputed list of the hashes of the transactions in this block.
|
||||||
|
pub(crate) transaction_hashes: Vec<transaction::Hash>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doing precomputation in this From impl means that it will be done in
|
// Doing precomputation in this From impl means that it will be done in
|
||||||
|
@ -108,10 +112,19 @@ impl From<Arc<Block>> for FinalizedBlock {
|
||||||
.coinbase_height()
|
.coinbase_height()
|
||||||
.expect("finalized blocks must have a valid coinbase height");
|
.expect("finalized blocks must have a valid coinbase height");
|
||||||
let hash = block.hash();
|
let hash = block.hash();
|
||||||
|
let transaction_hashes = block
|
||||||
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.map(|tx| tx.hash())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut new_outputs = HashMap::default();
|
let mut new_outputs = HashMap::default();
|
||||||
for transaction in &block.transactions {
|
|
||||||
let hash = transaction.hash();
|
for (transaction, hash) in block
|
||||||
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.zip(transaction_hashes.iter().cloned())
|
||||||
|
{
|
||||||
let from_coinbase = transaction.is_coinbase();
|
let from_coinbase = transaction.is_coinbase();
|
||||||
for (index, output) in transaction.outputs().iter().cloned().enumerate() {
|
for (index, output) in transaction.outputs().iter().cloned().enumerate() {
|
||||||
let index = index as u32;
|
let index = index as u32;
|
||||||
|
@ -131,6 +144,7 @@ impl From<Arc<Block>> for FinalizedBlock {
|
||||||
height,
|
height,
|
||||||
hash,
|
hash,
|
||||||
new_outputs,
|
new_outputs,
|
||||||
|
transaction_hashes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,12 +156,14 @@ impl From<PreparedBlock> for FinalizedBlock {
|
||||||
height,
|
height,
|
||||||
hash,
|
hash,
|
||||||
new_outputs,
|
new_outputs,
|
||||||
|
transaction_hashes,
|
||||||
} = prepared;
|
} = prepared;
|
||||||
Self {
|
Self {
|
||||||
block,
|
block,
|
||||||
height,
|
height,
|
||||||
hash,
|
hash,
|
||||||
new_outputs,
|
new_outputs,
|
||||||
|
transaction_hashes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,6 +152,7 @@ impl FinalizedState {
|
||||||
hash,
|
hash,
|
||||||
height,
|
height,
|
||||||
new_outputs,
|
new_outputs,
|
||||||
|
transaction_hashes,
|
||||||
} = finalized;
|
} = finalized;
|
||||||
|
|
||||||
let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
|
let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
|
||||||
|
@ -215,8 +216,12 @@ impl FinalizedState {
|
||||||
|
|
||||||
// Index each transaction, spent inputs, nullifiers
|
// Index each transaction, spent inputs, nullifiers
|
||||||
// TODO: move computation into FinalizedBlock as with transparent outputs
|
// TODO: move computation into FinalizedBlock as with transparent outputs
|
||||||
for (transaction_index, transaction) in block.transactions.iter().enumerate() {
|
for (transaction_index, (transaction, transaction_hash)) in block
|
||||||
let transaction_hash = transaction.hash();
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.zip(transaction_hashes.into_iter())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
let transaction_location = TransactionLocation {
|
let transaction_location = TransactionLocation {
|
||||||
height,
|
height,
|
||||||
index: transaction_index
|
index: transaction_index
|
||||||
|
|
|
@ -136,7 +136,12 @@ trait UpdateWith<T> {
|
||||||
|
|
||||||
impl UpdateWith<PreparedBlock> for Chain {
|
impl UpdateWith<PreparedBlock> for Chain {
|
||||||
fn update_chain_state_with(&mut self, prepared: &PreparedBlock) {
|
fn update_chain_state_with(&mut self, prepared: &PreparedBlock) {
|
||||||
let (block, hash, height) = (prepared.block.as_ref(), prepared.hash, prepared.height);
|
let (block, hash, height, transaction_hashes) = (
|
||||||
|
prepared.block.as_ref(),
|
||||||
|
prepared.hash,
|
||||||
|
prepared.height,
|
||||||
|
&prepared.transaction_hashes,
|
||||||
|
);
|
||||||
|
|
||||||
// add hash to height_by_hash
|
// add hash to height_by_hash
|
||||||
let prior_height = self.height_by_hash.insert(hash, height);
|
let prior_height = self.height_by_hash.insert(hash, height);
|
||||||
|
@ -154,7 +159,12 @@ impl UpdateWith<PreparedBlock> for Chain {
|
||||||
self.partial_cumulative_work += block_work;
|
self.partial_cumulative_work += block_work;
|
||||||
|
|
||||||
// for each transaction in block
|
// for each transaction in block
|
||||||
for (transaction_index, transaction) in block.transactions.iter().enumerate() {
|
for (transaction_index, (transaction, transaction_hash)) in block
|
||||||
|
.transactions
|
||||||
|
.iter()
|
||||||
|
.zip(transaction_hashes.iter().cloned())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
let (inputs, shielded_data, joinsplit_data) = match transaction.deref() {
|
let (inputs, shielded_data, joinsplit_data) = match transaction.deref() {
|
||||||
transaction::Transaction::V4 {
|
transaction::Transaction::V4 {
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -168,7 +178,6 @@ impl UpdateWith<PreparedBlock> for Chain {
|
||||||
};
|
};
|
||||||
|
|
||||||
// add key `transaction.hash` and value `(height, tx_index)` to `tx_by_hash`
|
// add key `transaction.hash` and value `(height, tx_index)` to `tx_by_hash`
|
||||||
let transaction_hash = transaction.hash();
|
|
||||||
let prior_pair = self
|
let prior_pair = self
|
||||||
.tx_by_hash
|
.tx_by_hash
|
||||||
.insert(transaction_hash, (height, transaction_index));
|
.insert(transaction_hash, (height, transaction_index));
|
||||||
|
@ -190,7 +199,11 @@ impl UpdateWith<PreparedBlock> for Chain {
|
||||||
|
|
||||||
#[instrument(skip(self, prepared), fields(block = %prepared.block))]
|
#[instrument(skip(self, prepared), fields(block = %prepared.block))]
|
||||||
fn revert_chain_state_with(&mut self, prepared: &PreparedBlock) {
|
fn revert_chain_state_with(&mut self, prepared: &PreparedBlock) {
|
||||||
let (block, hash) = (prepared.block.as_ref(), prepared.hash);
|
let (block, hash, transaction_hashes) = (
|
||||||
|
prepared.block.as_ref(),
|
||||||
|
prepared.hash,
|
||||||
|
&prepared.transaction_hashes,
|
||||||
|
);
|
||||||
|
|
||||||
// remove the blocks hash from `height_by_hash`
|
// remove the blocks hash from `height_by_hash`
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -207,7 +220,9 @@ impl UpdateWith<PreparedBlock> for Chain {
|
||||||
self.partial_cumulative_work -= block_work;
|
self.partial_cumulative_work -= block_work;
|
||||||
|
|
||||||
// for each transaction in block
|
// for each transaction in block
|
||||||
for transaction in &block.transactions {
|
for (transaction, transaction_hash) in
|
||||||
|
block.transactions.iter().zip(transaction_hashes.iter())
|
||||||
|
{
|
||||||
let (inputs, shielded_data, joinsplit_data) = match transaction.deref() {
|
let (inputs, shielded_data, joinsplit_data) = match transaction.deref() {
|
||||||
transaction::Transaction::V4 {
|
transaction::Transaction::V4 {
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -221,9 +236,8 @@ impl UpdateWith<PreparedBlock> for Chain {
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove `transaction.hash` from `tx_by_hash`
|
// remove `transaction.hash` from `tx_by_hash`
|
||||||
let transaction_hash = transaction.hash();
|
|
||||||
assert!(
|
assert!(
|
||||||
self.tx_by_hash.remove(&transaction_hash).is_some(),
|
self.tx_by_hash.remove(transaction_hash).is_some(),
|
||||||
"transactions must be present if block was"
|
"transactions must be present if block was"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ impl Prepare for Arc<Block> {
|
||||||
let block = self;
|
let block = self;
|
||||||
let hash = block.hash();
|
let hash = block.hash();
|
||||||
let height = block.coinbase_height().unwrap();
|
let height = block.coinbase_height().unwrap();
|
||||||
|
let transaction_hashes = block.transactions.iter().map(|tx| tx.hash()).collect();
|
||||||
let new_outputs = crate::utxo::new_outputs(&block);
|
let new_outputs = crate::utxo::new_outputs(&block);
|
||||||
|
|
||||||
PreparedBlock {
|
PreparedBlock {
|
||||||
|
@ -28,6 +29,7 @@ impl Prepare for Arc<Block> {
|
||||||
hash,
|
hash,
|
||||||
height,
|
height,
|
||||||
new_outputs,
|
new_outputs,
|
||||||
|
transaction_hashes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue