1use 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#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Default)]
54pub struct InteractionPattern {
55 interactions: Vec<Interaction>,
57}
58
59impl InteractionPattern {
60 pub fn new(interactions: Vec<Interaction>) -> Result<Self, TranscriptError> {
71 let result = Self { interactions };
76 result.validate()?;
77 Ok(result)
78 }
79
80 #[must_use]
82 pub fn interactions(&self) -> &[Interaction] {
83 &self.interactions
84 }
85
86 #[must_use]
88 pub const fn len(&self) -> usize {
89 self.interactions.len()
90 }
91
92 #[must_use]
94 pub const fn is_empty(&self) -> bool {
95 self.interactions.is_empty()
96 }
97
98 #[must_use]
114 pub fn pattern_hash(&self) -> [u8; 32] {
115 let mut rendered = String::new();
119 write!(&mut rendered, "{self:#}").expect("writing into a String never fails");
122 Keccak256Hash.hash_iter(rendered.bytes())
124 }
125
126 fn validate(&self) -> Result<(), TranscriptError> {
130 let mut stack: Vec<(usize, &Interaction)> = Vec::new();
134
135 for (position, interaction) in self.interactions.iter().enumerate() {
136 match interaction.hierarchy() {
137 Hierarchy::Begin => {
141 Self::check_nested_kind(&stack, position, interaction)?;
142 stack.push((position, interaction));
143 }
144
145 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 Hierarchy::Atomic => Self::check_nested_kind(&stack, position, interaction)?,
173 }
174 }
175
176 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 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 let length = self.interactions.len();
219 let width = length.saturating_sub(1).to_string().len();
221 writeln!(f, "Plonky3 Fiat-Shamir Transcript ({length} interactions)")?;
222
223 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 indentation = indentation.saturating_sub(1);
234 }
235 for _ in 0..indentation {
236 write!(f, " ")?;
237 }
238 if f.alternate() {
242 writeln!(f, "{interaction:#}")?;
243 } else {
244 writeln!(f, "{interaction}")?;
245 }
246 if interaction.hierarchy() == Hierarchy::Begin {
247 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}