use crate::field::Goldilocks;
use sha2::{Digest, Sha256};
#[derive(Clone)]
pub struct Transcript {
hasher: Sha256,
}
impl Transcript {
pub fn new() -> Self {
let mut hasher = Sha256::new();
hasher.update(b"HYPERION_SOVEREIGN_SPECTRAL_v1");
Self { hasher }
}
pub fn append(&mut self, label: &[u8], data: &[u8]) {
self.hasher.update(&(label.len() as u64).to_le_bytes());
self.hasher.update(label);
self.hasher.update(&(data.len() as u64).to_le_bytes());
self.hasher.update(data);
}
pub fn challenge(&mut self) -> Goldilocks {
let result = self.hasher.clone().finalize();
self.hasher.update(&result);
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&result[0..16]);
let val = u128::from_le_bytes(bytes);
Goldilocks::reduce128(val)
}
#[deprecated(note = "Use challenge() returning Goldilocks for bias-free sampling")]
pub fn challenge_i64(&mut self) -> i64 {
self.challenge().0 as i64
}
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
}
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");
}
}