Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ZIP-221 and ZIP-244 commitment validation in non-finalized state #2609

Merged
merged 4 commits into from
Aug 17, 2021
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions zebra-chain/src/block/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ impl From<ChainHistoryMmrRootHash> for [u8; 32] {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ChainHistoryBlockTxAuthCommitmentHash([u8; 32]);

impl From<[u8; 32]> for ChainHistoryBlockTxAuthCommitmentHash {
fn from(hash: [u8; 32]) -> Self {
ChainHistoryBlockTxAuthCommitmentHash(hash)
}
}

impl From<ChainHistoryBlockTxAuthCommitmentHash> for [u8; 32] {
fn from(hash: ChainHistoryBlockTxAuthCommitmentHash) -> Self {
hash.0
}
}

/// Errors that can occur when checking RootHash consensus rules.
///
/// Each error variant corresponds to a consensus rule, so enumerating
Expand Down
12 changes: 12 additions & 0 deletions zebra-chain/src/block/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ impl fmt::Debug for AuthDataRoot {
}
}

impl From<[u8; 32]> for AuthDataRoot {
fn from(hash: [u8; 32]) -> Self {
AuthDataRoot(hash)
}
}

impl From<AuthDataRoot> for [u8; 32] {
fn from(hash: AuthDataRoot) -> Self {
hash.0
}
}

impl<T> std::iter::FromIterator<T> for AuthDataRoot
where
T: std::convert::AsRef<Transaction>,
Expand Down
5 changes: 5 additions & 0 deletions zebra-chain/src/history_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ impl HistoryTree {
};
Ok(())
}

/// Return the hash of the tree root if the tree is not empty.
pub fn hash(&self) -> Option<ChainHistoryMmrRootHash> {
Some(self.0.as_ref()?.hash())
}
}

