qualia_core_db/solvers/calculus/
tensor_integrity.rs1use super::tensor_provenance::{ProvenanceGraph, TensorProvenance, TensorState};
26
27const LINEAGE_DOMAIN: &[u8] = b"q42-tensor-lineage-v1";
29const ROOT_DOMAIN: &[u8] = b"q42-tensor-integrity-root-v1";
30const TRANSFORM_DOMAIN: &[u8] = b"q42-tensor-transform-v1";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct LineageCommitment(pub [u8; 32]);
35
36pub fn commit_state(state: &TensorState, parent: Option<&LineageCommitment>) -> LineageCommitment {
40 let mut h = blake3::Hasher::new();
41 h.update(LINEAGE_DOMAIN);
42 match parent {
43 Some(p) => h.update(&p.0),
44 None => h.update(b"GENESIS"),
45 };
46 match &state.provenance {
47 TensorProvenance::Root { source, .. } => {
48 h.update(b"root");
49 h.update(source.as_bytes());
50 }
51 TensorProvenance::Derived {
52 operation, params, ..
53 } => {
54 h.update(b"derived");
55 h.update(operation.as_bytes());
56 let mut keys: Vec<&String> = params.keys().collect();
58 keys.sort();
59 for k in keys {
60 h.update(k.as_bytes());
61 h.update(¶ms[k].to_bits().to_le_bytes());
62 }
63 }
64 }
65 h.update(&(state.data.len() as u64).to_le_bytes());
67 for &x in &state.data {
68 h.update(&x.to_bits().to_le_bytes());
69 }
70 LineageCommitment(h.finalize().into())
71}
72
73pub fn lineage_commitment(graph: &ProvenanceGraph, state_id: u64) -> Option<LineageCommitment> {
76 let lineage = graph.get_lineage(state_id);
78 if lineage.is_empty() {
79 return None;
80 }
81 let mut commitment: Option<LineageCommitment> = None;
82 for &id in lineage.iter().rev() {
83 let state = graph.get_state(id)?;
84 commitment = Some(commit_state(state, commitment.as_ref()));
85 }
86 commitment
87}
88
89pub fn verify_lineage(
92 graph: &ProvenanceGraph,
93 state_id: u64,
94 expected: &LineageCommitment,
95) -> bool {
96 lineage_commitment(graph, state_id).is_some_and(|c| c.0 == expected.0)
97}
98
99pub fn integrity_root(commitments: &[LineageCommitment]) -> LineageCommitment {
103 let mut sorted: Vec<[u8; 32]> = commitments.iter().map(|c| c.0).collect();
104 sorted.sort_unstable();
105 let mut h = blake3::Hasher::new();
106 h.update(ROOT_DOMAIN);
107 h.update(&(sorted.len() as u64).to_le_bytes());
108 for c in &sorted {
109 h.update(c);
110 }
111 LineageCommitment(h.finalize().into())
112}
113
114pub fn transformation_commitment(input: &TensorState, output: &TensorState) -> [u8; 32] {
119 let mut h = blake3::Hasher::new();
120 h.update(TRANSFORM_DOMAIN);
121 h.update(&input.state_id.to_le_bytes());
122 h.update(&output.state_id.to_le_bytes());
123 if let TensorProvenance::Derived { operation, .. } = &output.provenance {
124 h.update(operation.as_bytes());
125 }
126 h.finalize().into()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::collections::HashMap;
133
134 fn scale_params(factor: f64) -> HashMap<String, f64> {
135 let mut p = HashMap::new();
136 p.insert("factor".to_string(), factor);
137 p
138 }
139
140 #[test]
141 fn commitment_is_deterministic_and_data_sensitive() {
142 let a = TensorState::new(vec![1.0, 2.0, 3.0], vec![3]);
143 let b = TensorState::new(vec![1.0, 2.0, 3.0], vec![3]);
144 let c = TensorState::new(vec![1.0, 2.0, 3.5], vec![3]); assert_eq!(
146 commit_state(&a, None),
147 commit_state(&b, None),
148 "same data ⇒ same commitment"
149 );
150 assert_ne!(
151 commit_state(&a, None),
152 commit_state(&c, None),
153 "different data ⇒ different commitment"
154 );
155 }
156
157 #[test]
158 fn lineage_commitment_chains_parent_into_child() {
159 let mut graph = ProvenanceGraph::new();
160 let root = TensorState::new(vec![1.0], vec![1]);
161 let root_id = root.state_id;
162 graph.add_state(root.clone());
163 let child = root.apply_operation("scale", &scale_params(2.0));
164 let child_id = child.state_id;
165 graph.add_state(child);
166
167 let root_commit = lineage_commitment(&graph, root_id).unwrap();
168 let child_commit = lineage_commitment(&graph, child_id).unwrap();
169 assert_ne!(root_commit, child_commit);
171 assert!(verify_lineage(&graph, child_id, &child_commit));
173 }
174
175 #[test]
176 fn tampering_with_an_ancestor_is_detected() {
177 let mut genuine = ProvenanceGraph::new();
179 let root = TensorState::new(vec![10.0], vec![1]);
180 let root_id = root.state_id;
181 genuine.add_state(root.clone());
182 let child = root.apply_operation("scale", &scale_params(2.0));
183 let child_id = child.state_id;
184 genuine.add_state(child.clone());
185 let genuine_commit = lineage_commitment(&genuine, child_id).unwrap();
186
187 let mut forged = ProvenanceGraph::new();
189 let mut tampered_root = root.clone();
190 tampered_root.data = vec![999.0]; forged.add_state(tampered_root);
192 forged.add_state(child);
193
194 assert!(
196 !verify_lineage(&forged, child_id, &genuine_commit),
197 "altered ancestor data must break the lineage commitment"
198 );
199 let _ = root_id;
200 }
201
202 #[test]
203 fn integrity_root_is_order_independent_and_change_sensitive() {
204 let c1 = LineageCommitment([1u8; 32]);
205 let c2 = LineageCommitment([2u8; 32]);
206 let c3 = LineageCommitment([3u8; 32]);
207 let r_ab = integrity_root(&[c1, c2]);
208 let r_ba = integrity_root(&[c2, c1]);
209 assert_eq!(r_ab, r_ba, "root must not depend on commitment order");
210 let r_abc = integrity_root(&[c1, c2, c3]);
211 assert_ne!(r_ab, r_abc, "adding a head must change the integrity root");
212 }
213
214 #[test]
215 fn transformation_commitment_binds_the_operation() {
216 let input = TensorState::new(vec![1.0, 2.0], vec![2]);
217 let scaled = input.apply_operation("scale", &scale_params(2.0));
218 let mut add_params = HashMap::new();
219 add_params.insert("value".to_string(), 1.0);
220 let added = input.apply_operation("add", &add_params);
221 let t_scale = transformation_commitment(&input, &scaled);
222 let t_add = transformation_commitment(&input, &added);
223 assert_ne!(
224 t_scale, t_add,
225 "different operations ⇒ different transformation commitments"
226 );
227 }
228}