271 lines
7.6 KiB
Rust
271 lines
7.6 KiB
Rust
|
|
use ed25519_dalek::SigningKey;
|
||
|
|
use ontoref_types::{
|
||
|
|
operation::{OpBody, OpPayload, Operation},
|
||
|
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||
|
|
};
|
||
|
|
|
||
|
|
use ontoref_commit::{verify_witness, CommitLayer};
|
||
|
|
|
||
|
|
fn signing_key() -> SigningKey {
|
||
|
|
SigningKey::from_bytes(&[11u8; 32])
|
||
|
|
}
|
||
|
|
|
||
|
|
fn assert_op(entity: &str, attrs: Vec<(&str, Value)>, tx: Hlc) -> Operation {
|
||
|
|
let key = signing_key();
|
||
|
|
let body = OpBody {
|
||
|
|
actor: PublicKey::from_signing_key(&key),
|
||
|
|
parents: vec![],
|
||
|
|
payload: OpPayload::EntityAssert {
|
||
|
|
attrs: attrs
|
||
|
|
.into_iter()
|
||
|
|
.map(|(name, value)| (AttrId::new(name), value))
|
||
|
|
.collect(),
|
||
|
|
entity: EntityId::new(entity),
|
||
|
|
valid_from: None,
|
||
|
|
},
|
||
|
|
timestamp: tx,
|
||
|
|
};
|
||
|
|
Operation::sign(body, &key)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn fixture_ops() -> Vec<Operation> {
|
||
|
|
vec![
|
||
|
|
assert_op(
|
||
|
|
"alice",
|
||
|
|
vec![
|
||
|
|
("name", Value::Str("Alice".into())),
|
||
|
|
("age", Value::Int(30)),
|
||
|
|
],
|
||
|
|
Hlc::new(1, 0),
|
||
|
|
),
|
||
|
|
assert_op(
|
||
|
|
"bob",
|
||
|
|
vec![
|
||
|
|
("name", Value::Str("Bob".into())),
|
||
|
|
("age", Value::Int(25)),
|
||
|
|
],
|
||
|
|
Hlc::new(2, 0),
|
||
|
|
),
|
||
|
|
assert_op(
|
||
|
|
"alice",
|
||
|
|
vec![("age", Value::Int(31))],
|
||
|
|
Hlc::new(3, 0),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn determinism_same_ops_same_root() {
|
||
|
|
let mut a = CommitLayer::new();
|
||
|
|
let mut b = CommitLayer::new();
|
||
|
|
for op in fixture_ops() {
|
||
|
|
a.apply(&op);
|
||
|
|
b.apply(&op);
|
||
|
|
}
|
||
|
|
assert_eq!(a.root(), b.root());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn determinism_empty_root_is_fixed() {
|
||
|
|
let layer = CommitLayer::new();
|
||
|
|
let expected = *blake3::hash(b"empty-tree-v1").as_bytes();
|
||
|
|
assert_eq!(layer.root(), expected);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn determinism_root_changes_with_value_overwrite() {
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
layer.apply(&assert_op(
|
||
|
|
"e",
|
||
|
|
vec![("k", Value::Str("v1".into()))],
|
||
|
|
Hlc::new(1, 0),
|
||
|
|
));
|
||
|
|
let r1 = layer.root();
|
||
|
|
layer.apply(&assert_op(
|
||
|
|
"e",
|
||
|
|
vec![("k", Value::Str("v2".into()))],
|
||
|
|
Hlc::new(2, 0),
|
||
|
|
));
|
||
|
|
let r2 = layer.root();
|
||
|
|
assert_ne!(r1, r2, "root must change when an existing cell's value changes");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn witness_verify_succeeds_for_present_cell() {
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
for op in fixture_ops() {
|
||
|
|
layer.apply(&op);
|
||
|
|
}
|
||
|
|
let root = layer.root();
|
||
|
|
|
||
|
|
let witness = layer
|
||
|
|
.witness(&EntityId::new("alice"), &AttrId::new("name"))
|
||
|
|
.unwrap();
|
||
|
|
assert!(verify_witness(&witness, &root));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn witness_verify_succeeds_after_overwrite() {
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
for op in fixture_ops() {
|
||
|
|
layer.apply(&op);
|
||
|
|
}
|
||
|
|
let root = layer.root();
|
||
|
|
|
||
|
|
// alice's age was overwritten from 30 to 31; the witness should reflect 31.
|
||
|
|
let witness = layer
|
||
|
|
.witness(&EntityId::new("alice"), &AttrId::new("age"))
|
||
|
|
.unwrap();
|
||
|
|
assert!(verify_witness(&witness, &root));
|
||
|
|
|
||
|
|
// The witnessed value should be the canonical encoding of Int(31).
|
||
|
|
let expected = ontoref_types::canonical::encode(&Value::Int(31)).unwrap();
|
||
|
|
assert_eq!(witness.value, expected);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn witness_verify_fails_for_tampered_value() {
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
for op in fixture_ops() {
|
||
|
|
layer.apply(&op);
|
||
|
|
}
|
||
|
|
let root = layer.root();
|
||
|
|
|
||
|
|
let mut witness = layer
|
||
|
|
.witness(&EntityId::new("alice"), &AttrId::new("name"))
|
||
|
|
.unwrap();
|
||
|
|
witness.value[0] ^= 0x01;
|
||
|
|
assert!(!verify_witness(&witness, &root));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn witness_verify_fails_for_tampered_path() {
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
for op in fixture_ops() {
|
||
|
|
layer.apply(&op);
|
||
|
|
}
|
||
|
|
let root = layer.root();
|
||
|
|
|
||
|
|
let mut witness = layer
|
||
|
|
.witness(&EntityId::new("alice"), &AttrId::new("name"))
|
||
|
|
.unwrap();
|
||
|
|
if !witness.path.is_empty() {
|
||
|
|
witness.path[0].sibling[0] ^= 0x01;
|
||
|
|
} else {
|
||
|
|
// For a 1-cell tree, witness.path is empty; force the tree larger so
|
||
|
|
// the test exercises tampering at a real path step.
|
||
|
|
panic!("fixture must produce a non-trivial Merkle path");
|
||
|
|
}
|
||
|
|
assert!(!verify_witness(&witness, &root));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn witness_verify_fails_against_wrong_root() {
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
for op in fixture_ops() {
|
||
|
|
layer.apply(&op);
|
||
|
|
}
|
||
|
|
|
||
|
|
let witness = layer
|
||
|
|
.witness(&EntityId::new("alice"), &AttrId::new("name"))
|
||
|
|
.unwrap();
|
||
|
|
let bogus_root = [0xFFu8; 32];
|
||
|
|
assert!(!verify_witness(&witness, &bogus_root));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn cross_instance_determinism_with_independent_apply_order_per_entity() {
|
||
|
|
// Two instances apply ops in the same logical order; their state roots
|
||
|
|
// must be byte-identical.
|
||
|
|
let ops = fixture_ops();
|
||
|
|
|
||
|
|
let mut left = CommitLayer::new();
|
||
|
|
for op in &ops {
|
||
|
|
left.apply(op);
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut right = CommitLayer::new();
|
||
|
|
for op in &ops {
|
||
|
|
right.apply(op);
|
||
|
|
}
|
||
|
|
|
||
|
|
assert_eq!(left.root(), right.root());
|
||
|
|
assert_eq!(left.len(), right.len());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn cross_instance_witness_verifies_under_other_instance_root() {
|
||
|
|
// A witness built on instance A verifies against instance B's root,
|
||
|
|
// because both roots are identical.
|
||
|
|
let ops = fixture_ops();
|
||
|
|
|
||
|
|
let mut left = CommitLayer::new();
|
||
|
|
let mut right = CommitLayer::new();
|
||
|
|
for op in &ops {
|
||
|
|
left.apply(op);
|
||
|
|
right.apply(op);
|
||
|
|
}
|
||
|
|
|
||
|
|
let witness = left
|
||
|
|
.witness(&EntityId::new("bob"), &AttrId::new("name"))
|
||
|
|
.unwrap();
|
||
|
|
let right_root = right.root();
|
||
|
|
assert!(verify_witness(&witness, &right_root));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── ADR-041 / bl-029 29b: per-cell typed CRDT merge ──────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn union_cell_accumulates_and_is_order_independent() {
|
||
|
|
use ontoref_commit::{CellMergeKind, MergePolicy};
|
||
|
|
|
||
|
|
// The "tags" attribute folds as a grow-only set (GSet).
|
||
|
|
let policy = MergePolicy::new().with("tags", CellMergeKind::Union);
|
||
|
|
|
||
|
|
// Two actors assert different tag elements on the same cell. Applied in
|
||
|
|
// either order, the cell must hold BOTH and hash to the same root.
|
||
|
|
let a = assert_op("doc", vec![("tags", Value::Str("red".into()))], Hlc::new(1, 0));
|
||
|
|
let b = assert_op("doc", vec![("tags", Value::Str("blue".into()))], Hlc::new(2, 0));
|
||
|
|
|
||
|
|
let mut forward = CommitLayer::new().with_policy(policy.clone());
|
||
|
|
forward.apply(&a);
|
||
|
|
forward.apply(&b);
|
||
|
|
|
||
|
|
let mut reverse = CommitLayer::new().with_policy(policy.clone());
|
||
|
|
reverse.apply(&b);
|
||
|
|
reverse.apply(&a);
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
forward.root(),
|
||
|
|
reverse.root(),
|
||
|
|
"union merge is order-independent — actors converge regardless of arrival order"
|
||
|
|
);
|
||
|
|
|
||
|
|
// The union root differs from either single-element assertion: both
|
||
|
|
// elements are retained, not last-writer-wins.
|
||
|
|
let mut only_a = CommitLayer::new().with_policy(policy.clone());
|
||
|
|
only_a.apply(&a);
|
||
|
|
assert_ne!(
|
||
|
|
forward.root(),
|
||
|
|
only_a.root(),
|
||
|
|
"the second actor's element is retained, not overwritten"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn lww_is_unchanged_without_a_policy() {
|
||
|
|
// Default (empty) policy reproduces last-writer-wins: the later (higher
|
||
|
|
// HLC, applied last) assertion wins the cell.
|
||
|
|
let a = assert_op("e", vec![("v", Value::Int(1))], Hlc::new(1, 0));
|
||
|
|
let b = assert_op("e", vec![("v", Value::Int(2))], Hlc::new(2, 0));
|
||
|
|
|
||
|
|
let mut layer = CommitLayer::new();
|
||
|
|
layer.apply(&a);
|
||
|
|
layer.apply(&b);
|
||
|
|
|
||
|
|
// Equals a layer that only ever saw the winning (b) assertion.
|
||
|
|
let mut only_b = CommitLayer::new();
|
||
|
|
only_b.apply(&b);
|
||
|
|
assert_eq!(layer.root(), only_b.root(), "default policy stays last-writer-wins");
|
||
|
|
}
|