Skip to main content

proof_cat/
transcript.rs

1//! Fiat-Shamir transcript for non-interactive proofs.
2//!
3//! The [`Transcript`] accumulates protocol messages (absorb) and
4//! produces verifier challenges (squeeze) deterministically via
5//! SHA-256.  It is functional: each operation consumes the
6//! transcript and returns a new one, following the same pattern
7//! as [`WireAllocator`](plonkish_cat::WireAllocator).
8
9use field_cat::FieldBytes;
10use sha2::{Digest, Sha256};
11
12use crate::error::Error;
13
14/// A Fiat-Shamir transcript.
15///
16/// Operations consume `self` and return a new transcript,
17/// ensuring the hash state evolves deterministically.
18/// This follows the same functional-update pattern as
19/// [`WireAllocator`](plonkish_cat::WireAllocator).
20///
21/// # Examples
22///
23/// ```
24/// use field_cat::BabyBear;
25/// use proof_cat::Transcript;
26///
27/// // Create a transcript, absorb some data, squeeze a challenge.
28/// let transcript = Transcript::new(b"my-protocol")
29///     .absorb_field(&BabyBear::new(42));
30/// let (_challenge, _transcript): (BabyBear, _) =
31///     transcript.squeeze_challenge()?;
32///
33/// // The challenge is deterministic: same inputs produce
34/// // the same challenge every time.
35/// # Ok::<(), proof_cat::Error>(())
36/// ```
37#[derive(Debug, Clone)]
38pub struct Transcript {
39    state: Vec<u8>,
40}
41
42impl Transcript {
43    /// Create a new transcript with a domain separation label.
44    #[must_use]
45    pub fn new(label: &[u8]) -> Self {
46        Self {
47            state: label.to_vec(),
48        }
49    }
50
51    /// Absorb raw bytes into the transcript.
52    #[must_use]
53    pub fn absorb_bytes(self, data: &[u8]) -> Self {
54        Self {
55            state: self.state.into_iter().chain(data.iter().copied()).collect(),
56        }
57    }
58
59    /// Absorb a field element into the transcript.
60    #[must_use]
61    pub fn absorb_field<F: FieldBytes>(self, elem: &F) -> Self {
62        self.absorb_bytes(&elem.to_le_bytes())
63    }
64
65    /// Squeeze a challenge field element from the transcript.
66    ///
67    /// Hashes the current state with SHA-256, interprets the
68    /// output as a field element, and returns the challenge
69    /// along with an updated transcript.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`Error::FieldCat`] if the hash output cannot be
74    /// interpreted as a field element.
75    pub fn squeeze_challenge<F: FieldBytes>(self) -> Result<(F, Self), Error> {
76        let digest = Sha256::digest(&self.state);
77        let challenge = F::from_le_bytes(digest.as_slice())?;
78        let new_state = self
79            .state
80            .into_iter()
81            .chain(digest.iter().copied())
82            .collect();
83        Ok((challenge, Self { state: new_state }))
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use field_cat::{BabyBear, F101};
91
92    #[test]
93    fn deterministic_challenges() -> Result<(), Error> {
94        let t1 = Transcript::new(b"test").absorb_field(&BabyBear::new(42));
95        let t2 = Transcript::new(b"test").absorb_field(&BabyBear::new(42));
96        let (c1, _) = t1.squeeze_challenge::<BabyBear>()?;
97        let (c2, _) = t2.squeeze_challenge::<BabyBear>()?;
98        assert_eq!(c1, c2);
99        Ok(())
100    }
101
102    #[test]
103    fn different_inputs_different_challenges() -> Result<(), Error> {
104        let t1 = Transcript::new(b"test").absorb_field(&BabyBear::new(1));
105        let t2 = Transcript::new(b"test").absorb_field(&BabyBear::new(2));
106        let (c1, _) = t1.squeeze_challenge::<BabyBear>()?;
107        let (c2, _) = t2.squeeze_challenge::<BabyBear>()?;
108        assert_ne!(c1, c2);
109        Ok(())
110    }
111
112    #[test]
113    fn absorb_order_matters() -> Result<(), Error> {
114        let t1 = Transcript::new(b"test")
115            .absorb_field(&F101::new(1))
116            .absorb_field(&F101::new(2));
117        let t2 = Transcript::new(b"test")
118            .absorb_field(&F101::new(2))
119            .absorb_field(&F101::new(1));
120        let (c1, _) = t1.squeeze_challenge::<F101>()?;
121        let (c2, _) = t2.squeeze_challenge::<F101>()?;
122        assert_ne!(c1, c2);
123        Ok(())
124    }
125
126    #[test]
127    fn label_matters() -> Result<(), Error> {
128        let t1 = Transcript::new(b"label_a").absorb_field(&BabyBear::new(42));
129        let t2 = Transcript::new(b"label_b").absorb_field(&BabyBear::new(42));
130        let (c1, _) = t1.squeeze_challenge::<BabyBear>()?;
131        let (c2, _) = t2.squeeze_challenge::<BabyBear>()?;
132        assert_ne!(c1, c2);
133        Ok(())
134    }
135}