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 37 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
6 changes: 6 additions & 0 deletions zebra-chain/src/block/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ impl From<[u8; 32]> for ChainHistoryMmrRootHash {
}
}

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

/// A block commitment to chain history and transaction auth.
/// - the chain history tree for all ancestors in the current network upgrade,
/// and
Expand Down
255 changes: 255 additions & 0 deletions zebra-chain/src/history_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
//! History tree (Merkle mountain range) structure that contains information about
//! the block history as specified in ZIP-221.

use std::{
collections::{BTreeMap, HashSet},
io,
sync::Arc,
};

use thiserror::Error;

use crate::{
block::{Block, ChainHistoryMmrRootHash, Height},
orchard,
parameters::{Network, NetworkUpgrade},
primitives::zcash_history::{Entry, Tree as InnerHistoryTree},
sapling,
};

/// An error describing why a history tree operation failed.
#[derive(Debug, Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum HistoryTreeError {
#[error("zcash_history error: {inner:?}")]
#[non_exhaustive]
InnerError { inner: zcash_history::Error },

#[error("I/O error")]
IOError(#[from] io::Error),
}

/// History tree (Merkle mountain range) structure that contains information about
// the block history, as specified in [ZIP-221][https://zips.z.cash/zip-0221].
#[derive(Debug)]
pub struct HistoryTree {
network: Network,
network_upgrade: NetworkUpgrade,
/// Merkle mountain range tree from `zcash_history`.
/// This is a "runtime" structure used to add / remove nodes, and it's not
/// persistent.
inner: InnerHistoryTree,
/// The number of nodes in the tree.
size: u32,
/// The peaks of the tree, indexed by their position in the array representation
/// of the tree. This can be persisted to save the tree.
peaks: BTreeMap<u32, Entry>,
/// The height of the most recent block added to the tree.
current_height: Height,
}

