From b1689c5b378ebe96931b09dc81eb703cf984d6d7 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 18 Mar 2025 19:34:01 +0100 Subject: [PATCH] Merkleproof verify circuit (#143) * merkletree: add keypath circuit * merkletree-circuit: implement proof of existence verification in-circuit * parametrize max_depth at the tree circuit * Constrain selectors in-circuit * implement merketree nonexistence proof circuit, and add edgecase tests * add non-existence proofs documentation in the mdbook, mv EMPTY->EMPTY_VALUE & NULL->EMPTY_HASH, dependency clean and public exposure methods * review comments, some extra polishing and add a test that expects wrong proofs to fail * Add circuit to check only merkleproofs-of-existence With this, the merkletree_circuit module offers two different circuits: - `MerkleProofCircuit`: allows to verify both proofs of existence and proofs non-existence with the same circuit. - `MerkleProofExistenceCircuit`: allows to verify proofs of existence only. In this way, if only proofs of existence are needed, `MerkleProofExistenceCircuit` should be used, which requires less amount of constraints than `MerkleProofCircuit`. * Code review --------- Co-authored-by: Ahmad --- book/src/merkletree.md | 4 + src/backends/plonky2/basetypes.rs | 4 +- src/backends/plonky2/mock_signed.rs | 4 +- src/backends/plonky2/primitives/merkletree.rs | 34 +- .../plonky2/primitives/merkletree_circuit.rs | 648 ++++++++++++++++++ src/backends/plonky2/primitives/mod.rs | 1 + src/middleware/basetypes.rs | 4 +- src/middleware/containers.rs | 6 +- src/middleware/mod.rs | 4 +- 9 files changed, 683 insertions(+), 26 deletions(-) create mode 100644 src/backends/plonky2/primitives/merkletree_circuit.rs diff --git a/book/src/merkletree.md b/book/src/merkletree.md index db3dca3..574d06f 100644 --- a/book/src/merkletree.md +++ b/book/src/merkletree.md @@ -137,6 +137,10 @@ Since leaf positions are deterministic based on the key, the same approach is us For the current use cases, we don't need to prove that the key exists but the value is different on that leaf, so we only use the option 1. +There are 2 cases to have into account when dealing with non-inclusion proofs: +- case i) the expected leaf does not exist. +- case ii) the expected leaf does exist in the tree, but it has a different `key`. + ## Encoding > TODO: how key-values, nodes, merkle-proofs, ... are encoded. diff --git a/src/backends/plonky2/basetypes.rs b/src/backends/plonky2/basetypes.rs index 1256875..393654e 100644 --- a/src/backends/plonky2/basetypes.rs +++ b/src/backends/plonky2/basetypes.rs @@ -30,9 +30,9 @@ pub type Proof = Plonky2Proof; pub const HASH_SIZE: usize = 4; pub const VALUE_SIZE: usize = 4; -pub const EMPTY: Value = Value([F::ZERO, F::ZERO, F::ZERO, F::ZERO]); +pub const EMPTY_VALUE: Value = Value([F::ZERO, F::ZERO, F::ZERO, F::ZERO]); pub const SELF_ID_HASH: Hash = Hash([F::ONE, F::ZERO, F::ZERO, F::ZERO]); -pub const NULL: Hash = Hash([F::ZERO, F::ZERO, F::ZERO, F::ZERO]); +pub const EMPTY_HASH: Hash = Hash([F::ZERO, F::ZERO, F::ZERO, F::ZERO]); #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] pub struct Value(pub [F; VALUE_SIZE]); diff --git a/src/backends/plonky2/mock_signed.rs b/src/backends/plonky2/mock_signed.rs index d86264c..f85e797 100644 --- a/src/backends/plonky2/mock_signed.rs +++ b/src/backends/plonky2/mock_signed.rs @@ -110,7 +110,7 @@ pub mod tests { use super::*; use crate::constants::MAX_DEPTH; use crate::frontend; - use crate::middleware::{self, F, NULL}; + use crate::middleware::{self, EMPTY_HASH, F}; #[test] fn test_mock_signed_0() -> Result<()> { @@ -137,7 +137,7 @@ pub mod tests { assert!(!bad_pod.verify()); let mut bad_pod = pod.clone(); - let bad_kv = (hash_str(KEY_SIGNER).into(), Value(PodId(NULL).0 .0)); + let bad_kv = (hash_str(KEY_SIGNER).into(), Value(PodId(EMPTY_HASH).0 .0)); let bad_kvs_mt = &bad_pod .kvs() .into_iter() diff --git a/src/backends/plonky2/primitives/merkletree.rs b/src/backends/plonky2/primitives/merkletree.rs index 7ec4636..84dcf4c 100644 --- a/src/backends/plonky2/primitives/merkletree.rs +++ b/src/backends/plonky2/primitives/merkletree.rs @@ -1,13 +1,16 @@ //! Module that implements the MerkleTree specified at //! https://0xparc.github.io/pod2/merkletree.html . use anyhow::{anyhow, Result}; -use plonky2::field::goldilocks_field::GoldilocksField; +use plonky2::field::types::Field; use std::collections::HashMap; use std::fmt; use std::iter::IntoIterator; use crate::backends::counter; -use crate::backends::plonky2::basetypes::{hash_fields, Hash, Value, F, NULL}; +use crate::backends::plonky2::basetypes::{hash_fields, Hash, Value, EMPTY_HASH, F}; + +// mod merkletree_circuit; +pub use super::merkletree_circuit::*; /// Implements the MerkleTree specified at /// https://0xparc.github.io/pod2/merkletree.html @@ -178,8 +181,8 @@ impl MerkleTree { /// mitigate fake proofs. pub fn kv_hash(key: &Value, value: Option) -> Hash { value - .map(|v| hash_fields(&[key.0.to_vec(), v.0.to_vec(), vec![GoldilocksField(1)]].concat())) - .unwrap_or(Hash([GoldilocksField(0); 4])) + .map(|v| hash_fields(&[key.0.to_vec(), v.0.to_vec(), vec![F::ONE]].concat())) + .unwrap_or(EMPTY_HASH) } impl<'a> IntoIterator for &'a MerkleTree { @@ -209,10 +212,10 @@ pub struct MerkleProof { // note: currently we don't use the `_existence` field, we would use if we merge the methods // `verify` and `verify_nonexistence` into a single one #[allow(unused)] - existence: bool, - siblings: Vec, + pub(crate) existence: bool, + pub(crate) siblings: Vec, // other_leaf is used for non-existence proofs - other_leaf: Option<(Value, Value)>, + pub(crate) other_leaf: Option<(Value, Value)>, } impl fmt::Display for MerkleProof { @@ -244,11 +247,12 @@ impl MerkleProof { let path = keypath(max_depth, *key)?; let mut h = kv_hash(key, value); for (i, sibling) in self.siblings.iter().enumerate().rev() { - let input: Vec = if path[i] { + let mut input: Vec = if path[i] { [sibling.0, h.0].concat() } else { [h.0, sibling.0].concat() }; + input.push(F::TWO); h = hash_fields(&input); } Ok(h) @@ -302,14 +306,14 @@ impl Node { } fn compute_hash(&mut self) -> Hash { match self { - Self::None => NULL, + Self::None => EMPTY_HASH, Self::Leaf(l) => l.compute_hash(), Self::Intermediate(n) => n.compute_hash(), } } fn hash(&self) -> Hash { match self { - Self::None => NULL, + Self::None => EMPTY_HASH, Self::Leaf(l) => l.hash(), Self::Intermediate(n) => n.hash(), } @@ -360,7 +364,7 @@ impl Node { } // adds the leaf at the tree from the current node (self), without computing any hash - fn add_leaf(&mut self, lvl: usize, max_depth: usize, leaf: Leaf) -> Result<()> { + pub(crate) fn add_leaf(&mut self, lvl: usize, max_depth: usize, leaf: Leaf) -> Result<()> { counter::count_tree_insert(); if lvl >= max_depth { @@ -472,12 +476,12 @@ impl Intermediate { } fn compute_hash(&mut self) -> Hash { if self.left.clone().is_empty() && self.right.clone().is_empty() { - self.hash = Some(NULL); - return NULL; + self.hash = Some(EMPTY_HASH); + return EMPTY_HASH; } let l_hash = self.left.compute_hash(); let r_hash = self.right.compute_hash(); - let input: Vec = [l_hash.0, r_hash.0].concat(); + let input: Vec = [l_hash.0.to_vec(), r_hash.0.to_vec(), vec![F::TWO]].concat(); let h = hash_fields(&input); self.hash = Some(h); h @@ -520,7 +524,7 @@ impl Leaf { // max-depth? ie, what happens when two keys share the same path for more bits // than the max_depth? /// returns the path of the given key -fn keypath(max_depth: usize, k: Value) -> Result> { +pub(crate) fn keypath(max_depth: usize, k: Value) -> Result> { let bytes = k.to_bytes(); if max_depth > 8 * bytes.len() { // note that our current keys are of Value type, which are 4 Goldilocks diff --git a/src/backends/plonky2/primitives/merkletree_circuit.rs b/src/backends/plonky2/primitives/merkletree_circuit.rs new file mode 100644 index 0000000..410ec48 --- /dev/null +++ b/src/backends/plonky2/primitives/merkletree_circuit.rs @@ -0,0 +1,648 @@ +//! Circuits compatible with the merkletree.rs implementation. This module +//! offers two different circuits: +//! +//! - `MerkleProofCircuit`: allows to verify both proofs of existence and proofs +//! non-existence with the same circuit. +//! - `MerkleProofExistenceCircuit`: allows to verify proofs of existence only. +//! +//! If only proofs of existence are needed, use `MerkleProofExistenceCircuit`, +//! which requires less amount of constraints than `MerkleProofCircuit`. +//! +use anyhow::Result; +use plonky2::{ + field::types::Field, + hash::{ + hash_types::{HashOut, HashOutTarget}, + poseidon::PoseidonHash, + }, + iop::{ + target::{BoolTarget, Target}, + witness::{PartialWitness, WitnessWrite}, + }, + plonk::circuit_builder::CircuitBuilder, +}; + +use crate::backends::plonky2::basetypes::{Hash, Value, D, EMPTY_HASH, EMPTY_VALUE, F, VALUE_SIZE}; +use crate::backends::plonky2::primitives::merkletree::MerkleProof; + +/// `MerkleProofCircuit` allows to verify both proofs of existence and proofs +/// non-existence with the same circuit. +/// If only proofs of existence are needed, use `MerkleProofExistenceCircuit`, +/// which requires less amount of constraints. +pub struct MerkleProofCircuit { + root: HashOutTarget, + key: Vec, + value: Vec, + existence: BoolTarget, + siblings: Vec, + case_ii_selector: BoolTarget, // for case ii) + other_key: Vec, + other_value: Vec, +} + +impl MerkleProofCircuit { + /// creates the targets and defines the logic of the circuit + pub fn add_targets(builder: &mut CircuitBuilder) -> Result { + // create the targets + let key = builder.add_virtual_targets(VALUE_SIZE); + let value = builder.add_virtual_targets(VALUE_SIZE); + // from proof struct: + let existence = builder.add_virtual_bool_target_safe(); + // siblings are padded till MAX_DEPTH length + let siblings = builder.add_virtual_hashes(MAX_DEPTH); + + let case_ii_selector = builder.add_virtual_bool_target_safe(); + let other_key = builder.add_virtual_targets(VALUE_SIZE); + let other_value = builder.add_virtual_targets(VALUE_SIZE); + + // We have 3 cases for when computing the Leaf's hash: + // - existence: leaf contains the given key & value + // - non-existence: + // - case i) expected leaf does not exist + // - case ii) expected leaf does exist but it has a different key + // + // The following table expresses the options with their in-circuit + // selectors: + // | existence | case_ii | leaf_hash | + // | ----------- | --------- | ---------------------------- | + // | 1 | 0 | H(key, value, 1) | + // | 0 | 0 | EMPTY_HASH | + // | 0 | 1 | H(other_key, other_value, 1) | + // | 1 | 1 | invalid combination | + + // First, ensure that both existence & case_ii are not true at the same + // time: + // 1. sum = existence + case_ii_selector + let sum = builder.add(existence.target, case_ii_selector.target); + // 2. sum * (sum-1) == 0 + builder.assert_bool(BoolTarget::new_unsafe(sum)); + + // define the case_i_selector as true when both existence and + // case_ii_selector are false: + let not_existence = builder.not(existence); + let not_case_ii_selector = builder.not(case_ii_selector); + let case_i_selector = builder.and(not_existence, not_case_ii_selector); + + // use (key,value) or (other_key, other_value) depending if it's a proof + // of existence or of non-existence, ie: + // k = key * existence + other_key * (1-existence) + // v = value * existence + other_value * (1-existence) + let k: Vec = (0..4) + .map(|j| builder.select(existence, key[j], other_key[j])) + .collect(); + let v: Vec = (0..4) + .map(|j| builder.select(existence, value[j], other_value[j])) + .collect(); + + // get leaf's hash for the selected k & v + let h = kv_hash_target(builder, &k, &v); + + // if we're in the case i), use leaf_hash=EMPTY_HASH, else use the + // previously computed hash h. + let empty_hash = builder.constant_hash(HashOut::from(EMPTY_HASH.0)); + let leaf_hash = HashOutTarget::from_vec( + (0..4) + .map(|j| builder.select(case_i_selector, empty_hash.elements[j], h.elements[j])) + .collect(), + ); + + // get key's path + let path = keypath_target::(builder, &key); + + // compute the root for the given siblings and the computed leaf_hash + // (this is for the three cases (existence, non-existence case i, and + // non-existence case ii). + // This root will be assigned in the `set_targets` method, and it is a + // public input. + let root = compute_root_from_leaf::(builder, &path, &leaf_hash, &siblings)?; + + Ok(Self { + existence, + root, + siblings, + key, + value, + case_ii_selector, + other_key, + other_value, + }) + } + + /// assigns the given values to the targets + pub fn set_targets( + &self, + pw: &mut PartialWitness, + existence: bool, + root: Hash, + proof: MerkleProof, + key: Value, + value: Value, + ) -> Result<()> { + pw.set_hash_target(self.root, HashOut::from_vec(root.0.to_vec()))?; + pw.set_target_arr(&self.key, &key.0)?; + pw.set_target_arr(&self.value, &value.0)?; + pw.set_bool_target(self.existence, existence)?; + + // pad siblings with zeros to length MAX_DEPTH + let mut siblings = proof.siblings.clone(); + siblings.resize(MAX_DEPTH, EMPTY_HASH); + assert_eq!(self.siblings.len(), siblings.len()); + + for (i, sibling) in siblings.iter().enumerate() { + pw.set_hash_target(self.siblings[i], HashOut::from_vec(sibling.0.to_vec()))?; + } + + match proof.other_leaf { + Some((k, v)) if !existence => { + // non-existence case ii) expected leaf does exist but it has a different key + pw.set_bool_target(self.case_ii_selector, true)?; + pw.set_target_arr(&self.other_key, &k.0)?; + pw.set_target_arr(&self.other_value, &v.0)?; + } + _ => { + // existence & non-existence case i) expected leaf does not exist + pw.set_bool_target(self.case_ii_selector, false)?; + pw.set_target_arr(&self.other_key, &EMPTY_VALUE.0)?; + pw.set_target_arr(&self.other_value, &EMPTY_VALUE.0)?; + } + } + + Ok(()) + } +} + +/// `MerkleProofExistenceCircuit` allows to verify proofs of existence only. If +/// proofs of non-existence are needed, use `MerkleProofCircuit`. +pub struct MerkleProofExistenceCircuit { + root: HashOutTarget, + key: Vec, + value: Vec, + siblings: Vec, +} + +impl MerkleProofExistenceCircuit { + /// creates the targets and defines the logic of the circuit + pub fn add_targets(builder: &mut CircuitBuilder) -> Result { + // create the targets + let key = builder.add_virtual_targets(VALUE_SIZE); + let value = builder.add_virtual_targets(VALUE_SIZE); + // siblings are padded till MAX_DEPTH length + let siblings = builder.add_virtual_hashes(MAX_DEPTH); + + // get leaf's hash for the selected k & v + let leaf_hash = kv_hash_target(builder, &key, &value); + + // get key's path + let path = keypath_target::(builder, &key); + + // compute the root for the given siblings and the computed leaf_hash. + // This root will be assigned in the `set_targets` method, and it is a + // public input. + let root = compute_root_from_leaf::(builder, &path, &leaf_hash, &siblings)?; + + Ok(Self { + root, + siblings, + key, + value, + }) + } + + /// assigns the given values to the targets + pub fn set_targets( + &self, + pw: &mut PartialWitness, + root: Hash, + proof: MerkleProof, + key: Value, + value: Value, + ) -> Result<()> { + pw.set_hash_target(self.root, HashOut::from_vec(root.0.to_vec()))?; + pw.set_target_arr(&self.key, &key.0)?; + pw.set_target_arr(&self.value, &value.0)?; + + // pad siblings with zeros to length MAX_DEPTH + let mut siblings = proof.siblings.clone(); + siblings.resize(MAX_DEPTH, EMPTY_HASH); + assert_eq!(self.siblings.len(), siblings.len()); + + for (i, sibling) in siblings.iter().enumerate() { + pw.set_hash_target(self.siblings[i], HashOut::from_vec(sibling.0.to_vec()))?; + } + + Ok(()) + } +} + +fn compute_root_from_leaf( + builder: &mut CircuitBuilder, + path: &Vec, + leaf_hash: &HashOutTarget, + siblings: &Vec, +) -> Result { + assert_eq!(siblings.len(), MAX_DEPTH); + // Convenience constants + let zero = builder.zero(); + let one = builder.one(); + let two = builder.two(); + + // Generate/constrain sibling selectors + let sibling_selectors = siblings + .iter() + .rev() + .scan(zero, |cur_selector, sibling| { + let sibling_is_empty = sibling.elements.iter().fold(builder._true(), |acc, x| { + let x_is_zero = builder.is_equal(*x, zero); + builder.and(acc, x_is_zero) + }); + // If there is a sibling, the selector is true, else retain the + // current selector + *cur_selector = builder.select(sibling_is_empty, *cur_selector, one); + Some(BoolTarget::new_unsafe(*cur_selector)) + }) + .collect::>() + .into_iter() + .rev() + .collect::>(); + + let mut h = leaf_hash.clone(); + for (i, (sibling, selector)) in std::iter::zip(siblings, &sibling_selectors) + .enumerate() + .rev() + { + // to compute the hash, we want to do the following 3 steps: + // Let s := path[i], then + // input_1 = sibling * s + h * (1-s) = select(s, sibling, h) + // input_2 = sibling * (1-s) + h * s = select(s, h, sibling) + // new_h = hash([input_1, input_2]) + // TODO explore if to group multiple muls in a single gate + let input_1: Vec = (0..4) + .map(|j| builder.select(path[i], sibling.elements[j], h.elements[j])) + .collect(); + let input_2: Vec = (0..4) + .map(|j| builder.select(path[i], h.elements[j], sibling.elements[j])) + .collect(); + let new_h = + builder.hash_n_to_hash_no_pad::([input_1, input_2, vec![two]].concat()); + + let h_targ: Vec = (0..4) + .map(|j| builder.select(*selector, new_h.elements[j], h.elements[j])) + .collect(); + h = HashOutTarget::from_vec(h_targ); + } + Ok(h) +} + +// Note: this logic is in its own method for easy of reusability but +// specially to be able to test it isolated. +fn keypath_target( + builder: &mut CircuitBuilder, + key: &Vec, +) -> Vec { + assert_eq!(key.len(), VALUE_SIZE); + + let n_complete_field_elems: usize = MAX_DEPTH / F::BITS; + let n_extra_bits: usize = MAX_DEPTH - n_complete_field_elems * F::BITS; + + let path: Vec = key + .iter() + .take(n_complete_field_elems) + .flat_map(|e| builder.split_le(*e, F::BITS)) + .collect(); + + let extra_bits = if n_extra_bits > 0 { + let extra_bits: Vec = builder.split_le(key[n_complete_field_elems], F::BITS); + extra_bits[..n_extra_bits].to_vec() + // Note: ideally we would do: + // let extra_bits = builder.split_le(key[n_complete_field_elems], n_extra_bits); + // and directly get the extra_bits, but the `split_le` method + // returns the wrong bits, so currently we get the entire array of + // bits and crop it at the desired n_extra_bits amount. + } else { + vec![] + }; + [path, extra_bits].concat() +} + +fn kv_hash_target( + builder: &mut CircuitBuilder, + key: &Vec, + value: &Vec, +) -> HashOutTarget { + let inputs: Vec = [key.clone(), value.clone(), vec![builder.one()]].concat(); + builder.hash_n_to_hash_no_pad::(inputs) +} + +#[cfg(test)] +pub mod tests { + use plonky2::plonk::{circuit_builder::CircuitBuilder, circuit_data::CircuitConfig}; + use std::collections::HashMap; + + use super::*; + use crate::backends::plonky2::basetypes::hash_value; + use crate::backends::plonky2::basetypes::C; + use crate::backends::plonky2::primitives::merkletree::*; + + #[test] + fn test_keypath() -> Result<()> { + test_keypath_opt::<10>()?; + test_keypath_opt::<16>()?; + test_keypath_opt::<32>()?; + test_keypath_opt::<40>()?; + test_keypath_opt::<64>()?; + test_keypath_opt::<128>()?; + test_keypath_opt::<130>()?; + test_keypath_opt::<250>()?; + test_keypath_opt::<256>()?; + Ok(()) + } + + fn test_keypath_opt() -> Result<()> { + for i in 0..5 { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(config); + let mut pw = PartialWitness::::new(); + + let key = Value::from(hash_value(&Value::from(i))); + let expected_path = keypath(MD, key)?; + + // small circuit logic to check + // expected_path_targ==keypath_target(key_targ) + let expected_path_targ: Vec = (0..MD) + .map(|_| builder.add_virtual_bool_target_safe()) + .collect(); + let key_targ = builder.add_virtual_targets(VALUE_SIZE); + let computed_path_targ = keypath_target::(&mut builder, &key_targ); + for i in 0..MD { + builder.connect(computed_path_targ[i].target, expected_path_targ[i].target); + } + + // assign the input values to the targets + pw.set_target_arr(&key_targ, &key.0)?; + for i in 0..MD { + pw.set_bool_target(expected_path_targ[i], expected_path[i])?; + } + + // generate & verify proof + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof)?; + } + Ok(()) + } + + #[test] + fn test_kv_hash() -> Result<()> { + for i in 0..10 { + let key = Value::from(hash_value(&Value::from(i))); + let value = Value::from(1000 + i); + let h = kv_hash(&key, Some(value)); + + // circuit + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(config); + let mut pw = PartialWitness::::new(); + + let h_targ = builder.add_virtual_hash(); + let key_targ = builder.add_virtual_targets(VALUE_SIZE); + let value_targ = builder.add_virtual_targets(VALUE_SIZE); + + let computed_h = kv_hash_target(&mut builder, &key_targ, &value_targ); + builder.connect_hashes(computed_h, h_targ); + + // assign the input values to the targets + pw.set_target_arr(&key_targ, &key.0)?; + pw.set_target_arr(&value_targ, &value.0)?; + pw.set_hash_target(h_targ, HashOut::from_vec(h.0.to_vec()))?; + + // generate & verify proof + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof)?; + } + Ok(()) + } + + #[test] + fn test_merkleproof_verify_existence() -> Result<()> { + test_merkleproof_verify_opt::<10>(true)?; + test_merkleproof_verify_opt::<16>(true)?; + test_merkleproof_verify_opt::<32>(true)?; + test_merkleproof_verify_opt::<40>(true)?; + test_merkleproof_verify_opt::<64>(true)?; + test_merkleproof_verify_opt::<128>(true)?; + test_merkleproof_verify_opt::<130>(true)?; + test_merkleproof_verify_opt::<250>(true)?; + test_merkleproof_verify_opt::<256>(true)?; + Ok(()) + } + + #[test] + fn test_merkleproof_verify_nonexistence() -> Result<()> { + test_merkleproof_verify_opt::<10>(false)?; + test_merkleproof_verify_opt::<16>(false)?; + test_merkleproof_verify_opt::<32>(false)?; + test_merkleproof_verify_opt::<40>(false)?; + test_merkleproof_verify_opt::<64>(false)?; + test_merkleproof_verify_opt::<128>(false)?; + test_merkleproof_verify_opt::<130>(false)?; + test_merkleproof_verify_opt::<250>(false)?; + test_merkleproof_verify_opt::<256>(false)?; + Ok(()) + } + + // test logic to be reused both by the existence & nonexistence tests + fn test_merkleproof_verify_opt(existence: bool) -> Result<()> { + let mut kvs: HashMap = HashMap::new(); + for i in 0..10 { + kvs.insert(Value::from(hash_value(&Value::from(i))), Value::from(i)); + } + + let tree = MerkleTree::new(MD, &kvs)?; + + let (key, value, proof) = if existence { + let key = Value::from(hash_value(&Value::from(5))); + let (value, proof) = tree.prove(&key)?; + assert_eq!(value, Value::from(5)); + (key, value, proof) + } else { + let key = Value::from(hash_value(&Value::from(200))); + (key, EMPTY_VALUE, tree.prove_nonexistence(&key)?) + }; + assert_eq!(proof.existence, existence); + + if existence { + MerkleTree::verify(MD, tree.root(), &proof, &key, &value)?; + } else { + MerkleTree::verify_nonexistence(MD, tree.root(), &proof, &key)?; + } + + // circuit + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(config); + let mut pw = PartialWitness::::new(); + + let targets = MerkleProofCircuit::::add_targets(&mut builder)?; + targets.set_targets(&mut pw, existence, tree.root(), proof, key, value)?; + + // generate & verify proof + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof)?; + + Ok(()) + } + + #[test] + fn test_merkleproof_only_existence_verify() -> Result<()> { + test_merkleproof_only_existence_verify_opt::<10>()?; + test_merkleproof_only_existence_verify_opt::<16>()?; + test_merkleproof_only_existence_verify_opt::<32>()?; + test_merkleproof_only_existence_verify_opt::<40>()?; + test_merkleproof_only_existence_verify_opt::<64>()?; + test_merkleproof_only_existence_verify_opt::<128>()?; + test_merkleproof_only_existence_verify_opt::<130>()?; + test_merkleproof_only_existence_verify_opt::<250>()?; + test_merkleproof_only_existence_verify_opt::<256>()?; + Ok(()) + } + + fn test_merkleproof_only_existence_verify_opt() -> Result<()> { + let mut kvs: HashMap = HashMap::new(); + for i in 0..10 { + kvs.insert(Value::from(hash_value(&Value::from(i))), Value::from(i)); + } + + let tree = MerkleTree::new(MD, &kvs)?; + + let key = Value::from(hash_value(&Value::from(5))); + let (value, proof) = tree.prove(&key)?; + assert_eq!(value, Value::from(5)); + assert_eq!(proof.existence, true); + + MerkleTree::verify(MD, tree.root(), &proof, &key, &value)?; + + // circuit + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(config); + let mut pw = PartialWitness::::new(); + + let targets = MerkleProofExistenceCircuit::::add_targets(&mut builder)?; + targets.set_targets(&mut pw, tree.root(), proof, key, value)?; + + // generate & verify proof + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof)?; + + Ok(()) + } + + #[test] + fn test_merkletree_edgecases() -> Result<()> { + // fill the tree as in https://0xparc.github.io/pod2/merkletree.html#example-3 + // + // root + // / \ + // () () + // / \ / + // 0 2 () + // \ + // () + // /\ + // 5 13 + + let mut kvs = HashMap::new(); + kvs.insert(Value::from(0), Value::from(1000)); + kvs.insert(Value::from(2), Value::from(1002)); + kvs.insert(Value::from(5), Value::from(1005)); + kvs.insert(Value::from(13), Value::from(1013)); + + const MD: usize = 5; + let tree = MerkleTree::new(MD, &kvs)?; + // existence + test_merkletree_edgecase_opt::(&tree, Value::from(5))?; + // non-existence case i) expected leaf does not exist + test_merkletree_edgecase_opt::(&tree, Value::from(1))?; + // non-existence case ii) expected leaf does exist but it has a different 'key' + test_merkletree_edgecase_opt::(&tree, Value::from(21))?; + + Ok(()) + } + + fn test_merkletree_edgecase_opt(tree: &MerkleTree, key: Value) -> Result<()> { + let contains = tree.contains(&key)?; + // generate merkleproof + let (value, proof) = if contains { + tree.prove(&key)? + } else { + let proof = tree.prove_nonexistence(&key)?; + (EMPTY_VALUE, proof) + }; + + assert_eq!(proof.existence, contains); + + // verify the proof (non circuit) + if proof.existence { + MerkleTree::verify(MD, tree.root(), &proof, &key, &value)?; + } else { + MerkleTree::verify_nonexistence(MD, tree.root(), &proof, &key)?; + } + + // circuit + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(config); + let mut pw = PartialWitness::::new(); + + let targets = MerkleProofCircuit::::add_targets(&mut builder)?; + targets.set_targets(&mut pw, proof.existence, tree.root(), proof, key, value)?; + + // generate & verify proof + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof)?; + + Ok(()) + } + + #[test] + fn test_wrong_witness() -> Result<()> { + let mut kvs: HashMap = HashMap::new(); + for i in 0..10 { + kvs.insert(Value::from(i), Value::from(i)); + } + const MD: usize = 16; + let tree = MerkleTree::new(MD, &kvs)?; + + let key = Value::from(3); + let (value, proof) = tree.prove(&key)?; + + // build another tree with an extra key-value, so that it has a + // different root + kvs.insert(Value::from(100), Value::from(100)); + let tree2 = MerkleTree::new(MD, &kvs)?; + + MerkleTree::verify(MD, tree.root(), &proof, &key, &value)?; + assert_eq!( + MerkleTree::verify(MD, tree2.root(), &proof, &key, &value) + .unwrap_err() + .to_string(), + "proof of inclusion does not verify" + ); + + // circuit + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(config); + let mut pw = PartialWitness::::new(); + + let targets = MerkleProofCircuit::::add_targets(&mut builder)?; + targets.set_targets(&mut pw, true, tree2.root(), proof, key, value)?; + + // generate proof, expecting it to fail (since we're using the wrong + // root) + let data = builder.build::(); + assert!(data.prove(pw).is_err()); + + Ok(()) + } +} diff --git a/src/backends/plonky2/primitives/mod.rs b/src/backends/plonky2/primitives/mod.rs index 7dcd478..bddbc4d 100644 --- a/src/backends/plonky2/primitives/mod.rs +++ b/src/backends/plonky2/primitives/mod.rs @@ -1,2 +1,3 @@ pub mod merkletree; +mod merkletree_circuit; pub mod signature; diff --git a/src/middleware/basetypes.rs b/src/middleware/basetypes.rs index 3f893e8..f9960d0 100644 --- a/src/middleware/basetypes.rs +++ b/src/middleware/basetypes.rs @@ -35,6 +35,6 @@ /// then the Value, Hash and F types would come from the plonky3 backend. #[cfg(feature = "backend_plonky2")] pub use crate::backends::plonky2::basetypes::{ - hash_fields, hash_str, hash_value, Hash, Value, EMPTY, F, HASH_SIZE, NULL, SELF_ID_HASH, - VALUE_SIZE, + hash_fields, hash_str, hash_value, Hash, Value, EMPTY_HASH, EMPTY_VALUE, F, HASH_SIZE, + SELF_ID_HASH, VALUE_SIZE, }; diff --git a/src/middleware/containers.rs b/src/middleware/containers.rs index 193a7e9..11b58b0 100644 --- a/src/middleware/containers.rs +++ b/src/middleware/containers.rs @@ -8,7 +8,7 @@ use crate::constants::MAX_DEPTH; #[cfg(feature = "backend_plonky2")] use crate::backends::plonky2::primitives::merkletree::{Iter as TreeIter, MerkleProof, MerkleTree}; -use super::basetypes::{hash_value, Hash, Value, EMPTY}; +use super::basetypes::{hash_value, Hash, Value, EMPTY_VALUE}; /// Dictionary: the user original keys and values are hashed to be used in the leaf. /// leaf.key=hash(original_key) @@ -78,7 +78,7 @@ impl Set { .iter() .map(|e| { let h = hash_value(e); - (Value::from(h), EMPTY) + (Value::from(h), EMPTY_VALUE) }) .collect(); Ok(Self { @@ -99,7 +99,7 @@ impl Set { self.mt.prove_nonexistence(value) } pub fn verify(root: Hash, proof: &MerkleProof, value: &Value) -> Result<()> { - MerkleTree::verify(MAX_DEPTH, root, proof, value, &EMPTY) + MerkleTree::verify(MAX_DEPTH, root, proof, value, &EMPTY_VALUE) } pub fn verify_nonexistence(root: Hash, proof: &MerkleProof, value: &Value) -> Result<()> { MerkleTree::verify_nonexistence(MAX_DEPTH, root, proof, value) diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 5ae6107..af0c2cb 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -23,7 +23,7 @@ impl fmt::Display for PodId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if *self == SELF { write!(f, "self") - } else if self.0 == NULL { + } else if self.0 == EMPTY_HASH { write!(f, "null") } else { write!(f, "{}", self.0) @@ -185,7 +185,7 @@ impl Pod for NonePod { true } fn id(&self) -> PodId { - PodId(NULL) + PodId(EMPTY_HASH) } fn pub_statements(&self) -> Vec { Vec::new()