//! 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(®istry, &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, 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(®istry, &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(®istry, &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" ); }