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: Validate chain history commitments in the non-finalized state #2301

Merged
merged 38 commits into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5c7d941
sketch of implementation
conradoplg Jun 14, 2021
5226145
refined implementation; still incomplete
conradoplg Jun 16, 2021
3abbfa5
update librustzcash, change zcash_history to work with it
conradoplg Jun 17, 2021
e929f3f
simplified code per review; renamed MMR to HistoryTree
conradoplg Jun 17, 2021
c1dd319
expand HistoryTree implementation
conradoplg Jun 17, 2021
e290fbd
Merge remote-tracking branch 'origin/main' into zip221-non-finalized-…
conradoplg Jun 17, 2021
edc3fe0
Merge remote-tracking branch 'origin/main' into zip221-non-finalized-…
conradoplg Jun 21, 2021
59b46fe
handle and propagate errors
conradoplg Jun 21, 2021
d06dfcc
simplify check.rs tracing
conradoplg Jun 21, 2021
1cf9013
add suggested TODO
conradoplg Jun 21, 2021
89c12c6
add HistoryTree::prune
conradoplg Jun 21, 2021
b3d773a
fix bug in pruning
conradoplg Jun 22, 2021
e2ab46c
fix compilation of tests; still need to make them pass
conradoplg Jun 22, 2021
c52a66d
Apply suggestions from code review
conradoplg Jun 23, 2021
e977cd0
Apply suggestions from code review
conradoplg Jun 23, 2021
030240f
improvements from code review
conradoplg Jun 23, 2021
e416ef6
improve check.rs comments and variable names
conradoplg Jun 23, 2021
23e9bc2
fix HistoryTree which should use BTreeMap and not HashMap; fix non_fi…
conradoplg Jun 24, 2021
cf339a3
fix finalized_state proptest
conradoplg Jun 25, 2021
2271d73
fix non_finalized_state tests by setting the correct commitments
conradoplg Jun 25, 2021
13e50e2
renamed mmr.rs to history_tree.rs
conradoplg Jun 25, 2021
e5a62e1
Merge remote-tracking branch 'origin/main' into zip221-non-finalized-…
conradoplg Jun 25, 2021
7837704
Add HistoryTree struct
conradoplg Jun 25, 2021
985c6aa
expand non_finalized_state protest
conradoplg Jun 28, 2021
d9a36ea
Merge branch 'history-tree' into zip221-non-finalized-state
conradoplg Jun 28, 2021
5249335
fix typo
conradoplg Jun 28, 2021
787cc60
Merge remote-tracking branch 'origin/main' into zip221-non-finalized-…
conradoplg Jun 29, 2021
fca06e6
Add HistoryTree struct
conradoplg Jun 25, 2021
fa847b8
Update zebra-chain/src/primitives/zcash_history.rs
conradoplg Jun 29, 2021
8e38ed4
fix formatting
conradoplg Jun 29, 2021
f154696
Apply suggestions from code review
conradoplg Jun 29, 2021
81b0ddb
history_tree.rs: fixes from code review
conradoplg Jun 29, 2021
63d4fb0
Merge remote-tracking branch 'origin/main' into zip221-non-finalized-…
conradoplg Jul 1, 2021
590a0d7
Merge branch 'history-tree' into zip221-non-finalized-state
conradoplg Jul 1, 2021
608c2e2
fixes to work with updated HistoryTree
conradoplg Jul 1, 2021
8dae6e7
Improvements from code review
conradoplg Jul 2, 2021
2e928db
Add Debug implementations to allow comparing Chains with proptest_ass…
conradoplg Jul 5, 2021
e9add63
Merge remote-tracking branch 'origin/main' into zip221-non-finalized-…
conradoplg Jul 6, 2021
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 zebra-chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ extern crate bitflags;
pub mod amount;
pub mod block;
pub mod fmt;
pub mod mmr;
pub mod orchard;
pub mod parameters;
pub mod primitives;
Expand Down
43 changes: 43 additions & 0 deletions zebra-chain/src/mmr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Merkle mountain range structure that contains information about
//! the block history as specified in ZIP-221.

use std::borrow::Borrow;

use crate::block::{Block, ChainHistoryMmrRootHash};

/// Merkle mountain range structure.
pub struct MerkleMountainRange {
// TODO
}

