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 commit::{TranscriptCommitment, TranscriptCommitmentKind},
16 hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret},
17 Direction, PartialTranscript, RangeSet, Transcript, TranscriptSecret,
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;
385
386 let (recv_hashes, recv_uncovered) = uncovered_query_idx.recv.cover_by(
387 self.hash_secrets.iter().filter(|hash| {
388 hash.direction == Direction::Received && &hash.alg == alg
389 }),
390 |hash| &hash.idx,
391 );
392 uncovered_query_idx.recv = recv_uncovered;
393
394 transcript_proof.hash_secrets.extend(
395 sent_hashes
396 .into_iter()
397 .map(|s| PlaintextHashSecret::clone(s)),
398 );
399 transcript_proof.hash_secrets.extend(
400 recv_hashes
401 .into_iter()
402 .map(|s| PlaintextHashSecret::clone(s)),
403 );
404 }
405 #[allow(unreachable_patterns)]
406 kind => {
407 return Err(TranscriptProofBuilderError::new(
408 BuilderErrorKind::NotSupported,
409 format!("opening {kind} transcript commitments is not yet supported"),
410 ));
411 }
412 }
413 } else {
414 break;
416 }
417 }
418
419 if !uncovered_query_idx.is_empty() {
422 return Err(TranscriptProofBuilderError::cover(
423 uncovered_query_idx,
424 &self.commitment_kinds,
425 ));
426 }
427
428 Ok(transcript_proof)
429 }
430}
431
432#[derive(Debug, thiserror::Error)]
434pub struct TranscriptProofBuilderError {
435 kind: BuilderErrorKind,
436 source: Option<Box<dyn std::error::Error + Send + Sync>>,
437}
438
439impl TranscriptProofBuilderError {
440 fn new<E>(kind: BuilderErrorKind, source: E) -> Self
441 where
442 E: Into<Box<dyn std::error::Error + Send + Sync>>,
443 {
444 Self {
445 kind,
446 source: Some(source.into()),
447 }
448 }
449
450 fn cover(uncovered: QueryIdx, kinds: &[TranscriptCommitmentKind]) -> Self {
451 Self {
452 kind: BuilderErrorKind::Cover {
453 uncovered,
454 kinds: kinds.to_vec(),
455 },
456 source: None,
457 }
458 }
459}
460
461#[derive(Debug, PartialEq)]
462enum BuilderErrorKind {
463 Index,
464 MissingCommitment,
465 Cover {
466 uncovered: QueryIdx,
467 kinds: Vec<TranscriptCommitmentKind>,
468 },
469 NotSupported,
470}
471
472impl fmt::Display for TranscriptProofBuilderError {
473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 f.write_str("transcript proof builder error: ")?;
475
476 match &self.kind {
477 BuilderErrorKind::Index => f.write_str("index error")?,
478 BuilderErrorKind::MissingCommitment => f.write_str("commitment error")?,
479 BuilderErrorKind::Cover { uncovered, kinds } => f.write_str(&format!(
480 "unable to cover the following ranges in transcript using available {kinds:?} commitments: {uncovered}"
481 ))?,
482 BuilderErrorKind::NotSupported => f.write_str("not supported")?,
483 }
484
485 if let Some(source) = &self.source {
486 write!(f, " caused by: {source}")?;
487 }
488
489 Ok(())
490 }
491}
492
493#[allow(clippy::single_range_in_vec_init)]
494#[cfg(test)]
495mod tests {
496 use rand::{Rng, SeedableRng};
497 use rangeset::prelude::*;
498 use rstest::rstest;
499 use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
500
501 use crate::hash::{Blinder, HashAlgId};
502
503 use super::*;
504
505 #[rstest]
506 fn test_reveal_range_out_of_bounds() {
507 let transcript = Transcript::new(
508 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
509 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
510 );
511 let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
512
513 let err = builder.reveal(&(10..15), Direction::Sent).unwrap_err();
514 assert!(matches!(err.kind, BuilderErrorKind::Index));
515
516 let err = builder
517 .reveal(&(10..15), Direction::Received)
518 .err()
519 .unwrap();
520 assert!(matches!(err.kind, BuilderErrorKind::Index));
521 }
522
523 #[rstest]
524 fn test_reveal_missing_commitment() {
525 let transcript = Transcript::new(
526 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
527 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
528 );
529 let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
530
531 let err = builder.reveal_recv(&(9..11)).unwrap_err();
532 assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
533 }
534
535 #[rstest]
536 #[case::sha256(HashAlgId::SHA256)]
537 #[case::blake3(HashAlgId::BLAKE3)]
538 #[case::keccak256(HashAlgId::KECCAK256)]
539 fn test_reveal_with_hash_commitment(#[case] alg: HashAlgId) {
540 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
541 let provider = HashProvider::default();
542 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
543
544 let direction = Direction::Sent;
545 let idx = RangeSet::from(0..10);
546 let blinder: Blinder = rng.random();
547 let hasher = provider.get(&alg).unwrap();
548
549 let commitment = PlaintextHash {
550 direction,
551 idx: idx.clone(),
552 hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
553 };
554
555 let secret = PlaintextHashSecret {
556 direction,
557 idx: idx.clone(),
558 alg,
559 blinder,
560 };
561
562 let secrets = vec![TranscriptSecret::Hash(secret)];
563 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
564
565 builder.reveal_sent(&(0..10)).unwrap();
566
567 let transcript_proof = builder.build().unwrap();
568
569 let partial_transcript = transcript_proof
570 .verify_with_provider(
571 &provider,
572 &transcript.length(),
573 &[TranscriptCommitment::Hash(commitment)],
574 )
575 .unwrap();
576
577 assert_eq!(
578 partial_transcript.sent_unsafe()[0..10],
579 transcript.sent()[0..10]
580 );
581 }
582
583 #[rstest]
584 #[case::sha256(HashAlgId::SHA256)]
585 #[case::blake3(HashAlgId::BLAKE3)]
586 #[case::keccak256(HashAlgId::KECCAK256)]
587 fn test_reveal_with_inconsistent_hash_commitment(#[case] alg: HashAlgId) {
588 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
589 let provider = HashProvider::default();
590 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
591
592 let direction = Direction::Sent;
593 let idx = RangeSet::from(0..10);
594 let blinder: Blinder = rng.random();
595 let hasher = provider.get(&alg).unwrap();
596
597 let commitment = PlaintextHash {
598 direction,
599 idx: idx.clone(),
600 hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
601 };
602
603 let secret = PlaintextHashSecret {
604 direction,
605 idx: idx.clone(),
606 alg,
607 blinder: rng.random(),
609 };
610
611 let secrets = vec![TranscriptSecret::Hash(secret)];
612 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
613
614 builder.reveal_sent(&(0..10)).unwrap();
615
616 let transcript_proof = builder.build().unwrap();
617
618 let err = transcript_proof
619 .verify_with_provider(
620 &provider,
621 &transcript.length(),
622 &[TranscriptCommitment::Hash(commitment)],
623 )
624 .unwrap_err();
625
626 assert!(matches!(err.kind, ErrorKind::Hash));
627 }
628
629 #[rstest]
630 fn test_set_commitment_kinds_with_duplicates() {
631 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
632 let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
633 builder.commitment_kinds(&[
634 TranscriptCommitmentKind::Hash {
635 alg: HashAlgId::SHA256,
636 },
637 TranscriptCommitmentKind::Hash {
638 alg: HashAlgId::SHA256,
639 },
640 TranscriptCommitmentKind::Hash {
641 alg: HashAlgId::SHA256,
642 },
643 ]);
644
645 assert_eq!(
646 builder.commitment_kinds,
647 vec![TranscriptCommitmentKind::Hash {
648 alg: HashAlgId::SHA256
649 },]
650 );
651 }
652
653 #[rstest]
654 #[case::reveal_all_rangesets_with_exact_set(
655 vec![RangeSet::from([0..10]), RangeSet::from([12..30]), RangeSet::from([0..5, 15..30]), RangeSet::from([70..75, 85..100])],
656 RangeSet::from([0..10, 12..30]),
657 true,
658 )]
659 #[case::reveal_all_rangesets_with_single_superset_range(
660 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])],
661 RangeSet::from([0..4, 6..9]),
662 true,
663 )]
664 #[case::reveal_all_rangesets_with_superset_range(
665 vec![RangeSet::from([0..1, 2..4]), RangeSet::from([1..3]), RangeSet::from([1..9]), RangeSet::from([2..3])],
666 RangeSet::from([0..4]),
667 true,
668 )]
669 #[case::failed_to_reveal_with_superset_range_missing_within(
670 vec![RangeSet::from([0..20, 45..56]), RangeSet::from([80..120]), RangeSet::from([50..53])],
671 RangeSet::from([0..120]),
672 false,
673 )]
674 #[case::failed_to_reveal_with_superset_range_missing_outside(
675 vec![RangeSet::from([2..20, 45..116]), RangeSet::from([20..45]), RangeSet::from([50..53])],
676 RangeSet::from([0..120]),
677 false,
678 )]
679 #[case::failed_to_reveal_with_superset_ranges_missing_outside(
680 vec![RangeSet::from([1..10]), RangeSet::from([1..20]), RangeSet::from([15..20, 75..110])],
681 RangeSet::from([0..41, 74..100]),
682 false,
683 )]
684 #[case::failed_to_reveal_as_no_subset_range(
685 vec![RangeSet::from([2..4]), RangeSet::from([1..2]), RangeSet::from([1..9]), RangeSet::from([2..3])],
686 RangeSet::from([0..1]),
687 false,
688 )]
689 #[allow(clippy::single_range_in_vec_init)]
690 fn test_reveal_multiple_rangesets_with_one_rangeset(
691 #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
692 #[case] reveal_recv_rangeset: RangeSet<usize>,
693 #[case] success: bool,
694 ) {
695 use rand::{Rng, SeedableRng};
696
697 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
698 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
699
700 let mut secrets = Vec::new();
702 for rangeset in commit_recv_rangesets.iter() {
703 let blinder: crate::hash::Blinder = rng.random();
704
705 let secret = PlaintextHashSecret {
706 direction: Direction::Received,
707 idx: rangeset.clone(),
708 alg: HashAlgId::BLAKE3,
709 blinder,
710 };
711 secrets.push(TranscriptSecret::Hash(secret));
712 }
713
714 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
715
716 if success {
717 assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok());
718 } else {
719 let err = builder.reveal_recv(&reveal_recv_rangeset).unwrap_err();
720 assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
721 }
722 }
723
724 #[rstest]
725 #[case::cover(
726 vec![RangeSet::from([1..5, 6..10])],
727 vec![RangeSet::from([2..4, 8..10])],
728 RangeSet::from([1..5, 6..10]),
729 RangeSet::from([2..4, 8..10]),
730 RangeSet::default(),
731 RangeSet::default(),
732 )]
733 #[case::failed_to_cover_sent(
734 vec![RangeSet::from([1..5, 6..10])],
735 vec![RangeSet::from([2..4, 8..10])],
736 RangeSet::from([1..5]),
737 RangeSet::from([2..4, 8..10]),
738 RangeSet::from([1..5]),
739 RangeSet::default(),
740 )]
741 #[case::failed_to_cover_recv(
742 vec![RangeSet::from([1..5, 6..10])],
743 vec![RangeSet::from([2..4, 8..10])],
744 RangeSet::from([1..5, 6..10]),
745 RangeSet::from([2..4]),
746 RangeSet::default(),
747 RangeSet::from([2..4]),
748 )]
749 #[case::failed_to_cover_both(
750 vec![RangeSet::from([1..5, 6..10])],
751 vec![RangeSet::from([2..4, 8..10])],
752 RangeSet::from([1..5]),
753 RangeSet::from([2..4]),
754 RangeSet::from([1..5]),
755 RangeSet::from([2..4]),
756 )]
757 #[allow(clippy::single_range_in_vec_init)]
758 fn test_transcript_proof_builder(
759 #[case] commit_sent_rangesets: Vec<RangeSet<usize>>,
760 #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
761 #[case] reveal_sent_rangeset: RangeSet<usize>,
762 #[case] reveal_recv_rangeset: RangeSet<usize>,
763 #[case] uncovered_sent_rangeset: RangeSet<usize>,
764 #[case] uncovered_recv_rangeset: RangeSet<usize>,
765 ) {
766 use rand::{Rng, SeedableRng};
767
768 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
769 let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
770
771 let mut secrets = Vec::new();
773 for rangeset in commit_sent_rangesets.iter() {
774 let blinder: crate::hash::Blinder = rng.random();
775 let secret = PlaintextHashSecret {
776 direction: Direction::Sent,
777 idx: rangeset.clone(),
778 alg: HashAlgId::BLAKE3,
779 blinder,
780 };
781 secrets.push(TranscriptSecret::Hash(secret));
782 }
783 for rangeset in commit_recv_rangesets.iter() {
784 let blinder: crate::hash::Blinder = rng.random();
785 let secret = PlaintextHashSecret {
786 direction: Direction::Received,
787 idx: rangeset.clone(),
788 alg: HashAlgId::BLAKE3,
789 blinder,
790 };
791 secrets.push(TranscriptSecret::Hash(secret));
792 }
793
794 let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
795 builder.reveal_sent(&reveal_sent_rangeset).unwrap();
796 builder.reveal_recv(&reveal_recv_rangeset).unwrap();
797
798 if uncovered_sent_rangeset.is_empty() && uncovered_recv_rangeset.is_empty() {
799 assert!(builder.build().is_ok());
800 } else {
801 let TranscriptProofBuilderError { kind, .. } = builder.build().unwrap_err();
802 match kind {
803 BuilderErrorKind::Cover { uncovered, .. } => {
804 if !uncovered_sent_rangeset.is_empty() {
805 assert_eq!(uncovered.sent, uncovered_sent_rangeset);
806 }
807 if !uncovered_recv_rangeset.is_empty() {
808 assert_eq!(uncovered.recv, uncovered_recv_rangeset);
809 }
810 }
811 _ => panic!("unexpected error kind: {kind:?}"),
812 }
813 }
814 }
815}