Add ZIP-221 (history tree) to finalized state (#2553)
* 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 <teor@riseup.net>
This commit is contained in:
parent
1a18f841f7
commit
bf713bec91
|
@ -352,6 +352,11 @@ impl HistoryTree {
|
||||||
pub fn current_height(&self) -> Height {
|
pub fn current_height(&self) -> Height {
|
||||||
self.current_height
|
self.current_height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the network where this tree is used.
|
||||||
|
pub fn network(&self) -> Network {
|
||||||
|
self.network
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for HistoryTree {
|
impl Clone for HistoryTree {
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde_big_array;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate bitflags;
|
extern crate bitflags;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
// TODO: remove after this module gets to be used
|
// TODO: remove after this module gets to be used
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
@ -17,6 +18,8 @@ use crate::{
|
||||||
sapling,
|
sapling,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
big_array! { BigArray; zcash_history::MAX_ENTRY_SIZE }
|
||||||
|
|
||||||
/// A trait to represent a version of `Tree`.
|
/// A trait to represent a version of `Tree`.
|
||||||
pub trait Version: zcash_history::Version {
|
pub trait Version: zcash_history::Version {
|
||||||
/// Convert a Block into the NodeData for this version.
|
/// Convert a Block into the NodeData for this version.
|
||||||
|
@ -59,8 +62,9 @@ impl From<&zcash_history::NodeData> for NodeData {
|
||||||
/// An encoded entry in the tree.
|
/// An encoded entry in the tree.
|
||||||
///
|
///
|
||||||
/// Contains the node data and information about its position in the tree.
|
/// Contains the node data and information about its position in the tree.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
|
#[serde(with = "BigArray")]
|
||||||
inner: [u8; zcash_history::MAX_ENTRY_SIZE],
|
inner: [u8; zcash_history::MAX_ENTRY_SIZE],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
|
||||||
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;
|
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;
|
||||||
|
|
||||||
/// The database format version, incremented each time the database format changes.
|
/// 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,
|
/// The maximum number of blocks to check for NU5 transactions,
|
||||||
/// before we assume we are on a pre-NU5 legacy chain.
|
/// before we assume we are on a pre-NU5 legacy chain.
|
||||||
|
|
|
@ -160,7 +160,9 @@ impl StateService {
|
||||||
let finalized = self.mem.finalize();
|
let finalized = self.mem.finalize();
|
||||||
self.disk
|
self.disk
|
||||||
.commit_finalized_direct(finalized, "best non-finalized chain root")
|
.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
|
self.queued_blocks
|
||||||
|
|
|
@ -9,8 +9,9 @@ use std::{collections::HashMap, convert::TryInto, path::Path, sync::Arc};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
|
history_tree::HistoryTree,
|
||||||
orchard,
|
orchard,
|
||||||
parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
|
parameters::{Network, NetworkUpgrade, GENESIS_PREVIOUS_BLOCK_HASH},
|
||||||
sapling, sprout,
|
sapling, sprout,
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
transparent,
|
transparent,
|
||||||
|
@ -36,6 +37,8 @@ pub struct FinalizedState {
|
||||||
ephemeral: bool,
|
ephemeral: bool,
|
||||||
/// Commit blocks to the finalized state up to this height, then exit Zebra.
|
/// Commit blocks to the finalized state up to this height, then exit Zebra.
|
||||||
debug_stop_at_height: Option<block::Height>,
|
debug_stop_at_height: Option<block::Height>,
|
||||||
|
|
||||||
|
network: Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FinalizedState {
|
impl FinalizedState {
|
||||||
|
@ -60,6 +63,7 @@ impl FinalizedState {
|
||||||
"orchard_note_commitment_tree",
|
"orchard_note_commitment_tree",
|
||||||
db_options.clone(),
|
db_options.clone(),
|
||||||
),
|
),
|
||||||
|
rocksdb::ColumnFamilyDescriptor::new("history_tree", db_options.clone()),
|
||||||
];
|
];
|
||||||
let db_result = rocksdb::DB::open_cf_descriptors(&db_options, &path, column_families);
|
let db_result = rocksdb::DB::open_cf_descriptors(&db_options, &path, column_families);
|
||||||
|
|
||||||
|
@ -83,6 +87,7 @@ impl FinalizedState {
|
||||||
db,
|
db,
|
||||||
ephemeral: config.ephemeral,
|
ephemeral: config.ephemeral,
|
||||||
debug_stop_at_height: config.debug_stop_at_height.map(block::Height),
|
debug_stop_at_height: config.debug_stop_at_height.map(block::Height),
|
||||||
|
network,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(tip_height) = new_state.finalized_tip_height() {
|
if let Some(tip_height) = new_state.finalized_tip_height() {
|
||||||
|
@ -190,7 +195,15 @@ impl FinalizedState {
|
||||||
|
|
||||||
/// Immediately commit `finalized` to the finalized state.
|
/// 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.
|
/// 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(
|
pub fn commit_finalized_direct(
|
||||||
&mut self,
|
&mut self,
|
||||||
finalized: FinalizedBlock,
|
finalized: FinalizedBlock,
|
||||||
|
@ -225,6 +238,7 @@ impl FinalizedState {
|
||||||
self.db.cf_handle("sapling_note_commitment_tree").unwrap();
|
self.db.cf_handle("sapling_note_commitment_tree").unwrap();
|
||||||
let orchard_note_commitment_tree_cf =
|
let orchard_note_commitment_tree_cf =
|
||||||
self.db.cf_handle("orchard_note_commitment_tree").unwrap();
|
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
|
// Assert that callers (including unit tests) get the chain order correct
|
||||||
if self.is_empty(hash_by_height) {
|
if self.is_empty(hash_by_height) {
|
||||||
|
@ -259,10 +273,14 @@ impl FinalizedState {
|
||||||
// state, these will contain the empty trees.
|
// state, these will contain the empty trees.
|
||||||
let mut sapling_note_commitment_tree = self.sapling_note_commitment_tree();
|
let mut sapling_note_commitment_tree = self.sapling_note_commitment_tree();
|
||||||
let mut orchard_note_commitment_tree = self.orchard_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
|
// We use a closure so we can use an early return for control flow in
|
||||||
// the genesis case
|
// the genesis case.
|
||||||
let prepare_commit = || -> rocksdb::WriteBatch {
|
// 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();
|
let mut batch = rocksdb::WriteBatch::default();
|
||||||
|
|
||||||
// Index the block
|
// Index the block
|
||||||
|
@ -288,7 +306,7 @@ impl FinalizedState {
|
||||||
height,
|
height,
|
||||||
orchard_note_commitment_tree,
|
orchard_note_commitment_tree,
|
||||||
);
|
);
|
||||||
return batch;
|
return Ok(batch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index all new transparent outputs
|
// Index all new transparent outputs
|
||||||
|
@ -335,25 +353,48 @@ impl FinalizedState {
|
||||||
}
|
}
|
||||||
|
|
||||||
for sapling_note_commitment in transaction.sapling_note_commitments() {
|
for sapling_note_commitment in transaction.sapling_note_commitments() {
|
||||||
sapling_note_commitment_tree
|
sapling_note_commitment_tree.append(*sapling_note_commitment)?;
|
||||||
.append(*sapling_note_commitment)
|
|
||||||
.expect("must work since it was already appended before in the non-finalized state");
|
|
||||||
}
|
}
|
||||||
for orchard_note_commitment in transaction.orchard_note_commitments() {
|
for orchard_note_commitment in transaction.orchard_note_commitments() {
|
||||||
orchard_note_commitment_tree
|
orchard_note_commitment_tree.append(*orchard_note_commitment)?;
|
||||||
.append(*orchard_note_commitment)
|
|
||||||
.expect("must work since it was already appended before in the non-finalized state");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Compute the new anchors and index them
|
||||||
batch.zs_insert(sapling_anchors, sapling_note_commitment_tree.root(), ());
|
batch.zs_insert(sapling_anchors, sapling_note_commitment_tree.root(), ());
|
||||||
batch.zs_insert(orchard_anchors, orchard_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 {
|
if let Some(h) = finalized_tip_height {
|
||||||
batch.zs_delete(sapling_note_commitment_tree_cf, h);
|
batch.zs_delete(sapling_note_commitment_tree_cf, h);
|
||||||
batch.zs_delete(orchard_note_commitment_tree_cf, h);
|
batch.zs_delete(orchard_note_commitment_tree_cf, h);
|
||||||
|
batch.zs_delete(history_tree_cf, h);
|
||||||
}
|
}
|
||||||
batch.zs_insert(
|
batch.zs_insert(
|
||||||
sapling_note_commitment_tree_cf,
|
sapling_note_commitment_tree_cf,
|
||||||
|
@ -365,11 +406,15 @@ impl FinalizedState {
|
||||||
height,
|
height,
|
||||||
orchard_note_commitment_tree,
|
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);
|
let result = self.db.write(batch).map(|()| hash);
|
||||||
|
|
||||||
|
@ -503,6 +548,14 @@ impl FinalizedState {
|
||||||
.expect("note commitment tree must exist if there is a finalized tip")
|
.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.
|
/// If the database is `ephemeral`, delete it.
|
||||||
fn delete_ephemeral(&self) {
|
fn delete_ephemeral(&self) {
|
||||||
if self.ephemeral {
|
if self.ephemeral {
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
//! Module defining exactly how to move types in and out of rocksdb
|
//! 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 bincode::Options;
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block,
|
block,
|
||||||
block::Block,
|
block::{Block, Height},
|
||||||
orchard, sapling,
|
history_tree::HistoryTree,
|
||||||
|
orchard,
|
||||||
|
parameters::Network,
|
||||||
|
primitives::zcash_history,
|
||||||
|
sapling,
|
||||||
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
|
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
|
||||||
sprout, transaction, transparent,
|
sprout, transaction, transparent,
|
||||||
};
|
};
|
||||||
|
@ -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
|
/// Helper trait for inserting (Key, Value) pairs into rocksdb with a consistently
|
||||||
/// defined format
|
/// defined format
|
||||||
pub trait DiskSerialize {
|
pub trait DiskSerialize {
|
||||||
|
|
Loading…
Reference in New Issue