impl MerkleMountainRange {
pub fn push(&mut self, block: &Block) {
// TODO: zcash_history::Tree::append_leaf receives an Arc<Block>. Should
// change that to match, or create an Arc to pass to it?
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
todo!();
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
}

/// Extend the MMR with the given blocks.
// TODO: add Sapling and Orchard roots to parameters
pub fn extend<C>(&mut self, _vals: &C)
where
C: IntoIterator,
C::Item: Borrow<Block>,
C::IntoIter: ExactSizeIterator,
{
// TODO: How to convert C to `impl Iterator<Item = (Arc<Block>, sapling::tree::Root)>`
// for zcash_history::Tree::append_leaf_iter? Should change that to match C?
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
todo!();
}
dconnolly marked this conversation as resolved.
Show resolved Hide resolved

/// Return the hash of the MMR root.
pub fn hash(&self) -> ChainHistoryMmrRootHash {
todo!();
}
}

impl Clone for MerkleMountainRange {
fn clone(&self) -> Self {
todo!();
}
}
15 changes: 14 additions & 1 deletion zebra-state/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::sync::Arc;
use chrono::{DateTime, Utc};
use thiserror::Error;

use zebra_chain::{block, work::difficulty::CompactDifficulty};
use zebra_chain::{
block::{self, ChainHistoryMmrRootHash, CommitmentError},
work::difficulty::CompactDifficulty,
};

/// A wrapper for type erased errors that is itself clonable and implements the
/// Error trait
Expand Down Expand Up @@ -74,4 +77,14 @@ pub enum ValidateContextError {
difficulty_threshold: CompactDifficulty,
expected_difficulty: CompactDifficulty,
},

#[error("block contains an invalid commitment")]
InvalidCommitment(#[from] CommitmentError),
dconnolly marked this conversation as resolved.
Show resolved Hide resolved

#[error("block history commitment {candidate_commitment:?} is different to the expected commitment {expected_commitment:?}")]
#[non_exhaustive]
InvalidHistoryCommitment {
candidate_commitment: ChainHistoryMmrRootHash,
expected_commitment: ChainHistoryMmrRootHash,
},
}
9 changes: 7 additions & 2 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,14 @@ impl StateService {
let parent_hash = prepared.block.header.previous_block_hash;

if self.disk.finalized_tip_hash() == parent_hash {
self.mem.commit_new_chain(prepared);
self.mem
.commit_new_chain(prepared, self.disk.mmr().clone())?;
} else {
self.mem.commit_block(prepared);
let block_iter = self.any_ancestor_blocks(parent_hash);
// TODO: the above creates a immutable borrow of self, and below it uses
// a mutable borrow. which causes a compiler error. How to solve this?
self.mem
.commit_block(prepared, self.disk.mmr(), &block_iter)?;
conradoplg marked this conversation as resolved.
Show resolved Hide resolved
}

Ok(())
Expand Down
37 changes: 36 additions & 1 deletion zebra-state/src/service/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::borrow::Borrow;

use chrono::Duration;
use zebra_chain::{
block::{self, Block},
block::{self, Block, ChainHistoryMmrRootHash},
parameters::POW_AVERAGING_WINDOW,
parameters::{Network, NetworkUpgrade},
work::difficulty::CompactDifficulty,
Expand Down Expand Up @@ -86,6 +86,41 @@ where
Ok(())
}

#[tracing::instrument(
name = "contextual_validation_for_chain",
fields(?network),
skip(prepared, network)
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
)]
pub(crate) fn block_is_contextually_valid_for_chain(
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
prepared: &PreparedBlock,
network: Network,
mmr_hash: &ChainHistoryMmrRootHash,
) -> 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(block_mmr_hash) => {
if block_mmr_hash == *mmr_hash {
Ok(())
} else {
Err(ValidateContextError::InvalidHistoryCommitment {
candidate_commitment: block_mmr_hash,
expected_commitment: *mmr_hash,
})
}
}
block::Commitment::ChainHistoryBlockTxAuthCommitment(_) => {
// TODO: Get auth_hash from block (ZIP-244), e.g.
// let auth_hash = prepared.block.auth_hash();
todo!("hash mmr_hash and auth_hash per ZIP-244 and compare")
}
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// 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
6 changes: 6 additions & 0 deletions zebra-state/src/service/finalized_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod tests;

