Skip to main content

northroot_proof_engine/
hash.rs

1//! Hash computation for v0.2 proof types
2//!
3//! This module provides SHA-256 hash computation functions for all proof types.
4//! All hashes are formatted as `sha256:<64 lowercase hex chars>`.
5
6use 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/// Error returned when hash computation fails
17#[derive(Debug, Error, PartialEq, Eq)]
18pub enum HashError {
19    /// Canonicalization failed
20    #[error("canonicalization error: {0}")]
21    Canonicalization(#[from] CanonicalError),
22
23    /// Invalid hash format
24    #[error("invalid hash format: {0}")]
25    InvalidFormat(String),
26}
27
28/// Computes SHA-256 hash from a canonical string.
29///
30/// Returns the hash in format: `sha256:<64 lowercase hex chars>`.
31///
32/// # Example
33///
34/// ```
35/// use northroot_proof_engine::hash::compute_sha256;
36///
37/// let canonical = r#"{"key":"value"}"#;
38/// let hash = compute_sha256(canonical);
39/// assert!(hash.starts_with("sha256:"));
40/// assert_eq!(hash.len(), 71); // "sha256:" (7) + 64 hex = 71
41/// ```
42pub 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 as sha256:<64 lowercase hex chars>
48    format!("sha256:{:x}", hash)
49}
50
51/// Computes the span shape hash.
52///
53/// `span_shape_hash = sha256(JCS(span_shape))`
54pub fn compute_span_shape_hash(shape: &SpanShape) -> Result<Sha256, HashError> {
55    let canonical = canonicalize_span_shape(shape)?;
56    Ok(compute_sha256(&canonical))
57}
58
59/// Computes the method shape root from a sorted list of span shape hashes.
60///
61/// `method_shape_root = sha256(join_sorted(span_shape_hash_i, "|"))`
62///
63/// The span shape hashes must be sorted lexicographically before joining.
64pub fn compute_method_shape_root(span_shapes: &[Sha256]) -> Result<Sha256, HashError> {
65    // Validate all hashes
66    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    // Sort and join
72    let mut sorted = span_shapes.to_vec();
73    sorted.sort();
74    let joined = sorted.join("|");
75    Ok(compute_sha256(&joined))
76}
77
78/// Computes the span commitment hash.
79///
80/// `span_commitment_hash = sha256(canonical(span_commitment))`
81pub fn compute_span_commitment_hash(commitment: &SpanCommitment) -> Result<Sha256, HashError> {
82    let canonical = canonicalize_span_commitment(commitment)?;
83    Ok(compute_sha256(&canonical))
84}
85
86/// Computes the trace set root from a sorted list of span commitment hashes.
87///
88/// `trace_set_root = sha256(join_sorted(C_span_i, "|"))`
89///
90/// The span commitment hashes must be sorted lexicographically before joining.
91pub fn compute_trace_set_root(span_hashes: &[Sha256]) -> Result<Sha256, HashError> {
92    // Validate all hashes
93    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    // Sort and join
99    let mut sorted = span_hashes.to_vec();
100    sorted.sort();
101    let joined = sorted.join("|");
102    Ok(compute_sha256(&joined))
103}
104
105/// Computes the identity root.
106///
107/// `identity_root = sha256(JCS({trace_id, identity_route, sorted(C_span_i)}))`
108///
109/// This binds the trace to a specific identity while keeping the trace_set_root
110/// independent of identity.
111pub fn compute_identity_root(
112    trace_id: &str,
113    identity_route: &str,
114    span_hashes: &[Sha256],
115) -> Result<Sha256, HashError> {
116    // Validate all hashes
117    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    // Sort span hashes
123    let mut sorted_hashes = span_hashes.to_vec();
124    sorted_hashes.sort();
125
126    // Create canonical JSON object
127    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
152/// Computes the trace sequence root (optional).
153///
154/// `trace_seq_root = sha256(join_in_order(C_span_i, "|"))`
155///
156/// This preserves ordering information, unlike trace_set_root which is order-independent.
157pub fn compute_trace_seq_root(span_hashes: &[Sha256]) -> Result<Sha256, HashError> {
158    // Validate all hashes
159    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    // Join in order (no sorting)
165    let joined = span_hashes.join("|");
166    Ok(compute_sha256(&joined))
167}
168
169/// Computes hash of a method shape manifest.
170///
171/// This is used for verifying manifest integrity (excluding publisher_sig).
172pub fn compute_method_shape_hash(manifest: &MethodShapeManifest) -> Result<Sha256, HashError> {
173    let canonical = canonicalize_method_shape(manifest)?;
174    Ok(compute_sha256(&canonical))
175}
176
177/// Computes hash of a trace.
178pub fn compute_trace_hash(trace: &Trace) -> Result<Sha256, HashError> {
179    let canonical = canonicalize_trace(trace)?;
180    Ok(compute_sha256(&canonical))
181}
182
183/// Computes hash of trace roots.
184pub 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); // "sha256:" (7) + 64 hex = 71
198        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        // Different identity should produce different root
254        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}