northroot_proof_engine/
hash.rs1use sha2::{Digest, Sha256 as Sha256Hasher};
7use thiserror::Error;
8
9use crate::canonical::{
10 canonicalize_method_shape, canonicalize_span_commitment, canonicalize_span_shape,
11 canonicalize_trace, canonicalize_trace_roots, CanonicalError,
12};
13use crate::types::{MethodShapeManifest, Sha256, SpanCommitment, SpanShape, Trace, TraceRoots};
14use crate::validation::validate_sha256_format;
15
16#[derive(Debug, Error, PartialEq, Eq)]
18pub enum HashError {
19 #[error("canonicalization error: {0}")]
21 Canonicalization(#[from] CanonicalError),
22
23 #[error("invalid hash format: {0}")]
25 InvalidFormat(String),
26}
27
28pub fn compute_sha256(canonical: &str) -> Sha256 {
43 let mut hasher = Sha256Hasher::new();
44 hasher.update(canonical.as_bytes());
45 let hash = hasher.finalize();
46
47 format!("sha256:{:x}", hash)
49}
50
51pub fn compute_span_shape_hash(shape: &SpanShape) -> Result<Sha256, HashError> {
55 let canonical = canonicalize_span_shape(shape)?;
56 Ok(compute_sha256(&canonical))
57}
58
59pub fn compute_method_shape_root(span_shapes: &[Sha256]) -> Result<Sha256, HashError> {
65 for (idx, hash) in span_shapes.iter().enumerate() {
67 validate_sha256_format(hash)
68 .map_err(|e| HashError::InvalidFormat(format!("span_shapes[{}]: {}", idx, e)))?;
69 }
70
71 let mut sorted = span_shapes.to_vec();
73 sorted.sort();
74 let joined = sorted.join("|");
75 Ok(compute_sha256(&joined))
76}
77
78pub fn compute_span_commitment_hash(commitment: &SpanCommitment) -> Result<Sha256, HashError> {
82 let canonical = canonicalize_span_commitment(commitment)?;
83 Ok(compute_sha256(&canonical))
84}
85
86pub fn compute_trace_set_root(span_hashes: &[Sha256]) -> Result<Sha256, HashError> {
92 for (idx, hash) in span_hashes.iter().enumerate() {
94 validate_sha256_format(hash)
95 .map_err(|e| HashError::InvalidFormat(format!("span_hashes[{}]: {}", idx, e)))?;
96 }
97
98 let mut sorted = span_hashes.to_vec();
100 sorted.sort();
101 let joined = sorted.join("|");
102 Ok(compute_sha256(&joined))
103}
104
105pub fn compute_identity_root(
112 trace_id: &str,
113 identity_route: &str,
114 span_hashes: &[Sha256],
115) -> Result<Sha256, HashError> {
116 for (idx, hash) in span_hashes.iter().enumerate() {
118 validate_sha256_format(hash)
119 .map_err(|e| HashError::InvalidFormat(format!("span_hashes[{}]: {}", idx, e)))?;
120 }
121
122 let mut sorted_hashes = span_hashes.to_vec();
124 sorted_hashes.sort();
125
126 use std::collections::BTreeMap;
128 let mut map = BTreeMap::new();
129 map.insert("trace_id", serde_json::Value::String(trace_id.to_string()));
130 map.insert(
131 "identity_route",
132 serde_json::Value::String(identity_route.to_string()),
133 );
134 map.insert(
135 "span_commitments",
136 serde_json::Value::Array(
137 sorted_hashes
138 .iter()
139 .map(|s| serde_json::Value::String(s.clone()))
140 .collect(),
141 ),
142 );
143
144 let canonical = serde_json::to_string(&serde_json::Value::Object(
145 map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
146 ))
147 .map_err(|e| HashError::Canonicalization(CanonicalError::Serialization(e.to_string())))?;
148
149 Ok(compute_sha256(&canonical))
150}
151
152pub fn compute_trace_seq_root(span_hashes: &[Sha256]) -> Result<Sha256, HashError> {
158 for (idx, hash) in span_hashes.iter().enumerate() {
160 validate_sha256_format(hash)
161 .map_err(|e| HashError::InvalidFormat(format!("span_hashes[{}]: {}", idx, e)))?;
162 }
163
164 let joined = span_hashes.join("|");
166 Ok(compute_sha256(&joined))
167}
168
169pub fn compute_method_shape_hash(manifest: &MethodShapeManifest) -> Result<Sha256, HashError> {
173 let canonical = canonicalize_method_shape(manifest)?;
174 Ok(compute_sha256(&canonical))
175}
176
177pub fn compute_trace_hash(trace: &Trace) -> Result<Sha256, HashError> {
179 let canonical = canonicalize_trace(trace)?;
180 Ok(compute_sha256(&canonical))
181}
182
183pub fn compute_trace_roots_hash(roots: &TraceRoots) -> Result<Sha256, HashError> {
185 let canonical = canonicalize_trace_roots(roots)?;
186 Ok(compute_sha256(&canonical))
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_compute_sha256_format() {
195 let hash = compute_sha256("test");
196 assert!(hash.starts_with("sha256:"));
197 assert_eq!(hash.len(), 71); assert!(hash
199 .chars()
200 .skip(7)
201 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
202 }
203
204 #[test]
205 fn test_compute_sha256_determinism() {
206 let hash1 = compute_sha256("test");
207 let hash2 = compute_sha256("test");
208 assert_eq!(hash1, hash2);
209 }
210
211 #[test]
212 fn test_compute_method_shape_root_sorted() {
213 let a =
214 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
215 let b =
216 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();
217 let c =
218 "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string();
219
220 let r1 = compute_method_shape_root(&[b.clone(), a.clone()]).unwrap();
221 let r2 = compute_method_shape_root(&[a.clone(), b.clone()]).unwrap();
222 assert_eq!(r1, r2, "Order must not matter");
223
224 let r3 = compute_method_shape_root(&[a, b, c.clone()]).unwrap();
225 assert_ne!(r2, r3, "Superset must produce different root");
226 }
227
228 #[test]
229 fn test_compute_trace_set_root_sorted() {
230 let a =
231 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
232 let b =
233 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();
234
235 let r1 = compute_trace_set_root(&[b.clone(), a.clone()]).unwrap();
236 let r2 = compute_trace_set_root(&[a.clone(), b.clone()]).unwrap();
237 assert_eq!(r1, r2, "Order must not matter");
238 }
239
240 #[test]
241 fn test_compute_identity_root() {
242 let trace_id = "tr_01H1234567890";
243 let identity_route = "did:key:zTest";
244 let spans = vec![
245 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
246 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
247 ];
248
249 let r1 = compute_identity_root(trace_id, identity_route, &spans).unwrap();
250 let r2 = compute_identity_root(trace_id, identity_route, &spans).unwrap();
251 assert_eq!(r1, r2, "Must be deterministic");
252
253 let r3 = compute_identity_root(trace_id, "did:key:zOther", &spans).unwrap();
255 assert_ne!(r1, r3, "Different identity must produce different root");
256 }
257
258 #[test]
259 fn test_compute_trace_seq_root_preserves_order() {
260 let a =
261 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
262 let b =
263 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();
264
265 let r1 = compute_trace_seq_root(&[a.clone(), b.clone()]).unwrap();
266 let r2 = compute_trace_seq_root(&[b.clone(), a.clone()]).unwrap();
267 assert_ne!(r1, r2, "Order must matter for sequence root");
268 }
269}