zcash_history: Introduce Version trait

Each Zcash epoch (between two network upgrades) has a separate history
tree, making it easy to switch the node data format at network upgrades.
This commit enables the general tree logic to be shared across history
tree versions.
This commit is contained in:
Jack Grigg 2021-06-11 01:11:30 +01:00
parent cc533a9da4
commit 63f554b308
8 changed files with 223 additions and 104 deletions

View File

@ -6,6 +6,17 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Support for multiple history tree versions:
- `zcash_history::Version` trait.
- `zcash_history::V1`, marking the original history tree version.
- `zcash_history::Entry::new_leaf`
### Changed
- `zcash_history::{Entry, IndexedNode, Tree}` now have a `Version` parameter.
### Removed
- `impl From<NodeData> for Entry` (replaced by `Entry::new_leaf`).
## [0.2.0] - 2020-03-13
No changes, just a version bump.

View File

@ -1,8 +1,8 @@
use zcash_history::{Entry, EntryLink, NodeData, Tree};
use zcash_history::{Entry, EntryLink, NodeData, Tree, V1};
pub struct NodeDataIterator {
return_stack: Vec<NodeData>,
tree: Tree,
tree: Tree<V1>,
cursor: usize,
leaf_cursor: usize,
}
@ -56,7 +56,7 @@ impl NodeDataIterator {
let tree = Tree::new(
3,
vec![(2, root)],
vec![(0, leaf(1).into()), (1, leaf(2).into())],
vec![(0, Entry::new_leaf(leaf(1))), (1, Entry::new_leaf(leaf(2)))],
);
NodeDataIterator {

View File

@ -1,12 +1,12 @@
use zcash_history::{Entry, EntryLink, NodeData, Tree};
use zcash_history::{Entry, EntryLink, NodeData, Tree, V1};
#[path = "lib/shared.rs"]
mod share;
fn draft(into: &mut Vec<(u32, Entry)>, vec: &[NodeData], peak_pos: usize, h: u32) {
fn draft(into: &mut Vec<(u32, Entry<V1>)>, vec: &[NodeData], peak_pos: usize, h: u32) {
let node_data = vec[peak_pos - 1].clone();
let peak: Entry = match h {
0 => node_data.into(),
let peak = match h {
0 => Entry::new_leaf(node_data),
_ => Entry::new(
node_data,
EntryLink::Stored((peak_pos - (1 << h) - 1) as u32),
@ -19,7 +19,7 @@ fn draft(into: &mut Vec<(u32, Entry)>, vec: &[NodeData], peak_pos: usize, h: u32
into.push(((peak_pos - 1) as u32, peak));
}
fn prepare_tree(vec: &[NodeData]) -> Tree {
fn prepare_tree(vec: &[NodeData]) -> Tree<V1> {
assert!(!vec.is_empty());
// integer log2 of (vec.len()+1), -1

View File

@ -1,26 +1,34 @@
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::{EntryKind, EntryLink, Error, NodeData, MAX_NODE_DATA_SIZE};
use crate::{EntryKind, EntryLink, Error, Version, MAX_NODE_DATA_SIZE};
/// Max serialized length of entry data.
pub const MAX_ENTRY_SIZE: usize = MAX_NODE_DATA_SIZE + 9;
/// MMR Entry.
#[derive(Debug)]
pub struct Entry {
pub struct Entry<V: Version> {
pub(crate) kind: EntryKind,
pub(crate) data: NodeData,
pub(crate) data: V::NodeData,
}
impl Entry {
impl<V: Version> Entry<V> {
/// New entry of type node.
pub fn new(data: NodeData, left: EntryLink, right: EntryLink) -> Self {
pub fn new(data: V::NodeData, left: EntryLink, right: EntryLink) -> Self {
Entry {
kind: EntryKind::Node(left, right),
data,
}
}
/// Creates a new leaf.
pub fn new_leaf(data: V::NodeData) -> Self {
Entry {
kind: EntryKind::Leaf,
data,
}
}
/// Returns if is this node complete (has total of 2^N leaves)
pub fn complete(&self) -> bool {
let leaves = self.leaf_count();
@ -29,7 +37,7 @@ impl Entry {
/// Number of leaves under this node.
pub fn leaf_count(&self) -> u64 {
self.data.end_height - (self.data.start_height - 1)
V::end_height(&self.data) - (V::start_height(&self.data) - 1)
}
/// Is this node a leaf.
@ -67,7 +75,7 @@ impl Entry {
}
};
let data = NodeData::read(consensus_branch_id, r)?;
let data = V::read(consensus_branch_id, r)?;
Ok(Entry { kind, data })
}
@ -88,7 +96,7 @@ impl Entry {
}
}
self.data.write(w)?;
V::write(&self.data, w)?;
Ok(())
}
@ -100,16 +108,7 @@ impl Entry {
}
}
impl From<NodeData> for Entry {
fn from(s: NodeData) -> Self {
Entry {
kind: EntryKind::Leaf,
data: s,
}
}
}
impl std::fmt::Display for Entry {
impl<V: Version> std::fmt::Display for Entry<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
EntryKind::Node(l, r) => write!(f, "node({}, {}, ..)", l, r),

View File

@ -9,10 +9,12 @@
mod entry;
mod node_data;
mod tree;
mod version;
pub use entry::{Entry, MAX_ENTRY_SIZE};
pub use node_data::{NodeData, MAX_NODE_DATA_SIZE};
pub use tree::Tree;
pub use version::{Version, V1};
/// Crate-level error type
#[derive(Debug)]

View File

@ -1,6 +1,7 @@
use bigint::U256;
use blake2::Params as Blake2Params;
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::Version;
/// Maximum serialized size of the node metadata.
pub const MAX_NODE_DATA_SIZE: usize = 32 + // subtree commitment
@ -16,7 +17,7 @@ pub const MAX_NODE_DATA_SIZE: usize = 32 + // subtree commitment
9; // Sapling tx count (compact uint)
// = total of 171
/// Node metadata.
/// V1 node metadata.
#[repr(C)]
#[derive(Debug, Clone, Default)]
#[cfg_attr(test, derive(PartialEq))]
@ -47,49 +48,20 @@ pub struct NodeData {
pub sapling_tx: u64,
}
fn blake2b_personal(personalization: &[u8], input: &[u8]) -> [u8; 32] {
let hash_result = Blake2Params::new()
.hash_length(32)
.personal(personalization)
.to_state()
.update(input)
.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(hash_result.as_bytes());
result
}
fn personalization(branch_id: u32) -> [u8; 16] {
let mut result = [0u8; 16];
result[..12].copy_from_slice(b"ZcashHistory");
LittleEndian::write_u32(&mut result[12..], branch_id);
result
}
impl NodeData {
/// Combine two nodes metadata.
pub fn combine(left: &NodeData, right: &NodeData) -> NodeData {
assert_eq!(left.consensus_branch_id, right.consensus_branch_id);
let mut hash_buf = [0u8; MAX_NODE_DATA_SIZE * 2];
let size = {
let mut cursor = ::std::io::Cursor::new(&mut hash_buf[..]);
left.write(&mut cursor)
.expect("Writing to memory buf with enough length cannot fail; qed");
right
.write(&mut cursor)
.expect("Writing to memory buf with enough length cannot fail; qed");
cursor.position() as usize
};
let hash = blake2b_personal(
&personalization(left.consensus_branch_id),
&hash_buf[..size],
);
crate::V1::combine(left, right)
}
pub(crate) fn combine_inner(
subtree_commitment: [u8; 32],
left: &NodeData,
right: &NodeData,
) -> NodeData {
NodeData {
consensus_branch_id: left.consensus_branch_id,
subtree_commitment: hash,
subtree_commitment,
start_time: left.start_time,
end_time: right.end_time,
start_target: left.start_target,
@ -180,27 +152,17 @@ impl NodeData {
/// Convert to byte representation.
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = [0u8; MAX_NODE_DATA_SIZE];
let pos = {
let mut cursor = std::io::Cursor::new(&mut buf[..]);
self.write(&mut cursor).expect("Cursor cannot fail");
cursor.position() as usize
};
buf[0..pos].to_vec()
crate::V1::to_bytes(self)
}
/// Convert from byte representation.
pub fn from_bytes<T: AsRef<[u8]>>(consensus_branch_id: u32, buf: T) -> std::io::Result<Self> {
let mut cursor = std::io::Cursor::new(buf);
Self::read(consensus_branch_id, &mut cursor)
crate::V1::from_bytes(consensus_branch_id, buf)
}
/// Hash node metadata
pub fn hash(&self) -> [u8; 32] {
let bytes = self.to_bytes();
blake2b_personal(&personalization(self.consensus_branch_id), &bytes)
crate::V1::hash(self)
}
}

View File

@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::{Entry, EntryKind, EntryLink, Error, NodeData};
use crate::{Entry, EntryKind, EntryLink, Error, Version};
/// Represents partially loaded tree.
///
@ -13,11 +13,11 @@ use crate::{Entry, EntryKind, EntryLink, Error, NodeData};
/// Intended use of this `Tree` is to instantiate it based on partially loaded data (see example
/// how to pick right nodes from the array representation of MMR Tree), perform several operations
/// (append-s/delete-s) and then drop it.
pub struct Tree {
stored: HashMap<u32, Entry>,
pub struct Tree<V: Version> {
stored: HashMap<u32, Entry<V>>,
// This can grow indefinitely if `Tree` is misused as a self-contained data structure
generated: Vec<Entry>,
generated: Vec<Entry<V>>,
// number of persistent(!) tree entries
stored_count: u32,
@ -25,9 +25,9 @@ pub struct Tree {
root: EntryLink,
}
impl Tree {
impl<V: Version> Tree<V> {
/// Resolve link originated from this tree
pub fn resolve_link(&self, link: EntryLink) -> Result<IndexedNode, Error> {
pub fn resolve_link(&self, link: EntryLink) -> Result<IndexedNode<V>, Error> {
match link {
EntryLink::Generated(index) => self.generated.get(index as usize),
EntryLink::Stored(index) => self.stored.get(&index),
@ -36,14 +36,14 @@ impl Tree {
.ok_or(Error::ExpectedInMemory(link))
}
fn push(&mut self, data: Entry) -> EntryLink {
fn push(&mut self, data: Entry<V>) -> EntryLink {
let idx = self.stored_count;
self.stored_count += 1;
self.stored.insert(idx, data);
EntryLink::Stored(idx)
}
fn push_generated(&mut self, data: Entry) -> EntryLink {
fn push_generated(&mut self, data: Entry<V>) -> EntryLink {
self.generated.push(data);
EntryLink::Generated(self.generated.len() as u32 - 1)
}
@ -51,7 +51,7 @@ impl Tree {
/// Populate tree with plain list of the leaves/nodes. For now, only for tests,
/// since this `Tree` structure is for partially loaded tree (but it might change)
#[cfg(test)]
pub fn populate(loaded: Vec<Entry>, root: EntryLink) -> Self {
pub fn populate(loaded: Vec<Entry<V>>, root: EntryLink) -> Self {
let mut result = Tree::invalid();
result.stored_count = loaded.len() as u32;
for (idx, item) in loaded.into_iter().enumerate() {
@ -83,7 +83,7 @@ impl Tree {
/// # Panics
///
/// Will panic if `peaks` is empty.
pub fn new(length: u32, peaks: Vec<(u32, Entry)>, extra: Vec<(u32, Entry)>) -> Self {
pub fn new(length: u32, peaks: Vec<(u32, Entry<V>)>, extra: Vec<(u32, Entry<V>)>) -> Self {
assert!(!peaks.is_empty());
let mut result = Tree::invalid();
@ -135,9 +135,9 @@ impl Tree {
/// Returns links to actual nodes that has to be persisted as the result of the append.
/// If completed without error, at least one link to the appended
/// node (with metadata provided in `new_leaf`) will be returned.
pub fn append_leaf(&mut self, new_leaf: NodeData) -> Result<Vec<EntryLink>, Error> {
pub fn append_leaf(&mut self, new_leaf: V::NodeData) -> Result<Vec<EntryLink>, Error> {
let root = self.root;
let new_leaf_link = self.push(new_leaf.into());
let new_leaf_link = self.push(Entry::new_leaf(new_leaf));
let mut appended = vec![new_leaf_link];
let mut peaks = Vec::new();
@ -274,7 +274,7 @@ impl Tree {
}
/// Reference to the root node.
pub fn root_node(&self) -> Result<IndexedNode, Error> {
pub fn root_node(&self) -> Result<IndexedNode<V>, Error> {
self.resolve_link(self.root)
}
@ -286,12 +286,12 @@ impl Tree {
/// Reference to the node with link attached.
#[derive(Debug)]
pub struct IndexedNode<'a> {
node: &'a Entry,
pub struct IndexedNode<'a, V: Version> {
node: &'a Entry<V>,
link: EntryLink,
}
impl<'a> IndexedNode<'a> {
impl<'a, V: Version> IndexedNode<'a, V> {
fn left(&self) -> Result<EntryLink, Error> {
self.node.left().map_err(|e| e.augment(self.link))
}
@ -301,12 +301,12 @@ impl<'a> IndexedNode<'a> {
}
/// Reference to the entry struct.
pub fn node(&self) -> &Entry {
pub fn node(&self) -> &Entry<V> {
self.node
}
/// Reference to the entry metadata.
pub fn data(&self) -> &NodeData {
pub fn data(&self) -> &V::NodeData {
&self.node.data
}
@ -316,17 +316,19 @@ impl<'a> IndexedNode<'a> {
}
}
fn combine_nodes<'a>(left: IndexedNode<'a>, right: IndexedNode<'a>) -> Entry {
fn combine_nodes<'a, V: Version>(left: IndexedNode<'a, V>, right: IndexedNode<'a, V>) -> Entry<V> {
Entry {
kind: EntryKind::Node(left.link, right.link),
data: NodeData::combine(&left.node.data, &right.node.data),
data: V::combine(&left.node.data, &right.node.data),
}
}
#[cfg(test)]
mod tests {
use super::{Entry, EntryKind, EntryLink, NodeData, Tree};
use super::{Entry, EntryKind, EntryLink, Tree};
use crate::{NodeData, V1};
use assert_matches::assert_matches;
use quickcheck::{quickcheck, TestResult};
@ -347,9 +349,9 @@ mod tests {
}
}
fn initial() -> Tree {
let node1: Entry = leaf(1).into();
let node2: Entry = leaf(2).into();
fn initial() -> Tree<V1> {
let node1 = Entry::new_leaf(leaf(1));
let node2 = Entry::new_leaf(leaf(2));
let node3 = Entry {
data: NodeData::combine(&node1.data, &node2.data),
@ -360,7 +362,7 @@ mod tests {
}
// returns tree with specified number of leafs and it's root
fn generated(length: u32) -> Tree {
fn generated(length: u32) -> Tree<V1> {
assert!(length >= 3);
let mut tree = initial();
for i in 2..length {

View File

@ -0,0 +1,143 @@
use std::fmt;
use std::io;
use blake2::Params as Blake2Params;
use byteorder::{ByteOrder, LittleEndian};
use crate::{NodeData, MAX_NODE_DATA_SIZE};
fn blake2b_personal(personalization: &[u8], input: &[u8]) -> [u8; 32] {
let hash_result = Blake2Params::new()
.hash_length(32)
.personal(personalization)
.to_state()
.update(input)
.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(hash_result.as_bytes());
result
}
fn personalization(branch_id: u32) -> [u8; 16] {
let mut result = [0u8; 16];
result[..12].copy_from_slice(b"ZcashHistory");
LittleEndian::write_u32(&mut result[12..], branch_id);
result
}
/// A version of the chain history tree.
pub trait Version {
/// The node data for this tree version.
type NodeData: fmt::Debug;
/// Returns the consensus branch ID for the given node data.
fn consensus_branch_id(data: &Self::NodeData) -> u32;
/// Returns the start height for the given node data.
fn start_height(data: &Self::NodeData) -> u64;
/// Returns the end height for the given node data.
fn end_height(data: &Self::NodeData) -> u64;
/// Combines two nodes' metadata.
fn combine(left: &Self::NodeData, right: &Self::NodeData) -> Self::NodeData {
assert_eq!(
Self::consensus_branch_id(left),
Self::consensus_branch_id(right)
);
let mut hash_buf = [0u8; MAX_NODE_DATA_SIZE * 2];
let size = {
let mut cursor = ::std::io::Cursor::new(&mut hash_buf[..]);
Self::write(left, &mut cursor)
.expect("Writing to memory buf with enough length cannot fail; qed");
Self::write(right, &mut cursor)
.expect("Writing to memory buf with enough length cannot fail; qed");
cursor.position() as usize
};
let hash = blake2b_personal(
&personalization(Self::consensus_branch_id(left)),
&hash_buf[..size],
);
Self::combine_inner(hash, left, right)
}
/// Combines two nodes metadata.
///
/// For internal use.
fn combine_inner(
subtree_commitment: [u8; 32],
left: &Self::NodeData,
right: &Self::NodeData,
) -> Self::NodeData;
/// Parses node data from the given reader.
fn read<R: io::Read>(consensus_branch_id: u32, r: &mut R) -> io::Result<Self::NodeData>;
/// Writes the byte representation of the given node data to the given writer.
fn write<W: io::Write>(data: &Self::NodeData, w: &mut W) -> io::Result<()>;
/// Converts to byte representation.
fn to_bytes(data: &Self::NodeData) -> Vec<u8> {
let mut buf = [0u8; MAX_NODE_DATA_SIZE];
let pos = {
let mut cursor = std::io::Cursor::new(&mut buf[..]);
Self::write(data, &mut cursor).expect("Cursor cannot fail");
cursor.position() as usize
};
buf[0..pos].to_vec()
}
/// Convert from byte representation.
fn from_bytes<T: AsRef<[u8]>>(consensus_branch_id: u32, buf: T) -> io::Result<Self::NodeData> {
let mut cursor = std::io::Cursor::new(buf);
Self::read(consensus_branch_id, &mut cursor)
}
/// Hash node metadata
fn hash(data: &Self::NodeData) -> [u8; 32] {
let bytes = Self::to_bytes(data);
blake2b_personal(&personalization(Self::consensus_branch_id(data)), &bytes)
}
}
/// Version 1 of the Zcash chain history tree.
///
/// This version was used for the Heartwood and Canopy epochs.
pub enum V1 {}
impl Version for V1 {
type NodeData = NodeData;
fn consensus_branch_id(data: &Self::NodeData) -> u32 {
data.consensus_branch_id
}
fn start_height(data: &Self::NodeData) -> u64 {
data.start_height
}
fn end_height(data: &Self::NodeData) -> u64 {
data.end_height
}
fn combine_inner(
subtree_commitment: [u8; 32],
left: &Self::NodeData,
right: &Self::NodeData,
) -> Self::NodeData {
NodeData::combine_inner(subtree_commitment, left, right)
}
fn read<R: io::Read>(consensus_branch_id: u32, r: &mut R) -> io::Result<Self::NodeData> {
NodeData::read(consensus_branch_id, r)
}
fn write<W: io::Write>(data: &Self::NodeData, w: &mut W) -> io::Result<()> {
data.write(w)
}
}