diff --git a/src/db.rs b/src/db.rs index af101a7..e81142a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -12,7 +12,7 @@ use std::{ sync::{Arc, Mutex}, }; -const HEADER_MAGIC: [u8; 9] = [b's', b'p', b'a', b'c', b'e', b':', b'/', b'/', b'.']; +const HEADER_MAGIC: [u8; 9] = *b"space://."; pub(crate) const CHUNK_SIZE: u64 = 4096; pub(crate) const HEADER_SIZE: u64 = CHUNK_SIZE * 2; diff --git a/src/tx.rs b/src/tx.rs index 5c2afae..6dfd3e1 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -518,9 +518,18 @@ impl ReadTransaction { left, right, } => { - // Exclude keys that are not in this subtree. - let end = keys.partition_point(|key| key.split_point(depth, *prefix).is_none()); - let keys = &keys[..end]; + // Exclude keys that are not in this subtree. Sorted keys that + // diverge below the prefix come before the matching band, ones + // that diverge above come after — trim both sides. + let start = keys.partition_point(|key| { + key.split_point(depth, *prefix) + .is_some_and(|p| key.direction(depth + p) == Direction::Left) + }); + let end = keys.partition_point(|key| { + key.split_point(depth, *prefix) + .is_none_or(|p| key.direction(depth + p) == Direction::Left) + }); + let keys = &keys[start..end]; // Keys are split based on their direction at the current depth. let depth = depth + prefix.bit_len(); @@ -611,6 +620,9 @@ impl ReadTransaction { } } + // `hash_index` is read directly only under the `hash-idx` feature; + // without it, clippy sees a recursion-only parameter. + #[cfg_attr(not(feature = "hash-idx"), allow(clippy::only_used_in_recursion))] fn hash_node<'c>( db: &Database, cache: &mut Cache, diff --git a/tests/batch_insert_proof.rs b/tests/batch_insert_proof.rs new file mode 100644 index 0000000..1c95239 --- /dev/null +++ b/tests/batch_insert_proof.rs @@ -0,0 +1,86 @@ +//! `prove(keys, Standard)` should return a subtree carrying enough of the tree +//! to insert those same keys. +use spacedb::db::Database; +use spacedb::subtree::ValueOrHash; +use spacedb::tx::ProofType; +use spacedb::{Hash, NodeHasher, Sha256Hasher}; + +fn key(tag: &str, i: usize) -> Hash { + Sha256Hasher::hash(format!("{}{}", tag, i).as_bytes()) +} + +/// In-memory tree pre-populated with `existing` keys. +fn tree_with(existing: usize) -> Database { + let db = Database::memory().expect("memory db"); + let mut tx = db.begin_write().expect("write tx"); + for i in 0..existing { + tx = tx + .insert(key("existing", i), b"v".to_vec()) + .expect("insert"); + } + tx.commit().expect("commit"); + db +} + +/// Prove `n` absent keys, then insert exactly those keys into the returned +/// subtree. Returns the index of the first insert that failed. +fn replay(db: &Database, n: usize) -> Option { + let keys: Vec = (0..n).map(|i| key("new", i)).collect(); + + let mut snapshot = db.begin_read().expect("read tx"); + let mut proof = snapshot + .prove(&keys, ProofType::Standard) + .expect("prove should succeed"); + + for (i, k) in keys.iter().enumerate() { + if proof.insert(*k, ValueOrHash::Hash(key("val", i))).is_err() { + return Some(i); + } + } + None +} + +/// A proof over N keys should support inserting those N keys. +#[test] +fn standard_proof_supports_inserting_its_own_keys() { + for existing in [2usize, 102, 1000] { + let db = tree_with(existing); + assert_eq!( + replay(&db, 900), + None, + "tree with {} existing keys: proving 900 absent keys returned a \ + subtree that cannot insert all 900", + existing + ); + } +} + +/// Proving a superset of keys must not support fewer insertions than proving +/// a subset — the larger proof strictly contains more of the tree. +#[test] +fn proving_more_keys_does_not_reduce_capability() { + let db = tree_with(102); + + // Largest batch that replays cleanly. + let mut largest = 0; + for n in 1..=512 { + if replay(&db, n).is_none() { + largest = n; + } else { + break; + } + } + assert!(largest > 0, "no batch size replayed at all"); + + // A proof over more keys must still handle at least that many inserts. + let failed_at = replay(&db, largest * 2); + assert!( + failed_at.is_none() || failed_at.unwrap() >= largest, + "proving {} keys supports {} inserts, but proving {} keys fails at \ + entry {} — a larger proof supports fewer insertions", + largest, + largest, + largest * 2, + failed_at.unwrap() + ); +}