impl HistoryTree {
/// Create a new history tree with a single block.
pub fn from_block(
network: Network,
block: Arc<Block>,
sapling_root: &sapling::tree::Root,
_orchard_root: Option<&orchard::tree::Root>,
) -> Result<Self, io::Error> {
let height = block
.coinbase_height()
.expect("block must have coinbase height during contextual verification");
let network_upgrade = NetworkUpgrade::current(network, height);
// TODO: handle Orchard root, see https://github.com/ZcashFoundation/zebra/issues/2283
let (tree, entry) = InnerHistoryTree::new_from_block(network, block, sapling_root)?;
let mut peaks = BTreeMap::new();
peaks.insert(0u32, entry);
Ok(HistoryTree {
network,
network_upgrade,
inner: tree,
size: 1,
peaks,
current_height: height,
})
}

/// Add block data to the tree.
///
/// # Panics
///
/// If the block height is not one more than the previously pushed block.
pub fn push(
&mut self,
block: Arc<Block>,
sapling_root: &sapling::tree::Root,
_orchard_root: Option<&orchard::tree::Root>,
) -> Result<(), HistoryTreeError> {
// Check if the block has the expected height.
// librustzcash assumes the heights are correct and corrupts the tree if they are wrong,
// resulting in a confusing error, which we prevent here.
let height = block
.coinbase_height()
.expect("block must have coinbase height during contextual verification");
if height - self.current_height != 1 {
panic!(
"added block with height {:?} but it must be {:?}+1",
height, self.current_height
);
}

// TODO: handle orchard root
let new_entries = self
.inner
.append_leaf(block, sapling_root)
.map_err(|e| HistoryTreeError::InnerError { inner: e })?;
for entry in new_entries {
// Not every entry is a peak; those will be trimmed later
self.peaks.insert(self.size, entry);
self.size += 1;
}
self.prune()?;
// TODO: implement network upgrade logic: drop previous history, start new history
self.current_height = height;
Ok(())
}

/// Extend the history tree with the given blocks.
pub fn try_extend<
'a,
T: IntoIterator<
Item = (
Arc<Block>,
&'a sapling::tree::Root,
Option<&'a orchard::tree::Root>,
),
>,
>(
&mut self,
iter: T,
) -> Result<(), HistoryTreeError> {
for (block, sapling_root, orchard_root) in iter {
self.push(block, sapling_root, orchard_root)?;
}
Ok(())
}

/// Prune tree, removing all non-peak entries.
fn prune(&mut self) -> Result<(), io::Error> {
// Go through all the peaks of the tree.
// This code is based on a librustzcash example:
// https://github.com/zcash/librustzcash/blob/02052526925fba9389f1428d6df254d4dec967e6/zcash_history/examples/long.rs
// The explanation of how it works is from zcashd:
// https://github.com/zcash/zcash/blob/0247c0c682d59184a717a6536edb0d18834be9a7/src/coins.cpp#L351

let mut peak_pos_set = HashSet::new();

// Assume the following example peak layout with 14 leaves, and 25 stored nodes in
// total (the "tree length"):
//
// P
// /\
// / \
// / \ \
// / \ \ Altitude
// _A_ \ \ 3
// _/ \_ B \ 2
// / \ / \ / \ C 1
// /\ /\ /\ /\ /\ /\ /\ 0
//
// We start by determining the altitude of the highest peak (A).
let mut alt = (32 - ((self.size + 1) as u32).leading_zeros() - 1) - 1;

// We determine the position of the highest peak (A) by pretending it is the right
// sibling in a tree, and its left-most leaf has position 0. Then the left sibling
// of (A) has position -1, and so we can "jump" to the peak's position by computing
// -1 + 2^(alt + 1) - 1.
let mut peak_pos = (1 << (alt + 1)) - 2;

// Now that we have the position and altitude of the highest peak (A), we collect
// the remaining peaks (B, C). We navigate the peaks as if they were nodes in this
// Merkle tree (with additional imaginary nodes 1 and 2, that have positions beyond
// the MMR's length):
//
// / \
// / \
// / \
// / \
// A ==========> 1
// / \ // \
// _/ \_ B ==> 2
// /\ /\ /\ //
// / \ / \ / \ C
// /\ /\ /\ /\ /\ /\ /\
//
loop {
// If peak_pos is out of bounds of the tree, we compute the position of its left
// child, and drop down one level in the tree.
if peak_pos >= self.size {
// left child, -2^alt
peak_pos -= 1 << alt;
alt -= 1;
}

// If the peak exists, we take it and then continue with its right sibling.
if peak_pos < self.size {
// There is a peak at index `peak_pos`
peak_pos_set.insert(peak_pos);

// right sibling
peak_pos = peak_pos + (1 << (alt + 1)) - 1;
}

if alt == 0 {
break;
}
}

// Remove all non-peak entries
self.peaks.retain(|k, _| peak_pos_set.contains(k));
// Rebuild tree
self.inner = InnerHistoryTree::new_from_cache(
self.network,
self.network_upgrade,
self.size,
&self.peaks,
&Default::default(),
)?;
Ok(())
}

/// Return the hash of the tree root.
pub fn hash(&self) -> ChainHistoryMmrRootHash {
self.inner.hash()
}
}

impl Clone for HistoryTree {
fn clone(&self) -> Self {
let tree = InnerHistoryTree::new_from_cache(
self.network,
self.network_upgrade,
self.size,
&self.peaks,
&Default::default(),
)
.expect("rebuilding an existing tree should always work");
HistoryTree {
network: self.network,
network_upgrade: self.network_upgrade,
inner: tree,
size: self.size,
peaks: self.peaks.clone(),
current_height: self.current_height,
}
}
}

impl PartialEq for HistoryTree {
fn eq(&self, other: &Self) -> bool {
self.hash() == other.hash()
}
}

impl Eq for HistoryTree {}
1 change: 1 addition & 0 deletions zebra-chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ extern crate bitflags;
pub mod amount;
pub mod block;
pub mod fmt;
pub mod history_tree;
pub mod orchard;
pub mod parameters;
pub mod primitives;
Expand Down
Loading