impl From<NonEmptyHistoryTree> for HistoryTree {
Expand Down
1 change: 1 addition & 0 deletions zebra-state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ rlimit = "0.5.4"
# TODO: this crate is not maintained anymore. Replace it?
# https://github.com/ZcashFoundation/zebra/issues/2523
multiset = "0.0.5"
blake2b_simd = "0.5.11"

proptest = { version = "0.10.1", optional = true }
zebra-test = { path = "../zebra-test/", optional = true }
Expand Down
3 changes: 3 additions & 0 deletions zebra-state/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ pub enum ValidateContextError {

#[error("error building the history tree")]
HistoryTreeError(#[from] HistoryTreeError),

#[error("block contains an invalid commitment")]
InvalidBlockCommitment(#[from] block::CommitmentError),
}

/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
Expand Down
2 changes: 1 addition & 1 deletion zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ impl StateService {
let relevant_chain = self.any_ancestor_blocks(prepared.block.header.previous_block_hash);

// Security: check proof of work before any other checks
check::block_is_contextually_valid(
check::block_is_valid_for_recent_chain(
prepared,
self.network,
self.disk.finalized_tip_height(),
Expand Down
4 changes: 3 additions & 1 deletion zebra-state/src/service/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ pub struct PreparedChain {

impl PreparedChain {
/// Create a PreparedChain strategy with Heartwood-onward blocks.
#[cfg(test)]
// dead_code is allowed because the function is called only by tests,
// but the code is also compiled when proptest-impl is activated.
#[allow(dead_code)]
pub(crate) fn new_heartwood() -> Self {
// The history tree only works with Heartwood onward.
// Since the network will be chosen later, we pick the larger
Expand Down
89 changes: 79 additions & 10 deletions zebra-state/src/service/check.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! Consensus critical contextual checks

use std::borrow::Borrow;
use std::{borrow::Borrow, convert::TryInto};

use chrono::Duration;

use zebra_chain::{
block::{self, Block},
block::{self, Block, CommitmentError},
history_tree::HistoryTree,
parameters::POW_AVERAGING_WINDOW,
parameters::{Network, NetworkUpgrade},
work::difficulty::CompactDifficulty,
Expand All @@ -24,8 +25,11 @@ pub(crate) mod utxo;
#[cfg(test)]
mod tests;

/// Check that `block` is contextually valid for `network`, based on the
/// `finalized_tip_height` and `relevant_chain`.
/// Check that the `prepared` block is contextually valid for `network`, based
/// on the `finalized_tip_height` and `relevant_chain`.
///
/// This function performs checks that require a small number of recent blocks,
/// including previous hash, previous height, and block difficulty.
///
/// The relevant chain is an iterator over the ancestors of `block`, starting
/// with its parent block.
Expand All @@ -34,12 +38,8 @@ mod tests;
///
/// If the state contains less than 28
/// (`POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN`) blocks.
#[tracing::instrument(
name = "contextual_validation",
fields(?network),
skip(prepared, network, finalized_tip_height, relevant_chain)
)]
pub(crate) fn block_is_contextually_valid<C>(
#[tracing::instrument(skip(prepared, finalized_tip_height, relevant_chain))]
pub(crate) fn block_is_valid_for_recent_chain<C>(
prepared: &PreparedBlock,
network: Network,
finalized_tip_height: Option<block::Height>,
Expand Down Expand Up @@ -100,6 +100,75 @@ where
Ok(())
}

/// Check that the `prepared` block is contextually valid for `network`, using
/// the `history_tree` up to and including the previous block.
#[tracing::instrument(skip(prepared, history_tree))]
pub(crate) fn block_commitment_is_valid_for_chain_history(
prepared: &PreparedBlock,
network: Network,
history_tree: &HistoryTree,
) -> Result<(), ValidateContextError> {
match prepared.block.commitment(network)? {
block::Commitment::PreSaplingReserved(_)
| block::Commitment::FinalSaplingRoot(_)
| block::Commitment::ChainHistoryActivationReserved => {
// No contextual checks needed for those.
Ok(())
}
block::Commitment::ChainHistoryRoot(actual_history_tree_root) => {
let history_tree_root = history_tree
.hash()
.expect("the history tree of the previous block must exist since the current block has a ChainHistoryRoot");
if actual_history_tree_root == history_tree_root {
Ok(())
} else {
Err(ValidateContextError::InvalidBlockCommitment(
CommitmentError::InvalidChainHistoryRoot {
actual: actual_history_tree_root.into(),
expected: history_tree_root.into(),
},
))
}
}
block::Commitment::ChainHistoryBlockTxAuthCommitment(actual_hash_block_commitments) => {
let actual_block_commitments: [u8; 32] = actual_hash_block_commitments.into();
let history_tree_root = history_tree
.hash()
.expect("the history tree of the previous block must exist since the current block has a ChainHistoryBlockTxAuthCommitment");
let auth_data_root = prepared.block.auth_data_root();

// > The value of this hash [hashBlockCommitments] is the BLAKE2b-256 hash personalized
// > by the string "ZcashBlockCommit" of the following elements:
// > hashLightClientRoot (as described in ZIP 221)
// > hashAuthDataRoot (as described below)
// > terminator [0u8;32]
// https://zips.z.cash/zip-0244#block-header-changes
let hash_block_commitments: [u8; 32] = blake2b_simd::Params::new()
.hash_length(32)
.personal(b"ZcashBlockCommit")
.to_state()
.update(&<[u8; 32]>::from(history_tree_root)[..])
.update(&<[u8; 32]>::from(auth_data_root))
.update(&[0u8; 32])
.finalize()
.as_bytes()
.try_into()
.expect("32 byte array");

if actual_block_commitments == hash_block_commitments {
Ok(())
} else {
Err(ValidateContextError::InvalidBlockCommitment(
CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
actual: actual_block_commitments,
expected: hash_block_commitments,
},
))
}
}
}
}

/// Returns `ValidateContextError::OrphanedBlock` if the height of the given
/// block is less than or equal to the finalized tip height.
fn block_is_not_orphaned(
Expand Down
5 changes: 5 additions & 0 deletions zebra-state/src/service/non_finalized_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ impl NonFinalizedState {
&parent_chain.spent_utxos,
finalized_state,
)?;
check::block_commitment_is_valid_for_chain_history(
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
&prepared,
self.network,
&parent_chain.history_tree,
)?;

parent_chain.push(prepared)
}
Expand Down
112 changes: 107 additions & 5 deletions zebra-state/src/service/non_finalized_state/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::sync::Arc;

use zebra_chain::{
block::Block,
history_tree::NonEmptyHistoryTree,
parameters::{Network, NetworkUpgrade},
serialization::ZcashDeserializeInto,
};
Expand Down Expand Up @@ -392,13 +393,13 @@ fn history_tree_is_updated_for_network_upgrade(
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid"),
);
let activation_block = prev_block.make_fake_child();
let next_block = activation_block.make_fake_child();

let mut state = NonFinalizedState::new(network);
let finalized_state = FinalizedState::new(&Config::ephemeral(), network);

state.commit_new_chain(prev_block.prepare(), &finalized_state)?;
state
.commit_new_chain(prev_block.clone().prepare(), &finalized_state)
.unwrap();

let chain = state.best_chain().unwrap();
if network_upgrade == NetworkUpgrade::Heartwood {
Expand All @@ -413,7 +414,12 @@ fn history_tree_is_updated_for_network_upgrade(
);
}

state.commit_block(activation_block.prepare(), &finalized_state)?;
// The Heartwood activation block has an all-zero commitment
let activation_block = prev_block.make_fake_child().set_block_commitment([0u8; 32]);

state
.commit_block(activation_block.clone().prepare(), &finalized_state)
.unwrap();

let chain = state.best_chain().unwrap();
assert!(
Expand All @@ -426,7 +432,22 @@ fn history_tree_is_updated_for_network_upgrade(
"history tree must have a single node"
);

state.commit_block(next_block.prepare(), &finalized_state)?;
// To fix the commitment in the next block we must recreate the history tree
let tree = NonEmptyHistoryTree::from_block(
Network::Mainnet,
activation_block.clone(),
&chain.sapling_note_commitment_tree.root(),
&chain.orchard_note_commitment_tree.root(),
)
.unwrap();

let next_block = activation_block
.make_fake_child()
.set_block_commitment(tree.hash().into());

state
.commit_block(next_block.prepare(), &finalized_state)
.unwrap();

assert!(
state.best_chain().unwrap().history_tree.as_ref().is_some(),
Expand All @@ -435,3 +456,84 @@ fn history_tree_is_updated_for_network_upgrade(

Ok(())
}

#[test]
fn commitment_is_validated() {
commitment_is_validated_for_network_upgrade(Network::Mainnet, NetworkUpgrade::Heartwood);
commitment_is_validated_for_network_upgrade(Network::Testnet, NetworkUpgrade::Heartwood);
// TODO: we can't test other upgrades until we have a method for creating a FinalizedState
// with a HistoryTree.
}

fn commitment_is_validated_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade) {
let blocks = match network {
Network::Mainnet => &*zebra_test::vectors::MAINNET_BLOCKS,
Network::Testnet => &*zebra_test::vectors::TESTNET_BLOCKS,
};
let height = network_upgrade.activation_height(network).unwrap().0;

let prev_block = Arc::new(
blocks
.get(&(height - 1))
.expect("test vector exists")
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid"),
);

let mut state = NonFinalizedState::new(network);
let finalized_state = FinalizedState::new(&Config::ephemeral(), network);

state
.commit_new_chain(prev_block.clone().prepare(), &finalized_state)
.unwrap();

// The Heartwood activation block must have an all-zero commitment.
// Test error return when committing the block with the wrong commitment
let activation_block = prev_block.make_fake_child();
let err = state
.commit_block(activation_block.clone().prepare(), &finalized_state)
.unwrap_err();
match err {
crate::ValidateContextError::InvalidBlockCommitment(
zebra_chain::block::CommitmentError::InvalidChainHistoryActivationReserved { .. },
) => {},
_ => panic!("Error must be InvalidBlockCommitment::InvalidChainHistoryActivationReserved instead of {:?}", err),
};

// Test committing the Heartwood activation block with the correct commitment
let activation_block = activation_block.set_block_commitment([0u8; 32]);
state
.commit_block(activation_block.clone().prepare(), &finalized_state)
.unwrap();

// To fix the commitment in the next block we must recreate the history tree
let chain = state.best_chain().unwrap();
let tree = NonEmptyHistoryTree::from_block(
Network::Mainnet,
activation_block.clone(),
&chain.sapling_note_commitment_tree.root(),
&chain.orchard_note_commitment_tree.root(),
)
.unwrap();

// Test committing the next block with the wrong commitment
let next_block = activation_block.make_fake_child();
let err = state
.commit_block(next_block.clone().prepare(), &finalized_state)
.unwrap_err();
match err {
crate::ValidateContextError::InvalidBlockCommitment(
zebra_chain::block::CommitmentError::InvalidChainHistoryRoot { .. },
) => {}
_ => panic!(
"Error must be InvalidBlockCommitment::InvalidChainHistoryRoot instead of {:?}",
err
),
};
Comment on lines +519 to +532
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💟


// Test committing the next block with the correct commitment
let next_block = next_block.set_block_commitment(tree.hash().into());
state
.commit_block(next_block.prepare(), &finalized_state)
.unwrap();
Comment on lines +534 to +538
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
8 changes: 8 additions & 0 deletions zebra-state/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub trait FakeChainHelper {
fn make_fake_child(&self) -> Arc<Block>;

fn set_work(self, work: u128) -> Arc<Block>;

fn set_block_commitment(self, commitment: [u8; 32]) -> Arc<Block>;
}

impl FakeChainHelper for Arc<Block> {
Expand Down Expand Up @@ -53,6 +55,12 @@ impl FakeChainHelper for Arc<Block> {
block.header.difficulty_threshold = expanded.into();
self
}

fn set_block_commitment(mut self, block_commitment: [u8; 32]) -> Arc<Block> {
let block = Arc::make_mut(&mut self);
block.header.commitment_bytes = block_commitment;
self
}
}

fn work_to_expanded(work: U256) -> ExpandedDifficulty {
Expand Down