northroot-proof-engine 0.3.2

Core proof computation and validation library for PoSH (Proof of Shape) and PoX (Proof of Execution)
Documentation
//! Integration tests for v0.2 engine
//!
//! These tests verify end-to-end workflows using golden files,
//! ensuring that canonicalization, hashing, and verification work
//! correctly together.

use northroot_proof_engine::{
    canonicalize_method_shape, canonicalize_span_commitment, canonicalize_trace,
    canonicalize_trace_roots, compute_method_shape_hash, compute_span_commitment_hash,
    compute_trace_hash, compute_trace_roots_hash, validate_signature_format, MethodShapeManifest,
    SignedSpan, SpanCommitment, Trace, TraceRoots,
};
use std::fs;
use std::path::PathBuf;

fn load_golden_file(name: &str) -> String {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("tests");
    path.push("golden");
    path.push(name);
    fs::read_to_string(&path).unwrap_or_else(|e| panic!("Failed to load {}: {}", name, e))
}

#[test]
fn test_posh_workflow() {
    // Load PoSH golden file
    let json = load_golden_file("posh_method_shape.json");
    let manifest: MethodShapeManifest =
        serde_json::from_str(&json).expect("Failed to parse PoSH golden file");

    // Verify signature format
    validate_signature_format(&manifest.publisher_sig)
        .expect("Publisher signature format should be valid");

    // Canonicalize (excluding signature)
    let canonical =
        canonicalize_method_shape(&manifest).expect("Failed to canonicalize method shape");

    // Compute hash
    let hash = compute_method_shape_hash(&manifest).expect("Failed to compute method shape hash");

    // Verify hash format
    assert!(
        hash.starts_with("sha256:"),
        "Hash should start with sha256: prefix"
    );
    assert_eq!(
        hash.len(),
        71,
        "Hash should be sha256: + 64 hex chars = 71 chars"
    );

    // Verify canonicalization is deterministic
    let canonical2 = canonicalize_method_shape(&manifest)
        .expect("Failed to canonicalize method shape (second time)");
    assert_eq!(
        canonical, canonical2,
        "Canonicalization must be deterministic"
    );

    // Verify hash is deterministic
    let hash2 = compute_method_shape_hash(&manifest)
        .expect("Failed to compute method shape hash (second time)");
    assert_eq!(hash, hash2, "Hash computation must be deterministic");
}

#[test]
fn test_pox_span_commitment_workflow() {
    // Load PoX span commitment golden file
    let json = load_golden_file("pox_span_commitment.json");
    let commitment: SpanCommitment =
        serde_json::from_str(&json).expect("Failed to parse span commitment golden file");

    // Validate commitment fields
    assert_eq!(commitment.schema_version, "proof.v2");
    assert!(!commitment.trace_id.is_empty());
    assert!(!commitment.span_id.is_empty());

    // Canonicalize
    let canonical =
        canonicalize_span_commitment(&commitment).expect("Failed to canonicalize span commitment");

    // Compute hash
    let hash =
        compute_span_commitment_hash(&commitment).expect("Failed to compute span commitment hash");

    // Verify hash format
    assert!(
        hash.starts_with("sha256:"),
        "Hash should start with sha256: prefix"
    );
    assert_eq!(hash.len(), 71, "Hash should be 71 chars");

    // Verify determinism
    let canonical2 = canonicalize_span_commitment(&commitment)
        .expect("Failed to canonicalize span commitment (second time)");
    assert_eq!(
        canonical, canonical2,
        "Canonicalization must be deterministic"
    );

    let hash2 = compute_span_commitment_hash(&commitment)
        .expect("Failed to compute span commitment hash (second time)");
    assert_eq!(hash, hash2, "Hash computation must be deterministic");
}

#[test]
fn test_pox_signed_span_workflow() {
    // Load PoX signed span golden file
    let json = load_golden_file("pox_signed_span.json");
    let signed_span: SignedSpan =
        serde_json::from_str(&json).expect("Failed to parse signed span golden file");

    // Verify signature format
    validate_signature_format(&signed_span.signature).expect("Signature format should be valid");

    // Note: We can't verify the actual signature without the verifying key,
    // but we can verify the format and that verification doesn't panic
    // In a real scenario, we'd need the verifying key bytes
    assert_eq!(signed_span.signature.alg, "ed25519");
    assert!(!signed_span.signature.kid.is_empty());
    assert!(!signed_span.signature.sig.is_empty());

    // Verify commitment can be canonicalized and hashed
    let hash = compute_span_commitment_hash(&signed_span.commitment)
        .expect("Failed to compute span commitment hash");

    assert!(
        hash.starts_with("sha256:"),
        "Hash should start with sha256: prefix"
    );
}

