Skip to main content

dirtydata_core/
hash.rs

1//! Deterministic hashing using BLAKE3.
2//!
3//! Git に人生相談してはいけない。
4//! DirtyData の因果の鎖は BLAKE3 で繋ぐ。
5
6use blake3::Hasher;
7
8use crate::ir::{Edge, Graph, Node};
9use crate::patch::{Operation, Patch};
10
11/// Hash arbitrary bytes.
12pub fn hash_bytes(bytes: &[u8]) -> [u8; 32] {
13    let mut hasher = Hasher::new();
14    hasher.update(bytes);
15    *hasher.finalize().as_bytes()
16}
17
18/// Hash a patch deterministically.
19/// The hash covers operations, intent, and parents — but NOT the identity or timestamp.
20/// This means the same logical change always produces the same hash.
21pub fn hash_patch(patch: &Patch) -> [u8; 32] {
22    let mut hasher = Hasher::new();
23    hasher.update(b"dirtydata:patch:v2:");
24
25    // Hash operations in order
26    for op in &patch.operations {
27        hash_operation(&mut hasher, op);
28    }
29
30    // Hash intent reference
31    if let Some(ref intent_id) = patch.intent_ref {
32        hasher.update(b"intent:");
33        hasher.update(intent_id.0.to_string().as_bytes());
34    }
35
36    // Hash parents (Merkle DAG lineage)
37    for (id, hash) in patch.parents.iter().zip(patch.parent_hashes.iter()) {
38        hasher.update(b"parent:");
39        hasher.update(id.0.to_string().as_bytes());
40        hasher.update(b":hash:");
41        hasher.update(hash);
42    }
43
44    *hasher.finalize().as_bytes()
45}
46
47fn hash_operation(hasher: &mut Hasher, op: &Operation) {
48    match op {
49        Operation::AddNode(node) => {
50            hasher.update(b"op:add_node:");
51            hash_node(hasher, node);
52        }
53        Operation::RemoveNode(id) => {
54            hasher.update(b"op:remove_node:");
55            hasher.update(id.0.to_string().as_bytes());
56        }
57        Operation::ReplaceNode(node) => {
58            hasher.update(b"op:replace_node:");
59            hash_node(hasher, node);
60        }
61        Operation::ModifyConfig { node_id, delta } => {
62            hasher.update(b"op:modify_config:");
63            hasher.update(node_id.0.to_string().as_bytes());
64            // BTreeMap guarantees deterministic key ordering.
65            // Serialize to JSON for canonical byte representation.
66            let json = serde_json::to_string(delta).unwrap_or_default();
67            hasher.update(json.as_bytes());
68        }
69        Operation::AddEdge(edge) => {
70            hasher.update(b"op:add_edge:");
71            hash_edge(hasher, edge);
72        }
73        Operation::RemoveEdge(id) => {
74            hasher.update(b"op:remove_edge:");
75            hasher.update(id.0.to_string().as_bytes());
76        }
77        Operation::ModifyEdge { edge_id, delta } => {
78            hasher.update(b"op:modify_edge:");
79            hasher.update(edge_id.0.to_string().as_bytes());
80            let json = serde_json::to_string(delta).unwrap_or_default();
81            hasher.update(json.as_bytes());
82        }
83        Operation::AddModulation(m) => {
84            hasher.update(b"op:add_modulation:");
85            hash_modulation(hasher, m);
86        }
87        Operation::RemoveModulation(id) => {
88            hasher.update(b"op:remove_modulation:");
89            hasher.update(id.0.to_string().as_bytes());
90        }
91    }
92}
93
94fn hash_node(hasher: &mut Hasher, node: &Node) {
95    hasher.update(b"node:");
96    hasher.update(node.id.0.to_string().as_bytes());
97
98    let kind_json = serde_json::to_string(&node.kind).unwrap_or_default();
99    hasher.update(kind_json.as_bytes());
100
101    // Ports — order matters
102    for port in &node.ports {
103        hasher.update(b"port:");
104        hasher.update(port.name.as_bytes());
105        let dir = serde_json::to_string(&port.direction).unwrap_or_default();
106        hasher.update(dir.as_bytes());
107        let domain = serde_json::to_string(&port.domain).unwrap_or_default();
108        hasher.update(domain.as_bytes());
109        let dtype = serde_json::to_string(&port.data_type).unwrap_or_default();
110        hasher.update(dtype.as_bytes());
111    }
112
113    // Config — BTreeMap ordering is deterministic
114    let config_json = serde_json::to_string(&node.config).unwrap_or_default();
115    hasher.update(config_json.as_bytes());
116
117    // Metadata ref
118    let meta = serde_json::to_string(&node.metadata).unwrap_or_default();
119    hasher.update(meta.as_bytes());
120}
121
122fn hash_edge(hasher: &mut Hasher, edge: &Edge) {
123    hasher.update(b"edge:");
124    hasher.update(edge.id.0.to_string().as_bytes());
125    hasher.update(edge.source.node_id.0.to_string().as_bytes());
126    hasher.update(b":");
127    hasher.update(edge.source.port_name.as_bytes());
128    hasher.update(b"->");
129    hasher.update(edge.target.node_id.0.to_string().as_bytes());
130    hasher.update(b":");
131    hasher.update(edge.target.port_name.as_bytes());
132    hasher.update(&[edge.kind as u8]);
133}
134
135fn hash_modulation(hasher: &mut Hasher, m: &crate::ir::Modulation) {
136    hasher.update(b"modulation:");
137    hasher.update(m.id.0.to_string().as_bytes());
138    hasher.update(m.source.node_id.0.to_string().as_bytes());
139    hasher.update(b":");
140    hasher.update(m.source.port_name.as_bytes());
141    hasher.update(b"->param:");
142    hasher.update(m.target_node.0.to_string().as_bytes());
143    hasher.update(b":");
144    hasher.update(m.target_param.as_bytes());
145    hasher.update(&m.amount.to_le_bytes());
146}
147
148/// Hash an entire graph for integrity verification.
149/// BTreeMap iteration gives deterministic key order.
150pub fn hash_graph(graph: &Graph) -> [u8; 32] {
151    let mut hasher = Hasher::new();
152    hasher.update(b"dirtydata:graph:v1:");
153
154    for node in graph.nodes.values() {
155        hash_node(&mut hasher, node);
156    }
157    for edge in graph.edges.values() {
158        hash_edge(&mut hasher, edge);
159    }
160    for (_, m) in &graph.modulations {
161        hash_modulation(&mut hasher, m);
162    }
163
164    hasher.update(&graph.revision.0.to_le_bytes());
165
166    *hasher.finalize().as_bytes()
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::ir::Node;
173    use crate::patch::Operation;
174
175    #[test]
176    fn test_graph_hash_deterministic() {
177        let mut g = Graph::new();
178        let node = Node::new_source("Sine");
179        let patch = Patch::from_operations(vec![Operation::AddNode(node)]);
180        g.apply(&patch).unwrap();
181
182        let h1 = hash_graph(&g);
183        let h2 = hash_graph(&g);
184        assert_eq!(h1, h2);
185    }
186
187    #[test]
188    fn test_different_graphs_different_hashes() {
189        let mut g1 = Graph::new();
190        g1.apply(&Patch::from_operations(vec![Operation::AddNode(
191            Node::new_source("Sine"),
192        )]))
193        .unwrap();
194
195        let mut g2 = Graph::new();
196        g2.apply(&Patch::from_operations(vec![Operation::AddNode(
197            Node::new_source("Noise"),
198        )]))
199        .unwrap();
200
201        assert_ne!(hash_graph(&g1), hash_graph(&g2));
202    }
203
204    #[test]
205    fn test_patch_hash_stable() {
206        let node = Node::new_processor("EQ");
207        let patch = Patch::from_operations(vec![Operation::AddNode(node.clone())]);
208        let h1 = hash_patch(&patch);
209        let h2 = hash_patch(&patch);
210        assert_eq!(h1, h2);
211    }
212}