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 <root@ahmadafuni.com>
This commit is contained in:
arnaucube 2025-03-18 19:34:01 +01:00 committed by GitHub
parent abce0af675
commit b1689c5b37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 683 additions and 26 deletions

View file

@ -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<Value>) -> 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<Hash>,
pub(crate) existence: bool,
pub(crate) siblings: Vec<Hash>,
// 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<F> = if path[i] {
let mut input: Vec<F> = 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<F> = [l_hash.0, r_hash.0].concat();
let input: Vec<F> = [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<Vec<bool>> {
pub(crate) fn keypath(max_depth: usize, k: Value) -> Result<Vec<bool>> {
let bytes = k.to_bytes();
if max_depth > 8 * bytes.len() {
// note that our current keys are of Value type, which are 4 Goldilocks