From 8f8eff23d81076090f52b54df8cdad5d3d4b8d59 Mon Sep 17 00:00:00 2001 From: therealyingtong Date: Tue, 8 Jun 2021 15:27:08 +0800 Subject: [PATCH] Update proptests to generate Merkle paths --- Cargo.toml | 1 + src/builder.rs | 12 +--- src/bundle.rs | 2 +- src/note.rs | 2 +- src/note/commitment.rs | 7 ++- src/tree.rs | 135 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5690a069..4842be2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ blake2b_simd = "0.5" ff = "0.10" fpe = "0.4" group = "0.10" +lazy_static = "1" pasta_curves = "0.1" proptest = { version = "1.0.0", optional = true } rand = "0.8" diff --git a/src/builder.rs b/src/builder.rs index 11e7272c..22aaea8e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -465,8 +465,7 @@ pub mod testing { testing::arb_spending_key, FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendingKey, }, - note::testing::arb_note, - tree::{Anchor, MerklePath}, + tree::{testing::arb_tree, Anchor, MerklePath}, value::{testing::arb_positive_note_value, NoteValue, MAX_NOTE_VALUE}, Address, Note, }; @@ -527,12 +526,7 @@ pub mod testing { n_recipients in 1..30, ) ( - anchor in prop::array::uniform32(prop::num::u8::ANY).prop_map(Anchor), - // generate note values that we're certain won't exceed MAX_NOTE_VALUE in total - notes in vec( - arb_positive_note_value(MAX_NOTE_VALUE / n_notes as u64).prop_flat_map(arb_note), - n_notes as usize - ), + (notes_and_auth_paths, anchor) in arb_tree(n_notes), recipient_amounts in vec( arb_address().prop_flat_map(move |a| { arb_positive_note_value(MAX_NOTE_VALUE / n_recipients as u64) @@ -546,7 +540,7 @@ pub mod testing { rng: StdRng::from_seed(rng_seed), sk: sk.clone(), anchor, - notes, + notes: notes_and_auth_paths, recipient_amounts } } diff --git a/src/bundle.rs b/src/bundle.rs index ac41e2fb..e5c758c7 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -114,7 +114,7 @@ impl Action { cv_net: self.cv_net.clone(), nf_old: self.nf, rk: self.rk.clone(), - cmx: self.cmx.clone(), + cmx: self.cmx, enable_spend: flags.spends_enabled, enable_output: flags.outputs_enabled, } diff --git a/src/note.rs b/src/note.rs index 860b0021..18153f61 100644 --- a/src/note.rs +++ b/src/note.rs @@ -59,7 +59,7 @@ impl RandomSeed { } /// A discrete amount of funds received by an address. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Note { /// The recipient of the funds. recipient: Address, diff --git a/src/note/commitment.rs b/src/note/commitment.rs index d543c0f3..efdd1037 100644 --- a/src/note/commitment.rs +++ b/src/note/commitment.rs @@ -43,10 +43,15 @@ impl NoteCommitment { } /// The x-coordinate of the commitment to a note. -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct ExtractedNoteCommitment(pub(super) pallas::Base); impl ExtractedNoteCommitment { + /// ExtractedNoteCommitment for an uncommitted note. + pub fn uncommitted() -> Self { + Self(pallas::Base::zero()) + } + /// Deserialize the extracted note commitment from a byte array. pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { pallas::Base::from_bytes(bytes).map(ExtractedNoteCommitment) diff --git a/src/tree.rs b/src/tree.rs index a7f94799..b9267595 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -101,3 +101,138 @@ fn hash_layer(l_star: usize, pair: Pair) -> pallas::Base { ) .unwrap() } + +/// Generators for property testing. +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use lazy_static::lazy_static; + use std::convert::TryInto; + + use crate::{ + constants::{MERKLE_CRH_PERSONALIZATION, MERKLE_DEPTH_ORCHARD}, + note::{commitment::ExtractedNoteCommitment, testing::arb_note, Note}, + primitives::sinsemilla::{i2lebsp_k, HashDomain, K}, + value::{testing::arb_positive_note_value, MAX_NOTE_VALUE}, + }; + use ff::{PrimeField, PrimeFieldBits}; + use pasta_curves::pallas; + + use proptest::collection::vec; + use proptest::prelude::*; + + use super::{hash_layer, Anchor, MerklePath, Pair}; + + lazy_static! { + static ref EMPTY_ROOTS: Vec = { + // MerkleCRH Sinsemilla hash domain. + let domain = HashDomain::new(MERKLE_CRH_PERSONALIZATION); + + let mut v = vec![pallas::Base::zero()]; + for l_star in 0..MERKLE_DEPTH_ORCHARD { + let next = domain + .hash( + std::iter::empty() + .chain(i2lebsp_k(l_star).iter().copied().take(K)) + .chain( + v[l_star] + .to_le_bits() + .iter() + .by_val() + .take(pallas::Base::NUM_BITS as usize) + ) + .chain( + v[l_star] + .to_le_bits() + .iter() + .by_val() + .take(pallas::Base::NUM_BITS as usize) + ), + ) + .unwrap(); + v.push(next); + } + v + }; + } + + prop_compose! { + /// Generates an arbitrary Merkle tree of with `n_notes` nonempty leaves. + pub fn arb_tree(n_notes: i32) + ( + // generate note values that we're certain won't exceed MAX_NOTE_VALUE in total + notes in vec( + arb_positive_note_value(MAX_NOTE_VALUE / n_notes as u64).prop_flat_map(arb_note), + n_notes as usize + ), + ) + -> (Vec<(Note, MerklePath)>, Anchor) { + // Inefficient algorithm to build a perfect subtree containing all notes. + let perfect_subtree_depth = (n_notes as f64).log2().ceil() as usize; + let commitments: Vec = notes.iter().map(|note| { + let cmx: ExtractedNoteCommitment = note.commitment().into(); + cmx + }).collect(); + + let padded_leaves = { + let mut padded_leaves = commitments.clone(); + + let pad = (0..((1 << perfect_subtree_depth) - n_notes as usize)).map( + |_| ExtractedNoteCommitment::uncommitted() + ).collect::>(); + + padded_leaves.extend_from_slice(&pad); + padded_leaves + }; + + let tree = { + let mut tree: Vec> = vec![padded_leaves.into_iter().map(|leaf| *leaf).collect()]; + for height in 1..perfect_subtree_depth { + let inner_nodes = (0..(perfect_subtree_depth >> height)).map(|pos| { + hash_layer(height, Pair { + left: tree[height - 1][pos * 2], + right: tree[height - 1][pos * 2 + 1], + }) + }).collect(); + tree.push(inner_nodes); + }; + tree + }; + + // Get Merkle path for each note commitment + let auth_paths = { + let mut auth_paths: Vec = Vec::new(); + for (pos, _) in commitments.iter().enumerate() { + let mut auth_path: [pallas::Base; MERKLE_DEPTH_ORCHARD] = (0..MERKLE_DEPTH_ORCHARD).map(|idx| EMPTY_ROOTS[idx]).collect::>().try_into().unwrap(); + + let mut layer_pos = pos; + for height in 0..perfect_subtree_depth { + let is_right_sibling = layer_pos & 1 == 1; + let sibling = if is_right_sibling { + tree[height][layer_pos - 1] + } else { + tree[height][layer_pos + 1] + }; + auth_path[height] = sibling; + layer_pos = (layer_pos - is_right_sibling as usize) / 2; + }; + + let path = MerklePath {position: pos as u32, auth_path}; + auth_paths.push(path); + } + auth_paths + }; + + // Compute anchor for this tree + let anchor = auth_paths[0].root(commitments[0]); + for (cmx, auth_path) in commitments.iter().zip(auth_paths.iter()) { + let computed_anchor = auth_path.root(*cmx); + assert_eq!(anchor, computed_anchor); + } + + ( + notes.into_iter().zip(auth_paths.into_iter()).map(|(note, auth_path)| (note, auth_path)).collect(), + anchor + ) + } + } +}