Skip to content

Commit f85d6b3

Browse files
Narrowing: edge case fixes, documentation, cleanup
- reverted the tendermint version bump - fixed edge cases on trees of total size 0 and 1 - fixed incorrect implementation in nmt_proof.rs (now just wraps the simple proof.rs) - fixed missing test cases for the above two issues - cleaned up inconsistent parameter naming to be more clear - cleaned up large argument sets in a few places to make data flow easier to follow - documented all assertions with a description of why they should never break - added documentation to the functions and descriptions of the narrowing logic
1 parent b4ca705 commit f85d6b3

File tree

5 files changed

+298
-129
lines changed

5 files changed

+298
-129
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ nmt-rs = { path = ".", features = ["borsh", "serde"] }
2121
borsh = { version = "1" }
2222
serde_json = "1.0.96"
2323
postcard = { version = "1.0.4", features = ["use-std"] }
24-
tendermint = { version = "0.39.1" }
24+
tendermint = { version = "0.35.0" }
2525

2626
[features]
2727
default = ["std"]

src/lib.rs

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ pub enum RangeProofType {
422422
#[cfg(test)]
423423
mod tests {
424424
use crate::maybestd::vec::Vec;
425+
use crate::simple_merkle::error::RangeProofError;
425426
use crate::NamespaceMerkleHasher;
426427
use crate::{
427428
namespaced_hash::{NamespaceId, NamespacedSha2Hasher},
@@ -458,6 +459,20 @@ mod tests {
458459
tree
459460
}
460461

462+
fn tree_from_one_namespace<const NS_ID_SIZE: usize>(
463+
leaves: u64,
464+
namespace: u64,
465+
) -> DefaultNmt<NS_ID_SIZE> {
466+
let mut tree = DefaultNmt::new();
467+
let namespace = ns_id_from_u64(namespace);
468+
for i in 0..leaves {
469+
let data = format!("leaf_{i}");
470+
tree.push_leaf(data.as_bytes(), namespace)
471+
.expect("Failed to push the leaf");
472+
}
473+
tree
474+
}
475+
461476
/// Builds a tree with N leaves
462477
fn tree_with_n_leaves<const NS_ID_SIZE: usize>(n: usize) -> DefaultNmt<NS_ID_SIZE> {
463478
tree_from_namespace_ids((0..n as u64).collect::<Vec<_>>())
@@ -587,6 +602,63 @@ mod tests {
587602
}
588603
}
589604

605+
fn test_range_proof_narrowing_within_namespace<const NS_ID_SIZE: usize>(n: usize) {
606+
let ns_id = 4;
607+
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
608+
let root = tree.root();
609+
for i in 1..=n {
610+
for j in 0..=i {
611+
let proof_nmt = NamespaceProof::PresenceProof {
612+
proof: tree.build_range_proof(j..i),
613+
ignore_max_ns: tree.ignore_max_ns,
614+
};
615+
for k in (j + 1)..=i {
616+
for l in j..=k {
617+
let left_leaf_datas: Vec<_> =
618+
tree.leaves()[j..l].iter().map(|l| l.data()).collect();
619+
let right_leaf_datas: Vec<_> =
620+
tree.leaves()[k..i].iter().map(|l| l.data()).collect();
621+
let narrowed_proof_nmt = proof_nmt.narrow_range(
622+
&left_leaf_datas,
623+
&right_leaf_datas,
624+
ns_id_from_u64(ns_id),
625+
);
626+
if k == l {
627+
// Cannot prove the empty range!
628+
assert!(narrowed_proof_nmt.is_err());
629+
assert_eq!(
630+
narrowed_proof_nmt.unwrap_err(),
631+
RangeProofError::NoLeavesProvided
632+
);
633+
continue;
634+
} else {
635+
assert!(narrowed_proof_nmt.is_ok());
636+
}
637+
let narrowed_proof = narrowed_proof_nmt.unwrap();
638+
let new_leaves: Vec<_> = tree.leaves()[l..k]
639+
.iter()
640+
.map(|l| l.hash().clone())
641+
.collect();
642+
tree.check_range_proof(&root, &new_leaves, narrowed_proof.siblings(), l)
643+
.unwrap();
644+
}
645+
}
646+
}
647+
}
648+
test_min_and_max_ns_against(&mut tree)
649+
}
650+
651+
#[test]
652+
fn test_range_proof_narrowing_nmt() {
653+
for x in 0..20 {
654+
test_range_proof_narrowing_within_namespace::<8>(x);
655+
test_range_proof_narrowing_within_namespace::<17>(x);
656+
test_range_proof_narrowing_within_namespace::<24>(x);
657+
test_range_proof_narrowing_within_namespace::<CELESTIA_NS_ID_SIZE>(x);
658+
test_range_proof_narrowing_within_namespace::<32>(x);
659+
}
660+
}
661+
590662
/// Builds a tree with n leaves, and then creates and checks proofs of all valid
591663
/// ranges, and attempts to narrow every proof and re-check it for the narrowed range
592664
fn test_range_proof_narrowing_with_n_leaves<const NS_ID_SIZE: usize>(n: usize) {
@@ -595,12 +667,8 @@ mod tests {
595667
for i in 1..=n {
596668
for j in 0..=i {
597669
let proof = tree.build_range_proof(j..i);
598-
let leaf_hashes: Vec<_> = tree.leaves()[j..i]
599-
.iter()
600-
.map(|l| l.hash().clone())
601-
.collect();
602-
for k in (j + 1)..i {
603-
for l in j..k {
670+
for k in (j + 1)..=i {
671+
for l in j..=k {
604672
let left_hashes: Vec<_> = tree.leaves()[j..l]
605673
.iter()
606674
.map(|l| l.hash().clone())
@@ -609,47 +677,38 @@ mod tests {
609677
.iter()
610678
.map(|l| l.hash().clone())
611679
.collect();
612-
let narrowed_proof = proof
613-
.narrow_range_with_hasher(
614-
&left_hashes,
615-
&right_hashes,
616-
NamespacedSha2Hasher::with_ignore_max_ns(tree.ignore_max_ns),
617-
)
618-
.unwrap();
680+
let narrowed_proof_simple = proof.narrow_range_with_hasher(
681+
&left_hashes,
682+
&right_hashes,
683+
NamespacedSha2Hasher::with_ignore_max_ns(tree.ignore_max_ns),
684+
);
685+
if k == l {
686+
// Cannot prove the empty range!
687+
assert!(narrowed_proof_simple.is_err());
688+
assert_eq!(
689+
narrowed_proof_simple.unwrap_err(),
690+
RangeProofError::NoLeavesProvided
691+
);
692+
continue;
693+
} else {
694+
assert!(narrowed_proof_simple.is_ok());
695+
}
696+
let narrowed_proof = narrowed_proof_simple.unwrap();
619697
let new_leaves: Vec<_> = tree.leaves()[l..k]
620698
.iter()
621699
.map(|l| l.hash().clone())
622700
.collect();
623-
let res = tree.check_range_proof(
624-
&root,
625-
&new_leaves,
626-
narrowed_proof.siblings(),
627-
l,
628-
);
629-
if l != k {
630-
assert!(res.is_ok());
631-
assert_eq!(res.unwrap(), RangeProofType::Complete)
632-
} else {
633-
// Cannot prove the empty range!
634-
assert!(res.is_err())
635-
}
701+
tree.check_range_proof(&root, &new_leaves, narrowed_proof.siblings(), l)
702+
.unwrap();
636703
}
637704
}
638-
let res = tree.check_range_proof(&root, &leaf_hashes, proof.siblings(), j);
639-
if i != j {
640-
assert!(res.is_ok());
641-
assert_eq!(res.unwrap(), RangeProofType::Complete)
642-
} else {
643-
// Cannot prove the empty range!
644-
assert!(res.is_err())
645-
}
646705
}
647706
}
648707
test_min_and_max_ns_against(&mut tree)
649708
}
650709

651710
#[test]
652-
fn test_range_proof_narrowing() {
711+
fn test_range_proof_narrowing_simple() {
653712
for x in 0..20 {
654713
test_range_proof_narrowing_with_n_leaves::<8>(x);
655714
test_range_proof_narrowing_with_n_leaves::<17>(x);

src/nmt_proof.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ where
100100

101101
/// Narrows the proof range: uses an existing proof to create
102102
/// a new proof for a subrange of the original proof's range
103+
///
104+
/// # Arguments
105+
/// - left_extra_raw_leaves: The data for the leaves that will narrow the range from the left
106+
/// side (i.e. all the leaves from the left edge of the currently proven range, to the left
107+
/// edge of the new desired shrunk range)
108+
/// - right_extra_raw_leaves: Analogously, data for all the leaves between the right edge of
109+
/// the desired shrunken range, and the right edge of the current proof's range
103110
pub fn narrow_range<L: AsRef<[u8]>>(
104111
&self,
105112
left_extra_raw_leaves: &[L],
@@ -112,11 +119,6 @@ where
112119
));
113120
}
114121

115-
let new_leaf_len = left_extra_raw_leaves.len() + right_extra_raw_leaves.len();
116-
if new_leaf_len >= self.range_len() {
117-
return Err(RangeProofError::WrongAmountOfLeavesProvided);
118-
}
119-
120122
let leaves_to_hashes = |l: &[L]| -> Vec<NamespacedHash<NS_ID_SIZE>> {
121123
l.iter()
122124
.map(|data| {
@@ -128,16 +130,10 @@ where
128130
let left_extra_hashes = leaves_to_hashes(left_extra_raw_leaves);
129131
let right_extra_hashes = leaves_to_hashes(right_extra_raw_leaves);
130132

131-
let mut tree = NamespaceMerkleTree::<NoopDb, M, NS_ID_SIZE>::with_hasher(
132-
M::with_ignore_max_ns(self.ignores_max_ns()),
133-
);
134-
135-
let proof = tree.inner.narrow_range_proof(
133+
let proof = self.merkle_proof().narrow_range_with_hasher(
136134
&left_extra_hashes,
137-
self.start_idx() as usize..(self.range_len() - new_leaf_len),
138135
&right_extra_hashes,
139-
&mut self.siblings(),
140-
self.start_idx() as usize,
136+
M::with_ignore_max_ns(self.ignores_max_ns()),
141137
)?;
142138

143139
Ok(Self::PresenceProof {

src/simple_merkle/proof.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use core::ops::Range;
1+
use core::{cmp::Ordering, ops::Range};
22

33
use super::{
44
db::NoopDb,
@@ -84,6 +84,13 @@ where
8484

8585
/// Narrows the proof range: uses an existing proof to create
8686
/// a new proof for a subrange of the original proof's range
87+
///
88+
/// # Arguments
89+
/// - left_extra_leaves: The hashes of the leaves that will narrow the range from the left
90+
/// side (i.e. all the leaves from the left edge of the currently proven range, to the left
91+
/// edge of the new desired shrunk range)
92+
/// - right_extra_leaves: Analogously, hashes of all the leaves between the right edge of
93+
/// the desired shrunken range, and the right edge of the current proof's range
8794
pub fn narrow_range_with_hasher(
8895
&self,
8996
left_extra_leaves: &[M::Output],
@@ -94,9 +101,16 @@ where
94101
.len()
95102
.checked_add(right_extra_leaves.len())
96103
.ok_or(RangeProofError::TreeTooLarge)?;
97-
if new_leaf_len >= self.range_len() {
98-
return Err(RangeProofError::WrongAmountOfLeavesProvided);
104+
match new_leaf_len.cmp(&self.range_len()) {
105+
Ordering::Equal => {
106+
// We cannot prove the empty range!
107+
return Err(RangeProofError::NoLeavesProvided);
108+
}
109+
Ordering::Greater => return Err(RangeProofError::WrongAmountOfLeavesProvided),
110+
Ordering::Less => { /* Ok! */ }
99111
}
112+
113+
// Indices relative to the leaves of the entire tree
100114
let new_start_idx = (self.start_idx() as usize)
101115
.checked_add(left_extra_leaves.len())
102116
.ok_or(RangeProofError::TreeTooLarge)?;

0 commit comments

Comments
 (0)