Skip to main content

zip_plus/
pcs_transcript.rs

1use crate::{merkle::MerkleProof, pcs::structs::ZipPlusCommitment};
2use crypto_primitives::BaseFieldConfig;
3use itertools::Itertools;
4use std::{
5    borrow::Borrow,
6    io::{Cursor, ErrorKind, Read, Write},
7};
8use zinc_transcript::{
9    Blake3Transcript, TranscriptError,
10    traits::{ConstTranscribable, Transcribable, Transcript},
11};
12use zinc_utils::{add, mul, rem};
13
14macro_rules! safe_cast {
15    ($value:expr, $from:ident, $to:ident) => {
16        $to::try_from($value).map_err(|_err| {
17            TranscriptError(
18                ErrorKind::Unsupported,
19                format!(
20                    "Failed to convert {} to {}",
21                    stringify!($from),
22                    stringify!($to)
23                ),
24            )
25        })
26    };
27}
28
29macro_rules! common_methods {
30    () => {
31        /// Generates a pseudorandom index based on the current transcript state.
32        /// Used to create deterministic challenges for zero-knowledge protocols.
33        /// Returns an index between 0 and cap-1.
34        #[allow(clippy::unwrap_used)]
35        pub fn squeeze_challenge_idx(&mut self, cap: usize) -> usize {
36            let num = safe_cast!(self.fs_transcript.get_challenge::<u32>(), u32, usize)
37                .expect("Conversion from u32 to usize should never fail");
38            rem!(num, cap, "Challenge cap is zero")
39        }
40    };
41}
42
43/// A transcript for Polynomial Commitment Scheme (PCS) operations.
44/// Manages both Fiat-Shamir transformations and serialization/deserialization
45/// of proof data.
46///
47/// Every byte written to the proof stream is absorbed into the Fiat-Shamir
48/// transcript by the same call
49#[derive(Debug, Clone)]
50pub struct PcsProverTranscript {
51    /// Handles Fiat-Shamir transformations for non-interactive zero-knowledge
52    /// proofs. Used to absorb field elements and generate cryptographic
53    /// challenges.
54    pub fs_transcript: Blake3Transcript,
55
56    /// Manages serialization and deserialization of proof data as a byte
57    /// stream.
58    pub stream: Cursor<Vec<u8>>,
59}
60
61// TODO(alex): Review this vs Transcribable, there is some overlap that needs to
62//             be resolved
63impl PcsProverTranscript {
64    pub fn new_from_commitment(comm: &ZipPlusCommitment) -> Self {
65        Self::new_from_commitments(std::slice::from_ref(comm).iter())
66    }
67
68    pub fn new_from_commitments<'a>(comms: impl Iterator<Item = &'a ZipPlusCommitment>) -> Self {
69        let mut result = Self {
70            fs_transcript: Blake3Transcript::default(),
71            stream: Cursor::default(),
72        };
73
74        for comm in comms {
75            result.fs_transcript.absorb_bytes(&comm.root);
76        }
77
78        result
79    }
80
81    pub fn reserve_capacity(&mut self, additional_capacity: usize) {
82        self.stream.get_mut().reserve(additional_capacity)
83    }
84
85    /// Transform the prover transcript into a verifier transcript by resetting
86    /// the stream. Note that the commitment must be absorbed again into the
87    /// verifier transcript. This would normally be done by the verifier, but
88    /// this allows us more flexibility in how we use the transcript.
89    pub fn into_verification_transcript(self) -> PcsVerifierTranscript {
90        let mut result = PcsVerifierTranscript {
91            fs_transcript: Blake3Transcript::default(),
92            stream: self.stream,
93        };
94        result.stream.set_position(0);
95
96        result
97    }
98
99    common_methods!();
100
101    /// Writes field elements to the proof stream and absorbs them into the
102    /// transcript, as raw (inner-representation) bytes.
103    ///
104    /// The field modulus is NOT written here.
105    /// Absorbs the elements into the Fiat-Shamir transcript (raw inner
106    /// representation) and writes their canonical lifted integers to the
107    /// proof stream. The wire never carries raw Montgomery residues.
108    pub fn write_field_elements<C>(
109        &mut self,
110        cfg: &C,
111        elems: &[C::Element],
112    ) -> Result<(), TranscriptError>
113    where
114        C: BaseFieldConfig,
115        C::Integer: ConstTranscribable,
116    {
117        self.write_const_many_iter(elems.iter().map(|e| cfg.lift(e)), elems.len())
118    }
119
120    pub fn write<T: Transcribable>(&mut self, v: &T) -> Result<(), TranscriptError> {
121        let data_len = v.get_num_bytes();
122
123        // Write the length prefix when it is not known at compile time.
124        if T::LENGTH_NUM_BYTES > 0 {
125            let len_bytes = data_len
126                .to_le_bytes()
127                .into_iter()
128                .take(T::LENGTH_NUM_BYTES)
129                .collect_vec();
130            self.stream
131                .write_all(&len_bytes)
132                .map_err(to_transcript_error)?;
133            // A length that selects how much of the stream is read later is
134            // itself a prover message, so it has to bind.
135            self.fs_transcript.absorb_bytes(&len_bytes);
136        }
137
138        let prev_pos = safe_cast!(self.stream.position(), u64, usize)?;
139        let next_pos = add!(prev_pos, data_len);
140
141        let inner = self.stream.get_mut();
142        if inner.len() < next_pos {
143            inner.resize(next_pos, 0_u8);
144        }
145
146        let inner_slice = &mut inner[prev_pos..next_pos];
147        v.write_transcription_bytes_exact(inner_slice);
148        self.fs_transcript.absorb_bytes(inner_slice);
149
150        self.stream.set_position(safe_cast!(next_pos, usize, u64)?);
151        Ok(())
152    }
153
154    // Note(alex):
155    // Parallelizing this greatly degrades performance rather than improving it.
156    // Maybe we should think of breakpoints for parallelization later.
157    pub fn write_const_many<T: ConstTranscribable>(
158        &mut self,
159        vs: &[T],
160    ) -> Result<(), TranscriptError> {
161        self.write_const_many_iter::<T, _>(vs, vs.len())
162    }
163
164    // Note(alex):
165    // Parallelizing this greatly degrades performance rather than improving it.
166    // Maybe we should think of breakpoints for parallelization later.
167    pub fn write_const_many_iter<'a, T, I>(
168        &mut self,
169        vs: I,
170        vs_len: usize,
171    ) -> Result<(), TranscriptError>
172    where
173        T: ConstTranscribable + 'a,
174        I: IntoIterator,
175        I::Item: Borrow<T>,
176    {
177        let prev_pos = safe_cast!(self.stream.position(), u64, usize)?;
178        let data_len = mul!(vs_len, T::NUM_BYTES);
179        let next_pos = add!(prev_pos, data_len);
180
181        let inner = self.stream.get_mut();
182        // Enlarge the inner buffer if needed
183        if inner.len() < next_pos {
184            inner.resize(next_pos, 0_u8);
185        }
186
187        for (chunk, v) in inner[prev_pos..next_pos].chunks_mut(T::NUM_BYTES).zip(vs) {
188            v.borrow().write_transcription_bytes_exact(chunk);
189            self.fs_transcript.absorb_bytes(chunk);
190        }
191
192        self.stream.set_position(next_pos as u64);
193        Ok(())
194    }
195
196    fn write_usize(&mut self, value: usize) -> Result<(), TranscriptError> {
197        let value_u64 = safe_cast!(value, usize, u64)?;
198        self.write(&value_u64)
199    }
200
201    pub fn write_merkle_proof(&mut self, proof: &MerkleProof) -> Result<(), TranscriptError> {
202        // Write the dimensions of matrix used to construct the Merkle tree
203        self.write_usize(proof.leaf_index)?;
204        self.write_usize(proof.leaf_count)?;
205
206        // Write the length of the merkle path first
207        self.write_usize(proof.siblings.len())?;
208
209        // Write each element of the merkle path
210        self.write_const_many(&proof.siblings)?;
211        Ok(())
212    }
213}
214
215/// Version of [[PcsProverTranscript]] used for proof verification.
216#[derive(Debug, Clone)]
217pub struct PcsVerifierTranscript {
218    /// Handles Fiat-Shamir transformations for non-interactive zero-knowledge
219    /// proofs. Used to absorb field elements and generate cryptographic
220    /// challenges.
221    pub fs_transcript: Blake3Transcript,
222
223    /// Manages serialization and deserialization of proof data as a byte
224    /// stream.
225    pub stream: Cursor<Vec<u8>>,
226}
227
228impl PcsVerifierTranscript {
229    common_methods!();
230
231    /// Returns an error unless the whole proof stream has been consumed.
232    ///
233    /// Call this once at the end of verification, after every component
234    /// sharing the stream has read its section.
235    pub fn check_eof(&self) -> Result<(), TranscriptError> {
236        let position = safe_cast!(self.stream.position(), u64, usize)?;
237        let len = self.stream.get_ref().len();
238        if position == len {
239            Ok(())
240        } else {
241            Err(TranscriptError(
242                ErrorKind::InvalidData,
243                format!(
244                    "proof stream not fully consumed: {} unread byte(s)",
245                    len.saturating_sub(position)
246                ),
247            ))
248        }
249    }
250
251    /// Reads canonical lifted integers from the proof stream, strictly
252    /// validates them against the field modulus, projects them into the
253    /// field, and absorbs the resulting elements into the transcript. The
254    /// mirror of [`PcsProverTranscript::write_field_elements`].
255    ///
256    /// Rejects any integer `>= modulus`: every field value has exactly one
257    /// accepted encoding on the wire.
258    pub fn read_field_elements<C>(
259        &mut self,
260        cfg: &C,
261        n: usize,
262    ) -> Result<Vec<C::Element>, TranscriptError>
263    where
264        C: BaseFieldConfig,
265        C::Integer: ConstTranscribable,
266    {
267        let ints: Vec<C::Integer> = self.read_const_many(n)?;
268        let modulus = cfg.modulus();
269        if ints.iter().any(|int| *int >= modulus) {
270            return Err(TranscriptError(
271                ErrorKind::InvalidData,
272                "Non-canonical field element".to_owned(),
273            ));
274        }
275        let elems = ints.iter().map(|int| cfg.project(int)).collect_vec();
276        Ok(elems)
277    }
278
279    pub fn read<T: Transcribable>(&mut self) -> Result<T, TranscriptError> {
280        let data_len = if T::LENGTH_NUM_BYTES > 0 {
281            let mut len_buf = vec![0u8; T::LENGTH_NUM_BYTES];
282            self.stream
283                .read_exact(&mut len_buf)
284                .map_err(to_transcript_error)?;
285            self.fs_transcript.absorb_bytes(&len_buf);
286            T::read_num_bytes(&len_buf)
287        } else {
288            // LENGTH_NUM_BYTES == 0 means size is known at compile time via
289            // the ConstTranscribable blanket impl; read_num_bytes accepts an
290            // empty slice in that case.
291            T::read_num_bytes(&[])
292        };
293
294        read_stream_slice(&mut self.stream, data_len, |slice| {
295            self.fs_transcript.absorb_bytes(slice);
296            Ok(T::read_transcription_bytes_exact(slice))
297        })
298    }
299
300    pub fn read_const_many<T: ConstTranscribable>(
301        &mut self,
302        n: usize,
303    ) -> Result<Vec<T>, TranscriptError> {
304        read_stream_slice(&mut self.stream, mul!(n, T::NUM_BYTES), |slice| {
305            Ok(slice
306                .chunks(T::NUM_BYTES)
307                .map(|bs| {
308                    self.fs_transcript.absorb_bytes(bs);
309                    T::read_transcription_bytes_exact(bs)
310                })
311                .collect_vec())
312        })
313    }
314
315    fn read_usize(&mut self) -> Result<usize, TranscriptError> {
316        let value = self.read::<u64>()?;
317        safe_cast!(value, u64, usize)
318    }
319
320    pub fn read_merkle_proof(&mut self) -> Result<MerkleProof, TranscriptError> {
321        // Read the dimensions of matrix used to construct the Merkle tree
322        let leaf_index = self.read_usize()?;
323        let leaf_count = self.read_usize()?;
324
325        // Read the length of the merkle path first
326        let path_length = self.read_usize()?;
327
328        // Read each element of the merkle path
329        let merkle_path = self.read_const_many(path_length)?;
330
331        Ok(MerkleProof::new(leaf_index, leaf_count, merkle_path))
332    }
333}
334
335/// Perform a bounds-checked read from the stream for a length, and
336/// execute an action on the resulting slice. After the action is executed,
337/// advance the stream position by the length.
338#[inline]
339fn read_stream_slice<T>(
340    stream: &mut Cursor<Vec<u8>>,
341    length: usize,
342    mut action: impl FnMut(&[u8]) -> Result<T, TranscriptError>,
343) -> Result<T, TranscriptError> {
344    let prev_pos = safe_cast!(stream.position(), u64, usize)?;
345    let next_pos = add!(prev_pos, length);
346
347    let stream_vec = stream.get_ref();
348    if next_pos > stream_vec.len() {
349        return Err(TranscriptError(
350            ErrorKind::UnexpectedEof,
351            format!(
352                "Attempted to read beyond the end of the stream: {} + {} exceeds stream length {}",
353                prev_pos,
354                length,
355                stream_vec.len()
356            ),
357        ));
358    }
359    let res = action(&stream_vec[prev_pos..next_pos])?;
360    stream.set_position(safe_cast!(next_pos, usize, u64)?);
361    Ok(res)
362}
363
364// Do not expose this outside
365fn to_transcript_error(err: std::io::Error) -> TranscriptError {
366    TranscriptError(err.kind(), err.to_string())
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::merkle::MtHash;
373
374    #[allow(unused_macros)]
375    macro_rules! test_read_write {
376        // TODO: N is magic
377        ($write_fn:ident, $read_fn:ident, $original_value:expr, $assert_msg:expr) => {{
378            let comm = ZipPlusCommitment::default();
379            let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
380            transcript
381                .$write_fn(&$original_value)
382                .expect(&format!("Failed to write {}", $assert_msg));
383            let mut transcript: PcsVerifierTranscript = transcript.into_verification_transcript();
384            transcript.fs_transcript.absorb_bytes(&comm.root);
385            let read_value = transcript
386                .$read_fn()
387                .expect(&format!("Failed to read {}", $assert_msg));
388            assert_eq!(
389                $original_value, read_value,
390                "{} read does not match original",
391                $assert_msg
392            );
393        }};
394    }
395
396    #[allow(unused_macros)]
397    macro_rules! test_read_write_vec {
398        // TODO: N is magic
399        ($write_fn:ident, $read_fn:ident, $original_values:expr, $assert_msg:expr) => {{
400            let comm = ZipPlusCommitment::default();
401            let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
402            transcript
403                .$write_fn(&$original_values)
404                .expect(&format!("Failed to write {}", $assert_msg));
405            let mut transcript: PcsVerifierTranscript = transcript.into_verification_transcript();
406            transcript.fs_transcript.absorb_bytes(&comm.root);
407            let read_values = transcript
408                .$read_fn($original_values.len())
409                .expect(&format!("Failed to read {}", $assert_msg));
410            assert_eq!(
411                $original_values, read_values,
412                "{} read does not match original",
413                $assert_msg
414            );
415        }};
416    }
417
418    #[test]
419    fn test_pcs_transcript_read_write() {
420        // Test hash
421        let original_hash = MtHash::default();
422        test_read_write!(write, read, original_hash, "hash");
423
424        // Test vector of hashed
425        let original_hashes = vec![MtHash::default(); 1024];
426        test_read_write_vec!(
427            write_const_many,
428            read_const_many,
429            original_hashes,
430            "hashes vector"
431        );
432    }
433
434    const CAP: usize = 1 << 24;
435
436    /// Every byte on the wire must bind the challenges drawn after it.
437    ///
438    /// This pins the invariant whose absence let a prover choose
439    /// `combined_row` *after* learning the column indices it was supposed to
440    /// be committed to beforehand. Before wire writes were absorbed, the two
441    /// payloads below produced identical challenges.
442    #[test]
443    fn wire_writes_bind_subsequent_challenges() {
444        let comm = ZipPlusCommitment::default();
445
446        let challenge_after = |payload: &[u64]| {
447            let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
448            transcript
449                .write_const_many(payload)
450                .expect("write should succeed");
451            transcript.squeeze_challenge_idx(CAP)
452        };
453
454        assert_ne!(
455            challenge_after(&[1, 2, 3, 4]),
456            challenge_after(&[1, 2, 3, 5]),
457            "changing a written value left the next challenge unchanged: \
458             wire bytes are not entering the transcript"
459        );
460    }
461
462    /// A length prefix selects how much of the stream is read later, so it is
463    /// a prover message and must bind just like a payload.
464    #[test]
465    fn length_prefixed_writes_bind_subsequent_challenges() {
466        let comm = ZipPlusCommitment::default();
467
468        let challenge_after = |value: u64| {
469            let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
470            transcript.write(&value).expect("write should succeed");
471            transcript.squeeze_challenge_idx(CAP)
472        };
473
474        assert_ne!(
475            challenge_after(7),
476            challenge_after(8),
477            "`write` is not absorbing its payload"
478        );
479    }
480
481    /// Prover and verifier must reach the same state after the same logical
482    /// message, no matter how either side chunks it into calls.
483    ///
484    /// Absorbs are framed, so call granularity is normally significant. The
485    /// `write_const_many_iter` / `read_const_many` pair frames one fixed-size
486    /// element at a time rather than one call at a time, which is what makes
487    /// the split into calls irrelevant here. That property is load-bearing:
488    /// the prover writes column values one codeword matrix at a time while
489    /// the verifier reads them in a single batched call.
490    #[test]
491    fn transcript_state_is_independent_of_call_chunking() {
492        let comm = ZipPlusCommitment::default();
493        let payload: Vec<u64> = (0..8).collect();
494
495        // Prover emits the run as two calls ...
496        let mut prover = PcsProverTranscript::new_from_commitment(&comm);
497        prover
498            .write_const_many(&payload[..3])
499            .expect("write should succeed");
500        prover
501            .write_const_many(&payload[3..])
502            .expect("write should succeed");
503        let prover_challenge = prover.squeeze_challenge_idx(CAP);
504
505        // ... the verifier consumes it as one.
506        let mut verifier = prover.into_verification_transcript();
507        verifier.fs_transcript.absorb_bytes(&comm.root);
508        let read: Vec<u64> = verifier
509            .read_const_many(payload.len())
510            .expect("read should succeed");
511        let verifier_challenge = verifier.squeeze_challenge_idx(CAP);
512
513        assert_eq!(read, payload, "round-trip lost data");
514        assert_eq!(
515            prover_challenge, verifier_challenge,
516            "prover and verifier transcripts diverged on identical data"
517        );
518        verifier
519            .check_eof()
520            .expect("stream should be fully consumed");
521    }
522
523    /// Trailing bytes are bytes that never entered the transcript, so they
524    /// must be rejected rather than silently ignored.
525    #[test]
526    fn check_eof_rejects_unread_trailing_bytes() {
527        let comm = ZipPlusCommitment::default();
528        let mut prover = PcsProverTranscript::new_from_commitment(&comm);
529        prover
530            .write_const_many(&[1u64, 2, 3])
531            .expect("write should succeed");
532
533        let mut verifier = prover.into_verification_transcript();
534        verifier.fs_transcript.absorb_bytes(&comm.root);
535        let _: Vec<u64> = verifier.read_const_many(2).expect("read should succeed");
536
537        assert!(
538            verifier.check_eof().is_err(),
539            "check_eof accepted a stream with unread trailing bytes"
540        );
541    }
542}