#[test]
fn test_pox_trace_workflow() {
    // Load PoX trace golden file
    let json = load_golden_file("pox_trace.json");
    let trace: Trace = serde_json::from_str(&json).expect("Failed to parse trace golden file");

    // Validate trace fields
    assert_eq!(trace.schema_version, "trace.v2");
    assert!(!trace.trace_id.is_empty());
    assert!(!trace.method_ref.method_id.is_empty());

    // Canonicalize
    let canonical = canonicalize_trace(&trace).expect("Failed to canonicalize trace");

    // Compute hash
    let hash = compute_trace_hash(&trace).expect("Failed to compute trace hash");

    // Verify hash format
    assert!(
        hash.starts_with("sha256:"),
        "Hash should start with sha256: prefix"
    );
    assert_eq!(hash.len(), 71, "Hash should be 71 chars");

    // Verify determinism
    let canonical2 =
        canonicalize_trace(&trace).expect("Failed to canonicalize trace (second time)");
    assert_eq!(
        canonical, canonical2,
        "Canonicalization must be deterministic"
    );

    let hash2 = compute_trace_hash(&trace).expect("Failed to compute trace hash (second time)");
    assert_eq!(hash, hash2, "Hash computation must be deterministic");
}

#[test]
fn test_pox_trace_roots_workflow() {
    // Load PoX trace roots golden file
    let json = load_golden_file("pox_trace_roots.json");
    let roots: TraceRoots =
        serde_json::from_str(&json).expect("Failed to parse trace roots golden file");

    // Validate roots fields
    assert!(!roots.trace_id.is_empty());
    assert!(!roots.span_commitments.is_empty());
    assert!(!roots.trace_set_root.is_empty());
    if let Some(ref id_root) = roots.identity_root {
        assert!(!id_root.is_empty());
    }
    if let Some(ref seq_root) = roots.trace_seq_root {
        assert!(!seq_root.is_empty());
    }

    // Verify signature format (if present)
    if let Some(ref sig) = roots.server_sig {
        validate_signature_format(sig).expect("Server signature format should be valid");
    }

    // Canonicalize
    let canonical = canonicalize_trace_roots(&roots).expect("Failed to canonicalize trace roots");

    // Compute hash
    let hash = compute_trace_roots_hash(&roots).expect("Failed to compute trace roots hash");

    // Verify hash format
    assert!(
        hash.starts_with("sha256:"),
        "Hash should start with sha256: prefix"
    );
    assert_eq!(hash.len(), 71, "Hash should be 71 chars");

    // Verify determinism
    let canonical2 =
        canonicalize_trace_roots(&roots).expect("Failed to canonicalize trace roots (second time)");
    assert_eq!(
        canonical, canonical2,
        "Canonicalization must be deterministic"
    );

    let hash2 =
        compute_trace_roots_hash(&roots).expect("Failed to compute trace roots hash (second time)");
    assert_eq!(hash, hash2, "Hash computation must be deterministic");
}

#[test]
fn test_golden_file_roundtrip() {
    // Test that we can load, canonicalize, and hash all golden files
    // without errors and with deterministic results

    // PoSH
    let posh_json = load_golden_file("posh_method_shape.json");
    let posh: MethodShapeManifest = serde_json::from_str(&posh_json).expect("Failed to parse PoSH");
    let _posh_hash = compute_method_shape_hash(&posh).expect("Failed to hash PoSH");

    // PoX Span Commitment
    let commitment_json = load_golden_file("pox_span_commitment.json");
    let commitment: SpanCommitment =
        serde_json::from_str(&commitment_json).expect("Failed to parse span commitment");
    let _commitment_hash =
        compute_span_commitment_hash(&commitment).expect("Failed to hash span commitment");

    // PoX Signed Span
    let signed_json = load_golden_file("pox_signed_span.json");
    let signed: SignedSpan =
        serde_json::from_str(&signed_json).expect("Failed to parse signed span");
    let _signed_hash =
        compute_span_commitment_hash(&signed.commitment).expect("Failed to hash signed span");

    // PoX Trace
    let trace_json = load_golden_file("pox_trace.json");
    let trace: Trace = serde_json::from_str(&trace_json).expect("Failed to parse trace");
    let _trace_hash = compute_trace_hash(&trace).expect("Failed to hash trace");

    // PoX Trace Roots
    let roots_json = load_golden_file("pox_trace_roots.json");
    let roots: TraceRoots = serde_json::from_str(&roots_json).expect("Failed to parse trace roots");
    let _roots_hash = compute_trace_roots_hash(&roots).expect("Failed to hash trace roots");

    // All operations completed successfully
}