Skip to main content

qualia_core_db/solvers/calculus/
tensor_integrity.rs

1//! Tamper-evident, append-only lineage commitments for tensor provenance, plus a
2//! zero-knowledge binding to verified linear-transformation proofs.
3//!
4//! Companion to [`super::tensor_provenance`] (which tracks the parent→child DAG).
5//! This module adds the *integrity* layer the audit calls for:
6//!
7//! - **Immutable, append-only lineage DAG.** Each node is content-addressed by a
8//!   BLAKE3 commitment over `domain ++ parent_commitment ++ operation ++ params ++
9//!   data_bits`. Any post-hoc edit to an ancestor's data/operation changes that
10//!   node's commitment and therefore *every* descendant's — so tampering anywhere in
11//!   the lineage is detectable ([`verify_lineage`]). The DAG is append-only: the only
12//!   way to extend it is to derive a new child; there is no in-place mutation path.
13//! - **zk-transformation binding.** [`transformation_commitment`] binds
14//!   `(input, output, operation)` cryptographically — the public witness. For a
15//!   *linear* tensor map (prove `y = W·x` without revealing `W`), the actual
16//!   zero-knowledge proof is the real arkworks Groth16 `private_matrix_multiply`
17//!   (`crate::zk_proofs` / `linear_algebra`). General per-op zk-SNARKs over arbitrary
18//!   tensor ops are a recorded boundary (each op needs its own R1CS circuit).
19//!
20//! Heap note: like `tensor_provenance`, this is the **cold, host-side provenance
21//! layer** (it walks a `HashMap`-backed graph and allocates small scratch `Vec`s for
22//! sorting). It is off the zero-heap hot path — the hot numerical kernels are in
23//! `ode_advanced` / `ode_solver`, which allocate nothing.
24
25use super::tensor_provenance::{ProvenanceGraph, TensorProvenance, TensorState};
26
27/// Domain separators so commitments at different layers can never collide.
28const 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/// A 32-byte BLAKE3 commitment content-addressing a tensor state within its lineage.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct LineageCommitment(pub [u8; 32]);
35
36/// Content-address a single tensor `state` given its parent's commitment (or `None`
37/// for a genesis/root state). The commitment binds the parent, the operation + its
38/// params (in deterministic key order), and the full data bits.
39pub 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            // Deterministic param order so the commitment is reproducible.
57            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(&params[k].to_bits().to_le_bytes());
62            }
63        }
64    }
65    // Length-prefix then the full data bits (prevents data/shape extension ambiguity).
66    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
73/// Fold the lineage commitment for `state_id` from the root down to the node.
74/// Returns `None` if the chain is broken (a referenced ancestor is missing).
75pub fn lineage_commitment(graph: &ProvenanceGraph, state_id: u64) -> Option<LineageCommitment> {
76    // `get_lineage` returns [node, parent, …, root]; fold from the root forward.
77    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
89/// Verify that `state_id`'s lineage reproduces `expected` — i.e. nothing in the chain
90/// (data, operation, params, or structure) was altered after the commitment was taken.
91pub 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
99/// A Merkle-style integrity root over a set of leaf commitments (e.g. all current
100/// head states): a single 32-byte digest witnessing the whole provenance frontier.
101/// Order-independent (commitments are sorted first).
102pub 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
114/// Bind an input state, an output state, and the operation that produced it into a
115/// public 32-byte commitment — the witness a zero-knowledge transformation proof is
116/// checked against. (For linear maps, the ZK proof itself is the real Groth16
117/// `private_matrix_multiply`; this is the public binding it commits to.)
118pub 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]); // one bit different
145        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        // Child commitment differs from root (it chains the parent + the operation).
170        assert_ne!(root_commit, child_commit);
171        // And it reproduces on verification.
172        assert!(verify_lineage(&graph, child_id, &child_commit));
173    }
174
175    #[test]
176    fn tampering_with_an_ancestor_is_detected() {
177        // Genuine lineage vs a forged one where the root's data was altered.
178        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        // Forged graph: same child node, but the *root* it points to was tampered.
188        let mut forged = ProvenanceGraph::new();
189        let mut tampered_root = root.clone();
190        tampered_root.data = vec![999.0]; // alter ancestor data, keep the id
191        forged.add_state(tampered_root);
192        forged.add_state(child);
193
194        // The same child id now yields a different lineage commitment → tamper caught.
195        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}