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