Skip to content

Commit 5e05b43

Browse files
committed
test(crypto): CRP-2001 add multi leaf witnesses to witness equality fuzzer Currently, the witness equality fuzzer checks the witness equality only for paths to leaves. This MR adds 1) support to the fuzzer and tests in `canonical_state` for empty subtrees in addition to leaves (also fixes an inconsistency for handling empty subtrees in `canonical_state`) and 2) support for multi-leaf/empty-subtree witnesses to the fuzzer. For multi-leaf fuzzing, the fuzzer generates random partial trees, which include `[2,min(10, nun_leaves_and_empty_subtrees(full_tree) - 1]` leaves/empty subtrees each. The fuzzer now also tests pruning of the full tree. Closes CRP-2001 Closes CRP-2001 See merge request dfinity-lab/public/ic!12410
2 parents 34c9b90 + 1ad3e26 commit 5e05b43

File tree

9 files changed

+234
-243
lines changed

9 files changed

+234
-243
lines changed

rs/canonical_state/src/hash_tree.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,11 @@ impl HashTree {
574574
);
575575
}
576576
}
577+
578+
// Empty subtree.
579+
LabeledTree::SubTree(children) if children.is_empty() => B::make_empty(),
580+
581+
// Non-empty subtree.
577582
LabeledTree::SubTree(children) => children
578583
.iter()
579584
.map(|(l, t)| child_witness::<B>(ht, parent, pos, l, t))

rs/canonical_state/src/hash_tree/test.rs

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,30 +89,31 @@ fn build_witness_gen(t: &LabeledTree<Vec<u8>>) -> WitnessGeneratorImpl {
8989
builder.witness_generator().unwrap()
9090
}
9191