use std::{collections::HashMap, convert::TryInto, sync::Arc};

use zebra_chain::mmr::MerkleMountainRange;
use zebra_chain::transparent;
use zebra_chain::{
block::{self, Block},
Expand Down Expand Up @@ -378,6 +379,11 @@ impl FinalizedState {
})
}

/// Returns the MMR for the finalized state.
pub fn mmr(&self) -> &MerkleMountainRange {
todo!("add MMR to finalized state");
}

/// If the database is `ephemeral`, delete it.
fn delete_ephemeral(&self) {
if self.ephemeral {
Expand Down
42 changes: 36 additions & 6 deletions zebra-state/src/service/non_finalized_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ mod tests;

pub use queued_blocks::QueuedBlocks;

use std::{collections::BTreeSet, mem, ops::Deref, sync::Arc};
use std::{borrow::Borrow, collections::BTreeSet, mem, ops::Deref, sync::Arc};

use zebra_chain::{
block::{self, Block},
mmr::MerkleMountainRange,
parameters::{Network, NetworkUpgrade::Canopy},
transaction::{self, Transaction},
transparent,
};

use crate::{FinalizedBlock, HashOrHeight, PreparedBlock, Utxo};
use crate::{FinalizedBlock, HashOrHeight, PreparedBlock, Utxo, ValidateContextError};

use self::chain::Chain;

use super::check;

/// The state of the chains in memory, incuding queued blocks.
#[derive(Default)]
pub struct NonFinalizedState {
Expand Down Expand Up @@ -76,7 +79,17 @@ impl NonFinalizedState {
}

/// Commit block to the non-finalized state.
pub fn commit_block(&mut self, prepared: PreparedBlock) {
pub fn commit_block<C>(
&mut self,
prepared: PreparedBlock,
finalized_tip_mmr: &MerkleMountainRange,
block_iter: &C,
) -> Result<(), ValidateContextError>
where
C: IntoIterator,
C::Item: Borrow<Block>,
C::IntoIter: ExactSizeIterator,
{
let parent_hash = prepared.block.header.previous_block_hash;
let (height, hash) = (prepared.height, prepared.hash);

Expand All @@ -92,24 +105,41 @@ impl NonFinalizedState {
.or_else(|| {
self.chain_set
.iter()
.find_map(|chain| chain.fork(parent_hash))
.find_map(|chain| {
let mut mmr = finalized_tip_mmr.clone();
// TODO: pass Sapling and Orchard roots for each block. How?
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
mmr.extend(block_iter);
chain.fork(parent_hash, mmr)
})
.map(Box::new)
})
.expect("commit_block is only called with blocks that are ready to be commited");

check::block_is_contextually_valid_for_chain(
&prepared,
self.network,
&parent_chain.mmr_hash(),
)?;
parent_chain.push(prepared);
self.chain_set.insert(parent_chain);
self.update_metrics_for_committed_block(height, hash);
Ok(())
}

/// Commit block to the non-finalized state as a new chain where its parent
/// is the finalized tip.
pub fn commit_new_chain(&mut self, prepared: PreparedBlock) {
let mut chain = Chain::default();
pub fn commit_new_chain(
&mut self,
prepared: PreparedBlock,
mmr: MerkleMountainRange,
) -> Result<(), ValidateContextError> {
let mut chain = Chain::new(mmr);
let (height, hash) = (prepared.height, prepared.hash);
check::block_is_contextually_valid_for_chain(&prepared, self.network, &chain.mmr_hash())?;
chain.push(prepared);
self.chain_set.insert(Box::new(chain));
self.update_metrics_for_committed_block(height, hash);
Ok(())
}

/// Returns the length of the non-finalized portion of the current best chain.
Expand Down
67 changes: 62 additions & 5 deletions zebra-state/src/service/non_finalized_state/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ use std::{

use tracing::{debug_span, instrument, trace};
use zebra_chain::{
block, orchard, primitives::Groth16Proof, sapling, sprout, transaction,
transaction::Transaction::*, transparent, work::difficulty::PartialCumulativeWork,
block::{self, ChainHistoryMmrRootHash},
mmr::MerkleMountainRange,
orchard,
primitives::Groth16Proof,
sapling, sprout, transaction,
transaction::Transaction::*,
transparent,
work::difficulty::PartialCumulativeWork,
};

use crate::{PreparedBlock, Utxo};

#[derive(Default, Clone)]
dconnolly marked this conversation as resolved.
Show resolved Hide resolved
// #[derive(Clone)]
pub struct Chain {
pub blocks: BTreeMap<block::Height, PreparedBlock>,
pub height_by_hash: HashMap<block::Hash, block::Height>,
Expand All @@ -27,9 +33,30 @@ pub struct Chain {
sapling_nullifiers: HashSet<sapling::Nullifier>,
orchard_nullifiers: HashSet<orchard::Nullifier>,
partial_cumulative_work: PartialCumulativeWork,
mmr: MerkleMountainRange,
}

impl Chain {
/// Create a new empty non-finalized chain with the given MMR.
///
/// The MMR must contain the history of the previous (finalized) blocks.
pub fn new(mmr: MerkleMountainRange) -> Self {
Chain {
blocks: Default::default(),
height_by_hash: Default::default(),
tx_by_hash: Default::default(),
created_utxos: Default::default(),
spent_utxos: Default::default(),
sprout_anchors: Default::default(),
sapling_anchors: Default::default(),
sprout_nullifiers: Default::default(),
sapling_nullifiers: Default::default(),
orchard_nullifiers: Default::default(),
partial_cumulative_work: Default::default(),
mmr,
}
}

/// Push a contextually valid non-finalized block into a chain as the new tip.
#[instrument(level = "debug", skip(self, block), fields(block = %block.block))]
pub fn push(&mut self, block: PreparedBlock) {
Expand Down Expand Up @@ -67,12 +94,14 @@ impl Chain {

/// Fork a chain at the block with the given hash, if it is part of this
/// chain.
pub fn fork(&self, fork_tip: block::Hash) -> Option<Self> {
///
/// `mmr`: the MMR for the fork.
pub fn fork(&self, fork_tip: block::Hash, mmr: MerkleMountainRange) -> Option<Self> {
if !self.height_by_hash.contains_key(&fork_tip) {
return None;
}

let mut forked = self.clone();
let mut forked = self.clone_with_mmr(mmr);

while forked.non_finalized_tip_hash() != fork_tip {
forked.pop_tip();
Expand Down Expand Up @@ -117,6 +146,30 @@ impl Chain {
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}

pub fn mmr_hash(&self) -> ChainHistoryMmrRootHash {
self.mmr.hash()
}

/// Clone the Chain but not the MMR.
///
/// Useful when forking, where the MMR is rebuilt anyway.
fn clone_with_mmr(&self, mmr: MerkleMountainRange) -> Self {
Chain {
blocks: self.blocks.clone(),
height_by_hash: self.height_by_hash.clone(),
tx_by_hash: self.tx_by_hash.clone(),
created_utxos: self.created_utxos.clone(),
spent_utxos: self.spent_utxos.clone(),
sprout_anchors: self.sprout_anchors.clone(),
sapling_anchors: self.sapling_anchors.clone(),
sprout_nullifiers: self.sprout_nullifiers.clone(),
sapling_nullifiers: self.sapling_nullifiers.clone(),
orchard_nullifiers: self.orchard_nullifiers.clone(),
partial_cumulative_work: self.partial_cumulative_work,
mmr,
}
}
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
}

/// Helper trait to organize inverse operations done on the `Chain` type. Used to
Expand Down Expand Up @@ -160,6 +213,8 @@ impl UpdateWith<PreparedBlock> for Chain {
.expect("work has already been validated");
self.partial_cumulative_work += block_work;

self.mmr.push(block);

// for each transaction in block
for (transaction_index, (transaction, transaction_hash)) in block
.transactions
Expand Down Expand Up @@ -241,6 +296,8 @@ impl UpdateWith<PreparedBlock> for Chain {
.expect("work has already been validated");
self.partial_cumulative_work -= block_work;

// Note: the MMR is not reverted here; it's rebuilt and passed in fork()

teor2345 marked this conversation as resolved.
Show resolved Hide resolved
// for each transaction in block
for (transaction, transaction_hash) in
block.transactions.iter().zip(transaction_hashes.iter())
Expand Down