use hemera::Hasher;
use nebu::{field::P, Goldilocks};
use lens::Commitment;
use crate::types::{Statement, SumcheckPoly};
const DOM_INIT: &[u8] = b"\x01zheng-transcript-v1";
const DOM_COMMIT: &[u8] = b"\x02commit";
const DOM_SUMCHECK: u8 = 0x03;
const DOM_EVAL: &[u8] = b"\x04eval";
const DOM_PCS_OPEN: &[u8] = b"\x05pcs-open";
const DOM_RECURSE: &[u8] = b"\x06recurse";
const DOM_STATEMENT: &[u8] = b"\x07statement";
#[derive(Clone)]
pub struct Transcript {
hasher: Hasher,
}
impl Transcript {
pub fn new() -> Self {
let mut hasher = Hasher::new();
hasher.update(DOM_INIT);
Self { hasher }
}
pub fn new_recursive() -> Self {
let mut hasher = Hasher::new();
hasher.update(DOM_RECURSE);
Self { hasher }
}
pub fn absorb(&mut self, data: &[u8]) {
self.hasher.update(data);
}
pub fn squeeze_hash(&mut self) -> [u8; 32] {
let hash = self.hasher.finalize();
self.hasher = Hasher::new();
self.hasher.update(hash.as_bytes());
*hash.as_bytes()
}
pub fn squeeze_challenge(&mut self) -> Goldilocks {
let hash = self.squeeze_hash();
let raw = u64::from_le_bytes(hash[..8].try_into().unwrap());
Goldilocks::new(raw).canonicalize()
}
pub fn squeeze_challenges(&mut self, n: usize) -> Vec<Goldilocks> {
(0..n).map(|_| self.squeeze_challenge()).collect()
}
pub fn absorb_commitment(&mut self, c: &Commitment) {
self.absorb(DOM_COMMIT);
self.absorb(c.as_bytes());
}
pub fn absorb_sumcheck_poly(&mut self, round: usize, poly: &SumcheckPoly) {
self.absorb(&[DOM_SUMCHECK]);
self.absorb(&round.to_le_bytes());
self.absorb(&[poly.degree]);
for coeff in &poly.coeffs {
self.absorb(&encode_field(*coeff));
}
}
pub fn absorb_eval(&mut self, v: Goldilocks) {
self.absorb(DOM_EVAL);
self.absorb(&encode_field(v));
}
pub fn absorb_pcs_open_domain(&mut self) {
self.absorb(DOM_PCS_OPEN);
}
pub fn absorb_statement(&mut self, s: &Statement) {
self.absorb(DOM_STATEMENT);
self.absorb(&s.program_hash);
self.absorb(&s.input_hash);
self.absorb(&s.output_hash);
self.absorb(&s.focus_bound.to_le_bytes());
}
}
impl Default for Transcript {
fn default() -> Self {
Self::new()
}
}
pub fn encode_field(f: Goldilocks) -> [u8; 8] {
f.as_u64().to_le_bytes()
}
pub fn decode_field(bytes: &[u8]) -> Option<Goldilocks> {
let arr: [u8; 8] = bytes.get(..8)?.try_into().ok()?;
let v = u64::from_le_bytes(arr);
if v >= P {
return None;
}
Some(Goldilocks::new(v))
}
pub fn encode_fields(elems: &[Goldilocks]) -> Vec<u8> {
let mut out = Vec::with_capacity(elems.len() * 8);
for &f in elems {
out.extend_from_slice(&encode_field(f));
}
out
}
pub fn decode_fields(bytes: &[u8]) -> Option<Vec<Goldilocks>> {
if !bytes.len().is_multiple_of(8) {
return None;
}
bytes.chunks_exact(8).map(decode_field).collect()
}
pub fn encode_sumcheck_poly(poly: &SumcheckPoly) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + (poly.degree as usize + 1) * 8);
out.push(poly.degree);
for &c in &poly.coeffs {
out.extend_from_slice(&encode_field(c));
}
out
}
pub fn decode_sumcheck_poly(bytes: &[u8]) -> Option<(SumcheckPoly, usize)> {
let degree = *bytes.first()?;
let n = degree as usize + 1;
let end = 1 + n * 8;
if bytes.len() < end {
return None;
}
let coeffs = bytes[1..end].chunks_exact(8)
.map(decode_field)
.collect::<Option<Vec<_>>>()?;
Some((SumcheckPoly { degree, coeffs }, end))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_messages_same_challenge() {
let mut t1 = Transcript::new();
t1.absorb(b"hello");
let c1 = t1.squeeze_challenge();
let mut t2 = Transcript::new();
t2.absorb(b"hello");
let c2 = t2.squeeze_challenge();
assert_eq!(c1.as_u64(), c2.as_u64());
}
#[test]
fn different_messages_different_challenges() {
let mut t1 = Transcript::new();
t1.absorb(b"hello");
let c1 = t1.squeeze_challenge();
let mut t2 = Transcript::new();
t2.absorb(b"world");
let c2 = t2.squeeze_challenge();
assert_ne!(c1.as_u64(), c2.as_u64());
}
#[test]
fn different_domains_different_challenges() {
let mut t1 = Transcript::new();
t1.absorb(b"same");
let c1 = t1.squeeze_challenge();
let mut t2 = Transcript::new_recursive();
t2.absorb(b"same");
let c2 = t2.squeeze_challenge();
assert_ne!(c1.as_u64(), c2.as_u64());
}
#[test]
fn multiple_squeezes_are_independent() {
let mut t = Transcript::new();
t.absorb(b"test");
let c1 = t.squeeze_challenge();
let c2 = t.squeeze_challenge();
assert_ne!(c1.as_u64(), c2.as_u64());
}
#[test]
fn encode_decode_field_roundtrip() {
let f = Goldilocks::new(12345678);
let bytes = encode_field(f);
let decoded = decode_field(&bytes).unwrap();
assert_eq!(f.as_u64(), decoded.as_u64());
}
#[test]
fn decode_field_rejects_noncanonical() {
let bytes = P.to_le_bytes();
assert!(decode_field(&bytes).is_none());
let bytes2 = (P + 1).to_le_bytes();
assert!(decode_field(&bytes2).is_none());
}
#[test]
fn decode_field_accepts_p_minus_1() {
let bytes = (P - 1).to_le_bytes();
let f = decode_field(&bytes).unwrap();
assert_eq!(f.as_u64(), P - 1);
}
#[test]
fn encode_decode_fields_roundtrip() {
let elems: Vec<Goldilocks> = (0u64..8).map(Goldilocks::new).collect();
let bytes = encode_fields(&elems);
let decoded = decode_fields(&bytes).unwrap();
for (a, b) in elems.iter().zip(decoded.iter()) {
assert_eq!(a.as_u64(), b.as_u64());
}
}
#[test]
fn decode_fields_rejects_odd_length() {
assert!(decode_fields(&[0u8; 7]).is_none());
assert!(decode_fields(&[0u8; 9]).is_none());
}
#[test]
fn sumcheck_poly_encode_decode_roundtrip() {
let poly = SumcheckPoly {
degree: 3,
coeffs: vec![
Goldilocks::new(1),
Goldilocks::new(2),
Goldilocks::new(3),
Goldilocks::new(4),
],
};
let bytes = encode_sumcheck_poly(&poly);
let (decoded, consumed) = decode_sumcheck_poly(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.degree, poly.degree);
for (a, b) in poly.coeffs.iter().zip(decoded.coeffs.iter()) {
assert_eq!(a.as_u64(), b.as_u64());
}
}
#[test]
fn commitment_domain_separation() {
use lens::{brakedown::Brakedown, Lens, MultilinearPoly};
let poly = MultilinearPoly::new(vec![
Goldilocks::new(1), Goldilocks::new(2),
Goldilocks::new(3), Goldilocks::new(4),
]);
let c = Brakedown::commit(&poly);
let mut t1 = Transcript::new();
t1.absorb_commitment(&c);
let ch1 = t1.squeeze_challenge();
let mut t2 = Transcript::new();
t2.absorb(c.as_bytes());
let ch2 = t2.squeeze_challenge();
assert_ne!(ch1.as_u64(), ch2.as_u64());
}
}