Merge pull request #122 from nuttycom/incremental_merkle_tree

Add Orchard incremental merkle tree digests.
This commit is contained in:
str4d 2021-06-28 19:12:13 +01:00 committed by GitHub
commit 1f861423c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 262 additions and 82 deletions

View File

@ -32,8 +32,10 @@ pasta_curves = "0.1"
proptest = { version = "1.0.0", optional = true }
rand = "0.8"
nonempty = "0.6"
serde = { version = "1.0", features = ["derive"] }
subtle = "2.3"
zcash_note_encryption = "0.0"
incrementalmerkletree = "0.1"
# Developer tooling dependencies
plotters = { version = "0.3.0", optional = true }

View File

@ -211,7 +211,8 @@ impl Builder {
// Consistency check: all anchors must be equal.
let cm = note.commitment();
let path_root: Anchor = merkle_path.root(cm.into());
let path_root: Anchor =
<Option<_>>::from(merkle_path.root(cm.into())).ok_or("Derived the bottom anchor")?;
if path_root != self.anchor {
return Err("All anchors must be equal.");
}

View File

@ -566,9 +566,8 @@ pub mod testing {
/// Generate an arbitrary unauthorized bundle. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_bundle`]
pub fn arb_unauthorized_bundle()
pub fn arb_unauthorized_bundle(n_actions: usize)
(
n_actions in 1usize..100,
flags in arb_flags(),
)
(
@ -592,9 +591,8 @@ pub mod testing {
/// Generate an arbitrary bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_bundle`]
pub fn arb_bundle()
pub fn arb_bundle(n_actions: usize)
(
n_actions in 1usize..100,
flags in arb_flags(),
)
(

View File

@ -26,7 +26,7 @@ pub mod note;
mod note_encryption;
pub mod primitives;
mod spec;
mod tree;
pub mod tree;
pub mod value;
#[cfg(test)]

View File

@ -90,7 +90,7 @@ impl<I: Iterator<Item = bool>> Iterator for Pad<I> {
/// A domain in which $\mathsf{SinsemillaHashToPoint}$ and $\mathsf{SinsemillaHash}$ can
/// be used.
#[derive(Debug)]
#[derive(Debug, Clone)]
#[allow(non_snake_case)]
pub struct HashDomain {
Q: pallas::Point,

View File

@ -1,3 +1,5 @@
//! Types related to Orchard note commitment trees and anchors.
use crate::{
constants::{
util::gen_const_array, L_ORCHARD_MERKLE, MERKLE_CRH_PERSONALIZATION, MERKLE_DEPTH_ORCHARD,
@ -5,11 +7,41 @@ use crate::{
note::commitment::ExtractedNoteCommitment,
primitives::sinsemilla::{i2lebsp_k, HashDomain},
};
use pasta_curves::pallas;
use incrementalmerkletree::{Altitude, Hashable};
use pasta_curves::{arithmetic::FieldExt, pallas};
use ff::{Field, PrimeField, PrimeFieldBits};
use lazy_static::lazy_static;
use rand::RngCore;
use serde::de::{Deserializer, Error};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::iter;
use subtle::{ConstantTimeEq, CtOption};
// The uncommitted leaf is defined as pallas::Base(2).
// <https://zips.z.cash/protocol/protocol.pdf#thmuncommittedorchard>
lazy_static! {
static ref UNCOMMITTED_ORCHARD: pallas::Base = pallas::Base::from_u64(2);
static ref EMPTY_ROOTS: Vec<pallas::Base> = {
iter::empty()
.chain(Some(*UNCOMMITTED_ORCHARD))
.chain(
(0..MERKLE_DEPTH_ORCHARD).scan(*UNCOMMITTED_ORCHARD, |state, l| {
*state = hash_with_l(
l,
Pair {
left: *state,
right: *state,
},
)
.unwrap();
Some(*state)
}),
)
.collect()
};
}
/// The root of an Orchard commitment tree.
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
@ -33,6 +65,8 @@ impl Anchor {
}
}
/// The Merkle path from a leaf of the note commitment tree
/// to its anchor.
#[derive(Debug)]
pub struct MerklePath {
position: u32,
@ -52,20 +86,19 @@ impl MerklePath {
/// The layer with 2^n nodes is called "layer n":
/// - leaves are at layer MERKLE_DEPTH_ORCHARD = 32;
/// - the root is at layer 0.
/// `l_star` is MERKLE_DEPTH_ORCHARD - layer - 1.
/// `l` is MERKLE_DEPTH_ORCHARD - layer - 1.
/// - when hashing two leaves, we produce a node on the layer above the leaves, i.e.
/// layer = 31, l_star = 0
/// - when hashing to the final root, we produce the anchor with layer = 0, l_star = 31.
pub fn root(&self, cmx: ExtractedNoteCommitment) -> Anchor {
let node = self
.auth_path
/// layer = 31, l = 0
/// - when hashing to the final root, we produce the anchor with layer = 0, l = 31.
pub fn root(&self, cmx: ExtractedNoteCommitment) -> CtOption<Anchor> {
self.auth_path
.iter()
.enumerate()
.fold(*cmx, |node, (l_star, sibling)| {
let swap = self.position & (1 << l_star) != 0;
hash_layer(l_star, cond_swap(swap, node, *sibling))
});
Anchor(node)
.fold(CtOption::new(*cmx, 1.into()), |node, (l, sibling)| {
let swap = self.position & (1 << l) != 0;
node.and_then(|n| hash_with_l(l, cond_swap(swap, n, *sibling)))
})
.map(Anchor)
}
/// Returns the position of the leaf using this Merkle path.
@ -102,77 +135,151 @@ fn cond_swap(swap: bool, node: pallas::Base, sibling: pallas::Base) -> Pair {
/// The layer with 2^n nodes is called "layer n":
/// - leaves are at layer MERKLE_DEPTH_ORCHARD = 32;
/// - the root is at layer 0.
/// `l_star` is MERKLE_DEPTH_ORCHARD - layer - 1.
/// `l` is MERKLE_DEPTH_ORCHARD - layer - 1.
/// - when hashing two leaves, we produce a node on the layer above the leaves, i.e.
/// layer = 31, l_star = 0
/// - when hashing to the final root, we produce the anchor with layer = 0, l_star = 31.
fn hash_layer(l_star: usize, pair: Pair) -> pallas::Base {
/// layer = 31, l = 0
/// - when hashing to the final root, we produce the anchor with layer = 0, l = 31.
fn hash_with_l(l: usize, pair: Pair) -> CtOption<pallas::Base> {
// MerkleCRH Sinsemilla hash domain.
let domain = HashDomain::new(MERKLE_CRH_PERSONALIZATION);
domain
.hash(
iter::empty()
.chain(i2lebsp_k(l_star).iter().copied())
.chain(
pair.left
.to_le_bits()
.iter()
.by_val()
.take(L_ORCHARD_MERKLE),
)
.chain(
pair.right
.to_le_bits()
.iter()
.by_val()
.take(L_ORCHARD_MERKLE),
),
domain.hash(
iter::empty()
.chain(i2lebsp_k(l).iter().copied())
.chain(
pair.left
.to_le_bits()
.iter()
.by_val()
.take(L_ORCHARD_MERKLE),
)
.chain(
pair.right
.to_le_bits()
.iter()
.by_val()
.take(L_ORCHARD_MERKLE),
),
)
}
/// A newtype wrapper for leaves and internal nodes in the Orchard
/// incremental note commitment tree.
///
/// This wraps a CtOption<pallas::Base> because Sinsemilla hashes
/// can produce a bottom value which needs to be accounted for in
/// the production of a Merkle root. Leaf nodes are always wrapped
/// with the `Some` constructor.
#[derive(Clone, Debug)]
pub struct OrchardIncrementalTreeDigest(CtOption<pallas::Base>);
impl OrchardIncrementalTreeDigest {
/// Creates an incremental tree leaf digest from the specified
/// Orchard extracted note commitment.
pub fn from_cmx(value: &ExtractedNoteCommitment) -> Self {
OrchardIncrementalTreeDigest(CtOption::new(**value, 1.into()))
}
/// Convert this digest to its canonical byte representation.
pub fn to_bytes(&self) -> Option<[u8; 32]> {
<Option<pallas::Base>>::from(self.0).map(|b| b.to_bytes())
}
/// Parses a incremental tree leaf digest from the bytes of
/// a note commitment.
///
/// Returns the empty `CtOption` if the provided bytes represent
/// a non-canonical encoding.
pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Base::from_bytes(bytes)
.map(|b| OrchardIncrementalTreeDigest(CtOption::new(b, 1.into())))
}
}
/// This instance should only be used for hash table key comparisons.
impl std::cmp::PartialEq for OrchardIncrementalTreeDigest {
fn eq(&self, other: &Self) -> bool {
self.0.ct_eq(&other.0).into()
}
}
/// This instance should only be used for hash table key comparisons.
impl std::cmp::Eq for OrchardIncrementalTreeDigest {}
/// This instance should only be used for hash table key hashing.
impl std::hash::Hash for OrchardIncrementalTreeDigest {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
<Option<pallas::Base>>::from(self.0)
.map(|b| b.to_bytes())
.hash(state)
}
}
impl Hashable for OrchardIncrementalTreeDigest {
fn empty_leaf() -> Self {
OrchardIncrementalTreeDigest(CtOption::new(*UNCOMMITTED_ORCHARD, 1.into()))
}
fn combine(altitude: Altitude, left_opt: &Self, right_opt: &Self) -> Self {
OrchardIncrementalTreeDigest(left_opt.0.and_then(|left| {
right_opt
.0
.and_then(|right| hash_with_l(altitude.into(), Pair { left, right }))
}))
}
fn empty_root(altitude: Altitude) -> Self {
OrchardIncrementalTreeDigest(CtOption::new(
EMPTY_ROOTS[<usize>::from(altitude)],
1.into(),
))
}
}
impl Serialize for OrchardIncrementalTreeDigest {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.to_bytes().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for OrchardIncrementalTreeDigest {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let parsed = <[u8; 32]>::deserialize(deserializer)?;
<Option<_>>::from(Self::from_bytes(&parsed)).ok_or_else(|| {
Error::custom(
"Attempted to deserialize a non-canonical representation of a Pallas base field element.",
)
.unwrap()
})
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use lazy_static::lazy_static;
#[cfg(test)]
use incrementalmerkletree::{
bridgetree::Frontier as BridgeFrontier, Altitude, Frontier, Hashable,
};
use std::convert::TryInto;
use std::iter;
#[cfg(test)]
use subtle::CtOption;
use crate::{
constants::MERKLE_DEPTH_ORCHARD,
note::{commitment::ExtractedNoteCommitment, testing::arb_note, Note},
value::{testing::arb_positive_note_value, MAX_NOTE_VALUE},
};
use pasta_curves::{arithmetic::FieldExt, pallas};
#[cfg(test)]
use pasta_curves::arithmetic::FieldExt;
use pasta_curves::pallas;
use proptest::collection::vec;
use proptest::prelude::*;
use super::{hash_layer, Anchor, MerklePath, Pair};
// The uncommitted leaf is defined as pallas::Base(2).
// <https://zips.z.cash/protocol/protocol.pdf#thmuncommittedorchard>
lazy_static! {
static ref EMPTY_ROOTS: Vec<pallas::Base> = {
iter::empty()
.chain(Some(pallas::Base::from_u64(2)))
.chain((0..MERKLE_DEPTH_ORCHARD).scan(
pallas::Base::from_u64(2),
|state, l_star| {
*state = hash_layer(
l_star,
Pair {
left: *state,
right: *state,
},
);
Some(*state)
},
))
.collect()
};
}
#[cfg(test)]
use super::OrchardIncrementalTreeDigest;
use super::{hash_with_l, Anchor, MerklePath, Pair, EMPTY_ROOTS};
#[test]
fn test_vectors() {
@ -223,23 +330,22 @@ pub mod testing {
// The layer with 2^n nodes is called "layer n":
// - leaves are at layer MERKLE_DEPTH_ORCHARD = 32;
// - the root is at layer 0.
// `l_star` is MERKLE_DEPTH_ORCHARD - layer - 1.
// `l` is MERKLE_DEPTH_ORCHARD - layer - 1.
// - when hashing two leaves, we produce a node on the layer above the leaves, i.e.
// layer = 31, l_star = 0
// - when hashing to the final root, we produce the anchor with layer = 0, l_star = 31.
for height in 1..perfect_subtree_depth {
let l_star = height - 1;
let inner_nodes = (0..(n_leaves >> height)).map(|pos| {
let left = perfect_subtree[height - 1][pos * 2];
let right = perfect_subtree[height - 1][pos * 2 + 1];
// layer = 31, l = 0
// - when hashing to the final root, we produce the anchor with layer = 0, l = 31.
for l in 0..perfect_subtree_depth {
let inner_nodes = (0..(n_leaves >> (l + 1))).map(|pos| {
let left = perfect_subtree[l][pos * 2];
let right = perfect_subtree[l][pos * 2 + 1];
match (left, right) {
(None, None) => None,
(Some(left), None) => {
let right = EMPTY_ROOTS[height - 1];
Some(hash_layer(l_star, Pair {left, right}))
let right = EMPTY_ROOTS[l];
Some(hash_with_l(l, Pair {left, right}).unwrap())
},
(Some(left), Some(right)) => {
Some(hash_layer(l_star, Pair {left, right}))
Some(hash_with_l(l, Pair {left, right}).unwrap())
},
(None, Some(_)) => {
unreachable!("The perfect subtree is left-packed.")
@ -282,7 +388,7 @@ pub mod testing {
};
// Compute anchor for this tree
let anchor = auth_paths[0].root(notes[0].commitment().into());
let anchor = auth_paths[0].root(notes[0].commitment().into()).unwrap();
(
notes.into_iter().zip(auth_paths.into_iter()).map(|(note, auth_path)| (note, auth_path)).collect(),
@ -292,15 +398,88 @@ pub mod testing {
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))]
#[allow(clippy::redundant_closure)]
#[test]
fn tree(
(notes_and_auth_paths, anchor) in (1usize..4).prop_flat_map(|n_notes| arb_tree(n_notes))
) {
for (note, auth_path) in notes_and_auth_paths.iter() {
let computed_anchor = auth_path.root(note.commitment().into());
let computed_anchor = auth_path.root(note.commitment().into()).unwrap();
assert_eq!(anchor, computed_anchor);
}
}
}
#[test]
fn empty_roots_incremental() {
let tv_empty_roots = crate::test_vectors::commitment_tree::test_vectors().empty_roots;
for (altitude, tv_root) in tv_empty_roots.iter().enumerate() {
assert_eq!(
OrchardIncrementalTreeDigest::empty_root(Altitude::from(altitude as u8))
.0
.unwrap()
.to_bytes(),
*tv_root,
"Empty root mismatch at altitude {}",
altitude
);
}
}
#[test]
fn anchor_incremental() {
// These commitment values are derived from the bundle data that was generated for
// testing commitment tree construction inside of zcashd here.
// https://github.com/zcash/zcash/blob/ecec1f9769a5e37eb3f7fd89a4fcfb35bc28eed7/src/test/data/merkle_roots_orchard.h
let commitments = [
[
0x68, 0x13, 0x5c, 0xf4, 0x99, 0x33, 0x22, 0x90, 0x99, 0xa4, 0x4e, 0xc9, 0x9a, 0x75,
0xe1, 0xe1, 0xcb, 0x46, 0x40, 0xf9, 0xb5, 0xbd, 0xec, 0x6b, 0x32, 0x23, 0x85, 0x6f,
0xea, 0x16, 0x39, 0x0a,
],
[
0x78, 0x31, 0x50, 0x08, 0xfb, 0x29, 0x98, 0xb4, 0x30, 0xa5, 0x73, 0x1d, 0x67, 0x26,
0x20, 0x7d, 0xc0, 0xf0, 0xec, 0x81, 0xea, 0x64, 0xaf, 0x5c, 0xf6, 0x12, 0x95, 0x69,
0x01, 0xe7, 0x2f, 0x0e,
],
[
0xee, 0x94, 0x88, 0x05, 0x3a, 0x30, 0xc5, 0x96, 0xb4, 0x30, 0x14, 0x10, 0x5d, 0x34,
0x77, 0xe6, 0xf5, 0x78, 0xc8, 0x92, 0x40, 0xd1, 0xd1, 0xee, 0x17, 0x43, 0xb7, 0x7b,
0xb6, 0xad, 0xc4, 0x0a,
],
[
0x9d, 0xdc, 0xe7, 0xf0, 0x65, 0x01, 0xf3, 0x63, 0x76, 0x8c, 0x5b, 0xca, 0x3f, 0x26,
0x46, 0x60, 0x83, 0x4d, 0x4d, 0xf4, 0x46, 0xd1, 0x3e, 0xfc, 0xd7, 0xc6, 0xf1, 0x7b,
0x16, 0x7a, 0xac, 0x1a,
],
[
0xbd, 0x86, 0x16, 0x81, 0x1c, 0x6f, 0x5f, 0x76, 0x9e, 0xa4, 0x53, 0x9b, 0xba, 0xff,
0x0f, 0x19, 0x8a, 0x6c, 0xdf, 0x3b, 0x28, 0x0d, 0xd4, 0x99, 0x26, 0x16, 0x3b, 0xd5,
0x3f, 0x53, 0xa1, 0x21,
],
];
// This value was produced by the Python test vector generation code implemented here:
// https://github.com/zcash-hackworks/zcash-test-vectors/blob/f4d756410c8f2456f5d84cedf6dac6eb8c068eed/orchard_merkle_tree.py
let anchor = [
0xc8, 0x75, 0xbe, 0x2d, 0x60, 0x87, 0x3f, 0x8b, 0xcd, 0xeb, 0x91, 0x28, 0x2e, 0x64,
0x2e, 0x0c, 0xc6, 0x5f, 0xf7, 0xd0, 0x64, 0x2d, 0x13, 0x7b, 0x28, 0xcf, 0x28, 0xcc,
0x9c, 0x52, 0x7f, 0x0e,
];
let mut frontier = BridgeFrontier::<OrchardIncrementalTreeDigest, 32>::new();
for commitment in commitments.iter() {
let cmx = OrchardIncrementalTreeDigest(CtOption::new(
pallas::Base::from_bytes(commitment).unwrap(),
1.into(),
));
frontier.append(&cmx);
}
assert_eq!(
frontier.root().0.unwrap(),
pallas::Base::from_bytes(&anchor).unwrap()
);
}
}