Skip to content

Commit 1ad3e26

Browse files
committed
test(crypto): CRP-2001 add multi leaf witnesses to witness equality fuzzer
1 parent 91a6105 commit 1ad3e26

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)