Skip to main content

p3_challenger/fs/pattern/
sequence.rs

1//! A validated, hashable sequence of typed transcript steps.
2//!
3//! # Overview
4//!
5//! Holds a list of typed steps that has already passed structural validation.
6//!
7//! Acts as the source of truth for what a prover and verifier may do.
8//!
9//! Yields a stable 32-byte fingerprint suitable for use as a protocol identifier.
10//!
11//! # Validation
12//!
13//! Walk the steps left to right, maintaining a stack of openers:
14//!
15//! - On a Begin: check its kind against the enclosing container, then push
16//!   `(position, opener)` onto the stack.
17//! - On an End: pop the top of the stack and require it to match.
18//! - On an Atomic: check its kind against the enclosing container.
19//!
20//! The kind check is the same in both cases: a `Protocol` container accepts
21//! any nested kind, every other container requires an exact match.
22//!
23//! Checking openers as well as leaves is what stops
24//! `Begin Message { Begin Challenge { .. } }` from smuggling a challenge into
25//! a message block.
26//!
27//! At the end of the walk the stack must be empty.
28//!
29//! # Hashing
30//!
31//! The 32-byte fingerprint is computed by:
32//!
33//! 1. Rendering the pattern in alternate-mode `Display`.
34//! 2. Feeding the rendered bytes through Keccak-256.
35//!
36//! The alternate-mode rendering is type-name-free and length-prefixed,
37//! so the fingerprint is stable across compiler versions.
38
39use alloc::boxed::Box;
40use alloc::string::{String, ToString};
41use alloc::vec::Vec;
42use core::fmt::{Display, Formatter, Write};
43
44use p3_keccak::Keccak256Hash;
45use p3_symmetric::CryptographicHasher;
46
47use crate::fs::error::{
48    InvalidKindInfo, MismatchedBeginEndInfo, MissingBeginInfo, MissingEndInfo, TranscriptError,
49};
50use crate::fs::pattern::step::{Hierarchy, Interaction, Kind};
51
52/// A complete, validated transcript described as a sequence of typed steps.
53#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)]
54pub struct InteractionPattern {
55    /// The validated sequence in transcript order.
56    interactions: Vec<Interaction>,
57}
58
59impl InteractionPattern {
60    /// Build a pattern from a sequence of steps after validating its structure.
61    ///
62    /// # Errors
63    ///
64    /// Returns a structured error when the sequence is malformed;
65    /// See the variants of the error type for the exact failure modes.
66    ///
67    /// # Algorithm
68    ///
69    /// Walks the sequence once, maintaining a stack of currently-open sub-protocols.
70    pub fn new(interactions: Vec<Interaction>) -> Result<Self, TranscriptError> {
71        // Build first, validate second.
72        //
73        // This lets validation report the original positions of offending
74        // steps in the recorded sequence.
75        let result = Self { interactions };
76        result.validate()?;
77        Ok(result)
78    }
79
80    /// Read-only access to the recorded sequence.
81    #[must_use]
82    pub fn interactions(&self) -> &[Interaction] {
83        &self.interactions
84    }
85
86    /// Number of steps in the pattern.
87    #[must_use]
88    pub const fn len(&self) -> usize {
89        self.interactions.len()
90    }
91
92    /// Whether the pattern has zero steps.
93    #[must_use]
94    pub const fn is_empty(&self) -> bool {
95        self.interactions.is_empty()
96    }
97
98    /// Stable 32-byte fingerprint of the pattern.
99    ///
100    /// # Algorithm
101    ///
102    /// 1. Render the pattern via alternate-mode `Display`.
103    /// 2. Feed the rendered bytes into Keccak-256.
104    ///
105    /// The rendering is type-name-free and length-prefixed, so the
106    /// digest is stable across compiler versions and free of label
107    /// collision shenanigans.
108    ///
109    /// # Performance
110    ///
111    /// One allocation for the rendered string and one Keccak-256 call.
112    /// Intended for use at protocol setup, not in hot paths.
113    #[must_use]
114    pub fn pattern_hash(&self) -> [u8; 32] {
115        // Render into one contiguous buffer in alternate mode.
116        //
117        // The rendering is deterministic and self-delimiting.
118        let mut rendered = String::new();
119        // Writing into a String can only fail on allocation, which would
120        // already have aborted; the result is safe to unwrap.
121        write!(&mut rendered, "{self:#}").expect("writing into a String never fails");
122        // Hash the rendered bytes with Keccak-256.
123        Keccak256Hash.hash_iter(rendered.bytes())
124    }
125
126    /// Run the structural validation pass.
127    ///
128    /// See the module-level overview for the algorithm.
129    fn validate(&self) -> Result<(), TranscriptError> {
130        // Stack holds (position, opener) for each currently-open sub-protocol.
131        //
132        // Bounded by nesting depth, not by sequence length.
133        let mut stack: Vec<(usize, &Interaction)> = Vec::new();
134
135        for (position, interaction) in self.interactions.iter().enumerate() {
136            match interaction.hierarchy() {
137                // Phase: open a sub-protocol.
138                //
139                // A nested container is itself subject to the enclosing container's rule.
140                Hierarchy::Begin => {
141                    Self::check_nested_kind(&stack, position, interaction)?;
142                    stack.push((position, interaction));
143                }
144
145                // Phase: close a sub-protocol.
146                //
147                // The top of the stack must match this closer.
148                // - An empty stack means an orphan closer;
149                // - A non-matching top means a misnested begin/end pair.
150                Hierarchy::End => {
151                    let Some((begin_position, begin)) = stack.pop() else {
152                        return Err(TranscriptError::MissingBegin(Box::new(MissingBeginInfo {
153                            position,
154                            end: *interaction,
155                        })));
156                    };
157                    if !interaction.closes(begin) {
158                        return Err(TranscriptError::MismatchedBeginEnd(Box::new(
159                            MismatchedBeginEndInfo {
160                                begin_position,
161                                begin: *begin,
162                                end_position: position,
163                                end: *interaction,
164                            },
165                        )));
166                    }
167                }
168
169                // Phase: atomic step.
170                //
171                // A leaf is subject to the same rule as a nested container.
172                Hierarchy::Atomic => Self::check_nested_kind(&stack, position, interaction)?,
173            }
174        }
175
176        // Final invariant: the stack must drain to empty.
177        //
178        // A non-empty stack means at least one opener was never closed.
179        if let Some((position, begin)) = stack.pop() {
180            return Err(TranscriptError::MissingEnd(Box::new(MissingEndInfo {
181                position,
182                begin: *begin,
183            })));
184        }
185        Ok(())
186    }
187
188    /// Require `interaction` to be legal inside the innermost open container.
189    ///
190    /// - No open container: any kind is allowed at the top level.
191    /// - `Protocol` container: acts as a mixed bag and accepts any kind.
192    /// - Any other container: the nested kind must match it exactly.
193    fn check_nested_kind(
194        stack: &[(usize, &Interaction)],
195        position: usize,
196        interaction: &Interaction,
197    ) -> Result<(), TranscriptError> {
198        let Some(&(begin_position, begin)) = stack.last() else {
199            return Ok(());
200        };
201        let surrounding = begin.kind();
202        if surrounding != Kind::Protocol && surrounding != interaction.kind() {
203            return Err(TranscriptError::InvalidKind(Box::new(InvalidKindInfo {
204                begin_position,
205                begin: *begin,
206                interaction_position: position,
207                interaction: *interaction,
208            })));
209        }
210        Ok(())
211    }
212}
213
214impl Display for InteractionPattern {
215    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
216        // Header: include the step count first to guarantee no shorter
217        // pattern is a prefix of a longer one.
218        let length = self.interactions.len();
219        // Pad the position field so columns line up in default mode.
220        let width = length.saturating_sub(1).to_string().len();
221        writeln!(f, "Plonky3 Fiat-Shamir Transcript ({length} interactions)")?;
222
223        // Indentation tracks open sub-protocols.
224        //
225        // The closer for a Begin sits at the same indentation as its opener.
226        // So we decrement before printing when we hit an End.
227        let mut indentation: usize = 0;
228        for (position, interaction) in self.interactions.iter().enumerate() {
229            write!(f, "{position:0>width$} ")?;
230            if interaction.hierarchy() == Hierarchy::End {
231                // Close shifts left by one level;
232                // The body of this iteration prints at the closer's level.
233                indentation = indentation.saturating_sub(1);
234            }
235            for _ in 0..indentation {
236                write!(f, "  ")?;
237            }
238            // Forward the alternate flag so the whole pattern is either:
239            // - hash-stable or
240            // - human-readable consistently.
241            if f.alternate() {
242                writeln!(f, "{interaction:#}")?;
243            } else {
244                writeln!(f, "{interaction}")?;
245            }
246            if interaction.hierarchy() == Hierarchy::Begin {
247                // Open shifts subsequent lines right by one level.
248                indentation += 1;
249            }
250        }
251        Ok(())
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use alloc::{format, vec};
258
259    use p3_baby_bear::BabyBear;
260
261    use super::*;
262    use crate::fs::pattern::step::{Hierarchy, Kind, Length};
263
264    /// Concrete field exercised in this module's tests.
265    type F = BabyBear;
266
267    fn atomic(kind: Kind, label: &'static str, length: Length) -> Interaction {
268        Interaction::algebra::<F, F>(Hierarchy::Atomic, kind, label, length)
269    }
270
271    fn open<T: ?Sized>(kind: Kind, label: &'static str) -> Interaction {
272        Interaction::marker::<T>(Hierarchy::Begin, kind, label)
273    }
274
275    fn close<T: ?Sized>(kind: Kind, label: &'static str) -> Interaction {
276        Interaction::marker::<T>(Hierarchy::End, kind, label)
277    }
278
279    #[test]
280    fn empty_pattern_validates() {
281        // Boundary: a zero-step transcript is structurally legal.
282        let p = InteractionPattern::new(vec![]).expect("empty pattern is legal");
283        assert!(p.is_empty());
284        assert_eq!(p.len(), 0);
285    }
286
287    #[test]
288    fn well_nested_protocol_validates() {
289        // Fixture state: one outer protocol containing one challenge.
290        // Mutation: build, validate. Expectation: no error.
291        let p = InteractionPattern::new(vec![
292            open::<()>(Kind::Protocol, "outer"),
293            atomic(Kind::Challenge, "alpha", Length::Scalar),
294            close::<()>(Kind::Protocol, "outer"),
295        ])
296        .expect("well-nested protocol is legal");
297        assert_eq!(p.len(), 3);
298    }
299
300    #[test]
301    fn missing_begin_is_caught() {
302        // Orphan closer at position 0.
303        let err = InteractionPattern::new(vec![close::<()>(Kind::Protocol, "outer")])
304            .expect_err("orphan close must be rejected");
305        match err {
306            TranscriptError::MissingBegin(info) => assert_eq!(
307                *info,
308                MissingBeginInfo {
309                    position: 0,
310                    end: close::<()>(Kind::Protocol, "outer"),
311                }
312            ),
313            other => panic!("wrong variant: {other:?}"),
314        }
315    }
316
317    #[test]
318    fn missing_end_is_caught() {
319        // Opener at position 0 never closed.
320        let err = InteractionPattern::new(vec![open::<()>(Kind::Protocol, "outer")])
321            .expect_err("missing close must be rejected");
322        match err {
323            TranscriptError::MissingEnd(info) => assert_eq!(
324                *info,
325                MissingEndInfo {
326                    position: 0,
327                    begin: open::<()>(Kind::Protocol, "outer"),
328                }
329            ),
330            other => panic!("wrong variant: {other:?}"),
331        }
332    }
333
334    #[test]
335    fn mismatched_begin_end_is_caught() {
336        // Close label "inner" does not match open label "outer".
337        let err = InteractionPattern::new(vec![
338            open::<()>(Kind::Protocol, "outer"),
339            close::<()>(Kind::Protocol, "inner"),
340        ])
341        .expect_err("mismatched begin/end must be rejected");
342        match err {
343            TranscriptError::MismatchedBeginEnd(info) => assert_eq!(
344                *info,
345                MismatchedBeginEndInfo {
346                    begin_position: 0,
347                    begin: open::<()>(Kind::Protocol, "outer"),
348                    end_position: 1,
349                    end: close::<()>(Kind::Protocol, "inner"),
350                }
351            ),
352            other => panic!("wrong variant: {other:?}"),
353        }
354    }
355
356    #[test]
357    fn protocol_kind_acts_as_a_mixed_container() {
358        // Invariant: a Protocol-kind opener accepts any nested kind.
359        //
360        // Fixture state: a Protocol container with Message + Challenge inside.
361        let p = InteractionPattern::new(vec![
362            open::<()>(Kind::Protocol, "outer"),
363            atomic(Kind::Message, "commitment", Length::Scalar),
364            atomic(Kind::Challenge, "alpha", Length::Scalar),
365            close::<()>(Kind::Protocol, "outer"),
366        ])
367        .expect("Protocol container accepts mixed kinds");
368        assert_eq!(p.len(), 4);
369    }
370
371    #[test]
372    fn non_protocol_sub_protocol_enforces_kind_match() {
373        // Message-kind container rejects a Challenge-kind atomic.
374        let err = InteractionPattern::new(vec![
375            open::<()>(Kind::Message, "msg-block"),
376            atomic(Kind::Challenge, "alpha", Length::Scalar),
377            close::<()>(Kind::Message, "msg-block"),
378        ])
379        .expect_err("non-Protocol container must enforce kind match");
380
381        match err {
382            TranscriptError::InvalidKind(info) => assert_eq!(
383                *info,
384                InvalidKindInfo {
385                    begin_position: 0,
386                    begin: open::<()>(Kind::Message, "msg-block"),
387                    interaction_position: 1,
388                    interaction: atomic(Kind::Challenge, "alpha", Length::Scalar),
389                }
390            ),
391            other => panic!("wrong variant: {other:?}"),
392        }
393    }
394
395    #[test]
396    fn nested_container_of_a_foreign_kind_is_rejected() {
397        // Invariant: an opener is checked against its enclosing container too.
398        //
399        // Without that check, wrapping the challenge in its own Begin block
400        // would smuggle it into a Message-only container.
401        let err = InteractionPattern::new(vec![
402            open::<()>(Kind::Message, "msg-block"),
403            open::<()>(Kind::Challenge, "sneaky"),
404            atomic(Kind::Challenge, "alpha", Length::Scalar),
405            close::<()>(Kind::Challenge, "sneaky"),
406            close::<()>(Kind::Message, "msg-block"),
407        ])
408        .expect_err("a Challenge container inside a Message container must be rejected");
409
410        match err {
411            TranscriptError::InvalidKind(info) => assert_eq!(
412                *info,
413                InvalidKindInfo {
414                    begin_position: 0,
415                    begin: open::<()>(Kind::Message, "msg-block"),
416                    interaction_position: 1,
417                    interaction: open::<()>(Kind::Challenge, "sneaky"),
418                }
419            ),
420            other => panic!("wrong variant: {other:?}"),
421        }
422    }
423
424    #[test]
425    fn nested_container_of_the_same_kind_is_accepted() {
426        // Boundary: the rule is "match or Protocol", so a Message block nests in a Message block.
427        let p = InteractionPattern::new(vec![
428            open::<()>(Kind::Message, "outer"),
429            open::<()>(Kind::Message, "inner"),
430            atomic(Kind::Message, "commitment", Length::Scalar),
431            close::<()>(Kind::Message, "inner"),
432            close::<()>(Kind::Message, "outer"),
433        ])
434        .expect("same-kind nesting is legal");
435        assert_eq!(p.len(), 5);
436    }
437
438    #[test]
439    fn pattern_hash_is_stable() {
440        // Invariant: pattern_hash is a deterministic function of the alternate-mode rendering.
441        //
442        // Fixture state: a small protocol with one nested challenge.
443        let p = InteractionPattern::new(vec![
444            open::<()>(Kind::Protocol, "test"),
445            atomic(Kind::Challenge, "nonce", Length::Scalar),
446            close::<()>(Kind::Protocol, "test"),
447        ])
448        .unwrap();
449
450        // Rendered string matches a known layout.
451        let rendered = format!("{p:#}");
452        let expected = "Plonky3 Fiat-Shamir Transcript (3 interactions)\n\
453                        0 Begin Protocol 4 test None Marker\n\
454                        1   Atomic Challenge 5 nonce Scalar Algebra(2013265921^1;cfa353fe66eeeae7600254a68073d57125abac3820fde7a3e32c84482b826469)\n\
455                        2 End Protocol 4 test None Marker\n";
456        assert_eq!(rendered, expected);
457
458        // The hash is deterministic, so two calls must agree.
459        let h = p.pattern_hash();
460        assert_eq!(h.len(), 32);
461        assert_eq!(p.pattern_hash(), h);
462    }
463
464    #[test]
465    fn pattern_hash_distinguishes_two_patterns() {
466        // Invariant:
467        //
468        // Two patterns differing in even one byte of their rendering produce different hashes.
469        let p1 =
470            InteractionPattern::new(vec![atomic(Kind::Challenge, "a", Length::Scalar)]).unwrap();
471        let p2 =
472            InteractionPattern::new(vec![atomic(Kind::Challenge, "b", Length::Scalar)]).unwrap();
473        assert_ne!(p1.pattern_hash(), p2.pattern_hash());
474    }
475
476    #[test]
477    fn pattern_hash_distinguishes_two_fields_of_the_same_width() {
478        // Invariant: the fingerprint binds the modulus, not just the shape.
479        //
480        // BabyBear and KoalaBear share a bit width, so only the tag separates them.
481        let bb = InteractionPattern::new(vec![Interaction::algebra::<BabyBear, BabyBear>(
482            Hierarchy::Atomic,
483            Kind::Challenge,
484            "alpha",
485            Length::Scalar,
486        )])
487        .unwrap();
488        let kb = InteractionPattern::new(vec![Interaction::algebra::<
489            p3_koala_bear::KoalaBear,
490            p3_koala_bear::KoalaBear,
491        >(
492            Hierarchy::Atomic,
493            Kind::Challenge,
494            "alpha",
495            Length::Scalar,
496        )])
497        .unwrap();
498        assert_ne!(bb.pattern_hash(), kb.pattern_hash());
499    }
500}