ontoref-code/crates/ontoref-ontology-content/tests/cross_project_witness.rs

190 lines
7.1 KiB
Rust
Raw Normal View History

//! Cross-project witness fetch — Hard constraint of ADR-028.
//!
//! Sets up two foreign ontologies (X and Y) on disk, registers them in a
//! local ontology registry, then demonstrates that
//! [`fetch_cell_witness`] returns ONLY the witness + state root for the
//! requested cell. The caller never gains access to the foreign
//! ontology's full state — the `CellWitnessResponse` type structurally
//! precludes it.
//!
//! The test also verifies the witness round-trip: the returned witness
//! verifies under the returned root, but does NOT verify under a
//! different ontology's root (negative case).
use ed25519_dalek::SigningKey;
use ontoref_commit::BinaryMerkle;
use ontoref_oplog::OpLog;
use ontoref_types::{
operation::{OpBody, OpPayload, Operation},
AttrId, EntityId, Hlc, PublicKey, Value,
};
use ontoref_ontology_content::{
compute_ontology_id, fetch_cell_witness, verify_cell, CellQuery, Error, OntologyRegistry,
OntologyType, RegistryEntry,
};
fn signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn assert_op(entity: &str, attr: &str, value: Value, ts_logical: u64, seed: u8) -> Operation {
let key = signing_key(seed);
let body = OpBody {
actor: PublicKey::from_signing_key(&key),
parents: vec![],
payload: OpPayload::EntityAssert {
attrs: vec![(AttrId::new(attr), value)],
entity: EntityId::new(entity),
valid_from: None,
},
timestamp: Hlc::new(ts_logical, 0),
};
Operation::sign(body, &key)
}
/// Append `ops` to a fresh oplog at `path` and return the resulting
/// [`OntologyId`] computed from the oplog's heads.
fn build_ontology(path: &std::path::Path, ops: &[Operation]) -> ontoref_ontology_content::OntologyId {
let log = OpLog::open(path).expect("open oplog");
for op in ops {
log.append(op).expect("append op");
}
let heads = log.heads().expect("heads ok");
compute_ontology_id(&heads)
}
#[test]
fn fetch_cell_witness_returns_only_witness_and_root() {
let tmp = tempfile::tempdir().expect("tempdir");
// ── Foreign ontology X — carries an "x-entity" cell. ────────────────────
let x_oplog = tmp.path().join("x.oplog");
let x_ops = vec![
assert_op(
"x-entity",
"x-attr",
Value::Str("x-value".into()),
1,
0xAA,
),
assert_op(
"x-entity-other",
"x-attr-other",
Value::Int(42),
2,
0xAA,
),
];
let x_id = build_ontology(&x_oplog, &x_ops);
// ── Foreign ontology Y — carries a "y-entity" cell. ─────────────────────
let y_oplog = tmp.path().join("y.oplog");
let y_ops = vec![assert_op(
"y-entity",
"y-attr",
Value::Str("y-value".into()),
1,
0xBB,
)];
let y_id = build_ontology(&y_oplog, &y_ops);
assert_ne!(x_id, y_id, "different ontologies must have different ids");
// ── Project A's local registry — knows X and Y. ─────────────────────────
let mut registry = OntologyRegistry::empty();
registry.register(RegistryEntry {
ontology_id: x_id,
oplog_path: x_oplog.clone(),
classification: OntologyType::Type2aFederable,
verified_state_root_hex: None,
});
registry.register(RegistryEntry {
ontology_id: y_id,
oplog_path: y_oplog,
classification: OntologyType::Type3ProjectSpecific,
verified_state_root_hex: None,
});
// ── Project A fetches a witness for X's x-entity/x-attr cell. ──────────
let query = CellQuery::new("x-entity", "x-attr");
let response = fetch_cell_witness(&registry, &x_id, &query).expect("fetch ok");
// Witness verifies against the returned state root.
assert!(
verify_cell(&response.witness, &response.state_root),
"witness must verify against its own state root"
);
// The witness contains the canonical encoding of "x-value".
let expected = ontoref_types::canonical::encode(&Value::Str("x-value".into()))
.expect("encode value");
assert_eq!(response.witness.value, expected);
// ── Negative: the witness must NOT verify under Y's state root. ────────
// Rebuild Y's commit layer and compute its root WITHOUT going
// through fetch_ontology — this proves the negative case is real.
let y_log = OpLog::open(x_oplog.with_file_name("y.oplog")).expect("open y oplog");
let y_state_ops = y_log.all_ops_in_hlc_order().expect("y ops");
let mut y_commit = BinaryMerkle::new();
for op in &y_state_ops {
y_commit.apply(op);
}
let y_root = y_commit.root();
assert_ne!(response.state_root, y_root, "X and Y must have different roots");
assert!(
!verify_cell(&response.witness, &y_root),
"witness from X must not verify under Y's state root"
);
// ── Hard ADR-028 surface check: the response type is narrow. ───────────
// CellWitnessResponse carries exactly { witness, state_root } — no
// Vec<Operation>, no full commit layer, no cell map. The size of the
// struct is bounded by the witness's Merkle path depth.
let response_size = std::mem::size_of_val(&response);
let full_oplog_size_bound = x_ops.len() * 1024; // pessimistic per-op size
assert!(
response_size < full_oplog_size_bound,
"response must be bounded — full oplog leakage would exceed {full_oplog_size_bound} bytes; got {response_size}"
);
// Fetching a cell that does not exist in X returns CellMissing,
// NOT a leak of Y's content.
let unknown_query = CellQuery::new("y-entity", "y-attr");
let err = fetch_cell_witness(&registry, &x_id, &unknown_query)
.expect_err("y-entity must not exist in X");
assert!(matches!(err, Error::CellMissing(_)));
}
#[test]
fn fetch_unknown_ontology_id_returns_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let oplog = tmp.path().join("x.oplog");
let _ = build_ontology(
&oplog,
&[assert_op("e", "a", Value::Int(1), 1, 0xCC)],
);
let registry = OntologyRegistry::empty();
let unknown_id = ontoref_ontology_content::OntologyId([0xFFu8; 32]);
let err = fetch_cell_witness(&registry, &unknown_id, &CellQuery::new("e", "a"))
.expect_err("unknown id must error");
assert!(matches!(err, Error::UnknownOntology(_)));
}
#[test]
fn ontology_id_is_deterministic_across_independent_rebuilds() {
let tmp = tempfile::tempdir().expect("tempdir");
let path_a = tmp.path().join("a.oplog");
let path_b = tmp.path().join("b.oplog");
let ops = vec![
assert_op("e1", "a1", Value::Str("v1".into()), 1, 0xDD),
assert_op("e2", "a2", Value::Int(7), 2, 0xDD),
];
let id_a = build_ontology(&path_a, &ops);
let id_b = build_ontology(&path_b, &ops);
assert_eq!(
id_a, id_b,
"two oplogs with identical op sequence must produce the same OntologyId"
);
}