Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
18 changes: 15 additions & 3 deletions src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,18 @@ impl<H: NodeHasher> ReadTransaction<H> {
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();
Expand Down Expand Up @@ -611,6 +620,9 @@ impl<H: NodeHasher> ReadTransaction<H> {
}
}

// `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<H>,
cache: &mut Cache,
Expand Down
86 changes: 86 additions & 0 deletions tests/batch_insert_proof.rs
Original file line number Diff line number Diff line change
@@ -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<Sha256Hasher> {
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<Sha256Hasher>, n: usize) -> Option<usize> {
let keys: Vec<Hash> = (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()
);
}
Loading