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