use crate::config::TreeConfig;
use crate::node::ProllyNode;
use crate::storage::NodeStorage;
use std::collections::VecDeque;
use std::hash::Hasher;
use twox_hash::XxHash64;
const HASH_SEED: u64 = 0;
pub trait Splitter {
fn append(&mut self, key: &[u8], value: &[u8]);
fn crossed_boundary(&self) -> bool;
fn reset(&mut self);
}
pub struct RollingHashSplitter {
base: u64,
modulus: u64,
min_chunk_size: usize,
pattern: u64,
base_exp_min: u64,
window: VecDeque<(u64, u64)>,
hash: u64,
count: usize,
crossed: bool,
}
impl RollingHashSplitter {
pub fn new<const N: usize>(config: &TreeConfig<N>) -> Self {
let base_exp_min = mod_exp(config.base, config.min_chunk_size as u64, config.modulus);
Self {
base: config.base,
modulus: config.modulus,
min_chunk_size: config.min_chunk_size,
pattern: config.pattern,
base_exp_min,
window: VecDeque::with_capacity(config.min_chunk_size),
hash: 0,
count: 0,
crossed: false,
}
}
}
impl Splitter for RollingHashSplitter {
fn append(&mut self, key: &[u8], value: &[u8]) {
if self.crossed {
return;
}
let kh = hash_item(key, self.modulus);
let vh = hash_item(value, self.modulus);
self.count += 1;
if self.window.len() < self.min_chunk_size {
self.hash = self
.hash
.wrapping_mul(self.base)
.wrapping_add(kh)
.wrapping_add(vh)
% self.modulus;
self.window.push_back((kh, vh));
} else {
let (old_kh, old_vh) = self.window.pop_front().unwrap();
let mut h = self
.hash
.wrapping_mul(self.base)
.wrapping_add(kh)
.wrapping_add(vh)
% self.modulus;
h = (h + self.modulus - (old_kh.wrapping_mul(self.base_exp_min)) % self.modulus)
% self.modulus;
h = (h + self.modulus - (old_vh.wrapping_mul(self.base_exp_min)) % self.modulus)
% self.modulus;
self.hash = h;
self.window.push_back((kh, vh));
}
if self.count >= self.min_chunk_size && (self.hash & self.pattern) == self.pattern {
self.crossed = true;
}
}
fn crossed_boundary(&self) -> bool {
self.crossed
}
fn reset(&mut self) {
self.window.clear();
self.hash = 0;
self.count = 0;
self.crossed = false;
}
}
fn hash_item(item: &[u8], modulus: u64) -> u64 {
let mut hasher = XxHash64::with_seed(HASH_SEED);
hasher.write(item);
hasher.finish() % modulus
}
fn mod_exp(mut base: u64, mut exp: u64, modulus: u64) -> u64 {
let mut result: u64 = 1;
base %= modulus;
while exp > 0 {
if exp & 1 == 1 {
result = (result.wrapping_mul(base)) % modulus;
}
exp >>= 1;
base = (base.wrapping_mul(base)) % modulus;
}
result
}
pub struct NodeBuilder<const N: usize> {
pub keys: Vec<Vec<u8>>,
pub values: Vec<Vec<u8>>,
level: u8,
is_leaf: bool,
config: TreeConfig<N>,
}
impl<const N: usize> NodeBuilder<N> {
pub fn new(config: &TreeConfig<N>, level: u8) -> Self {
Self {
keys: Vec::new(),
values: Vec::new(),
level,
is_leaf: level == 0,
config: config.clone(),
}
}
pub fn add(&mut self, key: Vec<u8>, value: Vec<u8>) {
self.keys.push(key);
self.values.push(value);
}
pub fn count(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn build(&mut self) -> ProllyNode<N> {
ProllyNode {
keys: std::mem::take(&mut self.keys),
key_schema: self.config.key_schema.clone(),
values: std::mem::take(&mut self.values),
value_schema: self.config.value_schema.clone(),
is_leaf: self.is_leaf,
level: self.level,
base: self.config.base,
modulus: self.config.modulus,
min_chunk_size: self.config.min_chunk_size,
max_chunk_size: self.config.max_chunk_size,
pattern: self.config.pattern,
split: false,
merged: false,
encode_types: Vec::new(),
encode_values: Vec::new(),
}
}
}
pub struct Chunker<'s, const N: usize, S: NodeStorage<N>> {
builder: NodeBuilder<N>,
splitter: RollingHashSplitter,
parent: Option<Box<Chunker<'s, N, S>>>,
config: TreeConfig<N>,
level: u8,
last_emit_hash: Option<crate::digest::ValueDigest<N>>,
_marker: std::marker::PhantomData<&'s mut S>,
}
impl<'s, const N: usize, S: NodeStorage<N>> Chunker<'s, N, S> {
pub fn new(config: &TreeConfig<N>, level: u8) -> Self {
Self {
builder: NodeBuilder::new(config, level),
splitter: RollingHashSplitter::new(config),
parent: None,
config: config.clone(),
level,
last_emit_hash: None,
_marker: std::marker::PhantomData,
}
}
pub fn add_pair(&mut self, storage: &mut S, key: Vec<u8>, value: Vec<u8>) {
self.append(storage, key, value);
}
pub fn take_last_emit_hash(&mut self) -> Option<crate::digest::ValueDigest<N>> {
self.last_emit_hash.take()
}
pub fn is_at_boundary(&self) -> bool {
self.builder.is_empty()
}
pub fn append_subtree_at_parent_level(
&mut self,
storage: &mut S,
first_key: Vec<u8>,
child_hash_bytes: Vec<u8>,
) {
debug_assert!(
self.is_at_boundary(),
"fast-forward append requires a freshly emitted boundary at this level"
);
if self.parent.is_none() {
self.parent = Some(Box::new(Chunker::new(&self.config, self.level + 1)));
}
let parent = self.parent.as_mut().expect("parent created");
parent.append(storage, first_key, child_hash_bytes);
}
fn append(&mut self, storage: &mut S, key: Vec<u8>, value: Vec<u8>) {
self.last_emit_hash = None;
self.splitter.append(&key, &value);
self.builder.add(key, value);
let degenerate_internal = self.level > 0 && self.builder.count() < 2;
let at_max = self.builder.count() >= self.config.max_chunk_size;
if (self.splitter.crossed_boundary() || at_max) && !degenerate_internal {
self.handle_boundary(storage);
}
}
fn handle_boundary(&mut self, storage: &mut S) {
debug_assert!(
self.builder.count() > 0,
"in-progress chunk must be non-empty at a boundary"
);
let first_key = self
.builder
.keys
.first()
.cloned()
.expect("non-empty builder");
let node = self.builder.build();
let hash = node.get_hash();
let _ = storage.insert_node(hash.clone(), node);
if self.parent.is_none() {
self.parent = Some(Box::new(Chunker::new(&self.config, self.level + 1)));
}
let parent = self.parent.as_mut().expect("parent created");
parent.append(storage, first_key, hash.as_bytes().to_vec());
self.splitter.reset();
self.last_emit_hash = Some(hash);
}
pub fn done(mut self, storage: &mut S) -> ProllyNode<N> {
if self.builder.count() == 0 {
if let Some(parent) = self.parent.take() {
return parent.done(storage);
}
return NodeBuilder::<N>::new(&self.config, 0).build();
}
if self.parent.is_some() {
let first_key = self.builder.keys.first().cloned().expect("non-empty");
let node = self.builder.build();
let hash = node.get_hash();
let _ = storage.insert_node(hash.clone(), node);
let mut parent = self.parent.take().expect("parent present");
parent.append(storage, first_key, hash.as_bytes().to_vec());
return parent.done(storage);
}
let root = self.builder.build();
let root_hash = root.get_hash();
let _ = storage.insert_node(root_hash, root.clone());
let mut node = root;
while !node.is_leaf && node.values.len() == 1 {
let child_digest = crate::digest::ValueDigest::raw_hash(&node.values[0]);
match storage.get_node_by_hash(&child_digest) {
Some(child) => node = (*child).clone(),
None => break,
}
}
node
}
}
pub fn build_tree_from_sorted_pairs<const N: usize, S: NodeStorage<N>>(
pairs: impl IntoIterator<Item = (Vec<u8>, Vec<u8>)>,
config: &TreeConfig<N>,
storage: &mut S,
) -> ProllyNode<N> {
let mut chunker = Chunker::<N, S>::new(config, 0);
for (k, v) in pairs {
chunker.add_pair(storage, k, v);
}
chunker.done(storage)
}
#[derive(Clone)]
pub struct NodeCursor<const N: usize> {
pub nd: ProllyNode<N>,
pub idx: i32,
pub parent: Option<Box<NodeCursor<N>>>,
}
impl<const N: usize> NodeCursor<N> {
pub fn at_start<S: NodeStorage<N>>(root: ProllyNode<N>, storage: &S) -> Self {
let mut cur = Self {
nd: root,
idx: 0,
parent: None,
};
while !cur.nd.is_leaf {
let child_hash_bytes = cur.nd.values[cur.idx as usize].clone();
let child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(&child_hash_bytes))
.map(|arc| (*arc).clone())
.expect("child reachable from cursor");
let parent = std::mem::replace(
&mut cur,
Self {
nd: child,
idx: 0,
parent: None,
},
);
cur.parent = Some(Box::new(parent));
}
cur
}
pub fn at_key<S: NodeStorage<N>>(root: ProllyNode<N>, target_key: &[u8], storage: &S) -> Self {
let mut cur = Self {
nd: root,
idx: 0,
parent: None,
};
loop {
if cur.nd.is_leaf {
let pos = match cur
.nd
.keys
.binary_search_by(|k| k.as_slice().cmp(target_key))
{
Ok(i) => i as i32,
Err(i) => i as i32,
};
cur.idx = pos;
return cur;
}
let i = cur
.nd
.keys
.iter()
.rposition(|k| target_key >= k.as_slice())
.unwrap_or(0);
cur.idx = i as i32;
let child_hash_bytes = cur.nd.values[i].clone();
let child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(&child_hash_bytes))
.map(|arc| (*arc).clone())
.expect("child reachable from cursor");
let parent = std::mem::replace(
&mut cur,
Self {
nd: child,
idx: 0,
parent: None,
},
);
cur.parent = Some(Box::new(parent));
}
}
pub fn valid(&self) -> bool {
!self.nd.keys.is_empty() && self.idx >= 0 && (self.idx as usize) < self.nd.keys.len()
}
pub fn current_key(&self) -> &[u8] {
&self.nd.keys[self.idx as usize]
}
pub fn current_value(&self) -> &[u8] {
&self.nd.values[self.idx as usize]
}
pub fn at_node_end(&self) -> bool {
(self.idx as usize) + 1 >= self.nd.keys.len()
}
fn has_next_in_node(&self) -> bool {
(self.idx as usize) + 1 < self.nd.keys.len()
}
fn invalidate_at_end(&mut self) {
self.idx = self.nd.keys.len() as i32;
}
fn out_of_bounds(&self) -> bool {
self.idx < 0 || (self.idx as usize) >= self.nd.keys.len()
}
fn skip_to_node_start(&mut self) {
self.idx = 0;
}
pub fn advance<S: NodeStorage<N>>(&mut self, storage: &S) {
if self.has_next_in_node() {
self.idx += 1;
return;
}
if self.parent.is_none() {
self.invalidate_at_end();
return;
}
let parent = self.parent.as_mut().expect("parent present");
parent.advance(storage);
if parent.out_of_bounds() {
self.invalidate_at_end();
return;
}
let child_hash_bytes = parent.nd.values[parent.idx as usize].clone();
let child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(&child_hash_bytes))
.map(|arc| (*arc).clone())
.expect("child reachable from cursor");
self.nd = child;
self.skip_to_node_start();
}
}
pub fn apply_mutations<const N: usize, S, I>(
root: ProllyNode<N>,
mutations: I,
config: &TreeConfig<N>,
storage: &mut S,
) -> ProllyNode<N>
where
S: NodeStorage<N>,
I: IntoIterator<Item = (Vec<u8>, Option<Vec<u8>>)>,
{
if root.is_leaf && root.keys.is_empty() {
let mut chunker = Chunker::<N, S>::new(config, 0);
for (k, opt_v) in mutations {
if let Some(v) = opt_v {
chunker.add_pair(storage, k, v);
}
}
return chunker.done(storage);
}
let mut muts_vec: Vec<(Vec<u8>, Option<Vec<u8>>)> = mutations.into_iter().collect();
if let Some(result) = try_pure_append(&root, &mut muts_vec, config, storage) {
return result;
}
let mut cur = NodeCursor::at_start(root, storage);
let mut chunker = Chunker::<N, S>::new(config, 0);
let mut muts = muts_vec.into_iter().peekable();
while cur.valid() {
let cur_key = cur.current_key().to_vec();
let mut consumed_cur = false;
while let Some((mk, _)) = muts.peek() {
match mk.as_slice().cmp(&cur_key) {
std::cmp::Ordering::Less => {
let (mk, mv) = muts.next().expect("peeked");
if let Some(v) = mv {
chunker.add_pair(storage, mk, v);
}
}
std::cmp::Ordering::Equal => {
let (_, mv) = muts.next().expect("peeked");
match mv {
Some(v) => chunker.add_pair(storage, cur_key.clone(), v),
None => { }
}
consumed_cur = true;
break;
}
std::cmp::Ordering::Greater => break,
}
}
let about_to_leave_leaf = cur.at_node_end() && muts.peek().is_none();
let leaf_hash_being_left = if about_to_leave_leaf {
Some(cur.nd.get_hash())
} else {
None
};
if !consumed_cur {
let v = cur.current_value().to_vec();
chunker.add_pair(storage, cur_key, v);
}
cur.advance(storage);
if let Some(old_hash) = leaf_hash_being_left {
if let Some(emit_hash) = chunker.take_last_emit_hash() {
if emit_hash == old_hash && chunker.is_at_boundary() {
fast_forward_to_end(&mut chunker, &mut cur, storage);
break;
}
}
}
}
for (k, opt_v) in muts {
if let Some(v) = opt_v {
chunker.add_pair(storage, k, v);
}
}
chunker.done(storage)
}
fn try_pure_append<const N: usize, S: NodeStorage<N>>(
root: &ProllyNode<N>,
muts: &mut Vec<(Vec<u8>, Option<Vec<u8>>)>,
config: &TreeConfig<N>,
storage: &mut S,
) -> Option<ProllyNode<N>> {
if muts.is_empty() {
return None;
}
if muts.iter().any(|(_, v)| v.is_none()) {
return None;
}
let max_key = tree_max_key(root, storage)?;
if muts[0].0.as_slice() <= max_key.as_slice() {
return None;
}
let mut chunker = Chunker::<N, S>::new(config, 0);
let mut last_leaf: Option<ProllyNode<N>> = None;
pure_append_walk(root, storage, &mut chunker, &mut last_leaf);
let last_leaf = last_leaf?;
for (k, v) in last_leaf.keys.iter().zip(last_leaf.values.iter()) {
chunker.add_pair(storage, k.clone(), v.clone());
}
for (k, opt_v) in muts.drain(..) {
let v = opt_v.expect("pre-checked: no deletes in pure-append batch");
chunker.add_pair(storage, k, v);
}
Some(chunker.done(storage))
}
fn pure_append_walk<const N: usize, S: NodeStorage<N>>(
node: &ProllyNode<N>,
storage: &mut S,
chunker: &mut Chunker<'_, N, S>,
last_leaf: &mut Option<ProllyNode<N>>,
) {
if node.is_leaf {
if node.keys.is_empty() {
return;
}
if let Some(prev) = last_leaf.take() {
let fk = prev.keys[0].clone();
let h = prev.get_hash().as_bytes().to_vec();
chunker.append_subtree_at_parent_level(storage, fk, h);
}
*last_leaf = Some(node.clone());
return;
}
for child_hash_bytes in &node.values {
let child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(child_hash_bytes))
.map(|arc| (*arc).clone())
.expect("child reachable");
pure_append_walk(&child, storage, chunker, last_leaf);
}
}
fn tree_max_key<const N: usize, S: NodeStorage<N>>(
root: &ProllyNode<N>,
storage: &S,
) -> Option<Vec<u8>> {
if root.keys.is_empty() {
return None;
}
if root.is_leaf {
return root.keys.last().cloned();
}
let last_child_hash_bytes = root.values.last()?.clone();
let last_child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(
&last_child_hash_bytes,
))
.map(|arc| (*arc).clone())?;
tree_max_key(&last_child, storage)
}
fn fast_forward_to_end<const N: usize, S: NodeStorage<N>>(
chunker: &mut Chunker<'_, N, S>,
cur: &mut NodeCursor<N>,
storage: &mut S,
) {
debug_assert_eq!(
chunker.level, 0,
"fast_forward_to_end currently supports leaf-level chunkers only"
);
debug_assert!(
chunker.is_at_boundary(),
"fast_forward_to_end requires the chunker to be at a boundary"
);
while cur.valid() {
let first_key = match cur.nd.keys.first() {
Some(k) => k.clone(),
None => return,
};
let leaf_hash = cur.nd.get_hash();
chunker.append_subtree_at_parent_level(storage, first_key, leaf_hash.as_bytes().to_vec());
if cur.parent.is_none() {
cur.invalidate_at_end();
return;
}
let parent = cur.parent.as_mut().expect("parent present");
let next_idx = parent.idx + 1;
if (next_idx as usize) >= parent.nd.values.len() {
parent.advance(storage);
if parent.out_of_bounds() {
cur.invalidate_at_end();
return;
}
let child_hash_bytes = parent.nd.values[parent.idx as usize].clone();
let child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(&child_hash_bytes))
.map(|arc| (*arc).clone())
.expect("child reachable");
cur.nd = child;
cur.idx = 0;
continue;
}
parent.idx = next_idx;
let child_hash_bytes = parent.nd.values[parent.idx as usize].clone();
let child = storage
.get_node_by_hash(&crate::digest::ValueDigest::raw_hash(&child_hash_bytes))
.map(|arc| (*arc).clone())
.expect("child reachable");
cur.nd = child;
cur.idx = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::InMemoryNodeStorage;
fn key(i: u64) -> Vec<u8> {
i.to_be_bytes().to_vec()
}
fn val(i: u64) -> Vec<u8> {
let mut v = Vec::with_capacity(16);
v.extend_from_slice(&i.to_be_bytes());
v.extend_from_slice(&(!i).to_be_bytes());
v
}
#[test]
fn empty_input_produces_empty_leaf() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let root = build_tree_from_sorted_pairs::<32, _>(std::iter::empty(), &cfg, &mut storage);
assert!(root.is_leaf);
assert!(root.keys.is_empty());
assert_eq!(root.level, 0);
}
#[test]
fn single_item_is_a_single_leaf() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let root = build_tree_from_sorted_pairs::<32, _>(
std::iter::once((key(0), val(0))),
&cfg,
&mut storage,
);
assert!(root.is_leaf);
assert_eq!(root.keys.len(), 1);
}
#[test]
fn many_items_below_min_stay_in_one_leaf() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let pairs: Vec<_> = (0..4u64).map(|i| (key(i), val(i))).collect();
let root = build_tree_from_sorted_pairs::<32, _>(pairs.clone(), &cfg, &mut storage);
assert!(root.is_leaf);
assert_eq!(root.keys.len(), pairs.len());
}
#[test]
fn root_hash_independent_of_iteration_order() {
let cfg = TreeConfig::<32>::default();
let mut s1 = InMemoryNodeStorage::<32>::default();
let mut s2 = InMemoryNodeStorage::<32>::default();
let pairs: Vec<_> = (0..256u64).map(|i| (key(i), val(i))).collect();
let r1 = build_tree_from_sorted_pairs::<32, _>(pairs.clone(), &cfg, &mut s1);
let r2 = build_tree_from_sorted_pairs::<32, _>(pairs.iter().cloned(), &cfg, &mut s2);
assert_eq!(r1.get_hash(), r2.get_hash());
}
#[test]
fn matches_node_build_canonical_from_pairs() {
let cfg = TreeConfig::<32>::default();
let pairs: Vec<_> = (0..1024u64).map(|i| (key(i), val(i))).collect();
let mut s_stream = InMemoryNodeStorage::<32>::default();
let root_stream = build_tree_from_sorted_pairs::<32, _>(pairs.clone(), &cfg, &mut s_stream);
let mut s_batch = InMemoryNodeStorage::<32>::default();
let root_batch = ProllyNode::<32>::build_canonical_from_pairs(pairs, &cfg, &mut s_batch);
assert_eq!(
root_stream.get_hash(),
root_batch.get_hash(),
"streaming chunker produced a different canonical root than ProllyNode::build_canonical_from_pairs"
);
}
#[test]
fn apply_mutations_against_empty_tree() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let empty = NodeBuilder::<32>::new(&cfg, 0).build();
let muts: Vec<(Vec<u8>, Option<Vec<u8>>)> =
(0..200u64).map(|i| (key(i), Some(val(i)))).collect();
let root = apply_mutations(empty, muts, &cfg, &mut storage);
let mut s2 = InMemoryNodeStorage::<32>::default();
let pairs: Vec<_> = (0..200u64).map(|i| (key(i), val(i))).collect();
let expected = build_tree_from_sorted_pairs::<32, _>(pairs, &cfg, &mut s2);
assert_eq!(root.get_hash(), expected.get_hash());
}
#[test]
fn apply_mutations_passes_through_unchanged_tree() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let pairs: Vec<_> = (0..500u64).map(|i| (key(i), val(i))).collect();
let original = build_tree_from_sorted_pairs::<32, _>(pairs, &cfg, &mut storage);
let original_hash = original.get_hash();
let no_muts: Vec<(Vec<u8>, Option<Vec<u8>>)> = vec![];
let result = apply_mutations(original, no_muts, &cfg, &mut storage);
assert_eq!(result.get_hash(), original_hash);
}
#[test]
fn apply_mutations_insert_into_middle() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let evens: Vec<_> = (0..100u64).map(|i| (key(2 * i), val(2 * i))).collect();
let original = build_tree_from_sorted_pairs::<32, _>(evens, &cfg, &mut storage);
let odds: Vec<(Vec<u8>, Option<Vec<u8>>)> = (0..100u64)
.map(|i| (key(2 * i + 1), Some(val(2 * i + 1))))
.collect();
let result = apply_mutations(original, odds, &cfg, &mut storage);
let mut s2 = InMemoryNodeStorage::<32>::default();
let pairs: Vec<_> = (0..200u64).map(|i| (key(i), val(i))).collect();
let expected = build_tree_from_sorted_pairs::<32, _>(pairs, &cfg, &mut s2);
assert_eq!(result.get_hash(), expected.get_hash());
}
#[test]
fn apply_mutations_delete_some_keys() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let all: Vec<_> = (0..200u64).map(|i| (key(i), val(i))).collect();
let original = build_tree_from_sorted_pairs::<32, _>(all, &cfg, &mut storage);
let dels: Vec<(Vec<u8>, Option<Vec<u8>>)> =
(0..100u64).map(|i| (key(2 * i + 1), None)).collect();
let result = apply_mutations(original, dels, &cfg, &mut storage);
let mut s2 = InMemoryNodeStorage::<32>::default();
let evens: Vec<_> = (0..100u64).map(|i| (key(2 * i), val(2 * i))).collect();
let expected = build_tree_from_sorted_pairs::<32, _>(evens, &cfg, &mut s2);
assert_eq!(result.get_hash(), expected.get_hash());
}
#[test]
fn apply_mutations_update_some_values() {
let cfg = TreeConfig::<32>::default();
let mut storage = InMemoryNodeStorage::<32>::default();
let pairs: Vec<_> = (0..200u64).map(|i| (key(i), val(i))).collect();
let original = build_tree_from_sorted_pairs::<32, _>(pairs, &cfg, &mut storage);
let updates: Vec<(Vec<u8>, Option<Vec<u8>>)> = (0..200u64)
.map(|i| (key(i), Some(val(i + 1_000_000))))
.collect();
let result = apply_mutations(original, updates, &cfg, &mut storage);
let mut s2 = InMemoryNodeStorage::<32>::default();
let expected_pairs: Vec<_> = (0..200u64).map(|i| (key(i), val(i + 1_000_000))).collect();
let expected = build_tree_from_sorted_pairs::<32, _>(expected_pairs, &cfg, &mut s2);
assert_eq!(result.get_hash(), expected.get_hash());
}
#[test]
fn degenerate_pattern_zero_does_not_cascade() {
let cfg = TreeConfig::<32> {
base: 4,
modulus: 64,
min_chunk_size: 1,
max_chunk_size: 4096,
pattern: 0,
root_hash: None,
key_schema: None,
value_schema: None,
encode_types: vec![],
};
let mut storage = InMemoryNodeStorage::<32>::default();
let root = build_tree_from_sorted_pairs::<32, _>(
(0u64..5).map(|i| (key(i), val(i))),
&cfg,
&mut storage,
);
assert!(root.level < 16, "tree level grew too tall: {}", root.level);
}
}