92-
fn enumerate_leaves(t: &LabeledTree<Vec<u8>>, mut f: impl FnMut(LabeledTree<Vec<u8>>)) {
92+
fn enumerate_leaves_and_empty_subtrees(
93+
t: &LabeledTree<Vec<u8>>,
94+
mut f: impl FnMut(LabeledTree<Vec<u8>>),
95+
) {
9396
fn go<'a>(
9497
t: &'a LabeledTree<Vec<u8>>,
9598
path: &mut Vec<&'a Label>,
9699
f: &mut impl FnMut(LabeledTree<Vec<u8>>),
97100
) {
98101
match t {
99-
LabeledTree::Leaf(_) => {
100-
let mut subtree = t.clone();
101-
#[allow(clippy::unnecessary_to_owned)]
102-
for label in path.iter().rev().cloned() {
103-
subtree = LabeledTree::SubTree(flatmap! {
104-
label.clone() => subtree,
105-
});
106-
}
107-
f(subtree)
108-
}
109-
LabeledTree::SubTree(children) => {
102+
LabeledTree::SubTree(children) if !children.is_empty() => {
110103
for (k, v) in children.iter() {
111104
path.push(k);
112105
go(v, path, f);
113106
path.pop();
114107
}
115108
}
109+
LabeledTree::Leaf(_) | LabeledTree::SubTree(_) => {
110+
let subtree = path.iter().rev().fold(t.clone(), |acc, &label| {
111+
LabeledTree::SubTree(flatmap! {
112+
label.clone() => acc,
113+
})
114+
});
115+
f(subtree)
116+
}
116117
}
117118
}
118119
let mut path = vec![];
@@ -123,32 +124,42 @@ fn assert_same_witness(ht: &HashTree, wg: &WitnessGeneratorImpl, data: &LabeledT
123124
let ht_witness = ht.witness::<Witness>(data);
124125
let wg_witness = wg.witness(data).expect("failed to construct a witness");
125126

126-
assert_eq!(
127-
recompute_digest(data, &wg_witness).unwrap(),
128-
recompute_digest(data, &ht_witness).unwrap()
129-
);
130127
assert_eq!(
131128
wg_witness, ht_witness,
132129
"labeled tree: {:?}, hash_tree: {:?}",
133130
data, ht
134-
)
131+
);
132+
133+
assert_eq!(
134+
recompute_digest(data, &wg_witness).unwrap(),
135+
recompute_digest(data, &ht_witness).unwrap()
136+
);
135137
}
136138

137-
/// Check that for each leaf, the witness looks the same as with the
138-
/// old way of generating witnesses
139-
/// Also check that the new and old way of computing hash trees are equivalent
139+
/// Check that for each leaf or empty subtree, and the tree as a whole, the
140+
/// witness looks the same as with the old way of generating witnesses.
141+
///
142+
/// Also check that the new and old way of computing hash trees are equivalent.
140143
fn test_tree(t: &LabeledTree<Vec<u8>>) {
141144
let hash_tree = hash_lazy_tree(&as_lazy(t));
142145
let witness_gen = build_witness_gen(t);
143-
enumerate_leaves(t, |subtree| {
146+
enumerate_leaves_and_empty_subtrees(t, |subtree| {
144147
assert_same_witness(&hash_tree, &witness_gen, &subtree);
145148
});
146149

147-
let crypto_tree = crypto_hash_lazy_tree(&as_lazy(t));
150+
assert_same_witness(&hash_tree, &witness_gen, t);
148151

152+
let crypto_tree = crypto_hash_lazy_tree(&as_lazy(t));
149153
assert_eq!(hash_tree, crypto_tree);
150154
}
151155

156+
#[test]
157+
fn test_empty_subtree() {
158+
let t = LabeledTree::SubTree(flatmap! {});
159+
160+
test_tree(&t);
161+
}
162+
152163
#[test]
153164
fn test_one_level_tree() {
154165
let t = LabeledTree::SubTree(flatmap! {
@@ -272,7 +283,7 @@ fn test_non_existence_proof() {
272283

273284
proptest! {
274285
#[test]
275-
fn same_witness_on_all_leaves(t in arbitrary_labeled_tree()) {
286+
fn same_witness(t in arbitrary_labeled_tree()) {
276287
test_tree(&t);
277288
}
278289
}

rs/crypto/test_utils/reproducible_rng/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use rand::{CryptoRng, Error, Rng, RngCore, SeedableRng};
22
use rand_chacha::ChaCha20Rng;
33

4+
/// Byte length of the seed type used in [`ReproducibleRng`].
5+
pub const SEED_LEN: usize = 32;
6+
47
/// Provides a seeded RNG, where the randomly chosen seed is printed on standard output.
58
pub fn reproducible_rng() -> ReproducibleRng {
69
ReproducibleRng::new()
@@ -14,7 +17,7 @@ pub fn reproducible_rng() -> ReproducibleRng {
1417
/// (See [impl trait type](https://doc.rust-lang.org/reference/types/impl-trait.html)).
1518
pub struct ReproducibleRng {
1619
rng: ChaCha20Rng,
17-
seed: [u8; 32],
20+
seed: [u8; SEED_LEN],
1821
}
1922

2023
impl ReproducibleRng {

rs/crypto/tree_hash/fuzz/check_witness_equality_utils/src/lib.rs

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,52 @@
11
use ic_canonical_state::hash_tree::{crypto_hash_lazy_tree, hash_lazy_tree, HashTree};
22
use ic_canonical_state::lazy_tree::{LazyFork, LazyTree};
3-
use ic_crypto_test_utils_reproducible_rng::ReproducibleRng;
3+
use ic_crypto_test_utils_reproducible_rng::{ReproducibleRng, SEED_LEN};
4+
use ic_crypto_tree_hash::test_utils::{
5+
merge_path_into_labeled_tree, partial_trees_to_leaves_and_empty_subtrees,
6+
};
47
use ic_crypto_tree_hash::{
58
flatmap, FlatMap, HashTreeBuilder, HashTreeBuilderImpl, Label, LabeledTree, Witness,
69
WitnessGenerator, WitnessGeneratorImpl,
710
};
8-
use rand::{Rng, SeedableRng};
11+
use rand::{CryptoRng, Rng, SeedableRng};
912
use std::sync::Arc;
1013

1114
#[cfg(test)]
1215
mod tests;
1316

1417
/// Check that for each leaf, the witness looks the same for both implementations
1518
/// Also check that the new and old way of computing hash trees are equivalent
16-
pub fn test_tree(t: &LabeledTree<Vec<u8>>) {
17-
let hash_tree = hash_lazy_tree(&as_lazy(t));
18-
let witness_gen = build_witness_gen(t);
19-
// TODO(CRP-2001): add multi-leaf witnesses
20-
enumerate_leaves(t, |subtree| {
21-
assert_same_witness(&hash_tree, &witness_gen, &subtree);
22-
});
19+
pub fn test_tree<R: Rng + CryptoRng>(full_tree: &LabeledTree<Vec<u8>>, rng: &mut R) {
20+
let hash_tree = hash_lazy_tree(&as_lazy(full_tree));
21+
let witness_gen = build_witness_gen(full_tree);
22+
23+
let paths = partial_trees_to_leaves_and_empty_subtrees(full_tree);
24+
25+
// prune each path (1 node in each level) from `full_tree`
26+
for path in paths.iter() {
27+
assert_same_witness(&hash_tree, &witness_gen, path);
28+
}
29+
30+
// prune randomly combined paths
31+
const MAX_COMBINED_PATHS: usize = 10;
32+
for num_leaves_and_empty_subtrees in 2..MAX_COMBINED_PATHS.min(paths.len()) {
33+
let mut indices =
34+
rand::seq::index::sample(rng, paths.len(), num_leaves_and_empty_subtrees).into_vec();
35+
indices.sort_unstable();
2336

24-
let crypto_tree = crypto_hash_lazy_tree(&as_lazy(t));
37+
let mut partial_tree = paths[indices[0]].clone();
2538

39+
for index in indices[1..].iter() {
40+
merge_path_into_labeled_tree(&mut partial_tree, &paths[*index]);
41+
}
42+
assert_same_witness(&hash_tree, &witness_gen, &partial_tree);
43+
}
44+
45+
// prune the full tree
46+
assert_same_witness(&hash_tree, &witness_gen, full_tree);
47+
48+
// create a hash tree for the full tree
49+
let crypto_tree = crypto_hash_lazy_tree(&as_lazy(full_tree));
2650
assert_eq!(hash_tree, crypto_tree);
2751
}
2852

@@ -32,38 +56,8 @@ fn assert_same_witness(ht: &HashTree, wg: &WitnessGeneratorImpl, data: &LabeledT
3256

3357
assert_eq!(
3458
wg_witness, ht_witness,
35-
"labeled tree: {data:?}, hash_tree: {ht:?}",
36-
)
37-
}
38-
39-
fn enumerate_leaves(t: &LabeledTree<Vec<u8>>, mut f: impl FnMut(LabeledTree<Vec<u8>>)) {
40-
fn go<'a>(
41-
t: &'a LabeledTree<Vec<u8>>,
42-
path: &mut Vec<&'a Label>,
43-
f: &mut impl FnMut(LabeledTree<Vec<u8>>),
44-
) {
45-
match t {
46-
LabeledTree::Leaf(_) => {
47-
let mut subtree = t.clone();
48-
#[allow(clippy::unnecessary_to_owned)]
49-
for label in path.iter().rev().cloned() {
50-
subtree = LabeledTree::SubTree(flatmap! {
51-
label.clone() => subtree,
52-
});
53-
}
54-
f(subtree)
55-
}
56-
LabeledTree::SubTree(children) => {
57-
for (k, v) in children.iter() {
58-
path.push(k);
59-
go(v, path, f);
60-
path.pop();
61-
}
62-
}
63-
}
64-
}
65-
let mut path = vec![];
66-
go(t, &mut path, &mut f)
59+
"labeled tree: {data:?}, hash_tree: {ht:?}, wg: {wg:?}",
60+
);
6761
}
6862

6963
fn as_lazy(t: &LabeledTree<Vec<u8>>) -> LazyTree<'_> {
@@ -117,7 +111,6 @@ fn build_witness_gen(t: &LabeledTree<Vec<u8>>) -> WitnessGeneratorImpl {
117111
}
118112

119113
pub fn rng_from_u32(seed: u32) -> ReproducibleRng {
120-
const SEED_LEN: usize = std::mem::size_of::<<ReproducibleRng as rand::SeedableRng>::Seed>();
121114
let seed_bytes: Vec<u8> = seed
122115
.to_le_bytes()
123116
.into_iter()

rs/crypto/tree_hash/fuzz/fuzz_targets/check_witness_equality.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,42 @@
55
// let $data: &mut [u8] = unsafe { std::slice::from_raw_parts_mut($data, len) };"
66
#![allow(clippy::not_unsafe_ptr_arg_deref)]
77

8+
use ic_crypto_test_utils_reproducible_rng::{ReproducibleRng, SEED_LEN};
89
use ic_crypto_tree_hash::{flatmap, LabeledTree};
910
use ic_crypto_tree_hash_fuzz_check_witness_equality_utils::*;
1011
use ic_protobuf::messaging::xnet::v1::LabeledTree as ProtobufLabeledTree;
1112
use ic_protobuf::proxy::ProtoProxy;
1213
use libfuzzer_sys::fuzz_target;
13-
use rand::Rng;
14+
use rand::{Rng, RngCore, SeedableRng};
1415

1516
fuzz_target!(|data: &[u8]| {
16-
if let Ok(tree) = ProtobufLabeledTree::proxy_decode(data) {
17-
test_tree(&tree);
17+
if data.len() < SEED_LEN {
18+
return;
19+
}
20+
if let Ok(tree) = ProtobufLabeledTree::proxy_decode(&data[SEED_LEN..]) {
21+
let seed: [u8; SEED_LEN] = data[..SEED_LEN]
22+
.try_into()
23+
.expect("failed to copy seed bytes");
24+
test_tree(&tree, &mut ReproducibleRng::from_seed(seed));
1825
};
1926
});
2027

2128
libfuzzer_sys::fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
22-
let mut tree = match ProtobufLabeledTree::proxy_decode(&data[..size]) {
29+
let tree_data = if size < SEED_LEN {
30+
// invalid tree encoding if there's not enough bytes to construct a slice
31+
&[0u8; 0]
32+
} else {
33+
&data[SEED_LEN..size]
34+
};
35+
36+
let mut tree = match ProtobufLabeledTree::proxy_decode(tree_data) {
2337
Ok(tree) if matches!(tree, LabeledTree::SubTree(_)) => tree,
2438
Err(_) | Ok(_) /*if matches!(tree, LabeledTree::Leaf(_))*/ => {
25-
let bytes =
39+
let seed = [0u8; SEED_LEN];
40+
let encoded_tree =
2641
ProtobufLabeledTree::proxy_encode(LabeledTree::<Vec<u8>>::SubTree(flatmap!()))
2742
.expect("failed to serialize an empty labeled tree");
43+
let bytes: Vec<_> = seed.into_iter().chain(encoded_tree.into_iter()).collect();
2844
let new_size = bytes.len();
2945
if new_size <= max_size {
3046
data[..new_size].copy_from_slice(&bytes[..new_size]);
@@ -37,7 +53,7 @@ libfuzzer_sys::fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, see
3753

3854
let mut rng = rng_from_u32(seed);
3955

40-
let tree_was_modified = match rng.gen_range(0..8) {
56+
let data_size_changed = match rng.gen_range(0..9) {
4157
0 => try_remove_leaf(&mut tree, &mut rng),
4258
1 => try_remove_empty_subtree(&mut tree, &mut rng),
4359
// actions that increase the tree's size have twice the probability of those
@@ -50,15 +66,20 @@ libfuzzer_sys::fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, see
5066
7 => try_randomly_change_bytes_label(&mut tree, &mut rng, &|buffer: &mut Vec<u8>| {
5167
randomly_modify_buffer(buffer)
5268
}),
69+
// generate new seed
70+
8 => {
71+
rng.fill_bytes(&mut data[..SEED_LEN]);
72+
false
73+
}
5374
_ => unreachable!(),
5475
};
5576

56-
if tree_was_modified {
57-
let bytes = ProtobufLabeledTree::proxy_encode(tree)
77+
if data_size_changed {
78+
let encoded_tree = ProtobufLabeledTree::proxy_encode(tree)
5879
.expect("failed to serialize the labeled tree {tree}");
59-
let new_size = bytes.len();
80+
let new_size = SEED_LEN + encoded_tree.len();
6081
if new_size <= max_size {
61-
data[..new_size].copy_from_slice(&bytes[..]);
82+
data[SEED_LEN..new_size].copy_from_slice(&encoded_tree[..]);
6283
return new_size;
6384
} else {
6485
size

rs/crypto/tree_hash/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::ops::DerefMut;
1313
pub mod flat_map;
1414
pub mod hasher;
1515
pub mod proto;
16+
pub mod test_utils;
1617
pub(crate) mod tree_hash;
1718

1819
#[cfg(test)]

0 commit comments

Comments
 (0)