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