diff --git a/Cargo.lock b/Cargo.lock index 6a5529213a..132d6bcbb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,6 +1092,7 @@ dependencies = [ "loom", "lru", "nomt-core", + "nomt-test-utils", "parking_lot", "quickcheck", "rand", @@ -1114,6 +1115,8 @@ dependencies = [ "borsh", "digest", "hex", + "nomt-test-utils", + "quickcheck", "ruint", "serde", "sha2", @@ -1130,6 +1133,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nomt-test-utils" +version = "0.1.0" +dependencies = [ + "nomt-core", + "quickcheck", + "rand", + "rand_pcg", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" diff --git a/Cargo.toml b/Cargo.toml index 6132e4f935..f617240e21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "core", "nomt", + "nomt-test-utils", "fuzz", "torture", "examples/*", diff --git a/core/Cargo.toml b/core/Cargo.toml index f81b84c281..768aef324b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,6 +23,8 @@ digest = { workspace = true } [dev-dependencies] blake3.workspace = true +nomt-test-utils = { path = "../nomt-test-utils" } +quickcheck.workspace = true [features] default = ["std", "blake3-hasher", "sha2-hasher"] diff --git a/core/src/page_id.rs b/core/src/page_id.rs index 3dd708586f..8e89cd8c2b 100644 --- a/core/src/page_id.rs +++ b/core/src/page_id.rs @@ -288,15 +288,34 @@ impl Iterator for PageIdsIterator { mod tests { use super::{ ChildPageIdError, ChildPageIndex, InvalidPageIdBytes, Msb0, PageId, PageIdsIterator, Uint, - HIGHEST_ENCODED_42, MAX_CHILD_INDEX, ROOT_PAGE_ID, + HIGHEST_ENCODED_42, MAX_CHILD_INDEX, MAX_PAGE_DEPTH, ROOT_PAGE_ID, }; use bitvec::prelude::*; + use nomt_test_utils::TestKeyPath; + use quickcheck::{Arbitrary, Gen, QuickCheck}; const LOWEST_ENCODED_42: Uint<256, 4> = Uint::from_be_bytes([ 0, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, 4, 16, 65, ]); + #[derive(Clone, Debug)] + struct PagePathCase { + key_path: [u8; 32], + level: usize, + child_index: u8, + } + + impl Arbitrary for PagePathCase { + fn arbitrary(g: &mut Gen) -> Self { + Self { + key_path: TestKeyPath::arbitrary(g).into_inner(), + level: usize::arbitrary(g) % MAX_PAGE_DEPTH, + child_index: u8::arbitrary(g) & MAX_CHILD_INDEX, + } + } + } + fn child_page_id(page_id: &PageId, child_index: u8) -> Result { page_id.child_page_id(ChildPageIndex::new(child_index).unwrap()) } @@ -518,4 +537,59 @@ mod tests { } assert_eq!(min_page.max_key_path(), key_path); } + + #[test] + fn property_iterator_chain_contains_key_path() { + fn property(test_key_path: TestKeyPath) -> bool { + let key_path = test_key_path.into_inner(); + let page_ids = PageIdsIterator::new(key_path).collect::>(); + + assert_eq!(page_ids.len(), MAX_PAGE_DEPTH + 1); + assert_eq!(page_ids.first(), Some(&ROOT_PAGE_ID)); + + for (depth, page_id) in page_ids.iter().enumerate() { + assert_eq!(page_id.depth(), depth); + assert!(page_id.min_key_path() <= key_path); + assert!(key_path <= page_id.max_key_path()); + } + + for pair in page_ids.windows(2) { + let parent = &pair[0]; + let child = &pair[1]; + let child_index = child.child_index_at_level(parent.depth()); + + assert_eq!(parent.child_page_id(child_index).unwrap(), child.clone()); + assert_eq!(child.parent_page_id(), parent.clone()); + } + + true + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(TestKeyPath) -> bool); + } + + #[test] + fn property_child_parent_roundtrip() { + fn property(case: PagePathCase) -> bool { + let page_id = PageIdsIterator::new(case.key_path).nth(case.level).unwrap(); + let child_index = ChildPageIndex::new(case.child_index).unwrap(); + let child = page_id.child_page_id(child_index).unwrap(); + + assert!(page_id.min_key_path() <= case.key_path); + assert!(case.key_path <= page_id.max_key_path()); + assert_eq!(child.parent_page_id(), page_id.clone()); + assert_eq!(child.depth(), page_id.depth() + 1); + assert!(child.is_descendant_of(&page_id)); + assert!(page_id.min_key_path() <= child.min_key_path()); + assert!(child.max_key_path() <= page_id.max_key_path()); + + true + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(PagePathCase) -> bool); + } } diff --git a/core/src/proof/multi_proof.rs b/core/src/proof/multi_proof.rs index b975d38f47..949ad83320 100644 --- a/core/src/proof/multi_proof.rs +++ b/core/src/proof/multi_proof.rs @@ -909,11 +909,12 @@ mod tests { use crate::{ hasher::{Blake3Hasher, NodeHasher}, proof::{PathProof, PathProofTerminal}, - trie::{InternalData, KeyPath, LeafData, ValueHash, TERMINATOR}, + trie::{InternalData, LeafData, ValueHash, TERMINATOR}, trie_pos::TriePosition, update::build_trie, }; use bitvec::prelude::*; + use nomt_test_utils::key_with_prefix; #[test] pub fn test_multiproof_creation_single_path_proof() { @@ -1891,26 +1892,8 @@ mod tests { // 1. Define paths with prefix relationship let bits_prefix = bitvec![u8, Msb0; 1, 0, 1, 0]; // length 4 let bits_longer = bitvec![u8, Msb0; 1, 0, 1, 0, 1, 1]; // length 6 (prefix + 2 bits) - - // Helper to create KeyPath from BitVec (simplified, assumes short paths) - let keypath_from_bits = |bits: &BitSlice| -> KeyPath { - let mut kp = KeyPath::default(); - - for (i, bit) in bits.iter().by_vals().enumerate() { - if i >= 256 { - break; - } - - if bit { - kp[i / 8] |= 1 << (7 - (i % 8)); - } - } - - kp - }; - - let kp_prefix = keypath_from_bits(&bits_prefix); - let kp_longer = keypath_from_bits(&bits_longer); + let kp_prefix = key_with_prefix(bits_prefix.iter().by_vals()); + let kp_longer = key_with_prefix(bits_longer.iter().by_vals()); // 2. Create corresponding LeafData and Nodes let leaf_prefix = LeafData { @@ -1962,13 +1945,13 @@ mod tests { key_op1_bits.push(false); // e.g., 10100 (falls under 1010) - let key_op1 = keypath_from_bits(&key_op1_bits); + let key_op1 = key_with_prefix(key_op1_bits.iter().by_vals()); let mut key_op2_bits = bits_longer.clone(); key_op2_bits.push(true); // e.g., 1010111 (falls under 101011) - let key_op2 = keypath_from_bits(&key_op2_bits); + let key_op2 = key_with_prefix(key_op2_bits.iter().by_vals()); let ops = vec![ (key_op1, Some(ValueHash::default())), // Op under prefix path diff --git a/nomt-test-utils/Cargo.toml b/nomt-test-utils/Cargo.toml new file mode 100644 index 0000000000..dd3c0b0b28 --- /dev/null +++ b/nomt-test-utils/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "nomt-test-utils" +version = "0.1.0" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +nomt-core = { path = "../core", default-features = false, features = ["std"] } +quickcheck.workspace = true +rand.workspace = true +rand_pcg.workspace = true diff --git a/nomt-test-utils/src/lib.rs b/nomt-test-utils/src/lib.rs new file mode 100644 index 0000000000..5e33a962fb --- /dev/null +++ b/nomt-test-utils/src/lib.rs @@ -0,0 +1,540 @@ +use nomt_core::trie::KeyPath; +use quickcheck::{empty_shrinker, single_shrinker, Arbitrary, Gen}; +use rand::{Rng as _, SeedableRng as _}; +use rand_pcg::Lcg64Xsh32; +use std::collections::BTreeSet; + +const EDGE_PREFIX_LENS: &[usize] = &[0, 1, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255]; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct TestKeyPath(pub KeyPath); + +impl TestKeyPath { + pub fn raw(inner: KeyPath) -> Self { + Self(inner) + } + + pub fn account_like(id: u64) -> Self { + Self(seeded_key(id)) + } + + pub fn diverging_at(bit_depth: usize, right: bool) -> Self { + assert!(bit_depth < 256); + + let mut key = KeyPath::default(); + set_bit(&mut key, bit_depth, right); + Self(key) + } + + pub fn with_prefix_bits(bits: impl IntoIterator, seed: u64) -> Self { + let mut key = seeded_key(seed); + + for (i, bit) in bits.into_iter().enumerate() { + assert!(i < 256); + set_bit(&mut key, i, bit); + } + + Self(key) + } + + pub fn into_inner(self) -> KeyPath { + self.0 + } +} + +impl Arbitrary for TestKeyPath { + fn arbitrary(g: &mut Gen) -> Self { + match u8::arbitrary(g) % 100 { + 0..=29 => Self(raw_key(g)), + 30..=54 => Self::account_like(u64::arbitrary(g)), + 55..=79 => { + let prefix_len = arbitrary_prefix_len(g, true); + let prefix_bits = random_bits(g, prefix_len); + Self::with_prefix_bits(prefix_bits, u64::arbitrary(g)) + } + _ => arbitrary_edge_key(g), + } + } + + fn shrink(&self) -> Box> { + if self.0 == [0; 32] { + return empty_shrinker(); + } + + let current = *self; + let mut candidates = vec![Self::raw([0; 32]), Self::account_like(0)]; + + if let Some(bit) = first_set_bit(&self.0) { + candidates.push(Self::diverging_at(bit, true)); + } + + candidates.sort_unstable(); + candidates.dedup(); + + Box::new( + candidates + .into_iter() + .filter(move |candidate| *candidate != current), + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DivergingPair { + pub left: KeyPath, + pub right: KeyPath, + pub first_diff_bit: usize, +} + +impl DivergingPair { + pub fn from_prefix_bits( + bits: impl IntoIterator, + left_seed: u64, + right_seed: u64, + ) -> Self { + let prefix_bits = collect_bits(bits); + assert!(prefix_bits.len() < 256); + + let first_diff_bit = prefix_bits.len(); + let mut left = + TestKeyPath::with_prefix_bits(prefix_bits.iter().copied(), left_seed).into_inner(); + let mut right = + TestKeyPath::with_prefix_bits(prefix_bits.iter().copied(), right_seed).into_inner(); + + set_bit(&mut left, first_diff_bit, false); + set_bit(&mut right, first_diff_bit, true); + + Self { + left, + right, + first_diff_bit, + } + } +} + +impl Arbitrary for DivergingPair { + fn arbitrary(g: &mut Gen) -> Self { + let first_diff_bit = arbitrary_prefix_len(g, false); + let prefix_bits = random_bits(g, first_diff_bit); + Self::from_prefix_bits(prefix_bits, u64::arbitrary(g), u64::arbitrary(g)) + } + + fn shrink(&self) -> Box> { + single_shrinker(Self { + left: TestKeyPath::diverging_at(self.first_diff_bit, false).into_inner(), + right: TestKeyPath::diverging_at(self.first_diff_bit, true).into_inner(), + first_diff_bit: self.first_diff_bit, + }) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SharedPrefixCluster { + pub prefix_len_bits: usize, + pub members: Vec, +} + +impl SharedPrefixCluster { + /// `count` must satisfy `2 <= count <= max_cluster_members(prefix_bits.len())`, + /// otherwise the post-dedup length assertion will fire. + pub fn from_prefix_bits(bits: impl IntoIterator, count: usize, seed: u64) -> Self { + let prefix_bits = collect_bits(bits); + let prefix_len_bits = prefix_bits.len(); + assert!(prefix_len_bits < 256); + + let max_members = max_cluster_members(prefix_len_bits); + assert!(count >= 2); + assert!(count <= max_members); + + let extra_width = cluster_extra_width(prefix_len_bits); + let mut members = Vec::with_capacity(count); + + for index in 0..count { + let mut key = TestKeyPath::with_prefix_bits( + prefix_bits.iter().copied(), + seed.wrapping_add(index as u64), + ) + .into_inner(); + + set_bit(&mut key, prefix_len_bits, index % 2 == 1); + + let suffix_id = index / 2; + for offset in 0..extra_width { + let bit = ((suffix_id >> (extra_width - 1 - offset)) & 1) == 1; + set_bit(&mut key, prefix_len_bits + 1 + offset, bit); + } + + members.push(key); + } + + members.sort_unstable(); + members.dedup(); + assert_eq!(members.len(), count); + + Self { + prefix_len_bits, + members, + } + } +} + +impl Arbitrary for SharedPrefixCluster { + fn arbitrary(g: &mut Gen) -> Self { + let prefix_len_bits = arbitrary_prefix_len(g, false); + let prefix_bits = random_bits(g, prefix_len_bits); + let max_members = max_cluster_members(prefix_len_bits); + let count = 2 + (usize::arbitrary(g) % (max_members - 1)); + + Self::from_prefix_bits(prefix_bits, count, u64::arbitrary(g)) + } + + fn shrink(&self) -> Box> { + let prefix_bits = prefix_bits(&self.members[0], self.prefix_len_bits); + let count = if self.members.len() > 2 { + 2 + } else { + self.members.len() + }; + let candidate = Self::from_prefix_bits(prefix_bits, count, 0); + + if &candidate == self { + empty_shrinker() + } else { + single_shrinker(candidate) + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UniqueSortedKeyPaths(pub Vec); + +impl UniqueSortedKeyPaths { + pub fn new(paths: impl IntoIterator) -> Self { + let mut unique = paths.into_iter().collect::>(); + unique.sort_unstable(); + unique.dedup(); + Self(unique) + } + + pub fn as_slice(&self) -> &[KeyPath] { + &self.0 + } + + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl Arbitrary for UniqueSortedKeyPaths { + fn arbitrary(g: &mut Gen) -> Self { + let target_len = usize::arbitrary(g) % 9; + let mut paths = BTreeSet::new(); + let mut attempts = 0; + + while paths.len() < target_len && attempts < (target_len.saturating_mul(8) + 8) { + paths.insert(TestKeyPath::arbitrary(g).into_inner()); + attempts += 1; + } + + let mut fill_seed = 0u64; + while paths.len() < target_len { + paths.insert(TestKeyPath::account_like(fill_seed).into_inner()); + fill_seed += 1; + } + + Self(paths.into_iter().collect()) + } + + fn shrink(&self) -> Box> { + if self.0.is_empty() { + return empty_shrinker(); + } + + let next_len = self.0.len() / 2; + let candidate = Self(self.0[..next_len].to_vec()); + single_shrinker(candidate) + } +} + +pub fn account_path(id: u64) -> KeyPath { + TestKeyPath::account_like(id).into_inner() +} + +pub fn key_diverging_at(bit_depth: usize, right: bool) -> KeyPath { + TestKeyPath::diverging_at(bit_depth, right).into_inner() +} + +pub fn key_with_prefix(bits: impl IntoIterator) -> KeyPath { + let mut key = KeyPath::default(); + + for (index, bit) in bits.into_iter().enumerate() { + assert!(index < 256); + set_bit(&mut key, index, bit); + } + + key +} + +pub fn key_with_prefix_seed(bits: impl IntoIterator, seed: u64) -> KeyPath { + TestKeyPath::with_prefix_bits(bits, seed).into_inner() +} + +fn collect_bits(bits: impl IntoIterator) -> Vec { + let collected = bits.into_iter().collect::>(); + assert!(collected.len() <= 256); + collected +} + +fn arbitrary_prefix_len(g: &mut Gen, allow_full: bool) -> usize { + if bool::arbitrary(g) { + EDGE_PREFIX_LENS[usize::arbitrary(g) % EDGE_PREFIX_LENS.len()] + } else if allow_full { + usize::arbitrary(g) % 257 + } else { + usize::arbitrary(g) % 256 + } +} + +fn arbitrary_edge_key(g: &mut Gen) -> TestKeyPath { + match u8::arbitrary(g) % 7 { + 0 => TestKeyPath::raw([0; 32]), + 1 => TestKeyPath::raw([0xFF; 32]), + 2 => TestKeyPath::raw([0xAA; 32]), + 3 => TestKeyPath::raw([0x55; 32]), + 4 => TestKeyPath::diverging_at( + EDGE_PREFIX_LENS[usize::arbitrary(g) % EDGE_PREFIX_LENS.len()], + true, + ), + 5 => { + let mut key = [0; 32]; + let byte = usize::arbitrary(g) % key.len(); + key[byte] = 0xFF; + TestKeyPath::raw(key) + } + _ => TestKeyPath::account_like(0), + } +} + +fn cluster_extra_width(prefix_len_bits: usize) -> usize { + (255 - prefix_len_bits).min(3) +} + +fn first_set_bit(key: &KeyPath) -> Option { + (0..256).find(|&index| bit(key, index)) +} + +fn prefix_bits(key: &KeyPath, prefix_len_bits: usize) -> Vec { + (0..prefix_len_bits).map(|index| bit(key, index)).collect() +} + +fn random_bits(g: &mut Gen, len: usize) -> Vec { + (0..len).map(|_| bool::arbitrary(g)).collect() +} + +fn raw_key(g: &mut Gen) -> KeyPath { + let mut key = [0; 32]; + for byte in &mut key { + *byte = u8::arbitrary(g); + } + key +} + +fn seeded_key(seed: u64) -> KeyPath { + let mut rng_seed = [0; 16]; + rng_seed[..8].copy_from_slice(&seed.to_le_bytes()); + + let mut rng = Lcg64Xsh32::from_seed(rng_seed); + let mut key = [0; 32]; + + for chunk in key.chunks_exact_mut(4) { + chunk.copy_from_slice(&rng.next_u32().to_le_bytes()); + } + + key +} + +fn set_bit(key: &mut KeyPath, index: usize, value: bool) { + let byte = index / 8; + let shift = 7 - (index % 8); + let mask = 1 << shift; + + if value { + key[byte] |= mask; + } else { + key[byte] &= !mask; + } +} + +fn bit(key: &KeyPath, index: usize) -> bool { + let byte = index / 8; + let shift = 7 - (index % 8); + ((key[byte] >> shift) & 1) == 1 +} + +#[cfg(test)] +fn first_difference(left: &KeyPath, right: &KeyPath) -> Option { + (0..256).find(|&index| bit(left, index) != bit(right, index)) +} + +#[cfg(test)] +fn has_shared_prefix(a: &KeyPath, b: &KeyPath, prefix_len_bits: usize) -> bool { + (0..prefix_len_bits).all(|index| bit(a, index) == bit(b, index)) +} + +#[cfg(test)] +fn has_exact_cluster_prefix(cluster: &SharedPrefixCluster) -> bool { + if cluster.members.len() < 2 { + return false; + } + + cluster + .members + .iter() + .all(|member| has_shared_prefix(&cluster.members[0], member, cluster.prefix_len_bits)) + && cluster.members.iter().enumerate().any(|(i, left)| { + cluster.members[i + 1..] + .iter() + .any(|right| first_difference(left, right) == Some(cluster.prefix_len_bits)) + }) +} + +#[cfg(test)] +fn is_sorted_unique(paths: &[KeyPath]) -> bool { + paths.windows(2).all(|window| window[0] < window[1]) +} + +fn max_cluster_members(prefix_len_bits: usize) -> usize { + 2usize << cluster_extra_width(prefix_len_bits) +} + +#[cfg(test)] +mod tests { + use super::{ + account_path, bit, first_difference, has_exact_cluster_prefix, is_sorted_unique, + key_diverging_at, key_with_prefix, key_with_prefix_seed, DivergingPair, + SharedPrefixCluster, TestKeyPath, UniqueSortedKeyPaths, + }; + use quickcheck::{Arbitrary, QuickCheck}; + + #[test] + fn diverging_at_differs_at_requested_bit() { + for bit_depth in [0, 1, 7, 8, 15, 16, 127, 255] { + let left = key_diverging_at(bit_depth, false); + let right = key_diverging_at(bit_depth, true); + + assert_eq!(first_difference(&left, &right), Some(bit_depth)); + } + } + + #[test] + fn with_prefix_bits_preserves_prefix() { + let prefix = [true, false, true, true, false, false, true, false, true]; + let key = key_with_prefix_seed(prefix, 17); + + for (index, expected) in prefix.into_iter().enumerate() { + assert_eq!(bit(&key, index), expected); + } + + assert_eq!(key, key_with_prefix_seed(prefix, 17)); + } + + #[test] + fn key_with_prefix_zero_fills_suffix() { + let key = key_with_prefix([true, false, true, false]); + + assert!(bit(&key, 0)); + assert!(!bit(&key, 1)); + assert!(bit(&key, 2)); + assert!(!bit(&key, 3)); + assert!(!bit(&key, 4)); + assert_eq!(key[1..], [0; 31]); + } + + #[test] + fn account_like_is_stable_and_unique_for_nearby_seeds() { + let mut paths = Vec::new(); + + for seed in 0..64 { + let path = account_path(seed); + assert_eq!(path, account_path(seed)); + paths.push(path); + } + + paths.sort_unstable(); + paths.dedup(); + assert_eq!(paths.len(), 64); + } + + #[test] + fn unique_sorted_keypaths_sorts_and_deduplicates() { + let paths = UniqueSortedKeyPaths::new([ + TestKeyPath::account_like(7).into_inner(), + TestKeyPath::account_like(3).into_inner(), + TestKeyPath::account_like(7).into_inner(), + ]); + + assert!(is_sorted_unique(paths.as_slice())); + assert_eq!(paths.as_slice().len(), 2); + } + + #[test] + fn shared_prefix_cluster_shares_prefix_and_diverges() { + let cluster = SharedPrefixCluster::from_prefix_bits([true, false, true, false], 4, 11); + + assert_eq!(cluster.prefix_len_bits, 4); + assert!(has_exact_cluster_prefix(&cluster)); + } + + #[test] + fn quickcheck_diverging_pair_invariants() { + fn property(pair: DivergingPair) -> bool { + if first_difference(&pair.left, &pair.right) != Some(pair.first_diff_bit) { + return false; + } + + pair.shrink().take(8).all(|shrunk| { + first_difference(&shrunk.left, &shrunk.right) == Some(shrunk.first_diff_bit) + }) + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(DivergingPair) -> bool); + } + + #[test] + fn quickcheck_shared_prefix_cluster_invariants() { + fn property(cluster: SharedPrefixCluster) -> bool { + if !has_exact_cluster_prefix(&cluster) { + return false; + } + + cluster + .shrink() + .take(8) + .all(|shrunk| has_exact_cluster_prefix(&shrunk)) + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(SharedPrefixCluster) -> bool); + } + + #[test] + fn quickcheck_unique_sorted_keypaths_invariants() { + fn property(paths: UniqueSortedKeyPaths) -> bool { + if !is_sorted_unique(paths.as_slice()) { + return false; + } + + paths + .shrink() + .take(8) + .all(|shrunk| is_sorted_unique(shrunk.as_slice())) + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(UniqueSortedKeyPaths) -> bool); + } +} diff --git a/nomt/Cargo.toml b/nomt/Cargo.toml index aa89a61018..4d057b046a 100644 --- a/nomt/Cargo.toml +++ b/nomt/Cargo.toml @@ -48,6 +48,7 @@ lazy_static.workspace = true hex.workspace = true quickcheck.workspace = true blake3.workspace = true +nomt-test-utils = { path = "../nomt-test-utils" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)'] } diff --git a/nomt/src/beatree/ops/bit_ops.rs b/nomt/src/beatree/ops/bit_ops.rs index 3c9dd3ea98..e9924b54cb 100644 --- a/nomt/src/beatree/ops/bit_ops.rs +++ b/nomt/src/beatree/ops/bit_ops.rs @@ -393,6 +393,69 @@ mod tests { Key, }; use bitvec::{prelude::Msb0, view::BitView}; + use nomt_test_utils::{DivergingPair, TestKeyPath}; + use quickcheck::{Arbitrary, Gen, QuickCheck}; + + const EDGE_BIT_LENS: &[usize] = &[0, 1, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255, 256]; + + #[derive(Clone, Debug)] + struct ReconstructCase { + prefix_bytes: Option>, + prefix_bit_len: usize, + separator_bytes: Vec, + separator_bit_start: usize, + separator_bit_len: usize, + } + + impl Arbitrary for ReconstructCase { + fn arbitrary(g: &mut Gen) -> Self { + let use_prefix = bool::arbitrary(g); + let prefix_bit_len = if use_prefix { + arbitrary_bit_len(g, 256) + } else { + 0 + }; + let prefix_byte_len = (prefix_bit_len + 7) / 8; + let prefix_seed = TestKeyPath::arbitrary(g).into_inner(); + let prefix_bytes = use_prefix.then(|| prefix_seed[..prefix_byte_len].to_vec()); + + let separator_bit_start = if bool::arbitrary(g) { + [0, 1, 6, 7][usize::arbitrary(g) % 4] + } else { + usize::arbitrary(g) % 8 + }; + let separator_bit_len = arbitrary_bit_len(g, 256 - prefix_bit_len); + let separator_byte_len = + ((separator_bit_start + separator_bit_len + 7) / 8).next_multiple_of(8); + let separator_seed = TestKeyPath::arbitrary(g).into_inner(); + let mut separator_bytes = vec![0; separator_byte_len]; + for (index, byte) in separator_bytes.iter_mut().enumerate() { + *byte = + separator_seed[index % separator_seed.len()] ^ (index as u8).wrapping_mul(29); + } + + Self { + prefix_bytes, + prefix_bit_len, + separator_bytes, + separator_bit_start, + separator_bit_len, + } + } + } + + fn arbitrary_bit_len(g: &mut Gen, max: usize) -> usize { + // EDGE_BIT_LENS is sorted ascending, so a partition point gives the slice of + // entries `<= max` without allocating. + let valid_count = EDGE_BIT_LENS.partition_point(|len| *len <= max); + let interesting = &EDGE_BIT_LENS[..valid_count]; + + if bool::arbitrary(g) { + interesting[usize::arbitrary(g) % interesting.len()] + } else { + usize::arbitrary(g) % (max + 1) + } + } fn reference_reconstruct_key(maybe_prefix: Option, separator: RawSeparators) -> Key { let mut key = [0; 32]; @@ -628,4 +691,84 @@ mod tests { assert_eq!(expected_res, res); } } + + #[test] + fn property_reconstruct_key_matches_reference() { + fn property(case: ReconstructCase) -> bool { + let maybe_prefix = case + .prefix_bytes + .as_ref() + .map(|bytes| (&bytes[..], case.prefix_bit_len)); + let separator = ( + &case.separator_bytes[..], + case.separator_bit_start, + case.separator_bit_len, + ); + + assert_eq!( + reference_reconstruct_key(maybe_prefix, separator), + super::reconstruct_key(maybe_prefix, separator), + ); + true + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(ReconstructCase) -> bool); + } + + #[test] + fn property_prefix_len_matches_reference() { + fn property(a: TestKeyPath, b: TestKeyPath) -> bool { + let a = a.into_inner(); + let b = b.into_inner(); + + assert_eq!(reference_prefix_len(&a, &b), super::prefix_len(&a, &b)); + true + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(TestKeyPath, TestKeyPath) -> bool); + } + + #[test] + fn property_separator_len_matches_reference() { + fn property(key: TestKeyPath) -> bool { + let key = key.into_inner(); + let expected = if key == [0u8; 32] { + 1 + } else { + 256 - key.view_bits::().trailing_zeros() + }; + + assert_eq!(expected, super::separator_len(&key)); + true + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(TestKeyPath) -> bool); + } + + #[test] + fn property_separate_matches_reference() { + fn property(pair: DivergingPair) -> bool { + let (lower, upper) = if pair.left < pair.right { + (pair.left, pair.right) + } else { + (pair.right, pair.left) + }; + + assert_eq!( + reference_separate(&lower, &upper), + super::separate(&lower, &upper) + ); + true + } + + QuickCheck::new() + .tests(64) + .quickcheck(property as fn(DivergingPair) -> bool); + } } diff --git a/nomt/src/merkle/worker.rs b/nomt/src/merkle/worker.rs index bc159b5ba2..cb6f060983 100644 --- a/nomt/src/merkle/worker.rs +++ b/nomt/src/merkle/worker.rs @@ -248,7 +248,7 @@ fn update( } => { let ops = subtrie_ops(&shared.read_write[range_start..range_end]); let ops = nomt_core::update::leaf_ops_spliced(prev_terminal, &ops); - root_page_updater.advance_and_replace(&page_set, trie_pos.clone(), ops.clone()); + root_page_updater.advance_and_replace(&page_set, trie_pos.clone(), ops); } } } @@ -297,10 +297,10 @@ impl RangeUpdater { .binary_search_by_key(&key_range_start, |x| x.0) .unwrap_or_else(|i| i); + // `key_range_end` is inclusive for the worker region, so compute an upper bound here. let range_end = shared .read_write - .binary_search_by_key(&key_range_end, |x| x.0) - .unwrap_or_else(|i| i); + .partition_point(|(key, _)| *key <= key_range_end); RangeUpdater { shared, diff --git a/nomt/tests/add_remove.rs b/nomt/tests/add_remove.rs index 48647fcb85..28aae763c3 100644 --- a/nomt/tests/add_remove.rs +++ b/nomt/tests/add_remove.rs @@ -9,18 +9,19 @@ fn add_remove_1000() { let mut accounts = 0; let mut t = Test::new("add_remove"); + // These fixtures track the current `common::account_path` distribution. let expected_roots = [ hex!("0000000000000000000000000000000000000000000000000000000000000000"), - hex!("4a7a6fe118037086a49ff10484f4d80b0a9f31f1060eeb1c9f0162634604b0d9"), - hex!("7d5b013105d7b835225256f2233a458e1a158a53d20e0d3834886df89a26c27b"), - hex!("1a290e07bcacfb58ddcd0b9da348c740ca1bf87b05ed96752a1503ed7c187b69"), - hex!("5e9abfee6d927b084fed3e1306bbe65f0880d0b7de12522c38813014927f1336"), - hex!("57b39e06b2ee98dccd882033eb4136f5376699128b421c83bdc7c6ca96168938"), - hex!("7fd75809ef0e2133102eb5e31e47cb577149dcaebb42cddeb2fd6754256b365f"), - hex!("7c00cb11ec8262385078613e7b7977e50b0751f8cb2384fdccc048eea02acb63"), - hex!("516d6911c3b0a36c9227922ca0273a4aee44886201bd186f7ee7e538a769eaa5"), - hex!("381b24719ff91b13d36cf0dd7622f391f4a461452ed7547a46a992ee4a4025aa"), - hex!("207793e2ce76c1feb68c7259f883229f985706c8cc2fcf99f481b622a54ba375"), + hex!("18a457ed03d9d28f8bf84aa05dc3c8005e35564e91d05bf6131094d4f3398528"), + hex!("51c06663bc9f0e754ada8512e3b4f97958007cd37087084ac3b80417e6b7d0be"), + hex!("4ea35ff714b076a9467442f65e21ec888651f5120966f2681393e63dfd12645a"), + hex!("38e03f8eda1c1d47cad38420bcf467cd9bb08a8be3a81606194db53a94e988ab"), + hex!("7849ef9330e40dbe28b2005208c5af476ec43d10695df9b21abeec9df2194667"), + hex!("6d1de05e86b0448033a7a8a0d500fcb381396bfded615670b423defad34ce5a8"), + hex!("12c9d4879de20229f771bd62d92b860ee1f18cbef04b985625c7a473a6ef708e"), + hex!("74b43826e82d1c1ee9f7cd1e260bfdedb9db55504a34e7ddb86552fad5d11f8b"), + hex!("4afa73ab74926485eb4322b9c7f58cb70085b17c20cdc31f35017af9ee45a019"), + hex!("542b4aae8ba2e202184b04becaed29db107680cf3c58b54d7f2ff615bfbce458"), ]; let mut root = Node::default(); diff --git a/nomt/tests/common/mod.rs b/nomt/tests/common/mod.rs index a459ea035f..eafde17b1f 100644 --- a/nomt/tests/common/mod.rs +++ b/nomt/tests/common/mod.rs @@ -1,46 +1,22 @@ use nomt::{ trie::{KeyPath, Node}, - KeyReadWrite, Nomt, Options, Overlay, PanicOnSyncMode, Root, Session, SessionParams, Witness, - WitnessMode, + KeyReadWrite, Nomt, Options, Overlay, PanicOnSyncMode, Root, Session, SessionParams, Value, + Witness, WitnessMode, }; use nomt_core::proof::PathProof; +use nomt_test_utils::{DivergingPair, SharedPrefixCluster, TestKeyPath}; +use quickcheck::{Arbitrary, Gen}; use std::{ - collections::{hash_map::Entry, HashMap}, + collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, mem, path::{Path, PathBuf}, + sync::atomic::{AtomicUsize, Ordering}, }; -pub fn account_path(id: u64) -> KeyPath { - // KeyPaths must be uniformly distributed, but we don't want to spend time on a good hash. So - // the next best option is to use a PRNG seeded with the id. - use rand::{Rng as _, SeedableRng as _}; - let mut seed = [0; 16]; - seed[0..8].copy_from_slice(&id.to_le_bytes()); - let mut rng = rand_pcg::Lcg64Xsh32::from_seed(seed); - let mut path = KeyPath::default(); - for i in 0..4 { - path[i * 4..][..4].copy_from_slice(&rng.next_u32().to_le_bytes()); - } - path -} +static NEXT_TEST_ID: AtomicUsize = AtomicUsize::new(0); -/// Build a 32-byte key whose first `bit_depth` bits are 0 and whose bit at -/// MSB-position `bit_depth` is 1 iff `right`. Remaining bits are 0. -/// Two keys built with the same `bit_depth` and opposite `right` flags -/// differ in exactly one bit, at MSB-position `bit_depth`. -#[allow(dead_code)] -pub fn key_diverging_at(bit_depth: usize, right: bool) -> KeyPath { - assert!(bit_depth < 256); - let mut key = KeyPath::default(); - if right { - let byte = bit_depth / 8; - let bit_in_byte = 7 - (bit_depth % 8); - key[byte] = 1 << bit_in_byte; - } - key -} +pub use nomt_test_utils::{account_path, key_diverging_at}; -#[allow(dead_code)] pub fn expected_root(accounts: u64) -> Node { let mut ops = (0..accounts) .map(account_path) @@ -50,6 +26,256 @@ pub fn expected_root(accounts: u64) -> Node { nomt_core::update::build_trie::(0, ops, |_| {}) } +#[allow(dead_code)] +pub fn fresh_test_name(prefix: &str) -> String { + format!("{prefix}_{}", NEXT_TEST_ID.fetch_add(1, Ordering::Relaxed)) +} + +#[allow(dead_code)] +fn fill_missing_keys(g: &mut Gen, excluded: &mut BTreeSet, target: usize) -> Vec { + let mut out = Vec::with_capacity(target); + let attempt_budget = target.saturating_mul(8) + 8; + let mut attempts = 0; + while out.len() < target && attempts < attempt_budget { + let key = TestKeyPath::arbitrary(g).into_inner(); + if excluded.insert(key) { + out.push(key); + } + attempts += 1; + } + let mut fill_seed = 0u64; + while out.len() < target { + let key = account_path(fill_seed); + if excluded.insert(key) { + out.push(key); + } + fill_seed = fill_seed.wrapping_add(1); + } + out +} + +pub fn apply_accesses(t: &mut Test, accesses: &[(KeyPath, KeyReadWrite)]) { + for (key, access) in accesses { + match access { + KeyReadWrite::Read(expected) => { + assert_eq!(t.read(*key), *expected); + } + KeyReadWrite::Write(value) => t.write(*key, value.clone()), + KeyReadWrite::ReadThenWrite(expected, value) => { + assert_eq!(t.read(*key), *expected); + t.write(*key, value.clone()); + } + } + } +} + +#[allow(dead_code)] +pub fn arbitrary_small_value(g: &mut Gen) -> Value { + let len = usize::arbitrary(g) % 9; + let mut value = Vec::with_capacity(len); + for _ in 0..len { + value.push(u8::arbitrary(g)); + } + value +} + +#[allow(dead_code)] +pub fn arbitrary_optional_small_value(g: &mut Gen) -> Option { + if bool::arbitrary(g) { + Some(arbitrary_small_value(g)) + } else { + None + } +} + +#[allow(dead_code)] +pub fn arbitrary_interesting_keys(g: &mut Gen, max_len: usize) -> Vec { + let target_len = usize::arbitrary(g) % (max_len + 1); + let mut keys = BTreeSet::new(); + + if target_len >= 2 && bool::arbitrary(g) { + let pair = DivergingPair::arbitrary(g); + keys.insert(pair.left); + keys.insert(pair.right); + } + + if keys.len() < target_len && target_len >= 2 && bool::arbitrary(g) { + let cluster = SharedPrefixCluster::arbitrary(g); + for member in cluster.members { + if keys.len() >= target_len { + break; + } + keys.insert(member); + } + } + + while keys.len() < target_len { + keys.insert(TestKeyPath::arbitrary(g).into_inner()); + } + + keys.into_iter().collect() +} + +#[allow(dead_code)] +pub fn shuffle(items: &mut [T], g: &mut Gen) { + if items.len() < 2 { + return; + } + + for i in (1..items.len()).rev() { + let j = usize::arbitrary(g) % (i + 1); + items.swap(i, j); + } +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct SessionAccessCase { + pub prev_data: Vec<(KeyPath, Value)>, + pub accesses: Vec<(KeyPath, KeyReadWrite)>, +} + +#[allow(dead_code)] +impl SessionAccessCase { + pub fn prev_data_with_options(&self) -> Vec<(KeyPath, Option)> { + self.prev_data + .iter() + .map(|(key, value)| (*key, Some(value.clone()))) + .collect() + } + + pub fn expected_final_state(&self) -> BTreeMap { + let mut state = self + .prev_data + .iter() + .map(|(key, value)| (*key, value.clone())) + .collect::>(); + + for (key, access) in &self.accesses { + match access { + KeyReadWrite::Read(_) => {} + KeyReadWrite::Write(value) | KeyReadWrite::ReadThenWrite(_, value) => { + if let Some(value) = value { + state.insert(*key, value.clone()); + } else { + state.remove(key); + } + } + } + } + + state + } +} + +impl Arbitrary for SessionAccessCase { + fn arbitrary(g: &mut Gen) -> Self { + let mut prev_state = BTreeMap::new(); + for key in arbitrary_interesting_keys(g, 6) { + prev_state.insert(key, arbitrary_small_value(g)); + } + + let mut all_keys = prev_state.keys().copied().collect::>(); + let missing_target = 1 + (usize::arbitrary(g) % 4); + let missing_keys = fill_missing_keys(g, &mut all_keys, missing_target); + + let mut accesses = Vec::new(); + for (key, value) in &prev_state { + if bool::arbitrary(g) { + accesses.push((*key, arbitrary_present_access(g, value.clone()))); + } + } + let allow_missing_read_then_write = !prev_state.is_empty(); + for key in missing_keys { + if bool::arbitrary(g) { + accesses.push(( + key, + arbitrary_missing_access(g, allow_missing_read_then_write), + )); + } + } + + if accesses.is_empty() { + if let Some((key, value)) = prev_state.iter().next() { + accesses.push((*key, arbitrary_present_access(g, value.clone()))); + } else { + accesses.push(( + TestKeyPath::arbitrary(g).into_inner(), + arbitrary_missing_access(g, false), + )); + } + } + + shuffle(&mut accesses, g); + + Self { + prev_data: prev_state.into_iter().collect(), + accesses, + } + } +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct ProofCase { + pub state: Vec<(KeyPath, Value)>, + pub present_samples: Vec, + pub missing_samples: Vec, +} + +impl Arbitrary for ProofCase { + fn arbitrary(g: &mut Gen) -> Self { + let state_keys = arbitrary_interesting_keys(g, 8); + let mut state = state_keys + .iter() + .map(|key| (*key, arbitrary_small_value(g))) + .collect::>(); + state.sort_by_key(|(key, _)| *key); + + let mut present_samples = state_keys + .iter() + .copied() + .filter(|_| bool::arbitrary(g)) + .collect::>(); + if present_samples.is_empty() && !state_keys.is_empty() { + present_samples.push(state_keys[0]); + } + + let mut used_keys = state_keys.into_iter().collect::>(); + let missing_target = 1 + (usize::arbitrary(g) % 4); + let mut missing_samples = fill_missing_keys(g, &mut used_keys, missing_target); + + shuffle(&mut present_samples, g); + shuffle(&mut missing_samples, g); + + Self { + state, + present_samples, + missing_samples, + } + } +} + +fn arbitrary_present_access(g: &mut Gen, prior: Value) -> KeyReadWrite { + match u8::arbitrary(g) % 3 { + 0 => KeyReadWrite::Read(Some(prior)), + 1 => KeyReadWrite::Write(arbitrary_optional_small_value(g)), + _ => KeyReadWrite::ReadThenWrite(Some(prior), arbitrary_optional_small_value(g)), + } +} + +fn arbitrary_missing_access(g: &mut Gen, allow_read_then_write: bool) -> KeyReadWrite { + match if allow_read_then_write { + u8::arbitrary(g) % 3 + } else { + u8::arbitrary(g) % 2 + } { + 0 => KeyReadWrite::Read(None), + 1 => KeyReadWrite::Write(Some(arbitrary_small_value(g))), + _ => KeyReadWrite::ReadThenWrite(None, Some(arbitrary_small_value(g))), + } +} + fn opts(path: PathBuf) -> Options { let mut opts = Options::new(); opts.path(path); diff --git a/nomt/tests/compute_root.rs b/nomt/tests/compute_root.rs index ac943bcb49..db0111d5e3 100644 --- a/nomt/tests/compute_root.rs +++ b/nomt/tests/compute_root.rs @@ -1,10 +1,13 @@ mod common; -use common::{account_path, key_diverging_at, Test}; +use common::{ + account_path, apply_accesses, fresh_test_name, key_diverging_at, SessionAccessCase, Test, +}; use nomt::{hasher::Blake3Hasher, trie::NodeKind, KeyReadWrite, Value}; use nomt_core::hasher::ValueHasher; use nomt_core::proof::{verify_multi_proof, verify_multi_proof_update, MultiProof}; use nomt_core::trie::{KeyPath, Node, ValueHash}; +use quickcheck::QuickCheck; use std::path::Path; #[test] @@ -378,3 +381,104 @@ fn many_keys_deletes_only() { .collect(); test_root_match_with_inputs("many_keys_deletes_only", prev, &accesses); } + +#[test] +fn property_generated_roots_match() { + fn property(case: SessionAccessCase) -> bool { + let prev_data = case.prev_data_with_options(); + test_root_match_with_inputs( + fresh_test_name("compute_root_prop"), + prev_data, + &case.accesses, + ); + true + } + + QuickCheck::new() + .tests(24) + .quickcheck(property as fn(SessionAccessCase) -> bool); +} + +#[test] +fn property_post_commit_state_matches_reference() { + fn property(case: SessionAccessCase) -> bool { + let mut t = Test::new(fresh_test_name("state_oracle_prop")); + + for (key, value) in &case.prev_data { + t.write(*key, Some(value.clone())); + } + t.commit(); + + apply_accesses(&mut t, &case.accesses); + let (root, _) = t.commit(); + + let expected_state = case.expected_final_state(); + + for (key, expected_value) in &expected_state { + assert_eq!( + t.read(*key).as_ref(), + Some(expected_value), + "post-commit read mismatch for present key" + ); + } + + for (key, _) in &case.accesses { + if !expected_state.contains_key(key) { + assert_eq!( + t.read(*key), + None, + "expected key to be absent after committed deletes" + ); + } + } + + let mut ops = expected_state + .iter() + .map(|(k, v)| (*k, Blake3Hasher::hash_value(v))) + .collect::>(); + ops.sort_by_key(|(k, _)| *k); + let reference_root = nomt_core::update::build_trie::(0, ops, |_| {}); + + assert_eq!( + root.into_inner(), + reference_root, + "Nomt root disagrees with build_trie oracle" + ); + + true + } + + QuickCheck::new() + .tests(16) + .quickcheck(property as fn(SessionAccessCase) -> bool); +} + +#[test] +fn property_root_invariant_under_reopen() { + fn property(case: SessionAccessCase) -> bool { + let name = fresh_test_name("reopen_root_prop"); + + let pre_close_root = { + let mut t = Test::new(&name); + for (key, value) in &case.prev_data { + t.write(*key, Some(value.clone())); + } + t.commit(); + apply_accesses(&mut t, &case.accesses); + t.commit().0 + }; + + let reopened = Test::new_with_params(&name, 1, 64_000, None, false); + assert_eq!( + reopened.root().into_inner(), + pre_close_root.into_inner(), + "root drifted across close/reopen" + ); + + true + } + + QuickCheck::new() + .tests(16) + .quickcheck(property as fn(SessionAccessCase) -> bool); +} diff --git a/nomt/tests/prove_in_session.rs b/nomt/tests/prove_in_session.rs index 159f9df6d5..437da832db 100644 --- a/nomt/tests/prove_in_session.rs +++ b/nomt/tests/prove_in_session.rs @@ -1,8 +1,12 @@ mod common; use bitvec::prelude::*; -use common::Test; -use nomt_core::trie::LeafData; +use common::{fresh_test_name, ProofCase, Test}; +use nomt::hasher::Blake3Hasher; +use nomt_core::hasher::ValueHasher; +use nomt_core::trie::{KeyPath, LeafData, Node}; +use quickcheck::QuickCheck; +use std::collections::{BTreeMap, BTreeSet}; #[test] fn prove_in_session() { @@ -135,3 +139,97 @@ fn prove_in_session_no_cache() { .unwrap()); } } + +#[test] +fn property_generated_proofs_verify() { + fn property(case: ProofCase) -> bool { + let name = fresh_test_name("prove_prop"); + let sample_keys = case + .present_samples + .iter() + .chain(case.missing_samples.iter()) + .copied() + .collect::>() + .into_iter() + .collect::>(); + + let base_state = case + .state + .iter() + .map(|(key, value)| (*key, value.clone())) + .collect::>(); + + let base_root = { + let mut t = Test::new(&name); + for (key, value) in &case.state { + t.write(*key, Some(value.clone())); + } + let root = t.commit().0.into_inner(); + + assert_proofs_match(&t, root, &base_state, &sample_keys); + + let mut overlay_state = base_state.clone(); + if let Some(update_key) = overlay_state.keys().next().copied() { + let updated_value = vec![0xA5, overlay_state.len() as u8]; + t.write(update_key, Some(updated_value.clone())); + overlay_state.insert(update_key, updated_value); + } else { + let inserted_key = case.missing_samples[0]; + let inserted_value = vec![0x5A]; + t.write(inserted_key, Some(inserted_value.clone())); + overlay_state.insert(inserted_key, inserted_value); + } + + if overlay_state.len() > 1 { + let delete_key = overlay_state.keys().last().copied().unwrap(); + t.write(delete_key, None); + overlay_state.remove(&delete_key); + } + + let (overlay, _) = t.update(); + let overlay_root = overlay.root().into_inner(); + t.start_overlay_session([&overlay]); + assert_proofs_match(&t, overlay_root, &overlay_state, &sample_keys); + + root + }; + + let reopened = Test::new_with_params(&name, 1, 10_000, None, false); + assert_proofs_match(&reopened, base_root, &base_state, &sample_keys); + true + } + + QuickCheck::new() + .tests(12) + .quickcheck(property as fn(ProofCase) -> bool); +} + +fn assert_proofs_match( + test: &Test, + root: Node, + expected: &BTreeMap>, + sample_keys: &[KeyPath], +) { + for key in sample_keys { + let proof = test.prove(*key); + let verified = proof + .verify::(key.view_bits::(), root) + .expect("verification failed"); + + match expected.get(key) { + Some(value) => { + let expected_leaf = LeafData { + key_path: *key, + value_hash: Blake3Hasher::hash_value(value), + }; + assert_eq!( + proof.terminal.as_leaf_option().unwrap().value_hash, + expected_leaf.value_hash, + ); + assert!(verified.confirm_value(&expected_leaf).unwrap()); + assert!(proof.terminal.as_leaf_option().is_some()); + } + None => assert!(verified.confirm_nonexistence(key).unwrap()), + } + } +} diff --git a/nomt/tests/witness_check.rs b/nomt/tests/witness_check.rs index ca7d935c79..b9b1f0a93a 100644 --- a/nomt/tests/witness_check.rs +++ b/nomt/tests/witness_check.rs @@ -1,7 +1,18 @@ mod common; -use common::Test; +use common::{apply_accesses, fresh_test_name, SessionAccessCase, Test}; use nomt::{hasher::Blake3Hasher, proof, trie::LeafData}; +use quickcheck::QuickCheck; + +fn sort_updates_by_path(updates: &mut [proof::PathUpdate]) { + // Witness path proofs may be emitted in worker order rather than trie path order. + updates.sort_by(|a, b| { + a.inner + .path() + .partial_cmp(b.inner.path()) + .expect("verified witness paths should be comparable") + }); +} #[test] fn produced_witness_validity() { @@ -86,6 +97,7 @@ fn produced_witness_validity() { }); } } + sort_updates_by_path(&mut updates); assert_eq!( proof::verify_update::(prev_root.into_inner(), &updates).unwrap(), @@ -172,3 +184,75 @@ fn test_verify_update_with_identical_paths() { // the requirement of ascending keys. verify_update::(root.into_inner(), &updates).unwrap_err(); } + +#[test] +fn property_generated_witnesses_verify() { + fn property(case: SessionAccessCase) -> bool { + let mut t = Test::new(fresh_test_name("witness_prop")); + + for (key, value) in &case.prev_data { + t.write(*key, Some(value.clone())); + } + let (prev_root, _) = t.commit(); + + apply_accesses(&mut t, &case.accesses); + + let (new_root, witness) = t.commit(); + + let mut updates = Vec::new(); + for (i, witnessed_path) in witness.path_proofs.iter().enumerate() { + let verified = witnessed_path + .inner + .verify::(&witnessed_path.path.path(), prev_root.into_inner()) + .unwrap(); + + for read in witness + .operations + .reads + .iter() + .skip_while(|r| r.path_index != i) + .take_while(|r| r.path_index == i) + { + match read.value { + None => assert!(verified.confirm_nonexistence(&read.key).unwrap()), + Some(ref value_hash) => { + let leaf = LeafData { + key_path: read.key, + value_hash: *value_hash, + }; + assert!(verified.confirm_value(&leaf).unwrap()); + } + } + } + + let mut write_ops = Vec::new(); + for write in witness + .operations + .writes + .iter() + .skip_while(|w| w.path_index != i) + .take_while(|w| w.path_index == i) + { + write_ops.push((write.key, write.value.clone())); + } + + if !write_ops.is_empty() { + updates.push(proof::PathUpdate { + inner: verified, + ops: write_ops, + }); + } + } + sort_updates_by_path(&mut updates); + + assert_eq!( + proof::verify_update::(prev_root.into_inner(), &updates).unwrap(), + new_root.into_inner(), + ); + true + } + + QuickCheck::new() + .tests(16) + .quickcheck(property as fn(SessionAccessCase) -> bool); +}