spectral_vm 0.1.6

HYPERION: Production-ready zero-knowledge virtual machine with spectral analysis
Documentation
/*
 * ═══════════════════════════════════════════════════════════════════════════
 * TECHNICAL MANIFEST: Fiat-Shamir Transcript (HARDENED)
 * SOVEREIGN SPECTRAL ROLE: Random Oracle Model (Non-Interactive Arguments)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * COMPLEXITY: O(1) per append | O(1) per challenge
 * HASH: SHA-256 (256-bit security)
 * DOMAIN: Interactive → Non-Interactive transformation
 *
 * ARCHITECTURAL INVARIANTS:
 * - Causal Linkage: Each challenge depends on all prior appends
 * - Chain Update: challenge() finalizes and re-seeds state
 * - Domain Separation: Length-prefixed labels prevent collision attacks
 *
 * SECURITY PROPERTIES:
 * - Grinding Resistance: Each challenge updates state (no replay)
 * - Determinism: Same inputs → same challenges (reproducibility)
 * - HARDENED: 128-bit rejection sampling for bias-free challenges
 * ═══════════════════════════════════════════════════════════════════════════
 */

use crate::field::Goldilocks;
use sha2::{Digest, Sha256};

/// Fiat-Shamir Transcript for Sovereign Spectral arguments.
/// Transforms interactive proofs into non-interactive with soundness preservation.
#[derive(Clone)]
pub struct Transcript {
    /// Internal hasher for recursive chain.
    hasher: Sha256,
}

impl Transcript {
    /// Initializes a fresh transcript with domain separator.
    /// COMPLEXITY: O(1).
    pub fn new() -> Self {
        let mut hasher = Sha256::new();
        // Domain separation: Bind to protocol identifier
        hasher.update(b"HYPERION_SOVEREIGN_SPECTRAL_v1");
        Self { hasher }
    }

    /// Appends data to the transcript with length-prefixed domain separation.
    /// SECURITY FIX (R2): Prevents label/data collision attacks.
    /// COMPLEXITY: O(|data|).
    pub fn append(&mut self, label: &[u8], data: &[u8]) {
        // Length prefix for label
        self.hasher.update(&(label.len() as u64).to_le_bytes());
        self.hasher.update(label);
        // Length prefix for data
        self.hasher.update(&(data.len() as u64).to_le_bytes());
        self.hasher.update(data);
    }

    /// Generates a bias-free Goldilocks challenge using 128-bit reduction.
    /// SECURITY FIX (C3): Uses full 128-bit sample to eliminate modular bias.
    /// COMPLEXITY: O(1).
    pub fn challenge(&mut self) -> Goldilocks {
        let result = self.hasher.clone().finalize();
        self.hasher.update(&result);

        // Use 128 bits for bias-free reduction
        let mut bytes = [0u8; 16];
        bytes.copy_from_slice(&result[0..16]);
        let val = u128::from_le_bytes(bytes);

        Goldilocks::reduce128(val)
    }

    /// Legacy challenge method returning i64.
    /// DEPRECATED: Use challenge() returning Goldilocks instead.
    #[deprecated(note = "Use challenge() returning Goldilocks for bias-free sampling")]
    pub fn challenge_i64(&mut self) -> i64 {
        self.challenge().0 as i64
    }

    /// Generates multiple independent challenges.
    /// COMPLEXITY: O(n).
    pub fn challenge_vec(&mut self, n: usize) -> Vec<Goldilocks> {
        let mut challenges = Vec::with_capacity(n);
        for _ in 0..n {
            challenges.push(self.challenge());
        }
        challenges
    }

    /// Commits a usize value to the transcript.
    /// SECURITY FIX (L4): Used to bind n_trace to prevent manipulation.
    pub fn append_usize(&mut self, label: &[u8], val: usize) {
        self.append(label, &(val as u64).to_le_bytes());
    }
}

impl Default for Transcript {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_challenge_is_in_field() {
        let mut t = Transcript::new();
        t.append(b"test", b"data");
        let c = t.challenge();
        assert!(c.is_valid(), "Challenge must be valid field element");
    }

    #[test]
    fn test_domain_separation() {
        let mut t1 = Transcript::new();
        t1.append(b"ab", b"cd");
        let c1 = t1.challenge();

        let mut t2 = Transcript::new();
        t2.append(b"abc", b"d");
        let c2 = t2.challenge();

        assert_ne!(c1, c2, "Domain separation must prevent collision");
    }

    #[test]
    fn test_determinism() {
        let mut t1 = Transcript::new();
        t1.append(b"root", b"deadbeef");
        let c1 = t1.challenge();

        let mut t2 = Transcript::new();
        t2.append(b"root", b"deadbeef");
        let c2 = t2.challenge();

        assert_eq!(c1, c2, "Same inputs must produce same challenge");
    }
}