1use rangeset::{
4 iter::{FromRangeIterator, IntoRangeIterator, RangeIterator},
5 ops::{Cover, Set},
6};
7use serde::{Deserialize, Serialize};
8use std::{collections::HashSet, fmt};
9
10use crate::{
11 connection::TranscriptLength,
12 display::FmtRangeSet,
13 hash::{HashAlgId, HashProvider},
14 transcript::{
15 Direction, PartialTranscript, RangeSet, Transcript, TranscriptSecret,
16 commit::{TranscriptCommitment, TranscriptCommitmentKind},
17 hash::{PlaintextHash, PlaintextHashSecret, hash_plaintext},
18 },
19};
20
21const DEFAULT_COMMITMENT_KINDS: &[TranscriptCommitmentKind] = &[
24 TranscriptCommitmentKind::Hash {
25 alg: HashAlgId::SHA256,
26 },
27 TranscriptCommitmentKind::Hash {
28 alg: HashAlgId::BLAKE3,
29 },
30 TranscriptCommitmentKind::Hash {
31 alg: HashAlgId::KECCAK256,
32 },
33];
34
35#[derive(Clone, Serialize, Deserialize)]
37pub struct TranscriptProof {
38 transcript: PartialTranscript,
39 hash_secrets: Vec<PlaintextHashSecret>,
40}
41
42opaque_debug::implement!(TranscriptProof);
43
44impl TranscriptProof {
45 pub fn verify_with_provider<'a>(
55 self,
56 provider: &HashProvider,
57 length: &TranscriptLength,
58 commitments: impl IntoIterator<Item = &'a TranscriptCommitment>,
59 ) -> Result<PartialTranscript, TranscriptProofError> {
60 let mut hash_commitments = HashSet::new();
61 for commitment in commitments {
63 match commitment {
64 TranscriptCommitment::Hash(plaintext_hash) => {
65 hash_commitments.insert(plaintext_hash);
66 }
67 }
68 }
69
70 if self.transcript.len_sent() != length.sent as usize
71 || self.transcript.len_received() != length.received as usize
72 {
73 return Err(TranscriptProofError::new(
74 ErrorKind::Proof,
75 "transcript has incorrect length",
76 ));
77 }
78
79 let mut total_auth_sent = RangeSet::default();
80 let mut total_auth_recv = RangeSet::default();
81
82 let mut buffer = Vec::new();
83 for PlaintextHashSecret {
84 direction,
85 idx,
86 alg,
87 blinder,
88 } in self.hash_secrets
89 {
90 let hasher = provider.get(&alg).map_err(|_| {
91 TranscriptProofError::new(
92 ErrorKind::Hash,
93 format!("hash opening has unknown algorithm: {alg}"),
94 )
95 })?;
96
97 let total = match direction {
98 Direction::Sent => self.transcript.len_sent(),
99 Direction::Received => self.transcript.len_received(),
100 };
101
102 if idx.end().unwrap_or(0) > total {
103 return Err(TranscriptProofError::new(
104 ErrorKind::Hash,
105 "hash opening index is out of bounds",
106 ));
107 }
108
109 buffer.clear();
110 for range in idx.iter() {
111 match self.transcript.locate(direction, &range) {
112 Some(bytes) => buffer.extend_from_slice(bytes),
113 None => buffer
114 .extend_from_slice(&self.transcript.materialize_range(direction, &range)),
115 }
116 }
117
118 let expected = PlaintextHash {
119 direction,
120 idx,
121 hash: hash_plaintext(hasher, &buffer, &blinder),
122 };
123
124 if !hash_commitments.contains(&expected) {
125 return Err(TranscriptProofError::new(
126 ErrorKind::Hash,
127 "hash opening does not match any commitment",
128 ));
129 }
130
131 match direction {
132 Direction::Sent => total_auth_sent.union_mut(&expected.idx),
133 Direction::Received => total_auth_recv.union_mut(&expected.idx),
134 }
135 }
136
137 if &total_auth_sent != self.transcript.sent_authed()
139 || &total_auth_recv != self.transcript.received_authed()
140 {
141 return Err(TranscriptProofError::new(
142 ErrorKind::Proof,
143 "transcript proof contains unauthenticated data",
144 ));
145 }
146
147 Ok(self.transcript)
148 }
149}
150
151#[derive(Debug, thiserror::Error)]
153pub struct TranscriptProofError {
154 kind: ErrorKind,
155 source: Option<Box<dyn std::error::Error + Send + Sync>>,
156}
157
158impl TranscriptProofError {
159 fn new<E>(kind: ErrorKind, source: E) -> Self
160 where
161 E: Into<Box<dyn std::error::Error + Send + Sync>>,
162 {
163 Self {
164 kind,
165 source: Some(source.into()),
166 }
167 }
168}
169
170#[derive(Debug)]
171enum ErrorKind {
172 Hash,
173 Proof,
174}
175
176impl fmt::Display for TranscriptProofError {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str("transcript proof error: ")?;
179
180 match self.kind {
181 ErrorKind::Hash => f.write_str("hash error")?,
182 ErrorKind::Proof => f.write_str("proof error")?,
183 }
184
185 if let Some(source) = &self.source {
186 write!(f, " caused by: {source}")?;
187 }
188
189 Ok(())
190 }
191}
192
193#[derive(Clone, Debug, PartialEq)]
195struct QueryIdx {
196 sent: RangeSet<usize>,
197 recv: RangeSet<usize>,
198}
199
200impl QueryIdx {
201 fn new() -> Self {
202 Self {
203 sent: RangeSet::default(),
204 recv: RangeSet::default(),
205 }
206 }
207
208 fn is_empty(&self) -> bool {
209 self.sent.is_empty() && self.recv.is_empty()
210 }
211
212 fn union(&mut self, direction: &Direction, other: &RangeSet<usize>) {
213 match direction {
214 Direction::Sent => self.sent.union_mut(other),
215 Direction::Received => self.recv.union_mut(other),
216 }
217 }
218}
219
220impl std::fmt::Display for QueryIdx {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 write!(
223 f,
224 "sent: {}, received: {}",
225 FmtRangeSet(&self.sent),
226 FmtRangeSet(&self.recv)
227 )
228 }
229}
230
231#[derive(Debug)]
233pub struct TranscriptProofBuilder<'a> {
234 commitment_kinds: Vec<TranscriptCommitmentKind>,
236 transcript: &'a Transcript,
237 hash_secrets: Vec<&'a PlaintextHashSecret>,
238 committed_sent: RangeSet<usize>,
239 committed_recv: RangeSet<usize>,
240 query_idx: QueryIdx,
241}
242
243impl<'a> TranscriptProofBuilder<'a> {
244 pub fn new(
246 transcript: &'a Transcript,
247 secrets: impl IntoIterator<Item = &'a TranscriptSecret>,
248 ) -> Self {
249 let mut committed_sent = RangeSet::default();
250 let mut committed_recv = RangeSet::default();
251
252 let mut hash_secrets = Vec::new();
253 for secret in secrets {
254 match secret {
255 TranscriptSecret::Hash(hash) => {
256 match hash.direction {
257 Direction::Sent => committed_sent.union_mut(&hash.idx),
258 Direction::Received => committed_recv.union_mut(&hash.idx),
259 }
260 hash_secrets.push(hash);
261 }
262 }
263 }
264
265 Self {
266 commitment_kinds: DEFAULT_COMMITMENT_KINDS.to_vec(),
267 transcript,
268 hash_secrets,
269 committed_sent,
270 committed_recv,
271 query_idx: QueryIdx::new(),
272 }
273 }
274
275 pub fn commitment_kinds(&mut self, kinds: &[TranscriptCommitmentKind]) -> &mut Self {
278 if !kinds.is_empty() {
279 let mut seen = HashSet::new();
281 self.commitment_kinds = kinds
282 .iter()
283 .filter(|&kind| seen.insert(kind))
284 .cloned()
285 .collect();
286 }
287 self
288 }
289
290 pub fn reveal(
297 &mut self,
298 ranges: impl IntoRangeIterator<usize>,
299 direction: Direction,
300 ) -> Result<&mut Self, TranscriptProofBuilderError> {
301 self.reveal_inner(RangeSet::from_range_iter(ranges), direction)
302 }
303
304 fn reveal_inner(
305 &mut self,
306 idx: RangeSet<usize>,
307 direction: Direction,
308 ) -> Result<&mut Self, TranscriptProofBuilderError> {
309 if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
310 return Err(TranscriptProofBuilderError::new(
311 BuilderErrorKind::Index,
312 format!(
313 "range is out of bounds of the transcript ({}): {} > {}",
314 direction,
315 idx.end().unwrap_or(0),
316 self.transcript.len_of_direction(direction)
317 ),
318 ));
319 }
320
321 let committed = match direction {
322 Direction::Sent => &self.committed_sent,
323 Direction::Received => &self.committed_recv,
324 };
325
326 if idx.is_subset(committed) {
327 self.query_idx.union(&direction, &idx);
328 } else {
329 let missing = idx.difference(committed).into_set();
330 return Err(TranscriptProofBuilderError::new(
331 BuilderErrorKind::MissingCommitment,
332 format!(
333 "commitment is missing for ranges in {direction} transcript: {}",
334 FmtRangeSet(&missing)
335 ),
336 ));
337 }
338 Ok(self)
339 }
340
341 pub fn reveal_sent(
347 &mut self,
348 ranges: impl IntoRangeIterator<usize>,
349 ) -> Result<&mut Self, TranscriptProofBuilderError> {
350 self.reveal_inner(RangeSet::from_range_iter(ranges), Direction::Sent)
351 }
352
353 pub fn reveal_recv(
359 &mut self,
360 ranges: impl IntoRangeIterator<usize>,
361 ) -> Result<&mut Self, TranscriptProofBuilderError> {
362 self.reveal_inner(RangeSet::from_range_iter(ranges), Direction::Received)
363 }
364
365 pub fn build(self) -> Result<TranscriptProof, TranscriptProofBuilderError> {
367 let mut transcript_proof = TranscriptProof {
368 transcript: self
369 .transcript
370 .to_partial(self.query_idx.sent.clone(), self.query_idx.recv.clone()),
371 hash_secrets: Vec::new(),
372 };
373 let mut uncovered_query_idx = self.query_idx.clone();
374 let mut commitment_kinds_iter = self.commitment_kinds.iter();
375
376 while !uncovered_query_idx.is_empty() {
378 if let Some(kind) = commitment_kinds_iter.next() {
381 match kind {
382 TranscriptCommitmentKind::Hash { alg } => {
383 let (sent_hashes, sent_uncovered) = uncovered_query_idx.sent.cover_by(
384 self.hash_secrets.iter().filter(|hash| {
385 hash.direction == Direction::Sent && &hash.alg == alg
386 }),
387 |hash| &hash.idx,
388 );
389 uncovered_query_idx.sent = sent_uncovered;
393
394 let (recv_hashes, recv_uncovered) = uncovered_query_idx.recv.cover_by(
395 self.hash_secrets.iter().filter(|hash| {
396 hash.direction == Direction::Received && &hash.alg == alg
397 }),
398 |hash| &hash.idx,
399 );
400 uncovered_query_idx.recv = recv_uncovered;
401
402 transcript_proof.hash_secrets.extend(
403 sent_hashes
404 .into_iter()
405 .map(|s| PlaintextHashSecret::clone(s)),
406 );
407 transcript_proof.hash_secrets.extend(
408 recv_hashes
409 .into_iter()
410 .map(|s| PlaintextHashSecret::clone(s)),
411 );
412 }
413 #[allow(unreachable_patterns)]
414 kind => {
415 return Err(TranscriptProofBuilderError::new(
416 BuilderErrorKind::NotSupported,
417 format!("opening {kind} transcript commitments is not yet supported"),
418 ));
419 }
420 }
421 } else {
422 break;
425 }
426 }
427
428 if !uncovered_query_idx.is_empty() {
431 return Err(TranscriptProofBuilderError::cover(
432 uncovered_query_idx,
433 &self.commitment_kinds,
434 ));
435 }
436
437 Ok(transcript_proof)
438 }
439}
440
441#[derive(Debug, thiserror::Error)]
443pub struct TranscriptProofBuilderError {
444 kind: BuilderErrorKind,
445 source: Option<Box<dyn std::error::Error + Send + Sync>>,
446}
447
448impl TranscriptProofBuilderError {
449 fn new<E>(kind: BuilderErrorKind, source: E) -> Self
450 where
451 E: Into<Box<dyn std::error::Error + Send + Sync>>,
452 {
453 Self {
454 kind,
455 source: Some(source.into()),
456 }
457 }
458
459 fn cover(uncovered: QueryIdx, kinds: &[TranscriptCommitmentKind]) -> Self {
460 Self {
461 kind: BuilderErrorKind::Cover {
462 uncovered,
463 kinds: kinds.to_vec(),
464 },
465 source: None,
466 }
467 }
468}
469
470#[derive(Debug, PartialEq)]
471enum BuilderErrorKind {
472 Index,
473 MissingCommitment,
474 Cover {
475 uncovered: QueryIdx,
476 kinds: Vec<TranscriptCommitmentKind>,
477 },
478 NotSupported,
479}
480
481impl fmt::Display for TranscriptProofBuilderError {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.write_str("transcript proof builder error: ")?;
484
485 match &self.kind {
486 BuilderErrorKind::Index => f.write_str("index error")?,
487 BuilderErrorKind::MissingCommitment => f.write_str("commitment error")?,
488 BuilderErrorKind::Cover { uncovered, kinds } => f.write_str(&format!(
489 "unable to cover the following ranges in transcript using available {kinds:?} commitments: {uncovered}"
490 ))?,
491 BuilderErrorKind::NotSupported => f.write_str("not supported")?,
492 }
493
494 if let Some(source) = &self.source {
495 write!(f, " caused by: {source}")?;
496 }
497
498 Ok(())
499 }
500}
501
502#[allow(clippy::single_range_in_vec_init)]
503#[cfg(test)]
504mod tests {
505 use rand::{RngExt, SeedableRng};
506 use rangeset::prelude::*;
507 use rstest::rstest;
508 use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
509
510 use crate::hash::{Blinder, HashAlgId};
511
512 use super::*;
513
514 #[rstest]
515 fn test_reveal_range_out_of_bounds() {
516 let transcript = Transcript::new(
517 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
518 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
519 );
520 let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
521
522 let err = builder.reveal(&(10..15), Direction::Sent).unwrap_err();
523 assert!(matches!(err.kind, BuilderErrorKind::Index));
524
525 let err = builder
526 .reveal(&(10..15), Direction::Received)
527 .err()
528 .unwrap();
529 assert!(matches!(err.kind, BuilderErrorKind::Index));
530 }
531
532 #[rstest]
533 fn test_reveal_missing_commitment() {
534 let transcript = Transcript::new(
535 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
536 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
537 );
538 let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
539
540 let err = builder.reveal_recv(&(9..11)).unwrap_err();
541 assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
542 }
543
544 #[rstest]
545 #[case::sha256(HashAlgId::SHA256)]
546 #[case::blake3(HashAlgId::BLAKE3)]
547 #[case::keccak256(HashAlgId::KECCAK256)]
548 fn test_reveal_with_hash_commitment(#[case] alg: HashAlgId) {
549 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
550 let provider = HashProvider::default();
551 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
552
553 let direction = Direction::Sent;
554 let idx = RangeSet::from(0..10);
555 let blinder: Blinder = rng.random();
556 let hasher = provider.get(&alg).unwrap();
557
558 let commitment = PlaintextHash {
559 direction,
560 idx: idx.clone(),
561 hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
562 };
563
564 let secret = PlaintextHashSecret {
565 direction,
566 idx: idx.clone(),
567 alg,
568 blinder,
569 };
570
571 let secrets = vec![TranscriptSecret::Hash(secret)];
572 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
573
574 builder.reveal_sent(&(0..10)).unwrap();
575
576 let transcript_proof = builder.build().unwrap();
577
578 let partial_transcript = transcript_proof
579 .verify_with_provider(
580 &provider,
581 &transcript.length(),
582 &[TranscriptCommitment::Hash(commitment)],
583 )
584 .unwrap();
585
586 let sent = partial_transcript
587 .sent_unsafe(partial_transcript.len_sent())
588 .unwrap();
589 assert_eq!(sent[0..10], transcript.sent()[0..10]);
590 }
591
592 #[rstest]
593 #[case::sha256(HashAlgId::SHA256)]
594 #[case::blake3(HashAlgId::BLAKE3)]
595 #[case::keccak256(HashAlgId::KECCAK256)]
596 fn test_reveal_with_inconsistent_hash_commitment(#[case] alg: HashAlgId) {
597 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
598 let provider = HashProvider::default();
599 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
600
601 let direction = Direction::Sent;
602 let idx = RangeSet::from(0..10);
603 let blinder: Blinder = rng.random();
604 let hasher = provider.get(&alg).unwrap();
605
606 let commitment = PlaintextHash {
607 direction,
608 idx: idx.clone(),
609 hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
610 };
611
612 let secret = PlaintextHashSecret {
613 direction,
614 idx: idx.clone(),
615 alg,
616 blinder: rng.random(),
618 };
619
620 let secrets = vec![TranscriptSecret::Hash(secret)];
621 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
622
623 builder.reveal_sent(&(0..10)).unwrap();
624
625 let transcript_proof = builder.build().unwrap();
626
627 let err = transcript_proof
628 .verify_with_provider(
629 &provider,
630 &transcript.length(),
631 &[TranscriptCommitment::Hash(commitment)],
632 )
633 .unwrap_err();
634
635 assert!(matches!(err.kind, ErrorKind::Hash));
636 }
637
638 #[rstest]
639 fn test_set_commitment_kinds_with_duplicates() {
640 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
641 let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
642 builder.commitment_kinds(&[
643 TranscriptCommitmentKind::Hash {
644 alg: HashAlgId::SHA256,
645 },
646 TranscriptCommitmentKind::Hash {
647 alg: HashAlgId::SHA256,
648 },
649 TranscriptCommitmentKind::Hash {
650 alg: HashAlgId::SHA256,
651 },
652 ]);
653
654 assert_eq!(
655 builder.commitment_kinds,
656 vec![TranscriptCommitmentKind::Hash {
657 alg: HashAlgId::SHA256
658 },]
659 );
660 }
661
662 #[rstest]
663 #[case::reveal_all_rangesets_with_exact_set(
664 vec![RangeSet::from([0..10]), RangeSet::from([12..30]), RangeSet::from([0..5, 15..30]), RangeSet::from([70..75, 85..100])],
665 RangeSet::from([0..10, 12..30]),
666 true,
667 )]
668 #[case::reveal_all_rangesets_with_single_superset_range(
669 vec![RangeSet::from([0..1]), RangeSet::from([1..2, 8..9]), RangeSet::from([2..4, 6..8]), RangeSet::from([2..3, 6..7]), RangeSet::from([9..12])],
670 RangeSet::from([0..4, 6..9]),
671 true,
672 )]
673 #[case::reveal_all_rangesets_with_superset_range(
674 vec![RangeSet::from([0..1, 2..4]), RangeSet::from([1..3]), RangeSet::from([1..9]), RangeSet::from([2..3])],
675 RangeSet::from([0..4]),
676 true,
677 )]
678 #[case::failed_to_reveal_with_superset_range_missing_within(
679 vec![RangeSet::from([0..20, 45..56]), RangeSet::from([80..120]), RangeSet::from([50..53])],
680 RangeSet::from([0..120]),
681 false,
682 )]
683 #[case::failed_to_reveal_with_superset_range_missing_outside(
684 vec![RangeSet::from([2..20, 45..116]), RangeSet::from([20..45]), RangeSet::from([50..53])],
685 RangeSet::from([0..120]),
686 false,
687 )]
688 #[case::failed_to_reveal_with_superset_ranges_missing_outside(
689 vec![RangeSet::from([1..10]), RangeSet::from([1..20]), RangeSet::from([15..20, 75..110])],
690 RangeSet::from([0..41, 74..100]),
691 false,
692 )]
693 #[case::failed_to_reveal_as_no_subset_range(
694 vec![RangeSet::from([2..4]), RangeSet::from([1..2]), RangeSet::from([1..9]), RangeSet::from([2..3])],
695 RangeSet::from([0..1]),
696 false,
697 )]
698 #[allow(clippy::single_range_in_vec_init)]
699 fn test_reveal_multiple_rangesets_with_one_rangeset(
700 #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
701 #[case] reveal_recv_rangeset: RangeSet<usize>,
702 #[case] success: bool,
703 ) {
704 use rand::{RngExt, SeedableRng};
705
706 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
707 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
708
709 let mut secrets = Vec::new();
711 for rangeset in commit_recv_rangesets.iter() {
712 let blinder: crate::hash::Blinder = rng.random();
713
714 let secret = PlaintextHashSecret {
715 direction: Direction::Received,
716 idx: rangeset.clone(),
717 alg: HashAlgId::BLAKE3,
718 blinder,
719 };
720 secrets.push(TranscriptSecret::Hash(secret));
721 }
722
723 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
724
725 if success {
726 assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok());
727 } else {
728 let err = builder.reveal_recv(&reveal_recv_rangeset).unwrap_err();
729 assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
730 }
731 }
732
733 #[rstest]
734 #[case::cover(
735 vec![RangeSet::from([1..5, 6..10])],
736 vec![RangeSet::from([2..4, 8..10])],
737 RangeSet::from([1..5, 6..10]),
738 RangeSet::from([2..4, 8..10]),
739 RangeSet::default(),
740 RangeSet::default(),
741 )]
742 #[case::failed_to_cover_sent(
743 vec![RangeSet::from([1..5, 6..10])],
744 vec![RangeSet::from([2..4, 8..10])],
745 RangeSet::from([1..5]),
746 RangeSet::from([2..4, 8..10]),
747 RangeSet::from([1..5]),
748 RangeSet::default(),
749 )]
750 #[case::failed_to_cover_recv(
751 vec![RangeSet::from([1..5, 6..10])],
752 vec![RangeSet::from([2..4, 8..10])],
753 RangeSet::from([1..5, 6..10]),
754 RangeSet::from([2..4]),
755 RangeSet::default(),
756 RangeSet::from([2..4]),
757 )]
758 #[case::failed_to_cover_both(
759 vec![RangeSet::from([1..5, 6..10])],
760 vec![RangeSet::from([2..4, 8..10])],
761 RangeSet::from([1..5]),
762 RangeSet::from([2..4]),
763 RangeSet::from([1..5]),
764 RangeSet::from([2..4]),
765 )]
766 #[allow(clippy::single_range_in_vec_init)]
767 fn test_transcript_proof_builder(
768 #[case] commit_sent_rangesets: Vec<RangeSet<usize>>,
769 #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
770 #[case] reveal_sent_rangeset: RangeSet<usize>,
771 #[case] reveal_recv_rangeset: RangeSet<usize>,
772 #[case] uncovered_sent_rangeset: RangeSet<usize>,
773 #[case] uncovered_recv_rangeset: RangeSet<usize>,
774 ) {
775 use rand::{RngExt, SeedableRng};
776
777 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
778 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
779
780 let mut secrets = Vec::new();
782 for rangeset in commit_sent_rangesets.iter() {
783 let blinder: crate::hash::Blinder = rng.random();
784 let secret = PlaintextHashSecret {
785 direction: Direction::Sent,
786 idx: rangeset.clone(),
787 alg: HashAlgId::BLAKE3,
788 blinder,
789 };
790 secrets.push(TranscriptSecret::Hash(secret));
791 }
792 for rangeset in commit_recv_rangesets.iter() {
793 let blinder: crate::hash::Blinder = rng.random();
794 let secret = PlaintextHashSecret {
795 direction: Direction::Received,
796 idx: rangeset.clone(),
797 alg: HashAlgId::BLAKE3,
798 blinder,
799 };
800 secrets.push(TranscriptSecret::Hash(secret));
801 }
802
803 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
804 builder.reveal_sent(&reveal_sent_rangeset).unwrap();
805 builder.reveal_recv(&reveal_recv_rangeset).unwrap();
806
807 if uncovered_sent_rangeset.is_empty() && uncovered_recv_rangeset.is_empty() {
808 assert!(builder.build().is_ok());
809 } else {
810 let TranscriptProofBuilderError { kind, .. } = builder.build().unwrap_err();
811 match kind {
812 BuilderErrorKind::Cover { uncovered, .. } => {
813 if !uncovered_sent_rangeset.is_empty() {
814 assert_eq!(uncovered.sent, uncovered_sent_rangeset);
815 }
816 if !uncovered_recv_rangeset.is_empty() {
817 assert_eq!(uncovered.recv, uncovered_recv_rangeset);
818 }
819 }
820 _ => panic!("unexpected error kind: {kind:?}"),
821 }
822 }
823 }
824}