Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Theodore/proofnarrowing #34

Merged
merged 5 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nmt-rs"
version = "0.2.1"
version = "0.2.3"
edition = "2021"
description = "A namespaced merkle tree compatible with Celestia"
license = "MIT OR Apache-2.0"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This code has not been audited, and may contain critical vulnerabilities. Do not

- [x] Verify namespace range proofs

- [x] Narrow namespace range proofs: supply part of the range to generate a proof for the remaining sub-range

## License

Expand Down
131 changes: 131 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ pub enum RangeProofType {
#[cfg(test)]
mod tests {
use crate::maybestd::vec::Vec;
use crate::simple_merkle::error::RangeProofError;
use crate::NamespaceMerkleHasher;
use crate::{
namespaced_hash::{NamespaceId, NamespacedSha2Hasher},
Expand Down Expand Up @@ -458,6 +459,20 @@ mod tests {
tree
}

fn tree_from_one_namespace<const NS_ID_SIZE: usize>(
leaves: u64,
namespace: u64,
) -> DefaultNmt<NS_ID_SIZE> {
let mut tree = DefaultNmt::new();
let namespace = ns_id_from_u64(namespace);
for i in 0..leaves {
let data = format!("leaf_{i}");
tree.push_leaf(data.as_bytes(), namespace)
.expect("Failed to push the leaf");
}
tree
}

/// Builds a tree with N leaves
fn tree_with_n_leaves<const NS_ID_SIZE: usize>(n: usize) -> DefaultNmt<NS_ID_SIZE> {
tree_from_namespace_ids((0..n as u64).collect::<Vec<_>>())
Expand Down Expand Up @@ -587,6 +602,122 @@ mod tests {
}
}

fn test_range_proof_narrowing_within_namespace<const NS_ID_SIZE: usize>(n: usize) {
let ns_id = 4;
let mut tree = tree_from_one_namespace::<NS_ID_SIZE>(n as u64, ns_id); // since there's a single namespace, the actual ID shouldn't matter
let root = tree.root();
for i in 1..=n {
for j in 0..=i {
let proof_nmt = NamespaceProof::PresenceProof {
proof: tree.build_range_proof(j..i),
ignore_max_ns: tree.ignore_max_ns,
};
for k in (j + 1)..=i {
for l in j..=k {
let left_leaf_datas: Vec<_> =
tree.leaves()[j..l].iter().map(|l| l.data()).collect();
let right_leaf_datas: Vec<_> =
tree.leaves()[k..i].iter().map(|l| l.data()).collect();
let narrowed_proof_nmt = proof_nmt.narrow_range(
&left_leaf_datas,
&right_leaf_datas,
ns_id_from_u64(ns_id),
);
if k == l {
// Cannot prove the empty range!
assert!(narrowed_proof_nmt.is_err());
assert_eq!(
narrowed_proof_nmt.unwrap_err(),
RangeProofError::NoLeavesProvided
);
continue;
} else {
assert!(narrowed_proof_nmt.is_ok());
}
let narrowed_proof = narrowed_proof_nmt.unwrap();
let new_leaves: Vec<_> = tree.leaves()[l..k]
.iter()
.map(|l| l.hash().clone())
.collect();
tree.check_range_proof(&root, &new_leaves, narrowed_proof.siblings(), l)
.unwrap();
}
}
}
}
test_min_and_max_ns_against(&mut tree)
}

#[test]
fn test_range_proof_narrowing_nmt() {
for x in 0..20 {
test_range_proof_narrowing_within_namespace::<8>(x);
test_range_proof_narrowing_within_namespace::<17>(x);
test_range_proof_narrowing_within_namespace::<24>(x);
test_range_proof_narrowing_within_namespace::<CELESTIA_NS_ID_SIZE>(x);
test_range_proof_narrowing_within_namespace::<32>(x);
}
}

/// Builds a tree with n leaves, and then creates and checks proofs of all valid
/// ranges, and attempts to narrow every proof and re-check it for the narrowed range
fn test_range_proof_narrowing_with_n_leaves<const NS_ID_SIZE: usize>(n: usize) {
let mut tree = tree_with_n_leaves::<NS_ID_SIZE>(n);
let root = tree.root();
for i in 1..=n {
for j in 0..=i {
let proof = tree.build_range_proof(j..i);
for k in (j + 1)..=i {
for l in j..=k {
let left_hashes: Vec<_> = tree.leaves()[j..l]
.iter()
.map(|l| l.hash().clone())
.collect();
let right_hashes: Vec<_> = tree.leaves()[k..i]
.iter()
.map(|l| l.hash().clone())
.collect();
let narrowed_proof_simple = proof.narrow_range_with_hasher(
&left_hashes,
&right_hashes,
NamespacedSha2Hasher::with_ignore_max_ns(tree.ignore_max_ns),
);
if k == l {
// Cannot prove the empty range!
assert!(narrowed_proof_simple.is_err());
assert_eq!(
narrowed_proof_simple.unwrap_err(),
RangeProofError::NoLeavesProvided
);
continue;
} else {
assert!(narrowed_proof_simple.is_ok());
}
let narrowed_proof = narrowed_proof_simple.unwrap();
let new_leaves: Vec<_> = tree.leaves()[l..k]
.iter()
.map(|l| l.hash().clone())
.collect();
tree.check_range_proof(&root, &new_leaves, narrowed_proof.siblings(), l)
.unwrap();
}
}
}
}
test_min_and_max_ns_against(&mut tree)
}

#[test]
fn test_range_proof_narrowing_simple() {
for x in 0..20 {
test_range_proof_narrowing_with_n_leaves::<8>(x);
test_range_proof_narrowing_with_n_leaves::<17>(x);
test_range_proof_narrowing_with_n_leaves::<24>(x);
test_range_proof_narrowing_with_n_leaves::<CELESTIA_NS_ID_SIZE>(x);
test_range_proof_narrowing_with_n_leaves::<32>(x);
}
}

fn test_completeness_check_impl<const NS_ID_SIZE: usize>() {
// Build a tree with 32 leaves spread evenly across 8 namespaces
let mut tree = DefaultNmt::<NS_ID_SIZE>::new();
Expand Down
44 changes: 44 additions & 0 deletions src/nmt_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,50 @@ where
)
}

/// Narrows the proof range: uses an existing proof to create
/// a new proof for a subrange of the original proof's range
///
/// # Arguments
/// - left_extra_raw_leaves: The data for the leaves that will narrow the range from the left
/// side (i.e. all the leaves from the left edge of the currently proven range, to the left
/// edge of the new desired shrunk range)
/// - right_extra_raw_leaves: Analogously, data for all the leaves between the right edge of
/// the desired shrunken range, and the right edge of the current proof's range
pub fn narrow_range<L: AsRef<[u8]>>(
theodorebugnet marked this conversation as resolved.
Show resolved Hide resolved
&self,
left_extra_raw_leaves: &[L],
right_extra_raw_leaves: &[L],
leaf_namespace: NamespaceId<NS_ID_SIZE>,
) -> Result<Self, RangeProofError> {
if self.is_of_absence() {
return Err(RangeProofError::MalformedProof(
"Cannot narrow the range of an absence proof",
));
}

let leaves_to_hashes = |l: &[L]| -> Vec<NamespacedHash<NS_ID_SIZE>> {
l.iter()
.map(|data| {
M::with_ignore_max_ns(self.ignores_max_ns())
.hash_leaf_with_namespace(data.as_ref(), leaf_namespace)
})
.collect()
};
let left_extra_hashes = leaves_to_hashes(left_extra_raw_leaves);
let right_extra_hashes = leaves_to_hashes(right_extra_raw_leaves);

let proof = self.merkle_proof().narrow_range_with_hasher(
&left_extra_hashes,
&right_extra_hashes,
M::with_ignore_max_ns(self.ignores_max_ns()),
)?;

Ok(Self::PresenceProof {
proof,
ignore_max_ns: self.ignores_max_ns(),
})
}

/// Convert a proof of the presence of some leaf to the proof of the absence of another leaf
pub fn convert_to_absence_proof(&mut self, leaf: NamespacedHash<NS_ID_SIZE>) {
match self {
Expand Down
49 changes: 48 additions & 1 deletion src/simple_merkle/proof.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use core::ops::Range;
use core::{cmp::Ordering, ops::Range};

use super::{
db::NoopDb,
Expand Down Expand Up @@ -82,6 +82,53 @@ where
)
}

/// Narrows the proof range: uses an existing proof to create
/// a new proof for a subrange of the original proof's range
///
/// # Arguments
/// - left_extra_leaves: The hashes of the leaves that will narrow the range from the left
/// side (i.e. all the leaves from the left edge of the currently proven range, to the left
/// edge of the new desired shrunk range)
/// - right_extra_leaves: Analogously, hashes of all the leaves between the right edge of
/// the desired shrunken range, and the right edge of the current proof's range
pub fn narrow_range_with_hasher(
&self,
left_extra_leaves: &[M::Output],
right_extra_leaves: &[M::Output],
hasher: M,
) -> Result<Self, RangeProofError> {
let new_leaf_len = left_extra_leaves
.len()
.checked_add(right_extra_leaves.len())
.ok_or(RangeProofError::TreeTooLarge)?;
match new_leaf_len.cmp(&self.range_len()) {
Ordering::Equal => {
// We cannot prove the empty range!
return Err(RangeProofError::NoLeavesProvided);
}
Ordering::Greater => return Err(RangeProofError::WrongAmountOfLeavesProvided),
Ordering::Less => { /* Ok! */ }
}

// Indices relative to the leaves of the entire tree
let new_start_idx = (self.start_idx() as usize)
.checked_add(left_extra_leaves.len())
.ok_or(RangeProofError::TreeTooLarge)?;
let new_end_idx = new_start_idx
.checked_add(self.range_len())
.and_then(|i| i.checked_sub(new_leaf_len))
.ok_or(RangeProofError::TreeTooLarge)?;

let mut tree = MerkleTree::<NoopDb, M>::with_hasher(hasher);
tree.narrow_range_proof(
left_extra_leaves,
new_start_idx..new_end_idx,
right_extra_leaves,
&mut self.siblings().as_slice(),
self.start_idx() as usize,
)
}

/// Returns the siblings provided as part of the proof.
pub fn siblings(&self) -> &Vec<M::Output> {
&self.siblings
Expand Down
Loading
Loading