Skip to main content

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