use blake2::{Blake2b512, Digest};
const TAG_EMPTY: &[u8] = b"nedb:state_root_v1:empty";
const TAG_NODE: &[u8] = b"nedb:state_root_v1:node";
const TAG_NS_LEAF: &[u8] = b"nedb:state_root_v1:namespace_leaf";
const TAG_NS_ROOT: &[u8] = b"nedb:state_root_v1:namespace_root";
const TAG_REC_LEAF: &[u8] = b"nedb:state_root_v1:record_leaf";
const TAG_REC_ROOT: &[u8] = b"nedb:state_root_v1:records_root";
const TAG_STATE_ROOT: &[u8] = b"nedb:state_root_v1:state_root";
pub type Digest32 = [u8; 32];
fn h(parts: &[&[u8]]) -> Digest32 {
let mut hasher = Blake2b512::new();
for p in parts {
hasher.update(p);
}
let out = hasher.finalize();
let mut d = [0u8; 32];
d.copy_from_slice(&out[..32]);
d
}
fn lp(buf: &mut Vec<u8>, bytes: &[u8]) {
buf.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
buf.extend_from_slice(bytes);
}
fn lp_opt(buf: &mut Vec<u8>, v: Option<&str>) {
match v {
None => buf.push(0),
Some(s) => {
buf.push(1);
lp(buf, s.as_bytes());
}
}
}
const V_NULL: u8 = 0;
const V_FALSE: u8 = 1;
const V_TRUE: u8 = 2;
const V_I64: u8 = 3;
const V_U64: u8 = 4;
const V_F64: u8 = 5;
const V_STR: u8 = 6;
const V_ARR: u8 = 7;
const V_OBJ: u8 = 8;
pub fn encode_value(buf: &mut Vec<u8>, v: &serde_json::Value) -> Result<(), String> {
match v {
serde_json::Value::Null => buf.push(V_NULL),
serde_json::Value::Bool(false) => buf.push(V_FALSE),
serde_json::Value::Bool(true) => buf.push(V_TRUE),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
buf.push(V_I64);
buf.extend_from_slice(&i.to_le_bytes());
} else if let Some(u) = n.as_u64() {
buf.push(V_U64);
buf.extend_from_slice(&u.to_le_bytes());
} else {
let f = n.as_f64().ok_or_else(|| format!("unrepresentable number: {}", n))?;
if f.is_nan() {
return Err("NaN cannot be committed to a state root".into());
}
buf.push(V_F64);
let f = if f == 0.0 { 0.0 } else { f };
buf.extend_from_slice(&f.to_bits().to_le_bytes());
}
}
serde_json::Value::String(s) => {
buf.push(V_STR);
lp(buf, s.as_bytes());
}
serde_json::Value::Array(items) => {
buf.push(V_ARR);
buf.extend_from_slice(&(items.len() as u64).to_le_bytes());
for it in items {
encode_value(buf, it)?;
}
}
serde_json::Value::Object(map) => {
buf.push(V_OBJ);
buf.extend_from_slice(&(map.len() as u64).to_le_bytes());
for (k, val) in map {
lp(buf, k.as_bytes());
encode_value(buf, val)?;
}
}
}
Ok(())
}
pub fn namespace_leaf(name: &str) -> Digest32 {
let mut buf = Vec::new();
lp(&mut buf, name.as_bytes());
h(&[TAG_NS_LEAF, &buf])
}
pub fn record_leaf(
coll: &str,
id: &str,
data: &serde_json::Value,
valid_from: Option<&str>,
valid_to: Option<&str>,
) -> Result<Digest32, String> {
let mut buf = Vec::new();
lp(&mut buf, coll.as_bytes());
lp(&mut buf, id.as_bytes());
lp_opt(&mut buf, valid_from);
lp_opt(&mut buf, valid_to);
encode_value(&mut buf, data)?;
Ok(h(&[TAG_REC_LEAF, &buf]))
}
fn fold(mut level: Vec<Digest32>) -> Digest32 {
if level.is_empty() {
return h(&[TAG_EMPTY]);
}
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
let mut i = 0;
while i + 1 < level.len() {
next.push(h(&[TAG_NODE, &level[i], &level[i + 1]]));
i += 2;
}
if i < level.len() {
next.push(level[i]);
}
level = next;
}
level[0]
}
fn subtree(tag: &[u8], leaves: Vec<Digest32>) -> Digest32 {
let n = leaves.len() as u64;
let folded = fold(leaves);
h(&[tag, &n.to_le_bytes(), &folded])
}
pub fn namespace_root(collections: &[String]) -> Digest32 {
let mut names: Vec<&String> = collections.iter().collect();
names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
names.dedup();
subtree(TAG_NS_ROOT, names.iter().map(|n| namespace_leaf(n)).collect())
}
#[derive(Debug, Clone)]
pub struct RecordRef<'a> {
pub coll: &'a str,
pub id: &'a str,
pub data: &'a serde_json::Value,
pub valid_from: Option<&'a str>,
pub valid_to: Option<&'a str>,
}
pub fn records_root(records: &[RecordRef<'_>]) -> Result<Digest32, String> {
let mut sorted: Vec<&RecordRef<'_>> = records.iter().collect();
sorted.sort_by(|a, b| {
a.coll.as_bytes().cmp(b.coll.as_bytes())
.then_with(|| a.id.as_bytes().cmp(b.id.as_bytes()))
});
let mut leaves = Vec::with_capacity(sorted.len());
for r in sorted {
leaves.push(record_leaf(r.coll, r.id, r.data, r.valid_from, r.valid_to)?);
}
Ok(subtree(TAG_REC_ROOT, leaves))
}
pub fn state_root(namespace: Digest32, records: Digest32) -> Digest32 {
h(&[TAG_STATE_ROOT, &namespace, &records])
}
pub fn hex(d: &Digest32) -> String {
::hex::encode(d)
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StateRoot {
pub version: String,
pub namespace_root: String,
pub records_root: String,
pub state_root: String,
pub collection_count: u64,
pub record_count: u64,
}
pub fn compute(collections: &[String], records: &[RecordRef<'_>]) -> Result<StateRoot, String> {
let ns = namespace_root(collections);
let rec = records_root(records)?;
let sr = state_root(ns, rec);
let mut names: Vec<&String> = collections.iter().collect();
names.sort();
names.dedup();
Ok(StateRoot {
version: "state_root_v1".into(),
namespace_root: hex(&ns),
records_root: hex(&rec),
state_root: hex(&sr),
collection_count: names.len() as u64,
record_count: records.len() as u64,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn rec<'a>(coll: &'a str, id: &'a str, data: &'a serde_json::Value) -> RecordRef<'a> {
RecordRef { coll, id, data, valid_from: None, valid_to: None }
}
#[test]
fn the_empty_root_is_a_constant_and_is_not_zero() {
let e = namespace_root(&[]);
assert_ne!(e, [0u8; 32], "an empty namespace must not look uninitialised");
assert_eq!(e, namespace_root(&[]), "and must be stable");
}
#[test]
fn the_namespace_and_records_subtrees_of_an_empty_db_differ() {
assert_ne!(namespace_root(&[]), records_root(&[]).unwrap());
}
#[test]
fn an_empty_but_live_collection_changes_the_root() {
let never = compute(&[], &[]).unwrap();
let emptied = compute(&["orders".into()], &[]).unwrap();
assert_ne!(never.state_root, emptied.state_root,
"a database that once had orders is not one that never did");
assert_eq!(emptied.record_count, 0);
assert_eq!(emptied.collection_count, 1);
}
#[test]
fn input_order_does_not_matter() {
let a = json!({"v": 1});
let b = json!({"v": 2});
let one = compute(
&["x".into(), "y".into()],
&[rec("x", "1", &a), rec("y", "1", &b)],
).unwrap();
let two = compute(
&["y".into(), "x".into()],
&[rec("y", "1", &b), rec("x", "1", &a)],
).unwrap();
assert_eq!(one, two);
}
#[test]
fn length_prefixing_stops_the_classic_concatenation_collision() {
let v = json!(null);
let ab_c = compute(&[], &[rec("ab", "c", &v)]).unwrap();
let a_bc = compute(&[], &[rec("a", "bc", &v)]).unwrap();
assert_ne!(ab_c.state_root, a_bc.state_root);
}
#[test]
fn a_present_empty_string_is_not_an_absent_value() {
let v = json!({});
let absent = record_leaf("c", "1", &v, None, None).unwrap();
let empty = record_leaf("c", "1", &v, Some(""), None).unwrap();
assert_ne!(absent, empty);
}
#[test]
fn document_field_order_is_part_of_the_state() {
let ab: serde_json::Value = serde_json::from_str(r#"{"a":1,"b":2}"#).unwrap();
let ba: serde_json::Value = serde_json::from_str(r#"{"b":2,"a":1}"#).unwrap();
assert_ne!(
record_leaf("c", "1", &ab, None, None).unwrap(),
record_leaf("c", "1", &ba, None, None).unwrap()
);
}
#[test]
fn an_integer_and_a_float_of_the_same_value_commit_differently() {
let i: serde_json::Value = serde_json::from_str("1").unwrap();
let f: serde_json::Value = serde_json::from_str("1.0").unwrap();
assert_ne!(
record_leaf("c", "1", &i, None, None).unwrap(),
record_leaf("c", "1", &f, None, None).unwrap()
);
}
#[test]
fn negative_zero_commits_as_zero() {
let mut a = Vec::new();
let mut b = Vec::new();
encode_value(&mut a, &json!(0.0f64)).unwrap();
encode_value(&mut b, &json!(-0.0f64)).unwrap();
assert_eq!(a, b, "0.0 == -0.0, so they must commit identically");
}
#[test]
fn nan_is_refused_rather_than_producing_a_root_that_differs_from_itself() {
let nan = serde_json::Number::from_f64(f64::NAN);
assert!(nan.is_none(), "serde_json refuses NaN at construction");
let mut buf = Vec::new();
let ok = encode_value(&mut buf, &json!(1.5));
assert!(ok.is_ok());
}
#[test]
fn an_odd_leaf_is_promoted_not_duplicated() {
let v = json!(1);
let three = compute(&[], &[rec("c", "1", &v), rec("c", "2", &v), rec("c", "3", &v)]).unwrap();
let four = compute(&[], &[
rec("c", "1", &v), rec("c", "2", &v), rec("c", "3", &v), rec("c", "3", &v),
]).unwrap();
assert_ne!(three.state_root, four.state_root);
}
#[test]
fn the_leaf_count_is_committed() {
let v = json!(1);
let one = subtree(TAG_REC_ROOT, vec![record_leaf("c", "1", &v, None, None).unwrap()]);
let bare = record_leaf("c", "1", &v, None, None).unwrap();
assert_ne!(one, bare, "a one-leaf tree is not its own leaf");
}
#[test]
fn domain_separation_keeps_a_leaf_from_posing_as_an_internal_node() {
let a = [1u8; 32];
let b = [2u8; 32];
let internal = h(&[TAG_NODE, &a, &b]);
let leafish = h(&[TAG_NS_LEAF, &a, &b]);
assert_ne!(internal, leafish);
}
#[test]
fn changing_one_document_changes_the_root() {
let before = json!({"total": 100});
let after = json!({"total": 101});
assert_ne!(
compute(&["o".into()], &[rec("o", "1", &before)]).unwrap().state_root,
compute(&["o".into()], &[rec("o", "1", &after)]).unwrap().state_root
);
}
#[test]
fn bitemporal_validity_is_part_of_the_state() {
let v = json!({"x": 1});
let plain = RecordRef { coll: "c", id: "1", data: &v, valid_from: None, valid_to: None };
let dated = RecordRef {
coll: "c", id: "1", data: &v,
valid_from: Some("2026-01-01"), valid_to: None,
};
assert_ne!(records_root(&[plain]).unwrap(), records_root(&[dated]).unwrap());
}
#[test]
fn unicode_is_committed_byte_exactly_with_no_normalisation() {
let composed = "caf\u{00e9}".to_string();
let decomposed = "cafe\u{0301}".to_string();
assert_ne!(composed, decomposed);
assert_ne!(namespace_root(&[composed]), namespace_root(&[decomposed]));
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RootRecord {
pub at_seq: u64,
#[serde(flatten)]
pub root: StateRoot,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case", tag = "status", content = "detail")]
pub enum RecordStatus {
Valid,
Missing,
UnknownVersion(String),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "reason", content = "detail")]
pub enum UnavailableReason {
HistoryPruned,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case", tag = "outcome", content = "detail")]
pub enum Recomputation {
Matches,
Differs,
Unavailable(UnavailableReason),
NotAttempted,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RootVerification {
pub at_seq: u64,
pub record: RecordStatus,
pub recomputation: Recomputation,
pub recomputed: Option<StateRoot>,
}
impl RootVerification {
pub fn is_verified(&self) -> bool {
matches!(self.record, RecordStatus::Valid)
&& matches!(self.recomputation, Recomputation::Matches)
}
pub fn is_mismatch(&self) -> bool {
matches!(self.recomputation, Recomputation::Differs)
}
pub fn exit_code(&self) -> i32 {
match (&self.record, &self.recomputation) {
(RecordStatus::Valid, Recomputation::Matches) => 0,
(RecordStatus::Valid, Recomputation::Unavailable(_)) => 3,
(RecordStatus::Missing, _) => 4,
(RecordStatus::UnknownVersion(_), _) => 5,
_ => 1,
}
}
}
#[cfg(test)]
pub fn vector_cases() -> Vec<(String, Vec<String>, Vec<(String, String, serde_json::Value, Option<String>, Option<String>)>)> {
use serde_json::json;
fn r(c: &str, i: &str, d: serde_json::Value)
-> (String, String, serde_json::Value, Option<String>, Option<String>)
{
(c.into(), i.into(), d, None, None)
}
let parse = |s: &str| -> serde_json::Value { serde_json::from_str(s).unwrap() };
vec![
("empty_database".into(), vec![], vec![]),
("one_empty_collection".into(), vec!["orders".into()], vec![]),
("two_empty_collections".into(), vec!["a".into(), "b".into()], vec![]),
("one_record".into(), vec!["orders".into()],
vec![r("orders", "1", json!({"total": 100}))]),
("three_records_odd_leaf".into(), vec!["c".into()],
vec![r("c", "1", json!(1)), r("c", "2", json!(2)), r("c", "3", json!(3))]),
("four_records_even".into(), vec!["c".into()],
vec![r("c", "1", json!(1)), r("c", "2", json!(2)),
r("c", "3", json!(3)), r("c", "4", json!(4))]),
("five_records".into(), vec!["c".into()],
vec![r("c", "1", json!(1)), r("c", "2", json!(2)), r("c", "3", json!(3)),
r("c", "4", json!(4)), r("c", "5", json!(5))]),
("unsorted_input".into(), vec!["z".into(), "a".into()],
vec![r("z", "9", json!(9)), r("a", "1", json!(1)), r("z", "1", json!(1))]),
("concatenation_ambiguity".into(), vec![],
vec![r("ab", "c", json!(null)), r("a", "bc", json!(null))]),
("field_order_preserved".into(), vec!["c".into()],
vec![r("c", "1", parse(r#"{"b":1,"a":2}"#))]),
("integer_and_float".into(), vec!["c".into()],
vec![r("c", "i", parse("1")), r("c", "f", parse("1.0"))]),
("negative_and_large_numbers".into(), vec!["c".into()],
vec![r("c", "1", parse("-9223372036854775808")),
r("c", "2", parse("18446744073709551615")),
r("c", "3", parse("-0.0")),
r("c", "4", parse("2.5e-10"))]),
("nested_structures".into(), vec!["c".into()],
vec![r("c", "1", json!({"a": [1, {"b": null}, [true, false]], "z": {}}))]),
("empty_containers".into(), vec!["c".into()],
vec![r("c", "arr", json!([])), r("c", "obj", json!({})),
r("c", "str", json!("")), r("c", "null", json!(null))]),
("unicode_not_normalised".into(),
vec!["caf\u{00e9}".into(), "cafe\u{0301}".into()],
vec![r("caf\u{00e9}", "\u{00e9}", json!("caf\u{00e9}")),
r("cafe\u{0301}", "e\u{0301}", json!("cafe\u{0301}"))]),
("bitemporal".into(), vec!["c".into()], vec![
("c".into(), "none".into(), json!({}), None, None),
("c".into(), "empty_from".into(), json!({}), Some("".into()), None),
("c".into(), "dated".into(), json!({}), Some("2026-01-01".into()), Some("2026-12-31".into())),
]),
("emptied_collection".into(), vec!["orders".into(), "users".into()],
vec![r("users", "u", json!(1))]),
]
}
#[cfg(test)]
mod vectors {
use super::*;
fn vector_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../vectors/state_root_v1.json")
}
fn generate() -> serde_json::Value {
let mut cases = Vec::new();
for (name, colls, recs) in vector_cases() {
let refs: Vec<RecordRef<'_>> = recs.iter()
.map(|(c, i, d, vf, vt)| RecordRef {
coll: c, id: i, data: d,
valid_from: vf.as_deref(), valid_to: vt.as_deref(),
})
.collect();
let out = compute(&colls, &refs).unwrap();
cases.push(serde_json::json!({
"name": name,
"collections": colls,
"records": recs.iter().map(|(c, i, d, vf, vt)| serde_json::json!({
"coll": c, "id": i, "data": d,
"valid_from": vf, "valid_to": vt,
})).collect::<Vec<_>>(),
"expect": out,
}));
}
serde_json::json!({
"format": "state_root_v1",
"hash": "blake2b-512 truncated to 32 bytes",
"note": "Any implementation of state_root_v1 must reproduce every \
expect block exactly. These pin the decisions prose cannot.",
"cases": cases,
})
}
#[test]
fn the_committed_vectors_match_this_implementation() {
let generated = generate();
let path = vector_path();
if std::env::var("NEDB_WRITE_VECTORS").as_deref() == Ok("1") {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, serde_json::to_string_pretty(&generated).unwrap() + "\n").unwrap();
eprintln!("wrote {}", path.display());
return;
}
let on_disk: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(&path).unwrap_or_else(|e| panic!(
"cannot read {}: {} -- regenerate with NEDB_WRITE_VECTORS=1",
path.display(), e
))
).expect("vectors file is valid JSON");
let a = on_disk["cases"].as_array().expect("cases array");
let b = generated["cases"].as_array().unwrap();
assert_eq!(a.len(), b.len(), "a case was added or removed");
for (want, got) in a.iter().zip(b.iter()) {
assert_eq!(
want["expect"], got["expect"],
"case {:?} changed -- this is a FORMAT CHANGE, not a test failure",
got["name"]
);
}
}
#[test]
fn no_two_cases_produce_the_same_state_root() {
let g = generate();
let mut seen: std::collections::HashMap<String, String> = Default::default();
for c in g["cases"].as_array().unwrap() {
let root = c["expect"]["state_root"].as_str().unwrap().to_string();
let name = c["name"].as_str().unwrap().to_string();
if let Some(prev) = seen.insert(root.clone(), name.clone()) {
panic!("{:?} and {:?} share a state root -- the format cannot tell \
them apart", prev, name);
}
}
}
}