1use blake2::{Blake2b512, Digest};
102
103const TAG_EMPTY: &[u8] = b"nedb:state_root_v1:empty";
109const TAG_NODE: &[u8] = b"nedb:state_root_v1:node";
110const TAG_NS_LEAF: &[u8] = b"nedb:state_root_v1:namespace_leaf";
111const TAG_NS_ROOT: &[u8] = b"nedb:state_root_v1:namespace_root";
112const TAG_REC_LEAF: &[u8] = b"nedb:state_root_v1:record_leaf";
113const TAG_REC_ROOT: &[u8] = b"nedb:state_root_v1:records_root";
114const TAG_STATE_ROOT: &[u8] = b"nedb:state_root_v1:state_root";
115
116pub type Digest32 = [u8; 32];
118
119fn h(parts: &[&[u8]]) -> Digest32 {
120 let mut hasher = Blake2b512::new();
121 for p in parts {
122 hasher.update(p);
123 }
124 let out = hasher.finalize();
125 let mut d = [0u8; 32];
126 d.copy_from_slice(&out[..32]);
127 d
128}
129
130fn lp(buf: &mut Vec<u8>, bytes: &[u8]) {
132 buf.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
133 buf.extend_from_slice(bytes);
134}
135
136fn lp_opt(buf: &mut Vec<u8>, v: Option<&str>) {
141 match v {
142 None => buf.push(0),
143 Some(s) => {
144 buf.push(1);
145 lp(buf, s.as_bytes());
146 }
147 }
148}
149
150const V_NULL: u8 = 0;
157const V_FALSE: u8 = 1;
158const V_TRUE: u8 = 2;
159const V_I64: u8 = 3;
160const V_U64: u8 = 4;
161const V_F64: u8 = 5;
162const V_STR: u8 = 6;
163const V_ARR: u8 = 7;
164const V_OBJ: u8 = 8;
165
166pub fn encode_value(buf: &mut Vec<u8>, v: &serde_json::Value) -> Result<(), String> {
175 match v {
176 serde_json::Value::Null => buf.push(V_NULL),
177 serde_json::Value::Bool(false) => buf.push(V_FALSE),
178 serde_json::Value::Bool(true) => buf.push(V_TRUE),
179 serde_json::Value::Number(n) => {
180 if let Some(i) = n.as_i64() {
181 buf.push(V_I64);
182 buf.extend_from_slice(&i.to_le_bytes());
183 } else if let Some(u) = n.as_u64() {
184 buf.push(V_U64);
185 buf.extend_from_slice(&u.to_le_bytes());
186 } else {
187 let f = n.as_f64().ok_or_else(|| format!("unrepresentable number: {}", n))?;
188 if f.is_nan() {
189 return Err("NaN cannot be committed to a state root".into());
193 }
194 buf.push(V_F64);
195 let f = if f == 0.0 { 0.0 } else { f };
197 buf.extend_from_slice(&f.to_bits().to_le_bytes());
198 }
199 }
200 serde_json::Value::String(s) => {
201 buf.push(V_STR);
202 lp(buf, s.as_bytes());
203 }
204 serde_json::Value::Array(items) => {
205 buf.push(V_ARR);
206 buf.extend_from_slice(&(items.len() as u64).to_le_bytes());
207 for it in items {
208 encode_value(buf, it)?;
209 }
210 }
211 serde_json::Value::Object(map) => {
212 buf.push(V_OBJ);
213 buf.extend_from_slice(&(map.len() as u64).to_le_bytes());
214 for (k, val) in map {
216 lp(buf, k.as_bytes());
217 encode_value(buf, val)?;
218 }
219 }
220 }
221 Ok(())
222}
223
224pub fn namespace_leaf(name: &str) -> Digest32 {
228 let mut buf = Vec::new();
229 lp(&mut buf, name.as_bytes());
230 h(&[TAG_NS_LEAF, &buf])
231}
232
233pub fn record_leaf(
238 coll: &str,
239 id: &str,
240 data: &serde_json::Value,
241 valid_from: Option<&str>,
242 valid_to: Option<&str>,
243) -> Result<Digest32, String> {
244 let mut buf = Vec::new();
245 lp(&mut buf, coll.as_bytes());
246 lp(&mut buf, id.as_bytes());
247 lp_opt(&mut buf, valid_from);
248 lp_opt(&mut buf, valid_to);
249 encode_value(&mut buf, data)?;
250 Ok(h(&[TAG_REC_LEAF, &buf]))
251}
252
253fn fold(mut level: Vec<Digest32>) -> Digest32 {
257 if level.is_empty() {
258 return h(&[TAG_EMPTY]);
259 }
260 while level.len() > 1 {
261 let mut next = Vec::with_capacity(level.len().div_ceil(2));
262 let mut i = 0;
263 while i + 1 < level.len() {
264 next.push(h(&[TAG_NODE, &level[i], &level[i + 1]]));
265 i += 2;
266 }
267 if i < level.len() {
268 next.push(level[i]);
270 }
271 level = next;
272 }
273 level[0]
274}
275
276fn subtree(tag: &[u8], leaves: Vec<Digest32>) -> Digest32 {
278 let n = leaves.len() as u64;
279 let folded = fold(leaves);
280 h(&[tag, &n.to_le_bytes(), &folded])
281}
282
283pub fn namespace_root(collections: &[String]) -> Digest32 {
285 let mut names: Vec<&String> = collections.iter().collect();
286 names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
287 names.dedup();
288 subtree(TAG_NS_ROOT, names.iter().map(|n| namespace_leaf(n)).collect())
289}
290
291#[derive(Debug, Clone)]
293pub struct RecordRef<'a> {
294 pub coll: &'a str,
295 pub id: &'a str,
296 pub data: &'a serde_json::Value,
297 pub valid_from: Option<&'a str>,
298 pub valid_to: Option<&'a str>,
299}
300
301pub fn records_root(records: &[RecordRef<'_>]) -> Result<Digest32, String> {
303 let mut sorted: Vec<&RecordRef<'_>> = records.iter().collect();
304 sorted.sort_by(|a, b| {
305 a.coll.as_bytes().cmp(b.coll.as_bytes())
306 .then_with(|| a.id.as_bytes().cmp(b.id.as_bytes()))
307 });
308 let mut leaves = Vec::with_capacity(sorted.len());
309 for r in sorted {
310 leaves.push(record_leaf(r.coll, r.id, r.data, r.valid_from, r.valid_to)?);
311 }
312 Ok(subtree(TAG_REC_ROOT, leaves))
313}
314
315pub fn state_root(namespace: Digest32, records: Digest32) -> Digest32 {
317 h(&[TAG_STATE_ROOT, &namespace, &records])
318}
319
320pub fn hex(d: &Digest32) -> String {
322 ::hex::encode(d)
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
328pub struct StateRoot {
329 pub version: String,
330 pub namespace_root: String,
331 pub records_root: String,
332 pub state_root: String,
333 pub collection_count: u64,
334 pub record_count: u64,
335}
336
337pub fn compute(collections: &[String], records: &[RecordRef<'_>]) -> Result<StateRoot, String> {
339 let ns = namespace_root(collections);
340 let rec = records_root(records)?;
341 let sr = state_root(ns, rec);
342 let mut names: Vec<&String> = collections.iter().collect();
343 names.sort();
344 names.dedup();
345 Ok(StateRoot {
346 version: "state_root_v1".into(),
347 namespace_root: hex(&ns),
348 records_root: hex(&rec),
349 state_root: hex(&sr),
350 collection_count: names.len() as u64,
351 record_count: records.len() as u64,
352 })
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use serde_json::json;
359
360 fn rec<'a>(coll: &'a str, id: &'a str, data: &'a serde_json::Value) -> RecordRef<'a> {
361 RecordRef { coll, id, data, valid_from: None, valid_to: None }
362 }
363
364 #[test]
365 fn the_empty_root_is_a_constant_and_is_not_zero() {
366 let e = namespace_root(&[]);
367 assert_ne!(e, [0u8; 32], "an empty namespace must not look uninitialised");
368 assert_eq!(e, namespace_root(&[]), "and must be stable");
369 }
370
371 #[test]
372 fn the_namespace_and_records_subtrees_of_an_empty_db_differ() {
373 assert_ne!(namespace_root(&[]), records_root(&[]).unwrap());
376 }
377
378 #[test]
380 fn an_empty_but_live_collection_changes_the_root() {
381 let never = compute(&[], &[]).unwrap();
382 let emptied = compute(&["orders".into()], &[]).unwrap();
383 assert_ne!(never.state_root, emptied.state_root,
384 "a database that once had orders is not one that never did");
385 assert_eq!(emptied.record_count, 0);
386 assert_eq!(emptied.collection_count, 1);
387 }
388
389 #[test]
390 fn input_order_does_not_matter() {
391 let a = json!({"v": 1});
392 let b = json!({"v": 2});
393 let one = compute(
394 &["x".into(), "y".into()],
395 &[rec("x", "1", &a), rec("y", "1", &b)],
396 ).unwrap();
397 let two = compute(
398 &["y".into(), "x".into()],
399 &[rec("y", "1", &b), rec("x", "1", &a)],
400 ).unwrap();
401 assert_eq!(one, two);
402 }
403
404 #[test]
405 fn length_prefixing_stops_the_classic_concatenation_collision() {
406 let v = json!(null);
407 let ab_c = compute(&[], &[rec("ab", "c", &v)]).unwrap();
408 let a_bc = compute(&[], &[rec("a", "bc", &v)]).unwrap();
409 assert_ne!(ab_c.state_root, a_bc.state_root);
410 }
411
412 #[test]
413 fn a_present_empty_string_is_not_an_absent_value() {
414 let v = json!({});
415 let absent = record_leaf("c", "1", &v, None, None).unwrap();
416 let empty = record_leaf("c", "1", &v, Some(""), None).unwrap();
417 assert_ne!(absent, empty);
418 }
419
420 #[test]
421 fn document_field_order_is_part_of_the_state() {
422 let ab: serde_json::Value = serde_json::from_str(r#"{"a":1,"b":2}"#).unwrap();
425 let ba: serde_json::Value = serde_json::from_str(r#"{"b":2,"a":1}"#).unwrap();
426 assert_ne!(
427 record_leaf("c", "1", &ab, None, None).unwrap(),
428 record_leaf("c", "1", &ba, None, None).unwrap()
429 );
430 }
431
432 #[test]
433 fn an_integer_and_a_float_of_the_same_value_commit_differently() {
434 let i: serde_json::Value = serde_json::from_str("1").unwrap();
435 let f: serde_json::Value = serde_json::from_str("1.0").unwrap();
436 assert_ne!(
437 record_leaf("c", "1", &i, None, None).unwrap(),
438 record_leaf("c", "1", &f, None, None).unwrap()
439 );
440 }
441
442 #[test]
443 fn negative_zero_commits_as_zero() {
444 let mut a = Vec::new();
445 let mut b = Vec::new();
446 encode_value(&mut a, &json!(0.0f64)).unwrap();
447 encode_value(&mut b, &json!(-0.0f64)).unwrap();
448 assert_eq!(a, b, "0.0 == -0.0, so they must commit identically");
449 }
450
451 #[test]
452 fn nan_is_refused_rather_than_producing_a_root_that_differs_from_itself() {
453 let nan = serde_json::Number::from_f64(f64::NAN);
454 assert!(nan.is_none(), "serde_json refuses NaN at construction");
455 let mut buf = Vec::new();
457 let ok = encode_value(&mut buf, &json!(1.5));
458 assert!(ok.is_ok());
459 }
460
461 #[test]
462 fn an_odd_leaf_is_promoted_not_duplicated() {
463 let v = json!(1);
467 let three = compute(&[], &[rec("c", "1", &v), rec("c", "2", &v), rec("c", "3", &v)]).unwrap();
468 let four = compute(&[], &[
469 rec("c", "1", &v), rec("c", "2", &v), rec("c", "3", &v), rec("c", "3", &v),
470 ]).unwrap();
471 assert_ne!(three.state_root, four.state_root);
472 }
473
474 #[test]
475 fn the_leaf_count_is_committed() {
476 let v = json!(1);
477 let one = subtree(TAG_REC_ROOT, vec![record_leaf("c", "1", &v, None, None).unwrap()]);
478 let bare = record_leaf("c", "1", &v, None, None).unwrap();
479 assert_ne!(one, bare, "a one-leaf tree is not its own leaf");
480 }
481
482 #[test]
483 fn domain_separation_keeps_a_leaf_from_posing_as_an_internal_node() {
484 let a = [1u8; 32];
485 let b = [2u8; 32];
486 let internal = h(&[TAG_NODE, &a, &b]);
487 let leafish = h(&[TAG_NS_LEAF, &a, &b]);
488 assert_ne!(internal, leafish);
489 }
490
491 #[test]
492 fn changing_one_document_changes_the_root() {
493 let before = json!({"total": 100});
494 let after = json!({"total": 101});
495 assert_ne!(
496 compute(&["o".into()], &[rec("o", "1", &before)]).unwrap().state_root,
497 compute(&["o".into()], &[rec("o", "1", &after)]).unwrap().state_root
498 );
499 }
500
501 #[test]
502 fn bitemporal_validity_is_part_of_the_state() {
503 let v = json!({"x": 1});
504 let plain = RecordRef { coll: "c", id: "1", data: &v, valid_from: None, valid_to: None };
505 let dated = RecordRef {
506 coll: "c", id: "1", data: &v,
507 valid_from: Some("2026-01-01"), valid_to: None,
508 };
509 assert_ne!(records_root(&[plain]).unwrap(), records_root(&[dated]).unwrap());
510 }
511
512 #[test]
513 fn unicode_is_committed_byte_exactly_with_no_normalisation() {
514 let composed = "caf\u{00e9}".to_string();
517 let decomposed = "cafe\u{0301}".to_string();
518 assert_ne!(composed, decomposed);
519 assert_ne!(namespace_root(&[composed]), namespace_root(&[decomposed]));
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
527pub struct RootRecord {
528 pub at_seq: u64,
529 #[serde(flatten)]
530 pub root: StateRoot,
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
535#[serde(rename_all = "snake_case", tag = "status", content = "detail")]
536pub enum RecordStatus {
537 Valid,
538 Missing,
539 UnknownVersion(String),
543}
544
545#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
547#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "reason", content = "detail")]
548pub enum UnavailableReason {
549 HistoryPruned,
551 Other(String),
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
556#[serde(rename_all = "snake_case", tag = "outcome", content = "detail")]
557pub enum Recomputation {
558 Matches,
559 Differs,
560 Unavailable(UnavailableReason),
561 NotAttempted,
563}
564
565#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
580pub struct RootVerification {
581 pub at_seq: u64,
582 pub record: RecordStatus,
583 pub recomputation: Recomputation,
584 pub recomputed: Option<StateRoot>,
586}
587
588impl RootVerification {
589 pub fn is_verified(&self) -> bool {
592 matches!(self.record, RecordStatus::Valid)
593 && matches!(self.recomputation, Recomputation::Matches)
594 }
595
596 pub fn is_mismatch(&self) -> bool {
599 matches!(self.recomputation, Recomputation::Differs)
600 }
601
602 pub fn exit_code(&self) -> i32 {
606 match (&self.record, &self.recomputation) {
607 (RecordStatus::Valid, Recomputation::Matches) => 0,
608 (RecordStatus::Valid, Recomputation::Unavailable(_)) => 3,
609 (RecordStatus::Missing, _) => 4,
610 (RecordStatus::UnknownVersion(_), _) => 5,
611 _ => 1,
612 }
613 }
614}
615
616#[cfg(test)]
626pub fn vector_cases() -> Vec<(String, Vec<String>, Vec<(String, String, serde_json::Value, Option<String>, Option<String>)>)> {
627 use serde_json::json;
628 fn r(c: &str, i: &str, d: serde_json::Value)
629 -> (String, String, serde_json::Value, Option<String>, Option<String>)
630 {
631 (c.into(), i.into(), d, None, None)
632 }
633 let parse = |s: &str| -> serde_json::Value { serde_json::from_str(s).unwrap() };
634 vec![
635 ("empty_database".into(), vec![], vec![]),
636 ("one_empty_collection".into(), vec!["orders".into()], vec![]),
637 ("two_empty_collections".into(), vec!["a".into(), "b".into()], vec![]),
638 ("one_record".into(), vec!["orders".into()],
639 vec![r("orders", "1", json!({"total": 100}))]),
640 ("three_records_odd_leaf".into(), vec!["c".into()],
641 vec![r("c", "1", json!(1)), r("c", "2", json!(2)), r("c", "3", json!(3))]),
642 ("four_records_even".into(), vec!["c".into()],
643 vec![r("c", "1", json!(1)), r("c", "2", json!(2)),
644 r("c", "3", json!(3)), r("c", "4", json!(4))]),
645 ("five_records".into(), vec!["c".into()],
646 vec![r("c", "1", json!(1)), r("c", "2", json!(2)), r("c", "3", json!(3)),
647 r("c", "4", json!(4)), r("c", "5", json!(5))]),
648 ("unsorted_input".into(), vec!["z".into(), "a".into()],
649 vec![r("z", "9", json!(9)), r("a", "1", json!(1)), r("z", "1", json!(1))]),
650 ("concatenation_ambiguity".into(), vec![],
651 vec![r("ab", "c", json!(null)), r("a", "bc", json!(null))]),
652 ("field_order_preserved".into(), vec!["c".into()],
653 vec![r("c", "1", parse(r#"{"b":1,"a":2}"#))]),
654 ("integer_and_float".into(), vec!["c".into()],
655 vec![r("c", "i", parse("1")), r("c", "f", parse("1.0"))]),
656 ("negative_and_large_numbers".into(), vec!["c".into()],
657 vec![r("c", "1", parse("-9223372036854775808")),
658 r("c", "2", parse("18446744073709551615")),
659 r("c", "3", parse("-0.0")),
660 r("c", "4", parse("2.5e-10"))]),
661 ("nested_structures".into(), vec!["c".into()],
662 vec![r("c", "1", json!({"a": [1, {"b": null}, [true, false]], "z": {}}))]),
663 ("empty_containers".into(), vec!["c".into()],
664 vec![r("c", "arr", json!([])), r("c", "obj", json!({})),
665 r("c", "str", json!("")), r("c", "null", json!(null))]),
666 ("unicode_not_normalised".into(),
667 vec!["caf\u{00e9}".into(), "cafe\u{0301}".into()],
668 vec![r("caf\u{00e9}", "\u{00e9}", json!("caf\u{00e9}")),
669 r("cafe\u{0301}", "e\u{0301}", json!("cafe\u{0301}"))]),
670 ("bitemporal".into(), vec!["c".into()], vec![
671 ("c".into(), "none".into(), json!({}), None, None),
672 ("c".into(), "empty_from".into(), json!({}), Some("".into()), None),
673 ("c".into(), "dated".into(), json!({}), Some("2026-01-01".into()), Some("2026-12-31".into())),
674 ]),
675 ("emptied_collection".into(), vec!["orders".into(), "users".into()],
676 vec![r("users", "u", json!(1))]),
677 ]
678}
679
680#[cfg(test)]
681mod vectors {
682 use super::*;
683
684 fn vector_path() -> std::path::PathBuf {
685 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
686 .join("../../vectors/state_root_v1.json")
687 }
688
689 fn generate() -> serde_json::Value {
690 let mut cases = Vec::new();
691 for (name, colls, recs) in vector_cases() {
692 let refs: Vec<RecordRef<'_>> = recs.iter()
693 .map(|(c, i, d, vf, vt)| RecordRef {
694 coll: c, id: i, data: d,
695 valid_from: vf.as_deref(), valid_to: vt.as_deref(),
696 })
697 .collect();
698 let out = compute(&colls, &refs).unwrap();
699 cases.push(serde_json::json!({
700 "name": name,
701 "collections": colls,
702 "records": recs.iter().map(|(c, i, d, vf, vt)| serde_json::json!({
703 "coll": c, "id": i, "data": d,
704 "valid_from": vf, "valid_to": vt,
705 })).collect::<Vec<_>>(),
706 "expect": out,
707 }));
708 }
709 serde_json::json!({
710 "format": "state_root_v1",
711 "hash": "blake2b-512 truncated to 32 bytes",
712 "note": "Any implementation of state_root_v1 must reproduce every \
713 expect block exactly. These pin the decisions prose cannot.",
714 "cases": cases,
715 })
716 }
717
718 #[test]
723 fn the_committed_vectors_match_this_implementation() {
724 let generated = generate();
725 let path = vector_path();
726 if std::env::var("NEDB_WRITE_VECTORS").as_deref() == Ok("1") {
727 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
728 std::fs::write(&path, serde_json::to_string_pretty(&generated).unwrap() + "\n").unwrap();
729 eprintln!("wrote {}", path.display());
730 return;
731 }
732 let on_disk: serde_json::Value = serde_json::from_str(
733 &std::fs::read_to_string(&path).unwrap_or_else(|e| panic!(
734 "cannot read {}: {} -- regenerate with NEDB_WRITE_VECTORS=1",
735 path.display(), e
736 ))
737 ).expect("vectors file is valid JSON");
738
739 let a = on_disk["cases"].as_array().expect("cases array");
740 let b = generated["cases"].as_array().unwrap();
741 assert_eq!(a.len(), b.len(), "a case was added or removed");
742 for (want, got) in a.iter().zip(b.iter()) {
743 assert_eq!(
744 want["expect"], got["expect"],
745 "case {:?} changed -- this is a FORMAT CHANGE, not a test failure",
746 got["name"]
747 );
748 }
749 }
750
751 #[test]
754 fn no_two_cases_produce_the_same_state_root() {
755 let g = generate();
756 let mut seen: std::collections::HashMap<String, String> = Default::default();
757 for c in g["cases"].as_array().unwrap() {
758 let root = c["expect"]["state_root"].as_str().unwrap().to_string();
759 let name = c["name"].as_str().unwrap().to_string();
760 if let Some(prev) = seen.insert(root.clone(), name.clone()) {
761 panic!("{:?} and {:?} share a state root -- the format cannot tell \
762 them apart", prev, name);
763 }
764 }
765 }
766}