Skip to content

Commit fe03c66

Browse files
authored
tree(ics23): add bounding path calculation proptests (#115)
Close #104.
1 parent 9ba9071 commit fe03c66

File tree

1 file changed

+123
-10
lines changed

1 file changed

+123
-10
lines changed

src/tree/ics23_impl.rs

+123-10
Original file line numberDiff line numberDiff line change
@@ -253,24 +253,131 @@ pub fn ics23_spec() -> ics23::ProofSpec {
253253
#[cfg(test)]
254254
mod tests {
255255
use alloc::format;
256-
use ics23::HostFunctionsManager;
256+
use ics23::{commitment_proof::Proof, HostFunctionsManager, NonExistenceProof};
257257
use proptest::prelude::*;
258+
use proptest::strategy::Strategy;
258259
use sha2::Sha256;
259260

260261
use super::*;
261262
use crate::{mock::MockTreeStore, KeyHash, TransparentHasher, SPARSE_MERKLE_PLACEHOLDER_HASH};
262263

263-
#[test]
264-
#[should_panic]
265-
fn test_jmt_ics23_nonexistence_single_empty_key() {
266-
test_jmt_ics23_nonexistence_with_keys(vec![vec![]].into_iter());
267-
}
268-
269264
proptest! {
265+
#![proptest_config(ProptestConfig {
266+
cases: 1000, .. ProptestConfig::default()
267+
})]
268+
270269
#[test]
271-
fn test_jmt_ics23_nonexistence(keys: Vec<Vec<u8>>) {
272-
test_jmt_ics23_nonexistence_with_keys(keys.into_iter().filter(|k| k.len() != 0));
273-
}
270+
/// Assert that the Ics23 bonding path calculations are correct.
271+
/// To achieve this, the uses a strategy that consists in:
272+
/// 1. generating a sorted vector of key preimages
273+
/// 2. instantiating a JMT over a `TransparentHasher`
274+
///
275+
/// The last point allow us to easily test that for a given key
276+
/// that is *in* the JMT, we can generate two non-existent keys
277+
/// that are "neighbor" to `k`: (k-1, k+1).
278+
///
279+
/// To recap, we generate a vector of sorted key <k_1, ... k_n>;
280+
/// then, we iterate over each existing key `k_i` and compute a
281+
/// tuple of neighbors (`k_i - 1`, `k_i + 1`) which are *not*
282+
/// in the tree.
283+
/// Equipped with those nonexisting neighbors, we check for exclusion
284+
/// in the tree, and specifically assert that the generated proof contains:
285+
/// 1. the initial key we requested (i.e. `k_i + 1` or `k_i - 1`)
286+
/// 2. the correct left neighbor (i.e. `k_{i-1}`, or `k_{i+1}`, or none`)
287+
/// 2. the correct right neighbor (i.e. `k_{i-1}`, or `k_{i+1}`, or none`)
288+
/// across configurations e.g. bounding path for a leftmost or rightmost subtree.
289+
/// More context available in #104.
290+
fn test_ics23_bounding_path_simple(key_seeds in key_strategy()) {
291+
let mut preimages: Vec<String> = key_seeds.into_iter().filter(|k| *k!=0).map(|k| format!("{k:032x}")).collect();
292+
preimages.sort();
293+
preimages.dedup();
294+
295+
assert!(preimages.len() > 0);
296+
297+
let store = MockTreeStore::default();
298+
let tree = JellyfishMerkleTree::<_, TransparentHasher>::new(&store);
299+
300+
// Our key preimages and key hashes are identical, but we still need to populate
301+
// the mock store so that ics23 internal queries can resolve.
302+
for preimage in preimages.iter() {
303+
store.put_key_preimage(KeyHash::with::<TransparentHasher>(preimage.clone()), preimage.clone().as_bytes().to_vec().as_ref());
304+
}
305+
306+
let (_, write_batch) = tree.put_value_set(
307+
preimages.iter().enumerate().map(|(i,k)| (KeyHash::with::<TransparentHasher>(k.as_bytes()), Some(i.to_be_bytes().to_vec()))),
308+
1
309+
).unwrap();
310+
311+
store.write_tree_update_batch(write_batch).expect("can write to mock storage");
312+
313+
let len_preimages = preimages.len();
314+
315+
for (idx, existing_key) in preimages.iter().enumerate() {
316+
// For each existing key, we generate two alternative keys that are not
317+
// in the tree. One that is one bit "ahead" and one that is one bit after.
318+
// e.g. 0x5 -> 0x4 and 0x6
319+
let (smaller_key, bigger_key) = generate_adjacent_keys(existing_key);
320+
321+
let (v, proof) = tree.get_with_ics23_proof(smaller_key.as_bytes().to_vec(), 1).expect("can query tree");
322+
assert!(v.is_none(), "the key should not exist!");
323+
let proof = proof.proof.expect("a proof is present");
324+
if let Proof::Nonexist(NonExistenceProof { key, left, right }) = proof {
325+
// Basic check that we are getting back the key that we queried.
326+
assert_eq!(key, smaller_key.as_bytes().to_vec());
327+
328+
// The expected predecessor to the nonexistent key `k_i - 1` is `k_{i-1}`, unless `i=0`.
329+
let expected_left_neighbor = if idx == 0 { None } else { preimages.get(idx-1) };
330+
// The expected successor to the nonexistent key `k_i - 1` is `k_{i+1}`.
331+
let expected_right_neighbor = Some(existing_key);
332+
333+
let reported_left_neighbor = left.clone().map(|existence_proof| String::from_utf8_lossy(&existence_proof.key).into_owned());
334+
let reported_right_neighbor = right.clone().map(|existence_proof| String::from_utf8_lossy(&existence_proof.key).into_owned());
335+
336+
assert_eq!(expected_left_neighbor.cloned(), reported_left_neighbor);
337+
assert_eq!(expected_right_neighbor.cloned(), reported_right_neighbor);
338+
} else {
339+
unreachable!("we have assessed that the value is `None`")
340+
}
341+
342+
let (v, proof) = tree.get_with_ics23_proof(bigger_key.as_bytes().to_vec(), 1).expect("can query tree");
343+
assert!(v.is_none(), "the key should not exist!");
344+
let proof = proof.proof.expect("a proof is present");
345+
if let Proof::Nonexist(NonExistenceProof { key, left, right }) = proof {
346+
// Basic check that we are getting back the key that we queried.
347+
assert_eq!(key, bigger_key.as_bytes().to_vec());
348+
let reported_left_neighbor = left.clone().map(|existence_proof| String::from_utf8_lossy(&existence_proof.key).into_owned());
349+
let reported_right_neighbor = right.clone().map(|existence_proof| String::from_utf8_lossy(&existence_proof.key).into_owned());
350+
351+
// The expected predecessor to the nonexistent key `k_i + 1` is `k_{i}`.
352+
let expected_left_neighbor = Some(existing_key);
353+
// The expected successor to the nonexistent key `k_i + 1` is `k_{i+1}`.
354+
let expected_right_neighbor = if idx == len_preimages - 1 { None } else { preimages.get(idx+1) };
355+
356+
assert_eq!(expected_left_neighbor.cloned(), reported_left_neighbor);
357+
assert_eq!(expected_right_neighbor.cloned(), reported_right_neighbor);
358+
} else {
359+
unreachable!("we have assessed that the value is `None`")
360+
}
361+
}
362+
}
363+
364+
#[test]
365+
fn test_jmt_ics23_nonexistence(keys: Vec<Vec<u8>>) {
366+
test_jmt_ics23_nonexistence_with_keys(keys.into_iter().filter(|k| k.len() != 0));
367+
}
368+
}
369+
370+
fn key_strategy() -> impl Strategy<Value = Vec<u128>> {
371+
proptest::collection::btree_set(u64::MAX as u128..=u128::MAX, 200)
372+
.prop_map(|set| set.into_iter().collect())
373+
}
374+
fn generate_adjacent_keys(hex: &String) -> (String, String) {
375+
let value = u128::from_str_radix(hex.as_str(), 16).expect("valid hexstring");
376+
let prev = value - 1;
377+
let next = value + 1;
378+
let p = format!("{prev:032x}");
379+
let n = format!("{next:032x}");
380+
(p, n)
274381
}
275382

276383
fn test_jmt_ics23_nonexistence_with_keys(keys: impl Iterator<Item = Vec<u8>>) {
@@ -387,6 +494,12 @@ mod tests {
387494
));
388495
}
389496

497+
#[test]
498+
#[should_panic]
499+
fn test_jmt_ics23_nonexistence_single_empty_key() {
500+
test_jmt_ics23_nonexistence_with_keys(vec![vec![]].into_iter());
501+
}
502+
390503
#[test]
391504
fn test_jmt_ics23_existence() {
392505
let db = MockTreeStore::default();

0 commit comments

Comments
 (0)