northroot-proof-engine 0.3.0

Core proof computation and validation library for PoSH (Proof of Shape) and PoX (Proof of Execution)
Documentation
//! Hash computation for v0.2 proof types
//!
//! This module provides SHA-256 hash computation functions for all proof types.
//! All hashes are formatted as `sha256:<64 lowercase hex chars>`.

use sha2::{Digest, Sha256 as Sha256Hasher};
use thiserror::Error;

use crate::canonical::{
    canonicalize_method_shape, canonicalize_span_commitment, canonicalize_span_shape,
    canonicalize_trace, canonicalize_trace_roots, CanonicalError,
};
use crate::types::{MethodShapeManifest, Sha256, SpanCommitment, SpanShape, Trace, TraceRoots};
use crate::validation::validate_sha256_format;

/// Error returned when hash computation fails
#[derive(Debug, Error, PartialEq, Eq)]
pub enum HashError {
    /// Canonicalization failed
    #[error("canonicalization error: {0}")]
    Canonicalization(#[from] CanonicalError),

    /// Invalid hash format
    #[error("invalid hash format: {0}")]
    InvalidFormat(String),
}

/// Computes SHA-256 hash from a canonical string.
///
/// Returns the hash in format: `sha256:<64 lowercase hex chars>`.
///
/// # Example
///
/// ```
/// use northroot_proof_engine::hash::compute_sha256;
///
/// let canonical = r#"{"key":"value"}"#;
/// let hash = compute_sha256(canonical);
/// assert!(hash.starts_with("sha256:"));
/// assert_eq!(hash.len(), 71); // "sha256:" (7) + 64 hex = 71
/// ```
pub fn compute_sha256(canonical: &str) -> Sha256 {
    let mut hasher = Sha256Hasher::new();
    hasher.update(canonical.as_bytes());
    let hash = hasher.finalize();

    // Format as sha256:<64 lowercase hex chars>
    format!("sha256:{:x}", hash)
}

/// Computes the span shape hash.
///
/// `span_shape_hash = sha256(JCS(span_shape))`
pub fn compute_span_shape_hash(shape: &SpanShape) -> Result<Sha256, HashError> {
    let canonical = canonicalize_span_shape(shape)?;
    Ok(compute_sha256(&canonical))
}

/// Computes the method shape root from a sorted list of span shape hashes.
///
/// `method_shape_root = sha256(join_sorted(span_shape_hash_i, "|"))`
///
/// The span shape hashes must be sorted lexicographically before joining.
pub fn compute_method_shape_root(span_shapes: &[Sha256]) -> Result<Sha256, HashError> {
    // Validate all hashes
    for (idx, hash) in span_shapes.iter().enumerate() {
        validate_sha256_format(hash)
            .map_err(|e| HashError::InvalidFormat(format!("span_shapes[{}]: {}", idx, e)))?;
    }

    // Sort and join
    let mut sorted = span_shapes.to_vec();
    sorted.sort();
    let joined = sorted.join("|");
    Ok(compute_sha256(&joined))
}

/// Computes the span commitment hash.
///
/// `span_commitment_hash = sha256(canonical(span_commitment))`
pub fn compute_span_commitment_hash(commitment: &SpanCommitment) -> Result<Sha256, HashError> {
    let canonical = canonicalize_span_commitment(commitment)?;
    Ok(compute_sha256(&canonical))
}

/// Computes the trace set root from a sorted list of span commitment hashes.
///
/// `trace_set_root = sha256(join_sorted(C_span_i, "|"))`
///
/// The span commitment hashes must be sorted lexicographically before joining.
pub fn compute_trace_set_root(span_hashes: &[Sha256]) -> Result<Sha256, HashError> {
    // Validate all hashes
    for (idx, hash) in span_hashes.iter().enumerate() {
        validate_sha256_format(hash)
            .map_err(|e| HashError::InvalidFormat(format!("span_hashes[{}]: {}", idx, e)))?;
    }

    // Sort and join
    let mut sorted = span_hashes.to_vec();
    sorted.sort();
    let joined = sorted.join("|");
    Ok(compute_sha256(&joined))
}

/// Computes the identity root.
///
/// `identity_root = sha256(JCS({trace_id, identity_route, sorted(C_span_i)}))`
///
/// This binds the trace to a specific identity while keeping the trace_set_root
/// independent of identity.
pub fn compute_identity_root(
    trace_id: &str,
    identity_route: &str,
    span_hashes: &[Sha256],
) -> Result<Sha256, HashError> {
    // Validate all hashes
    for (idx, hash) in span_hashes.iter().enumerate() {
        validate_sha256_format(hash)
            .map_err(|e| HashError::InvalidFormat(format!("span_hashes[{}]: {}", idx, e)))?;
    }

    // Sort span hashes
    let mut sorted_hashes = span_hashes.to_vec();
    sorted_hashes.sort();

    // Create canonical JSON object
    use std::collections::BTreeMap;
    let mut map = BTreeMap::new();
    map.insert("trace_id", serde_json::Value::String(trace_id.to_string()));
    map.insert(
        "identity_route",
        serde_json::Value::String(identity_route.to_string()),
    );
    map.insert(
        "span_commitments",
        serde_json::Value::Array(
            sorted_hashes
                .iter()
                .map(|s| serde_json::Value::String(s.clone()))
                .collect(),
        ),
    );

    let canonical = serde_json::to_string(&serde_json::Value::Object(
        map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
    ))
    .map_err(|e| HashError::Canonicalization(CanonicalError::Serialization(e.to_string())))?;

    Ok(compute_sha256(&canonical))
}

/// Computes the trace sequence root (optional).
///
/// `trace_seq_root = sha256(join_in_order(C_span_i, "|"))`
///
/// This preserves ordering information, unlike trace_set_root which is order-independent.
pub fn compute_trace_seq_root(span_hashes: &[Sha256]) -> Result<Sha256, HashError> {
    // Validate all hashes
    for (idx, hash) in span_hashes.iter().enumerate() {
        validate_sha256_format(hash)
            .map_err(|e| HashError::InvalidFormat(format!("span_hashes[{}]: {}", idx, e)))?;
    }

    // Join in order (no sorting)
    let joined = span_hashes.join("|");
    Ok(compute_sha256(&joined))
}

/// Computes hash of a method shape manifest.
///
/// This is used for verifying manifest integrity (excluding publisher_sig).
pub fn compute_method_shape_hash(manifest: &MethodShapeManifest) -> Result<Sha256, HashError> {
    let canonical = canonicalize_method_shape(manifest)?;
    Ok(compute_sha256(&canonical))
}

/// Computes hash of a trace.
pub fn compute_trace_hash(trace: &Trace) -> Result<Sha256, HashError> {
    let canonical = canonicalize_trace(trace)?;
    Ok(compute_sha256(&canonical))
}

/// Computes hash of trace roots.
pub fn compute_trace_roots_hash(roots: &TraceRoots) -> Result<Sha256, HashError> {
    let canonical = canonicalize_trace_roots(roots)?;
    Ok(compute_sha256(&canonical))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_compute_sha256_format() {
        let hash = compute_sha256("test");
        assert!(hash.starts_with("sha256:"));
        assert_eq!(hash.len(), 71); // "sha256:" (7) + 64 hex = 71
        assert!(hash
            .chars()
            .skip(7)
            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
    }

    #[test]
    fn test_compute_sha256_determinism() {
        let hash1 = compute_sha256("test");
        let hash2 = compute_sha256("test");
        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_compute_method_shape_root_sorted() {
        let a =
            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
        let b =
            "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();
        let c =
            "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string();

        let r1 = compute_method_shape_root(&[b.clone(), a.clone()]).unwrap();
        let r2 = compute_method_shape_root(&[a.clone(), b.clone()]).unwrap();
        assert_eq!(r1, r2, "Order must not matter");

        let r3 = compute_method_shape_root(&[a, b, c.clone()]).unwrap();
        assert_ne!(r2, r3, "Superset must produce different root");
    }

    #[test]
    fn test_compute_trace_set_root_sorted() {
        let a =
            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
        let b =
            "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();

        let r1 = compute_trace_set_root(&[b.clone(), a.clone()]).unwrap();
        let r2 = compute_trace_set_root(&[a.clone(), b.clone()]).unwrap();
        assert_eq!(r1, r2, "Order must not matter");
    }

    #[test]
    fn test_compute_identity_root() {
        let trace_id = "tr_01H1234567890";
        let identity_route = "did:key:zTest";
        let spans = vec![
            "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
        ];

        let r1 = compute_identity_root(trace_id, identity_route, &spans).unwrap();
        let r2 = compute_identity_root(trace_id, identity_route, &spans).unwrap();
        assert_eq!(r1, r2, "Must be deterministic");

        // Different identity should produce different root
        let r3 = compute_identity_root(trace_id, "did:key:zOther", &spans).unwrap();
        assert_ne!(r1, r3, "Different identity must produce different root");
    }

    #[test]
    fn test_compute_trace_seq_root_preserves_order() {
        let a =
            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
        let b =
            "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();

        let r1 = compute_trace_seq_root(&[a.clone(), b.clone()]).unwrap();
        let r2 = compute_trace_seq_root(&[b.clone(), a.clone()]).unwrap();
        assert_ne!(r1, r2, "Order must matter for sequence root");
    }
}