From 7fd089e3b855c9ddcbb77f1ccfa0036dc74f033c Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 08:41:43 +0100 Subject: [PATCH 1/9] Improve comments for hasher unit test --- src/hasher.rs | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/hasher.rs b/src/hasher.rs index f28bce7f..58b256be 100644 --- a/src/hasher.rs +++ b/src/hasher.rs @@ -18,6 +18,24 @@ const DELIMITER: &[u8] = ";".as_bytes(); /// hasher.update("leaf".as_bytes()); /// let hash = hasher.finalize(); /// ``` +/// +/// Note that a delimiter is used to add extra security: +/// ``` +/// use dapol::Hasher; +/// let mut dapol_hasher = Hasher::new(); +/// dapol_hasher.update("leaf".as_bytes()); +/// dapol_hasher.update("node".as_bytes()); +/// let dapol_hash = dapol_hasher.finalize(); +/// +/// let mut blake_hasher = blake3::Hasher::new(); +/// blake_hasher.update("leaf".as_bytes()); +/// blake_hasher.update(";".as_bytes()); +/// blake_hasher.update("node".as_bytes()); +/// blake_hasher.update(";".as_bytes()); +/// let blake_hash = blake_hasher.finalize(); +/// +/// assert_eq!(dapol_hash.as_bytes(), blake_hash.as_bytes()); +/// ``` pub struct Hasher(blake3::Hasher); impl Hasher { @@ -49,19 +67,30 @@ mod tests { // Ensures Blake 3 library produces correct hashed output. // Comparison hash derived through the following urls: - // https://toolkitbay.com/tkb/tool/BLAKE3 - // https://connor4312.github.io/blake3/index.html // https://asecuritysite.com/hash/blake3 + // https://emn178.github.io/online-tools/blake3.html + // + // For https://connor4312.github.io/blake3/index.html do the following: + // -> select utf-8 input option + // -> paste in "dapol;PoR;" + // -> see resulting hash is equal to b0424ae23fcce672aaff99e9f433286e27119939a280743539783ba7aade8294 + // + // For https://toolkitbay.com/tkb/tool/BLAKE3 do the following: + // -> select "text input" option + // -> paste in "dapol;PoR;" + // -> click "process from text" + // -> see resulting hash is equal to b0424ae23fcce672aaff99e9f433286e27119939a280743539783ba7aade8294 #[test] fn verify_hasher() { use std::str::FromStr; let mut hasher = Hasher::new(); - hasher.update("dapol-PoR".as_bytes()); + hasher.update("dapol".as_bytes()); + hasher.update("PoR".as_bytes()); let hash = hasher.finalize(); assert_eq!( hash, - H256::from_str("09eb9ee70fc9df4d767b07cc5befc6f7a303fa0025fca014e22e8c3dc9927767") + H256::from_str("b0424ae23fcce672aaff99e9f433286e27119939a280743539783ba7aade8294") .unwrap() ); } From a324d82059cb7acfa6e5652e3989113ee6c83f66 Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:07:21 +0100 Subject: [PATCH 2/9] Add Display trait to generic type for Node This is needed for doing node.to_string() which will be useful in subsequent commits --- src/binary_tree.rs | 42 ++++++++++++------- src/binary_tree/node_content/full_node.rs | 15 ++++++- src/binary_tree/node_content/hidden_node.rs | 13 +++++- src/binary_tree/tree_builder.rs | 6 +-- .../tree_builder/multi_threaded.rs | 15 +++---- .../tree_builder/single_threaded.rs | 16 +++---- src/binary_tree/utils.rs | 6 +++ 7 files changed, 77 insertions(+), 36 deletions(-) diff --git a/src/binary_tree.rs b/src/binary_tree.rs index d87e6428..8f3e17f9 100644 --- a/src/binary_tree.rs +++ b/src/binary_tree.rs @@ -33,7 +33,7 @@ //! `x` coordinate (their `y` coordinate will be 0). use serde::{Deserialize, Serialize}; -use std::fmt; +use std::fmt::{self, Debug}; mod utils; @@ -83,7 +83,7 @@ pub const MIN_RECOMMENDED_SPARSITY: u8 = 2; /// /// The generic type `C` is for the content contained within each node. #[derive(Serialize, Deserialize)] -pub struct BinaryTree { +pub struct BinaryTree { root: Node, store: Store, height: Height, @@ -93,7 +93,7 @@ pub struct BinaryTree { /// The data contained in the node is completely generic, requiring only to have /// an associated merge function. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Node { +pub struct Node { pub coord: Coordinate, pub content: C, } @@ -118,7 +118,7 @@ pub struct Coordinate { /// traits; for more details see /// [this issue](https://github.com/dtolnay/typetag/issues/1). #[derive(Serialize, Deserialize)] -pub enum Store { +pub enum Store { MultiThreadedStore(multi_threaded::DashMapStore), SingleThreadedStore(single_threaded::HashMapStore), } @@ -126,7 +126,7 @@ pub enum Store { // ------------------------------------------------------------------------------------------------- // Accessor methods. -impl BinaryTree { +impl BinaryTree { pub fn height(&self) -> &Height { &self.height } @@ -265,7 +265,7 @@ impl Coordinate { } } -impl Node { +impl Node { /// Returns left if this node is a left sibling and vice versa for right. /// Since we are working with a binary tree we can tell if the node is a /// left sibling of the above layer by checking the x_coord modulus 2. @@ -318,7 +318,7 @@ impl Node { } /// Convert a `Node` to a `Node`. - pub fn convert>(self) -> Node { + pub fn convert + fmt::Display>(self) -> Node { Node { content: self.content.into(), coord: self.coord, @@ -326,7 +326,7 @@ impl Node { } } -impl Store { +impl Store { /// Simply delegate the call to the wrapped store. fn get_node(&self, coord: &Coordinate) -> Option> { match self { @@ -346,9 +346,21 @@ impl Store { /// We can't use the default Debug implementation because it prints the whole /// store. -impl fmt::Debug for BinaryTree { +impl fmt::Debug for BinaryTree { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "root: {:?}, height: {:?}", self.root, self.height) + write!(f, "root: {}, height: {:?}", self.root, self.height) + } +} + +impl fmt::Display for Node { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "(content: {}, coord: {:?})", self.content, self.coord) + } +} + +impl fmt::Display for Coordinate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "(x: {:?}, y: {:?})", self.x, self.y) } } @@ -364,18 +376,18 @@ enum NodeOrientation { /// Used to orient nodes inside a sibling pair so that the compiler can /// guarantee a left node is actually a left node. -enum Sibling { +enum Sibling { Left(Node), Right(Node), } /// A pair of sibling nodes. -struct MatchedPair { +struct MatchedPair { left: Node, right: Node, } -impl From> for Sibling { +impl From> for Sibling { /// Move a generic node into the left/right sibling type. fn from(node: Node) -> Self { match node.orientation() { @@ -385,7 +397,7 @@ impl From> for Sibling { } } -impl MatchedPair { +impl MatchedPair { /// Create a parent node by merging the 2 nodes in the pair. fn merge(&self) -> Node { Node { @@ -395,7 +407,7 @@ impl MatchedPair { } } -impl From<(Node, Node)> for MatchedPair { +impl From<(Node, Node)> for MatchedPair { /// Construct a [MatchedPair] using the 2 given nodes. /// /// Only build the pair if the 2 nodes are siblings, otherwise panic. diff --git a/src/binary_tree/node_content/full_node.rs b/src/binary_tree/node_content/full_node.rs index 37a8fa2c..9cde1427 100644 --- a/src/binary_tree/node_content/full_node.rs +++ b/src/binary_tree/node_content/full_node.rs @@ -154,7 +154,7 @@ impl FullNodeContent { } // ------------------------------------------------------------------------------------------------- -// Implement Mergeable trait +// Implement traits impl Mergeable for FullNodeContent { /// Returns the parent node content by merging two child node contents. @@ -188,6 +188,19 @@ impl Mergeable for FullNodeContent { } } +use std::fmt; + +impl fmt::Display for FullNodeContent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let commitment_bytes = H256::from_slice(self.commitment.compress().as_bytes()); + write!( + f, + "(liability: {}, blinding factor: {:?}, hash: {:?}, commitment: {:?})", + self.liability, self.blinding_factor, self.hash, commitment_bytes + ) + } +} + // ------------------------------------------------------------------------------------------------- // Unit tests diff --git a/src/binary_tree/node_content/hidden_node.rs b/src/binary_tree/node_content/hidden_node.rs index d7e78442..1b3b6cd9 100644 --- a/src/binary_tree/node_content/hidden_node.rs +++ b/src/binary_tree/node_content/hidden_node.rs @@ -110,7 +110,7 @@ impl From for HiddenNodeContent { } // ------------------------------------------------------------------------------------------------- -// Implement merge trait +// Implement trait impl Mergeable for HiddenNodeContent { /// Returns the parent node content by merging two child node contents. @@ -121,7 +121,7 @@ impl Mergeable for HiddenNodeContent { fn merge(left_sibling: &Self, right_sibling: &Self) -> Self { let parent_commitment = left_sibling.commitment + right_sibling.commitment; - // `hash = H(left.com | right.com | left.hash | right.hash` + // `hash = H(left.com | right.com | left.hash | right.hash)` let parent_hash = { let mut hasher = Hasher::new(); hasher.update(left_sibling.commitment.compress().as_bytes()); @@ -138,6 +138,15 @@ impl Mergeable for HiddenNodeContent { } } +use std::fmt; + +impl fmt::Display for HiddenNodeContent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let commitment_bytes = H256::from_slice(self.commitment.compress().as_bytes()); + write!(f, "(hash: {:x?}, commitment: {:?})", self.hash, commitment_bytes) + } +} + // ------------------------------------------------------------------------------------------------- // Unit tests diff --git a/src/binary_tree/tree_builder.rs b/src/binary_tree/tree_builder.rs index 4123089a..3ef46b88 100644 --- a/src/binary_tree/tree_builder.rs +++ b/src/binary_tree/tree_builder.rs @@ -9,7 +9,7 @@ //! generic type, `C`. use serde::Serialize; -use std::fmt::Debug; +use std::fmt::{self, Debug}; use crate::MaxThreadCount; @@ -67,7 +67,7 @@ pub struct InputLeafNode { // ------------------------------------------------------------------------------------------------- // Implementations. -impl BinaryTreeBuilder +impl BinaryTreeBuilder where C: Clone + Mergeable + 'static, /* The static is needed when the single threaded builder * builds the boxed hashmap. */ @@ -241,7 +241,7 @@ where } } -impl InputLeafNode { +impl InputLeafNode { /// Convert the simpler node type to the actual Node type. pub fn into_node(self) -> Node { Node { diff --git a/src/binary_tree/tree_builder/multi_threaded.rs b/src/binary_tree/tree_builder/multi_threaded.rs index 82a19c05..f1d947c2 100644 --- a/src/binary_tree/tree_builder/multi_threaded.rs +++ b/src/binary_tree/tree_builder/multi_threaded.rs @@ -34,6 +34,7 @@ //! root node down are stored. So if `store_depth == height` then all the nodes //! are stored. +use core::fmt; use std::fmt::Debug; use std::ops::Range; @@ -69,7 +70,7 @@ const BUG: &str = "[Bug in multi-threaded builder]"; /// - all x-coord <= max /// - checked for duplicates (duplicate if same x-coords) #[stime("info", "MultiThreadedBuilder::{}")] -pub fn build_tree( +pub fn build_tree( height: Height, store_depth: u8, mut input_leaf_nodes: Vec>, @@ -141,11 +142,11 @@ where type Map = DashMap>; #[derive(Serialize, Deserialize)] -pub struct DashMapStore { +pub struct DashMapStore { map: Map, } -impl DashMapStore { +impl DashMapStore { pub fn get_node(&self, coord: &Coordinate) -> Option> { self.map.get(coord).map(|n| n.clone()) } @@ -164,7 +165,7 @@ impl DashMapStore { /// If all nodes satisfy `node.coord.x <= mid` then `Full` is returned. /// If no nodes satisfy `node.coord.x <= mid` then `Empty` is returned. // TODO can be optimized using a binary search -fn num_nodes_left_of(x_coord_mid: u64, nodes: &Vec>) -> NumNodes { +fn num_nodes_left_of(x_coord_mid: u64, nodes: &Vec>) -> NumNodes { nodes .iter() .rposition(|leaf| leaf.coord.x <= x_coord_mid) @@ -183,7 +184,7 @@ enum NumNodes { Partial(usize), } -impl Node { +impl Node { /// New padding node contents are given by a closure. Why a closure? Because /// creating a padding node may require context outside of this scope, where /// type `C` is defined, for example. @@ -197,7 +198,7 @@ impl Node { } } -impl MatchedPair { +impl MatchedPair { /// Create a pair of left and right sibling nodes from only 1 node and the /// padding node generation function. /// @@ -388,7 +389,7 @@ impl RecursionParams { /// function anyway. If either case is reached then either there is a bug in the /// original calling code or there is a bug in the splitting algorithm in this /// function. There is no recovery from these 2 states so we panic. -pub fn build_node( +pub fn build_node( params: RecursionParams, mut leaves: Vec>, new_padding_node_content: Arc, diff --git a/src/binary_tree/tree_builder/single_threaded.rs b/src/binary_tree/tree_builder/single_threaded.rs index 8d7d3e6c..4499c2ac 100644 --- a/src/binary_tree/tree_builder/single_threaded.rs +++ b/src/binary_tree/tree_builder/single_threaded.rs @@ -16,7 +16,7 @@ //! are stored. use std::collections::HashMap; -use std::fmt::Debug; +use std::fmt::{self, Debug}; use log::warn; use logging_timer::stime; @@ -41,7 +41,7 @@ const BUG: &str = "[Bug in single-threaded builder]"; /// The leaf nodes are sorted by x-coord, checked for duplicates, and /// converted to the right type. #[stime("info", "SingleThreadedBuilder::{}")] -pub fn build_tree( +pub fn build_tree( height: Height, store_depth: u8, mut input_leaf_nodes: Vec>, @@ -88,11 +88,11 @@ where // Store. #[derive(Serialize, Deserialize)] -pub struct HashMapStore { +pub struct HashMapStore { map: Map, } -impl HashMapStore { +impl HashMapStore { pub fn get_node(&self, coord: &Coordinate) -> Option> { self.map.get(coord).map(|n| (*n).clone()) } @@ -109,12 +109,12 @@ impl HashMapStore { /// /// At least one of the fields is expected to be set. If this is not the case /// then it is assumed there is a bug in the code using this struct. -struct MaybeUnmatchedPair { +struct MaybeUnmatchedPair { left: Option>, right: Option>, } -impl MaybeUnmatchedPair { +impl MaybeUnmatchedPair { /// Convert the partially matched pair into a matched pair. /// /// If both left and right nodes are not present then the function will @@ -144,7 +144,7 @@ impl MaybeUnmatchedPair { } } -impl Node { +impl Node { /// New padding node contents are given by a closure. Why a closure? Because /// creating a padding node may require context outside of this scope, where /// type C is defined, for example. @@ -184,7 +184,7 @@ type RootNode = Node; /// /// Note that all bottom layer nodes are stored, both the inputted leaf /// nodes and their accompanying padding nodes. -pub fn build_node( +pub fn build_node( leaf_nodes: Vec>, height: &Height, store_depth: u8, diff --git a/src/binary_tree/utils.rs b/src/binary_tree/utils.rs index 5b49ac67..ffe7abd6 100644 --- a/src/binary_tree/utils.rs +++ b/src/binary_tree/utils.rs @@ -37,6 +37,12 @@ pub mod test_utils { } } + impl fmt::Display for TestContent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "(value: {:?}, hash: {:?})", self.value, self.hash) + } + } + pub fn generate_padding_closure() -> impl Fn(&Coordinate) -> TestContent { |_coord: &Coordinate| -> TestContent { TestContent { From a6654bc9970fc0ed202593792c804a45daad95a0 Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:13:30 +0100 Subject: [PATCH 3/9] Functions for writing path info to json file & stdout --- src/binary_tree.rs | 4 +- src/binary_tree/path_siblings.rs | 202 ++++++++++++++++++++++++++++--- src/inclusion_proof.rs | 170 ++++++++++++++++++-------- 3 files changed, 308 insertions(+), 68 deletions(-) diff --git a/src/binary_tree.rs b/src/binary_tree.rs index 8f3e17f9..f506dc1d 100644 --- a/src/binary_tree.rs +++ b/src/binary_tree.rs @@ -47,7 +47,9 @@ pub use tree_builder::{ }; mod path_siblings; -pub use path_siblings::{PathSiblings, PathSiblingsBuildError, PathSiblingsError}; +pub use path_siblings::{ + PathSiblings, PathSiblingsBuildError, PathSiblingsError, PathSiblingsWriteError, +}; mod height; pub use height::{Height, HeightError, MAX_HEIGHT, MIN_HEIGHT}; diff --git a/src/binary_tree/path_siblings.rs b/src/binary_tree/path_siblings.rs index cdfc9943..0de074e9 100644 --- a/src/binary_tree/path_siblings.rs +++ b/src/binary_tree/path_siblings.rs @@ -21,10 +21,14 @@ //! [super][tree_builder][multi_threaded] and //! [super][tree_builder][single_threaded]. -use super::{BinaryTree, Coordinate, Mergeable, Node, MIN_STORE_DEPTH}; -use crate::{binary_tree::multi_threaded::RecursionParamsBuilder, utils::Consume}; +use super::{BinaryTree, Coordinate, HiddenNodeContent, Mergeable, Node, MIN_STORE_DEPTH}; +use crate::{ + binary_tree::multi_threaded::RecursionParamsBuilder, read_write_utils, utils::Consume, +}; -use std::fmt::Debug; +use log::info; + +use std::{fmt::Debug, path::PathBuf}; // ------------------------------------------------------------------------------------------------- // Main struct and build functions. @@ -36,9 +40,9 @@ use std::fmt::Debug; /// (last, not included). The leaf node + the siblings can be used to /// reconstruct the actual nodes in the path as well as the root node. #[derive(Debug, Serialize, Deserialize)] -pub struct PathSiblings(pub Vec>); +pub struct PathSiblings(pub Vec>); -impl PathSiblings { +impl PathSiblings { /// High performance build algorithm utilizing parallelization. /// Uses the same code in [super][tree_builder][multi_threaded]. /// @@ -207,7 +211,7 @@ impl PathSiblings { // ------------------------------------------------------------------------------------------------- // Implementation. -impl PathSiblings { +impl PathSiblings { /// Number of sibling nodes. pub fn len(&self) -> usize { self.0.len() @@ -216,8 +220,12 @@ impl PathSiblings { /// Reconstructing each node in the path, from bottom layer /// to the root, using the given leaf and sibling nodes. /// - /// An error is returned if the number of siblings is less than the min - /// amount. + /// This function does exactly the same as [construct_path] but does not + /// store the intermediate nodes, only the final node. + /// + /// An error is returned if + /// 1. The number of siblings is less than the min amount. + /// 2. The [PathSiblings] data is invalid. pub fn construct_root_node(&self, leaf: &Node) -> Result, PathSiblingsError> { use super::MIN_HEIGHT; @@ -231,7 +239,7 @@ impl PathSiblings { .next() // We checked the length of the underlying vector above so this // should never panic. - .expect("There should be at least 1 sibling node"), + .expect("[Bug in path generation] There should be at least 1 sibling node"), leaf, )?; let mut parent = pair.merge(); @@ -250,8 +258,19 @@ impl PathSiblings { /// [PathSiblings] because they are not stored explicitly. The order of the /// returned path nodes is bottom first (leaf) and top last (root). /// - /// An error is returned if the [PathSiblings] data is invalid. + /// This function does exactly the same as [construct_root_node] but stores + /// all the intermediate nodes and returns them. + /// + /// An error is returned if + /// 1. The number of siblings is less than the min amount. + /// 2. The [PathSiblings] data is invalid. pub fn construct_path(&self, leaf: Node) -> Result>, PathSiblingsError> { + use super::MIN_HEIGHT; + + if self.len() < MIN_HEIGHT.as_usize() { + return Err(PathSiblingsError::TooFewSiblings); + } + // +1 because the root node is included in the returned vector let mut nodes = Vec::>::with_capacity(self.len() + 1); @@ -270,18 +289,161 @@ impl PathSiblings { } } +// ------------------------------------------------------------------------------------------------- +// Pretty printing + +impl PathSiblings { + /// Format the path & sibling nodes to a string. + /// + /// The path nodes are required as input, which can be generated using + /// [construct_path]. + pub fn path_to_str(&self, path_nodes: &Vec>) -> String { + use std::fmt::Write as _; + + let path_siblings = &self.0; + let mut path_siblings_str = String::new(); + path_siblings.iter().for_each(|node| { + path_siblings_str.push_str(&node.to_string()); + path_siblings_str.push_str("\n"); + }); + + let mut path_nodes_str = String::new(); + path_nodes.iter().for_each(|node| { + path_nodes_str.push_str(&node.to_string()); + path_nodes_str.push_str("\n"); + }); + + let mut output_str = String::new(); + + write!(&mut output_str, "\nNodes:\n{}", path_nodes_str) + .expect("[Bug in path to string conversion] Cannot write to string object"); + + write!(&mut output_str, "\nSiblings:\n{}", path_siblings_str) + .expect("[Bug in path to string conversion] Cannot write to string object"); + + output_str + } +} + +// ------------------------------------------------------------------------------------------------- +// Pretty printing for C=HiddenNodeContent + +/// Output shape for serializing to json. +#[derive(Debug, Serialize)] +struct PathWithSiblings { + path_nodes: Vec, + path_siblings: Vec, +} + +/// This is basically a PrettyNode. +/// +/// This is used to write out path information to a neat json file. +// +// Note that we could have just serialized Node, but the commitment and +// hash values serialize to different formats. The commitment serializes to +// [u8; 32], and the hash to a hex String. This is a pain to ingest with other +// software. One way to get both commitment & hash to be hex Strings is to +// simply do it manually, but that means knowing the specific type of C. So +// we cannot make this generic for all types of C. +#[derive(Debug, Serialize)] +struct PrettyNode { + coord: Coordinate, + hash: String, + commitment: String, +} + +impl From> for PrettyNode { + /// Convert from a Node type to a PrettyNode. + /// + /// The hash & commitment string fields are populated with hexadecimal + /// format of the underlying data. + fn from(node: Node) -> Self { + use primitive_types::H256; + use std::fmt::Write as _; + + let com_bytes = H256::from_slice(node.content.commitment.compress().as_bytes()); + let mut com_str = String::new(); + write!(&mut com_str, "{:x?}", com_bytes).expect("Cannot write to string object"); + + let mut hash_str = String::new(); + write!(&mut hash_str, "{:x?}", node.content.hash).expect("Cannot write to string object"); + + PrettyNode { + coord: node.coord, + hash: hash_str, + commitment: com_str, + } + } +} + +use std::ffi::OsString; + +impl PathSiblings { + /// Write the path & sibling nodes to a json file. + /// + /// The path nodes are required as input for efficiency reasons (don't + /// recompute in here if they have been computed elsewhere). The path nodes + /// can be generated using [construct_nodes]. + /// + /// Returns an error if the provided directory is invalid, or if the + /// serialization process fails. + pub fn write_path_to_json( + self, + path_nodes: Vec>, + dir: PathBuf, + mut file_name: OsString, + ) -> Result<(), PathSiblingsWriteError> { + if dir.is_dir() { + return Err(PathSiblingsWriteError::InvalidDirectory( + dir.into_os_string(), + )); + } + + file_name.push(".json"); + let file_path = dir.join(file_name); + + let siblings = self.0.into_iter().map(PrettyNode::from).collect(); + let nodes = path_nodes.into_iter().map(PrettyNode::from).collect(); + + let path_with_siblings = PathWithSiblings { + path_nodes: nodes, + path_siblings: siblings, + }; + + info!("Serializing inclusion proof path info to {:?}", file_path); + + read_write_utils::serialize_to_json_file(&path_with_siblings, file_path)?; + + Ok(()) + } +} + // ------------------------------------------------------------------------------------------------- // PathSiblings conversion. -impl PathSiblings { +impl PathSiblings { /// Convert `PathSiblings` to `PathSiblings`. /// /// `convert` is called on each of the sibling nodes & leaf node. - pub fn convert>(self) -> PathSiblings { + pub fn convert + fmt::Display>(self) -> PathSiblings { PathSiblings(self.0.into_iter().map(|node| node.convert()).collect()) } } +use std::fmt; + +impl fmt::Display for PathSiblings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str( + &self + .0 + .iter() + .map(|node| node.coord.to_string()) + .collect::(), + ) + } +} + // ------------------------------------------------------------------------------------------------- // Errors. @@ -310,6 +472,14 @@ pub enum PathSiblingsError { TooFewSiblings, } +#[derive(thiserror::Error, Debug)] +pub enum PathSiblingsWriteError { + #[error("Provided string '{0:?}' does not point to a valid directory")] + InvalidDirectory(OsString), + #[error("Error serializing")] + SerdeError(#[from] crate::read_write_utils::ReadWriteError), +} + // ------------------------------------------------------------------------------------------------- // Supporting structs and methods. @@ -319,7 +489,7 @@ pub enum PathSiblingsError { /// underlying node. The purpose of this type is for efficiency gains over /// [super][sparse_binary_tree][LeftSibling] when ownership of the Node type is /// not needed. -struct LeftSiblingRef<'a, C>(&'a Node); +struct LeftSiblingRef<'a, C: fmt::Display>(&'a Node); /// A reference to a right sibling node. /// @@ -327,7 +497,7 @@ struct LeftSiblingRef<'a, C>(&'a Node); /// underlying node. The purpose of this type is for efficiency gains over /// [super][sparse_binary_tree][RightSibling] when ownership of the Node type is /// not needed. -struct RightSiblingRef<'a, C>(&'a Node); +struct RightSiblingRef<'a, C: fmt::Display>(&'a Node); /// A reference to a pair of left and right sibling nodes. /// @@ -335,12 +505,12 @@ struct RightSiblingRef<'a, C>(&'a Node); /// underlying node. The purpose of this type is for efficiency gains over /// [super][sparse_binary_tree][MatchedPair] when ownership of the Node type is /// not needed. -struct MatchedPairRef<'a, C> { +struct MatchedPairRef<'a, C: fmt::Display> { left: LeftSiblingRef<'a, C>, right: RightSiblingRef<'a, C>, } -impl<'a, C: Mergeable> MatchedPairRef<'a, C> { +impl<'a, C: Mergeable + fmt::Display> MatchedPairRef<'a, C> { /// Create a parent node by merging the 2 nodes in the pair. fn merge(&self) -> Node { Node { diff --git a/src/inclusion_proof.rs b/src/inclusion_proof.rs index 660a8de9..de73e036 100644 --- a/src/inclusion_proof.rs +++ b/src/inclusion_proof.rs @@ -141,10 +141,36 @@ impl InclusionProof { }) } - /// Verify that an inclusion proof matches a given root hash. + /// Verify that an inclusion proof matches a the root hash. pub fn verify(&self, root_hash: H256) -> Result<(), InclusionProofError> { - use curve25519_dalek_ng::ristretto::CompressedRistretto; + info!("Verifying inclusion proof.."); + // Is this cast safe? Yes because the tree height (which is the same as the + // length of the input) is also stored as a u8, and so there would never + // be more siblings than max(u8). + let tree_height = Height::from_y_coord(self.path_siblings.len() as u8); + + let hidden_leaf_node: Node = self.leaf_node.clone().convert(); + let constructed_path = self.path_siblings.construct_path(hidden_leaf_node)?; + + self.verify_merkle_path(root_hash, tree_height, &constructed_path)?; + self.verify_range_proofs(tree_height, &constructed_path)?; + + info!("Succesfully verified proof"); + + Ok(()) + } + + /// Verify that an inclusion proof matches the root hash, and show path info. + /// + /// The path information is printed to stdout, and written to a json file + /// in the given location. + pub fn verify_and_show_path_info( + self, + root_hash: H256, + dir: PathBuf, + mut file_name: OsString, + ) -> Result<(), InclusionProofError> { info!("Verifying inclusion proof.."); // Is this cast safe? Yes because the tree height (which is the same as the @@ -153,67 +179,99 @@ impl InclusionProof { let tree_height = Height::from_y_coord(self.path_siblings.len() as u8); let hidden_leaf_node: Node = self.leaf_node.clone().convert(); + let constructed_path = self.path_siblings.construct_path(hidden_leaf_node)?; - { - // Merkle tree path verification + self.verify_merkle_path(root_hash, tree_height, &constructed_path)?; + self.verify_range_proofs(tree_height, &constructed_path)?; - use bulletproofs::PedersenGens; - use curve25519_dalek_ng::scalar::Scalar; + info!("Succesfully verified proof"); - // PartialEq for HiddenNodeContent does not depend on the commitment so we can - // make this whatever we like - let dummy_commitment = - PedersenGens::default().commit(Scalar::from(0u8), Scalar::from(0u8)); + let path_str = self.path_siblings.path_to_str(&constructed_path); + info!("{}", path_str); - let root = Node { - content: HiddenNodeContent::new(dummy_commitment, root_hash), - coord: Coordinate { - x: 0, - y: tree_height.as_y_coord(), - }, - }; + self.path_siblings + .write_path_to_json(constructed_path, dir, file_name)?; - let constructed_root = self.path_siblings.construct_root_node(&hidden_leaf_node)?; + Ok(()) + } - if constructed_root != root { - return Err(InclusionProofError::RootMismatch); - } + /// Merkle tree path verification. + fn verify_merkle_path( + &self, + root_hash: H256, + tree_height: Height, + path_nodes: &Vec>, + ) -> Result<(), InclusionProofError> { + use bulletproofs::PedersenGens; + use curve25519_dalek_ng::scalar::Scalar; + + // PartialEq for HiddenNodeContent does not depend on the commitment so we can + // make this whatever we like + let dummy_commitment = PedersenGens::default().commit(Scalar::from(0u8), Scalar::from(0u8)); + + let root = Node { + content: HiddenNodeContent::new(dummy_commitment, root_hash), + coord: Coordinate { + x: 0, + y: tree_height.as_y_coord(), + }, + }; + + // this should never panic because the path construction checks for min length + let constructed_root = path_nodes.last().expect( + "[Bug in proof verification] there should have been at least 1 node in the path", + ); + + if constructed_root != &root { + Err(InclusionProofError::RootMismatch) + } else { + Ok(()) } + } + + /// Range proof verification. + fn verify_range_proofs( + &self, + tree_height: Height, + path_nodes: &Vec>, + ) -> Result<(), InclusionProofError> { + use curve25519_dalek_ng::ristretto::CompressedRistretto; + + let aggregation_index = self.aggregation_factor.apply_to(&tree_height) as usize; - { - // Range proof verification + let mut commitments_for_aggregated_proofs: Vec = path_nodes + .iter() + .map(|node| node.content.commitment.compress()) + .collect(); - let aggregation_index = self.aggregation_factor.apply_to(&tree_height) as usize; + let commitments_for_individual_proofs = + commitments_for_aggregated_proofs.split_off(aggregation_index); - let mut commitments_for_aggregated_proofs: Vec = self - .path_siblings - .construct_path(hidden_leaf_node)? + let mut at_least_one_checked = false; + + if let Some(proofs) = &self.individual_range_proofs { + commitments_for_individual_proofs .iter() - .map(|node| node.content.commitment.compress()) - .collect(); - - let commitments_for_individual_proofs = - commitments_for_aggregated_proofs.split_off(aggregation_index); - - if let Some(proofs) = &self.individual_range_proofs { - commitments_for_individual_proofs - .iter() - .zip(proofs.iter()) - .map(|(com, proof)| proof.verify(com, self.upper_bound_bit_length)) - .collect::, _>>()?; - } + .zip(proofs.iter()) + .map(|(com, proof)| proof.verify(com, self.upper_bound_bit_length)) + .collect::, _>>()?; - if let Some(proof) = &self.aggregated_range_proof { - proof.verify( - &commitments_for_aggregated_proofs, - self.upper_bound_bit_length, - )?; - } + at_least_one_checked = true; } - info!("Succesfully verified proof"); + if let Some(proof) = &self.aggregated_range_proof { + proof.verify( + &commitments_for_aggregated_proofs, + self.upper_bound_bit_length, + )?; + at_least_one_checked = true; + } - Ok(()) + if !at_least_one_checked { + Err(InclusionProofError::MissingRangeProof) + } else { + Ok(()) + } } /// Serialize the [InclusionProof] structure to a binary file. @@ -238,8 +296,12 @@ impl InclusionProof { info!("Serializing inclusion proof to path {:?}", path); match file_type { - InclusionProofFileType::Binary => read_write_utils::serialize_to_bin_file(&self, path.clone())?, - InclusionProofFileType::Json => read_write_utils::serialize_to_json_file(&self, path.clone())?, + InclusionProofFileType::Binary => { + read_write_utils::serialize_to_bin_file(&self, path.clone())? + } + InclusionProofFileType::Json => { + read_write_utils::serialize_to_json_file(&self, path.clone())? + } } Ok(path) @@ -261,7 +323,9 @@ impl InclusionProof { info!("Deserializing inclusion proof from file {:?}", file_path); match ext { - SERIALIZED_PROOF_EXTENSION => Ok(read_write_utils::deserialize_from_bin_file(file_path)?), + SERIALIZED_PROOF_EXTENSION => { + Ok(read_write_utils::deserialize_from_bin_file(file_path)?) + } "json" => Ok(read_write_utils::deserialize_from_json_file(file_path)?), _ => Err(InclusionProofError::UnsupportedFileType { ext: ext.into() }), } @@ -334,12 +398,16 @@ pub enum InclusionProofError { RootMismatch, #[error("Issues with range proof")] RangeProofError(#[from] RangeProofError), + #[error("No range proofs detected")] + MissingRangeProof, #[error("Error serializing/deserializing file")] SerdeError(#[from] crate::read_write_utils::ReadWriteError), #[error("The file type with extension {ext:?} is not supported")] UnsupportedFileType { ext: String }, #[error("Unable to find file extension for path {0:?}")] UnknownFileType(OsString), + #[error("Error writing path info to file")] + PathWriteError(#[from] crate::binary_tree::PathSiblingsWriteError), } #[derive(thiserror::Error, Debug)] From 67d1468e59755fc743ba17b7aaedef675724961b Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:13:56 +0100 Subject: [PATCH 4/9] Pipe print functionality to CLI --- src/cli.rs | 5 +++++ src/main.rs | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1a9b48dc..059b5a2d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -106,6 +106,11 @@ pub enum Command { /// Hash digest/bytes for the root node of the tree. #[arg(short, long, value_parser = H256::from_str, value_name = "BYTES")] root_hash: H256, + + /// Create a json file containing all the path information, and print + /// the same path information to stdout. + #[arg(long, short, action)] + show_path: bool, }, /// Verify the root node of a DAPOL tree. diff --git a/src/main.rs b/src/main.rs index d8efa368..74f73b43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,8 @@ use dapol::{ cli::{BuildKindCommand, Cli, Command}, initialize_machine_parallelism, utils::{activate_logging, Consume, IfNoneThen, LogOnErr, LogOnErrUnwrap}, - AggregationFactor, DapolConfig, DapolConfigBuilder, DapolTree, EntityIdsParser, InclusionProof, InclusionProofFileType, + AggregationFactor, DapolConfig, DapolConfigBuilder, DapolTree, EntityIdsParser, InclusionProof, + InclusionProofFileType, }; use patharg::InputArg; @@ -107,7 +108,9 @@ fn main() { .generate_inclusion_proof(&entity_id) .log_on_err_unwrap(); - proof.serialize(&entity_id, dir.clone(), InclusionProofFileType::Json).log_on_err_unwrap(); + proof + .serialize(&entity_id, dir.clone(), InclusionProofFileType::Json) + .log_on_err_unwrap(); } } @@ -168,21 +171,39 @@ fn main() { .generate_inclusion_proof_with(&entity_id, aggregation_factor.clone()) .log_on_err_unwrap(); - proof.serialize(&entity_id, dir.clone(), file_type.clone()).log_on_err_unwrap(); + proof + .serialize(&entity_id, dir.clone(), file_type.clone()) + .log_on_err_unwrap(); } } Command::VerifyInclusionProof { file_path, root_hash, + show_path, } => { - let proof = InclusionProof::deserialize( - file_path - .into_path() - .expect("Expected file path, not stdin"), - ) - .log_on_err_unwrap(); - - proof.verify(root_hash).log_on_err_unwrap(); + let file_path = file_path + .into_path() + .expect("Expected file path, not stdin"); + + let proof = InclusionProof::deserialize(file_path.clone()).log_on_err_unwrap(); + + if show_path { + proof + .verify_and_show_path_info( + root_hash, + file_path + .parent() + .expect("Expected file_path to have a parent") + .to_path_buf(), + file_path + .file_name() + .expect("Expected file_path to have a file name") + .to_os_string(), + ) + .log_on_err_unwrap(); + } else { + proof.verify(root_hash).log_on_err_unwrap(); + } } Command::VerifyRoot { root_pub, root_pvt } => { let public_root_data = DapolTree::deserialize_public_root_data( From 402761ff1e02fbac7a5e83e1598f7c5af523e6a2 Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:23:17 +0100 Subject: [PATCH 5/9] Misc small fixes --- src/binary_tree/node_content/full_node.rs | 7 ++++++- src/binary_tree/node_content/hidden_node.rs | 5 +++++ src/binary_tree/path_siblings.rs | 13 ++++++------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/binary_tree/node_content/full_node.rs b/src/binary_tree/node_content/full_node.rs index 9cde1427..81553299 100644 --- a/src/binary_tree/node_content/full_node.rs +++ b/src/binary_tree/node_content/full_node.rs @@ -169,7 +169,7 @@ impl Mergeable for FullNodeContent { let parent_blinding_factor = left_sibling.blinding_factor + right_sibling.blinding_factor; let parent_commitment = left_sibling.commitment + right_sibling.commitment; - // `hash = H(left.com | right.com | left.hash | right.hash` + // `hash = H(left.com | right.com | left.hash | right.hash)` let parent_hash = { let mut hasher = Hasher::new(); hasher.update(left_sibling.commitment.compress().as_bytes()); @@ -192,7 +192,12 @@ use std::fmt; impl fmt::Display for FullNodeContent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This allows us to get the same format for hash & commitment. + // If we just try convert the compressed RistrettoPoint to string we + // get a [u8; 32] array, while the H256 type formats to a nice hex + // string. let commitment_bytes = H256::from_slice(self.commitment.compress().as_bytes()); + write!( f, "(liability: {}, blinding factor: {:?}, hash: {:?}, commitment: {:?})", diff --git a/src/binary_tree/node_content/hidden_node.rs b/src/binary_tree/node_content/hidden_node.rs index 1b3b6cd9..8bce0f59 100644 --- a/src/binary_tree/node_content/hidden_node.rs +++ b/src/binary_tree/node_content/hidden_node.rs @@ -142,7 +142,12 @@ use std::fmt; impl fmt::Display for HiddenNodeContent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This allows us to get the same format for hash & commitment. + // If we just try convert the compressed RistrettoPoint to string we + // get a [u8; 32] array, while the H256 type formats to a nice hex + // string. let commitment_bytes = H256::from_slice(self.commitment.compress().as_bytes()); + write!(f, "(hash: {:x?}, commitment: {:?})", self.hash, commitment_bytes) } } diff --git a/src/binary_tree/path_siblings.rs b/src/binary_tree/path_siblings.rs index 0de074e9..be76b75d 100644 --- a/src/binary_tree/path_siblings.rs +++ b/src/binary_tree/path_siblings.rs @@ -27,8 +27,13 @@ use crate::{ }; use log::info; +use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, path::PathBuf}; +use std::{ + ffi::OsString, + fmt::{self, Debug}, + path::PathBuf, +}; // ------------------------------------------------------------------------------------------------- // Main struct and build functions. @@ -376,8 +381,6 @@ impl From> for PrettyNode { } } -use std::ffi::OsString; - impl PathSiblings { /// Write the path & sibling nodes to a json file. /// @@ -430,8 +433,6 @@ impl PathSiblings { } } -use std::fmt; - impl fmt::Display for PathSiblings { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str( @@ -447,8 +448,6 @@ impl fmt::Display for PathSiblings { // ------------------------------------------------------------------------------------------------- // Errors. -use serde::{Deserialize, Serialize}; - #[derive(thiserror::Error, Debug)] pub enum PathSiblingsBuildError { #[error("The builder must be given a padding node generator function before building")] From 74cf957359e5eef49bea39f2bdabe63985af925f Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:47:24 +0100 Subject: [PATCH 6/9] Fix bug with directory check --- src/binary_tree/path_siblings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binary_tree/path_siblings.rs b/src/binary_tree/path_siblings.rs index be76b75d..fdce3077 100644 --- a/src/binary_tree/path_siblings.rs +++ b/src/binary_tree/path_siblings.rs @@ -396,7 +396,7 @@ impl PathSiblings { dir: PathBuf, mut file_name: OsString, ) -> Result<(), PathSiblingsWriteError> { - if dir.is_dir() { + if !dir.is_dir() { return Err(PathSiblingsWriteError::InvalidDirectory( dir.into_os_string(), )); From e3d61cdf5d8e1b0e5c38afbbfa46d55e6875f6cb Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:51:22 +0100 Subject: [PATCH 7/9] Smol additional text --- src/binary_tree/path_siblings.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/binary_tree/path_siblings.rs b/src/binary_tree/path_siblings.rs index fdce3077..5dfa80c7 100644 --- a/src/binary_tree/path_siblings.rs +++ b/src/binary_tree/path_siblings.rs @@ -320,9 +320,10 @@ impl PathSiblings { let mut output_str = String::new(); + write!(&mut output_str, "\nPath information:\n") + .expect("[Bug in path to string conversion] Cannot write to string object"); write!(&mut output_str, "\nNodes:\n{}", path_nodes_str) .expect("[Bug in path to string conversion] Cannot write to string object"); - write!(&mut output_str, "\nSiblings:\n{}", path_siblings_str) .expect("[Bug in path to string conversion] Cannot write to string object"); From 8e08981c5a475b4c94e64eac495d0200250430c5 Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:52:22 +0100 Subject: [PATCH 8/9] Add suffix for json file --- src/binary_tree/path_siblings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binary_tree/path_siblings.rs b/src/binary_tree/path_siblings.rs index 5dfa80c7..73281e09 100644 --- a/src/binary_tree/path_siblings.rs +++ b/src/binary_tree/path_siblings.rs @@ -403,7 +403,7 @@ impl PathSiblings { )); } - file_name.push(".json"); + file_name.push("_path_information.json"); let file_path = dir.join(file_name); let siblings = self.0.into_iter().map(PrettyNode::from).collect(); From 7086800e9615f91ca005dbe621ab39b92e013b67 Mon Sep 17 00:00:00 2001 From: Stent Date: Tue, 28 May 2024 09:53:24 +0100 Subject: [PATCH 9/9] Update suffix --- src/binary_tree/path_siblings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binary_tree/path_siblings.rs b/src/binary_tree/path_siblings.rs index 73281e09..620d16a5 100644 --- a/src/binary_tree/path_siblings.rs +++ b/src/binary_tree/path_siblings.rs @@ -403,7 +403,7 @@ impl PathSiblings { )); } - file_name.push("_path_information.json"); + file_name.push(".path_information.json"); let file_path = dir.join(file_name); let siblings = self.0.into_iter().map(PrettyNode::from).collect();