northroot-proof-engine 0.2.0

Core proof computation and validation library for PoSH (Proof of Shape) and PoX (Proof of Execution)
Documentation
//! Data shape hashing for v0.2
//!
//! This module provides functions for computing data shape hashes.
//! In v0.2, this is a minimal implementation that hashes the schema
//! and optional sketches.

use std::collections::BTreeMap;
use thiserror::Error;

use crate::hash::compute_sha256;
use crate::types::Sha256;
use crate::validation::validate_sha256_format;

/// Error returned when data shape operations fail
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DataShapeError {
    /// Serialization error
    #[error("serialization error: {0}")]
    Serialization(String),

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

/// Computes the data shape hash from a schema and optional sketches.
///
/// In v0.2, this is a minimal implementation:
/// - If sketches are provided: `H(JCS({schema, sketches}))`
/// - Otherwise: `H(JCS({schema}))`
///
/// The schema and sketches are canonicalized using BTreeMap for deterministic ordering.
pub fn compute_data_shape_hash(
    schema: &serde_json::Value,
    sketches: Option<&serde_json::Value>,
) -> Result<Sha256, DataShapeError> {
    let mut map = BTreeMap::new();
    map.insert("schema", schema.clone());

    if let Some(sketches_val) = sketches {
        map.insert("sketches", sketches_val.clone());
    }

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

    Ok(compute_sha256(&canonical))
}

/// Validates that a data shape hash has the correct format.
///
/// This is a convenience wrapper around `validate_sha256_format`.
pub fn validate_data_shape_hash(hash: &str) -> Result<(), DataShapeError> {
    validate_sha256_format(hash).map_err(|e| DataShapeError::InvalidFormat(e.to_string()))
}

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

    #[test]
    fn test_compute_data_shape_hash_schema_only() {
        let schema = serde_json::json!({
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "number"}
            }
        });

        let hash1 = compute_data_shape_hash(&schema, None).unwrap();
        let hash2 = compute_data_shape_hash(&schema, None).unwrap();
        assert_eq!(hash1, hash2, "Must be deterministic");
        assert!(hash1.starts_with("sha256:"));
    }

    #[test]
    fn test_compute_data_shape_hash_with_sketches() {
        let schema = serde_json::json!({
            "type": "object",
            "properties": {
                "name": {"type": "string"}
            }
        });

        let sketches = serde_json::json!({
            "cardinality": 1000,
            "null_count": 5
        });

        let hash1 = compute_data_shape_hash(&schema, Some(&sketches)).unwrap();
        let hash2 = compute_data_shape_hash(&schema, Some(&sketches)).unwrap();
        assert_eq!(hash1, hash2, "Must be deterministic");

        // Hash with sketches should differ from hash without
        let hash_no_sketches = compute_data_shape_hash(&schema, None).unwrap();
        assert_ne!(hash1, hash_no_sketches, "Sketches should affect hash");
    }

    #[test]
    fn test_validate_data_shape_hash() {
        let valid = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
        assert!(validate_data_shape_hash(valid).is_ok());

        let invalid = "invalid";
        assert!(validate_data_shape_hash(invalid).is_err());
    }
}