#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use std::collections::BTreeMap;
use proptest::prelude::*;
use super::mutate::{batch_mutate, set_test_target_size};
use super::node::{Hash, Node};
use crate::store::MemoryStore;
use crate::tree::cursor::load_node;
fn empty_tree() -> (MemoryStore, Hash) {
let mut store = MemoryStore::new();
let leaf = super::node::LeafNode::new(Vec::new()).unwrap();
let hash = store.put(&Node::Leaf(leaf));
(store, hash)
}
fn apply_ops(target_size: usize, ops: &[(Vec<u8>, Option<Vec<u8>>)]) -> Hash {
set_test_target_size(Some(target_size));
let (mut store, mut root) = empty_tree();
for (key, value) in ops {
let mutation = [(key.clone(), value.clone())];
root = batch_mutate(&mut store, root, &mutation).unwrap();
}
set_test_target_size(None);
root
}
fn collect_map(target_size: usize, root: Hash, ops: &[(Vec<u8>, Option<Vec<u8>>)]) -> ReadBack {
set_test_target_size(Some(target_size));
let (mut store, mut r) = empty_tree();
for (key, value) in ops {
let mutation = [(key.clone(), value.clone())];
r = batch_mutate(&mut store, r, &mutation).unwrap();
}
assert_eq!(r, root);
let mut map = BTreeMap::new();
let mut leaf_count = 0usize;
walk(&store, root, &mut map, &mut leaf_count);
set_test_target_size(None);
ReadBack {
map,
leaf_count,
root,
}
}
struct ReadBack {
map: BTreeMap<Vec<u8>, Vec<u8>>,
leaf_count: usize,
root: Hash,
}
fn walk(
store: &MemoryStore,
hash: Hash,
out: &mut BTreeMap<Vec<u8>, Vec<u8>>,
leaf_count: &mut usize,
) {
match &*load_node(store, hash).unwrap() {
Node::Leaf(leaf) => {
*leaf_count += 1;
for (k, v) in leaf.entries() {
out.insert(k.clone(), v.clone());
}
}
Node::Internal(internal) => {
for (_sep, child) in internal.children() {
walk(store, *child, out, leaf_count);
}
}
}
}
fn root_is_multi_leaf(target_size: usize, ops: &[(Vec<u8>, Option<Vec<u8>>)]) -> bool {
set_test_target_size(Some(target_size));
let (mut store, mut root) = empty_tree();
for (key, value) in ops {
let mutation = [(key.clone(), value.clone())];
root = batch_mutate(&mut store, root, &mutation).unwrap();
}
let result =
matches!(&*load_node(&store, root).unwrap(), Node::Internal(i) if i.children().len() > 1);
set_test_target_size(None);
result
}
fn key_value_set(
n_range: std::ops::Range<usize>,
) -> impl Strategy<Value = Vec<(Vec<u8>, Vec<u8>)>> {
proptest::collection::btree_map(
proptest::collection::vec(any::<u8>(), 1..6),
proptest::collection::vec(any::<u8>(), 0..6),
n_range,
)
.prop_map(|m| m.into_iter().collect::<Vec<_>>())
}
proptest! {
#![proptest_config(ProptestConfig { cases: 300, max_shrink_iters: 20000, ..ProptestConfig::default() })]
#[test]
fn prop_a_insertion_order_independence(
target_size in prop::sample::select(vec![4usize, 8, 16, 4096]),
pairs in key_value_set(1..200),
perm1 in any::<prop::sample::Index>(),
perm2 in any::<prop::sample::Index>(),
) {
let order1 = shuffle_with(&pairs, perm1.index(usize::MAX));
let order2 = shuffle_with(&pairs, perm2.index(usize::MAX).wrapping_add(1));
let ops1: Vec<(Vec<u8>, Option<Vec<u8>>)> =
order1.iter().map(|(k, v)| (k.clone(), Some(v.clone()))).collect();
let ops2: Vec<(Vec<u8>, Option<Vec<u8>>)> =
order2.iter().map(|(k, v)| (k.clone(), Some(v.clone()))).collect();
let h1 = apply_ops(target_size, &ops1);
let h2 = apply_ops(target_size, &ops2);
prop_assert_eq!(
h1, h2,
"insertion order changed the root hash (target_size={}, n={})\norder1={:?}\norder2={:?}",
target_size, pairs.len(), order1, order2
);
}
#[test]
fn prop_b_insert_delete_history_independence(
target_size in prop::sample::select(vec![4usize, 8, 16, 4096]),
final_pairs in key_value_set(1..120),
noise in proptest::collection::vec(
(proptest::collection::vec(any::<u8>(), 1..6), proptest::collection::vec(any::<u8>(), 0..6)),
0..80,
),
seed1 in any::<u64>(),
seed2 in any::<u64>(),
) {
let ops1 = build_sequence_to_map(&final_pairs, &noise, seed1);
let ops2 = build_sequence_to_map(&final_pairs, &noise, seed2);
let target: BTreeMap<Vec<u8>, Vec<u8>> = final_pairs.iter().cloned().collect();
prop_assert_eq!(&replay(&ops1), &target, "seq1 did not reach target map");
prop_assert_eq!(&replay(&ops2), &target, "seq2 did not reach target map");
let h1 = apply_ops(target_size, &ops1);
let h2 = apply_ops(target_size, &ops2);
prop_assert_eq!(
h1, h2,
"put/delete history changed the root hash (target_size={}, n={})\nops1={:?}\nops2={:?}",
target_size, final_pairs.len(), ops1, ops2
);
}
#[test]
fn prop_c_distinct_maps_distinct_roots(
target_size in prop::sample::select(vec![4usize, 8, 16, 4096]),
pairs in key_value_set(1..120),
extra_key in proptest::collection::vec(any::<u8>(), 1..6),
extra_val in proptest::collection::vec(any::<u8>(), 0..6),
) {
let map: BTreeMap<Vec<u8>, Vec<u8>> = pairs.iter().cloned().collect();
prop_assume!(!map.is_empty());
let ops_base: Vec<(Vec<u8>, Option<Vec<u8>>)> =
pairs.iter().map(|(k, v)| (k.clone(), Some(v.clone()))).collect();
let mut ops_diff = ops_base.clone();
let differs = if let Some(existing) = map.get(&extra_key) {
if existing == &extra_val { false }
else { ops_diff.push((extra_key, Some(extra_val))); true }
} else {
ops_diff.push((extra_key, Some(extra_val))); true
};
prop_assume!(differs);
let h_base = apply_ops(target_size, &ops_base);
let h_diff = apply_ops(target_size, &ops_diff);
prop_assert_ne!(h_base, h_diff, "two distinct maps produced the same root hash");
}
}
fn shuffle_with(pairs: &[(Vec<u8>, Vec<u8>)], seed: usize) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut v = pairs.to_vec();
let mut state = (seed as u64)
.wrapping_mul(0x9e37_79b9_7f4a_7c15)
.wrapping_add(1);
for i in (1..v.len()).rev() {
state = state
.wrapping_mul(0x5851_f42d_4c95_7f2d)
.wrapping_add(0x1405_7b7e_f767_814f);
let j = (state >> 33) as usize % (i + 1);
v.swap(i, j);
}
v
}
fn build_sequence_to_map(
final_pairs: &[(Vec<u8>, Vec<u8>)],
noise: &[(Vec<u8>, Vec<u8>)],
seed: u64,
) -> Vec<(Vec<u8>, Option<Vec<u8>>)> {
let final_map: BTreeMap<Vec<u8>, Vec<u8>> = final_pairs.iter().cloned().collect();
let noise: Vec<(Vec<u8>, Vec<u8>)> = noise
.iter()
.filter(|(k, _)| !final_map.contains_key(k))
.cloned()
.collect();
let order = shuffle_with(final_pairs, seed as usize);
let mut ops: Vec<(Vec<u8>, Option<Vec<u8>>)> = Vec::new();
let mut state = seed.wrapping_add(0xdead_beef);
let mut fi = 0usize;
let mut ni = 0usize;
while fi < order.len() || ni < noise.len() {
state = state.wrapping_mul(0x5851_f42d_4c95_7f2d).wrapping_add(1);
let pick_final = (state >> 40) & 1 == 0;
if (pick_final && fi < order.len()) || ni >= noise.len() {
let (k, v) = &order[fi];
ops.push((k.clone(), Some(v.clone())));
fi += 1;
} else if ni < noise.len() {
let (k, v) = &noise[ni];
ops.push((k.clone(), Some(v.clone())));
ni += 1;
}
}
for (idx, (k, v)) in order.iter().enumerate() {
if idx % 3 == 0 {
let mut wrong = v.clone();
wrong.push(0xff);
ops.push((k.clone(), Some(wrong)));
ops.push((k.clone(), Some(v.clone())));
}
}
for (k, _) in &noise {
ops.push((k.clone(), None));
}
ops
}
fn replay(ops: &[(Vec<u8>, Option<Vec<u8>>)]) -> BTreeMap<Vec<u8>, Vec<u8>> {
let mut map = BTreeMap::new();
for (k, v) in ops {
match v {
Some(value) => {
map.insert(k.clone(), value.clone());
}
None => {
map.remove(k);
}
}
}
map
}
#[test]
fn regression_minimal_insertion_order_counterexample() {
let order1: Vec<(Vec<u8>, Option<Vec<u8>>)> = [19u8, 1, 0, 8]
.iter()
.map(|&k| (vec![k], Some(vec![])))
.collect();
let order2: Vec<(Vec<u8>, Option<Vec<u8>>)> = [8u8, 19, 0, 1]
.iter()
.map(|&k| (vec![k], Some(vec![])))
.collect();
let h1 = apply_ops(4, &order1);
let h2 = apply_ops(4, &order2);
assert_eq!(
h1, h2,
"history-independence violated: {h1} (order [19,1,0,8]) != {h2} (order [8,19,0,1])"
);
}
#[test]
fn confirms_small_target_size_produces_multi_leaf_trees() {
let mut ops: Vec<(Vec<u8>, Option<Vec<u8>>)> = Vec::new();
for i in 0u32..120 {
ops.push((i.to_be_bytes().to_vec(), Some(vec![i as u8])));
}
assert!(
root_is_multi_leaf(4, &ops),
"expected a multi-leaf root at target_size=4 for 120 keys"
);
let root = apply_ops(4, &ops);
let read = collect_map(4, root, &ops);
assert!(
read.leaf_count > 1,
"expected >1 leaf, got {}",
read.leaf_count
);
assert_eq!(read.map.len(), 120);
assert_eq!(read.root, root);
println!(
"multi-leaf evidence: target_size=4, 120 keys -> {} leaves",
read.leaf_count
);
}