tlsn_core/transcript/
proof.rs

1//! Transcript proofs.
2
3use 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
22/// Default commitment kinds in order of preference for building transcript
23/// proofs.
24const 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/// Proof of the contents of a transcript.
37#[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    /// Verifies the proof.
47    ///
48    /// Returns a partial transcript of authenticated data.
49    ///
50    /// # Arguments
51    ///
52    /// * `provider` - The hash provider to use for verification.
53    /// * `length` - The transcript length.
54    /// * `commitments` - The commitments to verify against.
55    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        // Index commitments.
63        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        // Assert that all the authenticated data are covered by the proof.
132        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/// Error for [`TranscriptProof`].
146#[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/// Union of ranges to reveal.
188#[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/// Builder for [`TranscriptProof`].
226#[derive(Debug)]
227pub struct TranscriptProofBuilder<'a> {
228    /// Commitment kinds in order of preference for building transcript proofs.
229    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    /// Creates a new proof builder.
239    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    /// Sets the commitment kinds in order of preference for building transcript
270    /// proofs, i.e. the first one is the most preferred.
271    pub fn commitment_kinds(&mut self, kinds: &[TranscriptCommitmentKind]) -> &mut Self {
272        if !kinds.is_empty() {
273            // Removes duplicates from `kinds` while preserving its order.
274            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    /// Reveals the given ranges in the transcript.
285    ///
286    /// # Arguments
287    ///
288    /// * `ranges` - The ranges to reveal.
289    /// * `direction` - The direction of the transcript.
290    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    /// Reveals the given ranges in the sent transcript.
330    ///
331    /// # Arguments
332    ///
333    /// * `ranges` - The ranges to reveal.
334    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    /// Reveals the given ranges in the received transcript.
342    ///
343    /// # Arguments
344    ///
345    /// * `ranges` - The ranges to reveal.
346    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    /// Builds the transcript proof.
354    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        // Tries to cover the query ranges with committed ranges.
365        while !uncovered_query_idx.is_empty() {
366            // Committed ranges of different kinds are checked in order of preference set in
367            // self.commitment_kinds.
368            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 ranges will be checked with ranges of the next
378                        // preferred commitment kind.
379                        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                // Stops the set cover check if there are no more commitment kinds left.
410                break;
411            }
412        }
413
414        // If there are still uncovered ranges, it means that query ranges cannot be
415        // covered by committed ranges of any kind.
416        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/// Error for [`TranscriptProofBuilder`].
428#[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            // Use a different blinder to create an inconsistent commitment
603            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        // Create hash commitments for each rangeset
696        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        // Create hash commitments for each rangeset
767        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}