Skip to content

Commit

Permalink
Add ZIP-221 (history tree) to finalized state (#2553)
Browse files Browse the repository at this point in the history
* Add ZIP-221 history tree to finalized state

* Improve error / panic handling; improve documentation

* Return error again when preparing batch, fix expect messages

* Fix bug when pushing the Heartwood actiation block to the history tree

* Re-increase database version since it was increased in main

Co-authored-by: teor <[email protected]>
  • Loading branch information
conradoplg and teor2345 authored Aug 5, 2021
1 parent 1a18f84 commit bf713be
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 19 deletions.
5 changes: 5 additions & 0 deletions zebra-chain/src/history_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ impl HistoryTree {
pub fn current_height(&self) -> Height {
self.current_height
}

/// Return the network where this tree is used.
pub fn network(&self) -> Network {
self.network
}
}

impl Clone for HistoryTree {
Expand Down
2 changes: 2 additions & 0 deletions zebra-chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#[macro_use]
extern crate serde;
#[macro_use]
extern crate serde_big_array;

#[macro_use]
extern crate bitflags;
Expand Down
6 changes: 5 additions & 1 deletion zebra-chain/src/primitives/zcash_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// TODO: remove after this module gets to be used
#![allow(dead_code)]
#![allow(missing_docs)]

mod tests;

Expand All @@ -17,6 +18,8 @@ use crate::{
sapling,
};

big_array! { BigArray; zcash_history::MAX_ENTRY_SIZE }

/// A trait to represent a version of `Tree`.
pub trait Version: zcash_history::Version {
/// Convert a Block into the NodeData for this version.
Expand Down Expand Up @@ -59,8 +62,9 @@ impl From<&zcash_history::NodeData> for NodeData {
/// An encoded entry in the tree.
///
/// Contains the node data and information about its position in the tree.
#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub struct Entry {
#[serde(with = "BigArray")]
inner: [u8; zcash_history::MAX_ENTRY_SIZE],
}

Expand Down
2 changes: 1 addition & 1 deletion zebra-state/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;

/// The database format version, incremented each time the database format changes.
pub const DATABASE_FORMAT_VERSION: u32 = 7;
pub const DATABASE_FORMAT_VERSION: u32 = 8;

/// The maximum number of blocks to check for NU5 transactions,
/// before we assume we are on a pre-NU5 legacy chain.
Expand Down
4 changes: 3 additions & 1 deletion zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ impl StateService {
let finalized = self.mem.finalize();
self.disk
.commit_finalized_direct(finalized, "best non-finalized chain root")
.expect("expected that disk errors would not occur");
.expect(
"expected that errors would not occur when writing to disk or updating note commitment and history trees",
);
}

self.queued_blocks
Expand Down
79 changes: 66 additions & 13 deletions zebra-state/src/service/finalized_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ use std::{collections::HashMap, convert::TryInto, path::Path, sync::Arc};

use zebra_chain::{
block::{self, Block},
history_tree::HistoryTree,
orchard,
parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
parameters::{Network, NetworkUpgrade, GENESIS_PREVIOUS_BLOCK_HASH},
sapling, sprout,
transaction::{self, Transaction},
transparent,
Expand All @@ -36,6 +37,8 @@ pub struct FinalizedState {
ephemeral: bool,
/// Commit blocks to the finalized state up to this height, then exit Zebra.
debug_stop_at_height: Option<block::Height>,

network: Network,
}

impl FinalizedState {
Expand All @@ -60,6 +63,7 @@ impl FinalizedState {
"orchard_note_commitment_tree",
db_options.clone(),
),
rocksdb::ColumnFamilyDescriptor::new("history_tree", db_options.clone()),
];
let db_result = rocksdb::DB::open_cf_descriptors(&db_options, &path, column_families);

Expand All @@ -83,6 +87,7 @@ impl FinalizedState {
db,
ephemeral: config.ephemeral,
debug_stop_at_height: config.debug_stop_at_height.map(block::Height),
network,
};

if let Some(tip_height) = new_state.finalized_tip_height() {
Expand Down Expand Up @@ -190,7 +195,15 @@ impl FinalizedState {

/// Immediately commit `finalized` to the finalized state.
///
/// This can be called either by the non-finalized state (when finalizing
/// a block) or by the checkpoint verifier.
///
/// Use `source` as the source of the block in log messages.
///
/// # Errors
///
/// - Propagates any errors from writing to the DB
/// - Propagates any errors from updating history and note commitment trees
pub fn commit_finalized_direct(
&mut self,
finalized: FinalizedBlock,
Expand Down Expand Up @@ -225,6 +238,7 @@ impl FinalizedState {
self.db.cf_handle("sapling_note_commitment_tree").unwrap();
let orchard_note_commitment_tree_cf =
self.db.cf_handle("orchard_note_commitment_tree").unwrap();
let history_tree_cf = self.db.cf_handle("history_tree").unwrap();

// Assert that callers (including unit tests) get the chain order correct
if self.is_empty(hash_by_height) {
Expand Down Expand Up @@ -259,10 +273,14 @@ impl FinalizedState {
// state, these will contain the empty trees.
let mut sapling_note_commitment_tree = self.sapling_note_commitment_tree();
let mut orchard_note_commitment_tree = self.orchard_note_commitment_tree();
let mut history_tree = self.history_tree();

// Prepare a batch of DB modifications and return it (without actually writing anything).
// We use a closure so we can use an early return for control flow in
// the genesis case
let prepare_commit = || -> rocksdb::WriteBatch {
// the genesis case.
// If the closure returns an error it will be propagated and the batch will not be written
// to the BD afterwards.
let prepare_commit = || -> Result<rocksdb::WriteBatch, BoxError> {
let mut batch = rocksdb::WriteBatch::default();

// Index the block
Expand All @@ -288,7 +306,7 @@ impl FinalizedState {
height,
orchard_note_commitment_tree,
);
return batch;
return Ok(batch);
}

// Index all new transparent outputs
Expand Down Expand Up @@ -335,25 +353,48 @@ impl FinalizedState {
}

for sapling_note_commitment in transaction.sapling_note_commitments() {
sapling_note_commitment_tree
.append(*sapling_note_commitment)
.expect("must work since it was already appended before in the non-finalized state");
sapling_note_commitment_tree.append(*sapling_note_commitment)?;
}
for orchard_note_commitment in transaction.orchard_note_commitments() {
orchard_note_commitment_tree
.append(*orchard_note_commitment)
.expect("must work since it was already appended before in the non-finalized state");
orchard_note_commitment_tree.append(*orchard_note_commitment)?;
}
}

let sapling_root = sapling_note_commitment_tree.root();
let orchard_root = orchard_note_commitment_tree.root();

// Create the history tree if it's the Heartwood activation block.
let heartwood_height = NetworkUpgrade::Heartwood
.activation_height(self.network)
.expect("Heartwood height is known");
match height.cmp(&heartwood_height) {
std::cmp::Ordering::Less => assert!(
history_tree.is_none(),
"history tree must not exist pre-Heartwood"
),
std::cmp::Ordering::Equal => {
history_tree = Some(HistoryTree::from_block(
self.network,
block.clone(),
&sapling_root,
&orchard_root,
)?);
}
std::cmp::Ordering::Greater => history_tree
.as_mut()
.expect("history tree must exist Heartwood-onward")
.push(block.clone(), &sapling_root, &orchard_root)?,
}

// Compute the new anchors and index them
batch.zs_insert(sapling_anchors, sapling_note_commitment_tree.root(), ());
batch.zs_insert(orchard_anchors, orchard_note_commitment_tree.root(), ());

// Update the note commitment trees
// Update the trees in state
if let Some(h) = finalized_tip_height {
batch.zs_delete(sapling_note_commitment_tree_cf, h);
batch.zs_delete(orchard_note_commitment_tree_cf, h);
batch.zs_delete(history_tree_cf, h);
}
batch.zs_insert(
sapling_note_commitment_tree_cf,
Expand All @@ -365,11 +406,15 @@ impl FinalizedState {
height,
orchard_note_commitment_tree,
);
if let Some(history_tree) = history_tree {
batch.zs_insert(history_tree_cf, height, history_tree);
}

batch
Ok(batch)
};

let batch = prepare_commit();
// In case of errors, propagate and do not write the batch.
let batch = prepare_commit()?;

let result = self.db.write(batch).map(|()| hash);

Expand Down Expand Up @@ -503,6 +548,14 @@ impl FinalizedState {
.expect("note commitment tree must exist if there is a finalized tip")
}

/// Returns the ZIP-221 history tree of the finalized tip or `None`
/// if it does not exist yet in the state (pre-Heartwood).
pub fn history_tree(&self) -> Option<HistoryTree> {
let height = self.finalized_tip_height()?;
let history_tree = self.db.cf_handle("history_tree").unwrap();
self.db.zs_get(history_tree, &height)
}

/// If the database is `ephemeral`, delete it.
fn delete_ephemeral(&self) {
if self.ephemeral {
Expand Down
46 changes: 43 additions & 3 deletions zebra-state/src/service/finalized_state/disk_format.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
//! Module defining exactly how to move types in and out of rocksdb
use std::{convert::TryInto, fmt::Debug, sync::Arc};
use std::{collections::BTreeMap, convert::TryInto, fmt::Debug, sync::Arc};

use bincode::Options;
use zebra_chain::{
block,
block::Block,
orchard, sapling,
block::{Block, Height},
history_tree::HistoryTree,
orchard,
parameters::Network,
primitives::zcash_history,
sapling,
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
sprout, transaction, transparent,
};
Expand Down Expand Up @@ -292,6 +296,42 @@ impl FromDisk for orchard::tree::NoteCommitmentTree {
}
}

#[derive(serde::Serialize, serde::Deserialize)]
struct HistoryTreeParts {
network: Network,
size: u32,
peaks: BTreeMap<u32, zcash_history::Entry>,
current_height: Height,
}

impl IntoDisk for HistoryTree {
type Bytes = Vec<u8>;

fn as_bytes(&self) -> Self::Bytes {
let data = HistoryTreeParts {
network: self.network(),
size: self.size(),
peaks: self.peaks().clone(),
current_height: self.current_height(),
};
bincode::DefaultOptions::new()
.serialize(&data)
.expect("serialization to vec doesn't fail")
}
}

impl FromDisk for HistoryTree {
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
let parts: HistoryTreeParts = bincode::DefaultOptions::new()
.deserialize(bytes.as_ref())
.expect(
"deserialization format should match the serialization format used by IntoDisk",
);
HistoryTree::from_cache(parts.network, parts.size, parts.peaks, parts.current_height)
.expect("deserialization format should match the serialization format used by IntoDisk")
}
}

/// Helper trait for inserting (Key, Value) pairs into rocksdb with a consistently
/// defined format
pub trait DiskSerialize {
Expand Down

0 comments on commit bf713be

Please sign in to comment.