tlsn_core/transcript/
proof.rs

1//! Transcript proofs.
2
3use rangeset::{Cover, ToRangeSet};
4use serde::{Deserialize, Serialize};
5use std::{collections::HashSet, fmt};
6
7use crate::{
8    connection::TranscriptLength,
9    hash::{HashAlgId, HashProvider},
10    transcript::{
11        commit::{TranscriptCommitment, TranscriptCommitmentKind},
12        encoding::{EncodingProof, EncodingProofError, EncodingTree},
13        hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret},
14        Direction, Idx, PartialTranscript, Transcript, TranscriptSecret,
15    },
16};
17
18/// Default commitment kinds in order of preference for building transcript
19/// proofs.
20const DEFAULT_COMMITMENT_KINDS: &[TranscriptCommitmentKind] = &[
21    TranscriptCommitmentKind::Hash {
22        alg: HashAlgId::SHA256,
23    },
24    TranscriptCommitmentKind::Encoding,
25];
26
27/// Proof of the contents of a transcript.
28#[derive(Clone, Serialize, Deserialize)]
29pub struct TranscriptProof {
30    transcript: PartialTranscript,
31    encoding_proof: Option<EncodingProof>,
32    hash_secrets: Vec<PlaintextHashSecret>,
33}
34
35opaque_debug::implement!(TranscriptProof);
36
37impl TranscriptProof {
38    /// Verifies the proof.
39    ///
40    /// Returns a partial transcript of authenticated data.
41    ///
42    /// # Arguments
43    ///
44    /// * `provider` - The hash provider to use for verification.
45    /// * `attestation_body` - The attestation body to verify against.
46    pub fn verify_with_provider<'a>(
47        self,
48        provider: &HashProvider,
49        length: &TranscriptLength,
50        commitments: impl IntoIterator<Item = &'a TranscriptCommitment>,
51    ) -> Result<PartialTranscript, TranscriptProofError> {
52        let mut encoding_commitment = None;
53        let mut hash_commitments = HashSet::new();
54        // Index commitments.
55        for commitment in commitments {
56            match commitment {
57                TranscriptCommitment::Encoding(commitment) => {
58                    if encoding_commitment.replace(commitment).is_some() {
59                        return Err(TranscriptProofError::new(
60                            ErrorKind::Encoding,
61                            "multiple encoding commitments are present.",
62                        ));
63                    }
64                }
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 = Idx::default();
81        let mut total_auth_recv = Idx::default();
82
83        // Verify encoding proof.
84        if let Some(proof) = self.encoding_proof {
85            let commitment = encoding_commitment.ok_or_else(|| {
86                TranscriptProofError::new(
87                    ErrorKind::Encoding,
88                    "contains an encoding proof but missing encoding commitment",
89                )
90            })?;
91
92            let (auth_sent, auth_recv) = proof.verify_with_provider(
93                provider,
94                commitment,
95                self.transcript.sent_unsafe(),
96                self.transcript.received_unsafe(),
97            )?;
98
99            total_auth_sent.union_mut(&auth_sent);
100            total_auth_recv.union_mut(&auth_recv);
101        }
102
103        let mut buffer = Vec::new();
104        for PlaintextHashSecret {
105            direction,
106            idx,
107            alg,
108            blinder,
109        } in self.hash_secrets
110        {
111            let hasher = provider.get(&alg).map_err(|_| {
112                TranscriptProofError::new(
113                    ErrorKind::Hash,
114                    format!("hash opening has unknown algorithm: {alg}"),
115                )
116            })?;
117
118            let (plaintext, auth) = match direction {
119                Direction::Sent => (self.transcript.sent_unsafe(), &mut total_auth_sent),
120                Direction::Received => (self.transcript.received_unsafe(), &mut total_auth_recv),
121            };
122
123            if idx.end() > plaintext.len() {
124                return Err(TranscriptProofError::new(
125                    ErrorKind::Hash,
126                    "hash opening index is out of bounds",
127                ));
128            }
129
130            buffer.clear();
131            for range in idx.iter_ranges() {
132                buffer.extend_from_slice(&plaintext[range]);
133            }
134
135            let expected = PlaintextHash {
136                direction,
137                idx,
138                hash: hash_plaintext(hasher, &buffer, &blinder),
139            };
140
141            if !hash_commitments.contains(&expected) {
142                return Err(TranscriptProofError::new(
143                    ErrorKind::Hash,
144                    "hash opening does not match any commitment",
145                ));
146            }
147
148            auth.union_mut(&expected.idx);
149        }
150
151        // Assert that all the authenticated data are covered by the proof.
152        if &total_auth_sent != self.transcript.sent_authed()
153            || &total_auth_recv != self.transcript.received_authed()
154        {
155            return Err(TranscriptProofError::new(
156                ErrorKind::Proof,
157                "transcript proof contains unauthenticated data",
158            ));
159        }
160
161        Ok(self.transcript)
162    }
163}
164
165/// Error for [`TranscriptProof`].
166#[derive(Debug, thiserror::Error)]
167pub struct TranscriptProofError {
168    kind: ErrorKind,
169    source: Option<Box<dyn std::error::Error + Send + Sync>>,
170}
171
172impl TranscriptProofError {
173    fn new<E>(kind: ErrorKind, source: E) -> Self
174    where
175        E: Into<Box<dyn std::error::Error + Send + Sync>>,
176    {
177        Self {
178            kind,
179            source: Some(source.into()),
180        }
181    }
182}
183
184#[derive(Debug)]
185enum ErrorKind {
186    Encoding,
187    Hash,
188    Proof,
189}
190
191impl fmt::Display for TranscriptProofError {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.write_str("transcript proof error: ")?;
194
195        match self.kind {
196            ErrorKind::Encoding => f.write_str("encoding error")?,
197            ErrorKind::Hash => f.write_str("hash error")?,
198            ErrorKind::Proof => f.write_str("proof error")?,
199        }
200
201        if let Some(source) = &self.source {
202            write!(f, " caused by: {source}")?;
203        }
204
205        Ok(())
206    }
207}
208
209impl From<EncodingProofError> for TranscriptProofError {
210    fn from(e: EncodingProofError) -> Self {
211        TranscriptProofError::new(ErrorKind::Encoding, e)
212    }
213}
214
215/// Union of ranges to reveal.
216#[derive(Clone, Debug, PartialEq)]
217struct QueryIdx {
218    sent: Idx,
219    recv: Idx,
220}
221
222impl QueryIdx {
223    fn new() -> Self {
224        Self {
225            sent: Idx::empty(),
226            recv: Idx::empty(),
227        }
228    }
229
230    fn is_empty(&self) -> bool {
231        self.sent.is_empty() && self.recv.is_empty()
232    }
233
234    fn union(&mut self, direction: &Direction, other: &Idx) {
235        match direction {
236            Direction::Sent => self.sent.union_mut(other),
237            Direction::Received => self.recv.union_mut(other),
238        }
239    }
240}
241
242impl std::fmt::Display for QueryIdx {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        write!(f, "sent: {}, received: {}", self.sent, self.recv)
245    }
246}
247
248/// Builder for [`TranscriptProof`].
249#[derive(Debug)]
250pub struct TranscriptProofBuilder<'a> {
251    /// Commitment kinds in order of preference for building transcript proofs.
252    commitment_kinds: Vec<TranscriptCommitmentKind>,
253    transcript: &'a Transcript,
254    encoding_tree: Option<&'a EncodingTree>,
255    hash_secrets: Vec<&'a PlaintextHashSecret>,
256    committed_sent: Idx,
257    committed_recv: Idx,
258    query_idx: QueryIdx,
259}
260
261impl<'a> TranscriptProofBuilder<'a> {
262    /// Creates a new proof builder.
263    pub fn new(
264        transcript: &'a Transcript,
265        secrets: impl IntoIterator<Item = &'a TranscriptSecret>,
266    ) -> Self {
267        let mut committed_sent = Idx::empty();
268        let mut committed_recv = Idx::empty();
269
270        let mut encoding_tree = None;
271        let mut hash_secrets = Vec::new();
272        for secret in secrets {
273            match secret {
274                TranscriptSecret::Encoding(tree) => {
275                    committed_sent.union_mut(tree.idx(Direction::Sent));
276                    committed_recv.union_mut(tree.idx(Direction::Received));
277                    encoding_tree = Some(tree);
278                }
279                TranscriptSecret::Hash(hash) => {
280                    match hash.direction {
281                        Direction::Sent => committed_sent.union_mut(&hash.idx),
282                        Direction::Received => committed_recv.union_mut(&hash.idx),
283                    }
284                    hash_secrets.push(hash);
285                }
286            }
287        }
288
289        Self {
290            commitment_kinds: DEFAULT_COMMITMENT_KINDS.to_vec(),
291            transcript,
292            encoding_tree,
293            hash_secrets,
294            committed_sent,
295            committed_recv,
296            query_idx: QueryIdx::new(),
297        }
298    }
299
300    /// Sets the commitment kinds in order of preference for building transcript
301    /// proofs, i.e. the first one is the most preferred.
302    pub fn commitment_kinds(&mut self, kinds: &[TranscriptCommitmentKind]) -> &mut Self {
303        if !kinds.is_empty() {
304            // Removes duplicates from `kinds` while preserving its order.
305            let mut seen = HashSet::new();
306            self.commitment_kinds = kinds
307                .iter()
308                .filter(|&kind| seen.insert(kind))
309                .cloned()
310                .collect();
311        }
312        self
313    }
314
315    /// Reveals the given ranges in the transcript.
316    ///
317    /// # Arguments
318    ///
319    /// * `ranges` - The ranges to reveal.
320    /// * `direction` - The direction of the transcript.
321    pub fn reveal(
322        &mut self,
323        ranges: &dyn ToRangeSet<usize>,
324        direction: Direction,
325    ) -> Result<&mut Self, TranscriptProofBuilderError> {
326        let idx = Idx::new(ranges.to_range_set());
327
328        if idx.end() > self.transcript.len_of_direction(direction) {
329            return Err(TranscriptProofBuilderError::new(
330                BuilderErrorKind::Index,
331                format!(
332                    "range is out of bounds of the transcript ({}): {} > {}",
333                    direction,
334                    idx.end(),
335                    self.transcript.len_of_direction(direction)
336                ),
337            ));
338        }
339
340        let committed = match direction {
341            Direction::Sent => &self.committed_sent,
342            Direction::Received => &self.committed_recv,
343        };
344
345        if idx.is_subset(committed) {
346            self.query_idx.union(&direction, &idx);
347        } else {
348            let missing = idx.difference(committed);
349            return Err(TranscriptProofBuilderError::new(
350                BuilderErrorKind::MissingCommitment,
351                format!("commitment is missing for ranges in {direction} transcript: {missing}"),
352            ));
353        }
354        Ok(self)
355    }
356
357    /// Reveals the given ranges in the sent transcript.
358    ///
359    /// # Arguments
360    ///
361    /// * `ranges` - The ranges to reveal.
362    pub fn reveal_sent(
363        &mut self,
364        ranges: &dyn ToRangeSet<usize>,
365    ) -> Result<&mut Self, TranscriptProofBuilderError> {
366        self.reveal(ranges, Direction::Sent)
367    }
368
369    /// Reveals the given ranges in the received transcript.
370    ///
371    /// # Arguments
372    ///
373    /// * `ranges` - The ranges to reveal.
374    pub fn reveal_recv(
375        &mut self,
376        ranges: &dyn ToRangeSet<usize>,
377    ) -> Result<&mut Self, TranscriptProofBuilderError> {
378        self.reveal(ranges, Direction::Received)
379    }
380
381    /// Builds the transcript proof.
382    pub fn build(self) -> Result<TranscriptProof, TranscriptProofBuilderError> {
383        let mut transcript_proof = TranscriptProof {
384            transcript: self
385                .transcript
386                .to_partial(self.query_idx.sent.clone(), self.query_idx.recv.clone()),
387            encoding_proof: None,
388            hash_secrets: Vec::new(),
389        };
390        let mut uncovered_query_idx = self.query_idx.clone();
391        let mut commitment_kinds_iter = self.commitment_kinds.iter();
392
393        // Tries to cover the query ranges with committed ranges.
394        while !uncovered_query_idx.is_empty() {
395            // Committed ranges of different kinds are checked in order of preference set in
396            // self.commitment_kinds.
397            if let Some(kind) = commitment_kinds_iter.next() {
398                match kind {
399                    TranscriptCommitmentKind::Encoding => {
400                        let Some(encoding_tree) = self.encoding_tree else {
401                            // Proceeds to the next preferred commitment kind if encoding tree is
402                            // not available.
403                            continue;
404                        };
405
406                        let (sent_dir_idxs, sent_uncovered) =
407                            uncovered_query_idx.sent.as_range_set().cover_by(
408                                encoding_tree
409                                    .transcript_indices()
410                                    .filter(|(dir, _)| *dir == Direction::Sent),
411                                |(_, idx)| &idx.0,
412                            );
413                        // Uncovered ranges will be checked with ranges of the next
414                        // preferred commitment kind.
415                        uncovered_query_idx.sent = Idx(sent_uncovered);
416
417                        let (recv_dir_idxs, recv_uncovered) =
418                            uncovered_query_idx.recv.as_range_set().cover_by(
419                                encoding_tree
420                                    .transcript_indices()
421                                    .filter(|(dir, _)| *dir == Direction::Received),
422                                |(_, idx)| &idx.0,
423                            );
424                        uncovered_query_idx.recv = Idx(recv_uncovered);
425
426                        let dir_idxs = sent_dir_idxs
427                            .into_iter()
428                            .chain(recv_dir_idxs)
429                            .collect::<Vec<_>>();
430
431                        // Skip proof generation if there are no committed ranges that can cover the
432                        // query ranges.
433                        if !dir_idxs.is_empty() {
434                            transcript_proof.encoding_proof = Some(
435                                encoding_tree
436                                    .proof(dir_idxs.into_iter())
437                                    .expect("subsequences were checked to be in tree"),
438                            );
439                        }
440                    }
441                    TranscriptCommitmentKind::Hash { alg } => {
442                        let (sent_hashes, sent_uncovered) =
443                            uncovered_query_idx.sent.as_range_set().cover_by(
444                                self.hash_secrets.iter().filter(|hash| {
445                                    hash.direction == Direction::Sent && &hash.alg == alg
446                                }),
447                                |hash| &hash.idx.0,
448                            );
449                        // Uncovered ranges will be checked with ranges of the next
450                        // preferred commitment kind.
451                        uncovered_query_idx.sent = Idx(sent_uncovered);
452
453                        let (recv_hashes, recv_uncovered) =
454                            uncovered_query_idx.recv.as_range_set().cover_by(
455                                self.hash_secrets.iter().filter(|hash| {
456                                    hash.direction == Direction::Received && &hash.alg == alg
457                                }),
458                                |hash| &hash.idx.0,
459                            );
460                        uncovered_query_idx.recv = Idx(recv_uncovered);
461
462                        transcript_proof.hash_secrets.extend(
463                            sent_hashes
464                                .into_iter()
465                                .map(|s| PlaintextHashSecret::clone(s)),
466                        );
467                        transcript_proof.hash_secrets.extend(
468                            recv_hashes
469                                .into_iter()
470                                .map(|s| PlaintextHashSecret::clone(s)),
471                        );
472                    }
473                    #[allow(unreachable_patterns)]
474                    kind => {
475                        return Err(TranscriptProofBuilderError::new(
476                            BuilderErrorKind::NotSupported,
477                            format!("opening {kind} transcript commitments is not yet supported"),
478                        ));
479                    }
480                }
481            } else {
482                // Stops the set cover check if there are no more commitment kinds left.
483                break;
484            }
485        }
486
487        // If there are still uncovered ranges, it means that query ranges cannot be
488        // covered by committed ranges of any kind.
489        if !uncovered_query_idx.is_empty() {
490            return Err(TranscriptProofBuilderError::cover(
491                uncovered_query_idx,
492                &self.commitment_kinds,
493            ));
494        }
495
496        Ok(transcript_proof)
497    }
498}
499
500/// Error for [`TranscriptProofBuilder`].
501#[derive(Debug, thiserror::Error)]
502pub struct TranscriptProofBuilderError {
503    kind: BuilderErrorKind,
504    source: Option<Box<dyn std::error::Error + Send + Sync>>,
505}
506
507impl TranscriptProofBuilderError {
508    fn new<E>(kind: BuilderErrorKind, source: E) -> Self
509    where
510        E: Into<Box<dyn std::error::Error + Send + Sync>>,
511    {
512        Self {
513            kind,
514            source: Some(source.into()),
515        }
516    }
517
518    fn cover(uncovered: QueryIdx, kinds: &[TranscriptCommitmentKind]) -> Self {
519        Self {
520            kind: BuilderErrorKind::Cover {
521                uncovered,
522                kinds: kinds.to_vec(),
523            },
524            source: None,
525        }
526    }
527}
528
529#[derive(Debug, PartialEq)]
530enum BuilderErrorKind {
531    Index,
532    MissingCommitment,
533    Cover {
534        uncovered: QueryIdx,
535        kinds: Vec<TranscriptCommitmentKind>,
536    },
537    NotSupported,
538}
539
540impl fmt::Display for TranscriptProofBuilderError {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        f.write_str("transcript proof builder error: ")?;
543
544        match &self.kind {
545            BuilderErrorKind::Index => f.write_str("index error")?,
546            BuilderErrorKind::MissingCommitment => f.write_str("commitment error")?,
547            BuilderErrorKind::Cover { uncovered, kinds } => f.write_str(&format!(
548                "unable to cover the following ranges in transcript using available {kinds:?} commitments: {uncovered}"
549            ))?,
550            BuilderErrorKind::NotSupported => f.write_str("not supported")?,
551        }
552
553        if let Some(source) = &self.source {
554            write!(f, " caused by: {source}")?;
555        }
556
557        Ok(())
558    }
559}
560
561#[allow(clippy::single_range_in_vec_init)]
562#[cfg(test)]
563mod tests {
564    use rand::{Rng, SeedableRng};
565    use rangeset::RangeSet;
566    use rstest::rstest;
567    use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
568
569    use crate::{
570        fixtures::encoding_provider,
571        hash::{Blake3, Blinder, HashAlgId},
572        transcript::TranscriptCommitConfigBuilder,
573    };
574
575    use super::*;
576
577    #[rstest]
578    fn test_verify_missing_encoding_commitment_root() {
579        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
580        let idxs = vec![(Direction::Received, Idx::new(0..transcript.len().1))];
581        let encoding_tree = EncodingTree::new(
582            &Blake3::default(),
583            &idxs,
584            &encoding_provider(transcript.sent(), transcript.received()),
585        )
586        .unwrap();
587
588        let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
589        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
590
591        builder.reveal_recv(&(0..transcript.len().1)).unwrap();
592
593        let transcript_proof = builder.build().unwrap();
594
595        let provider = HashProvider::default();
596        let err = transcript_proof
597            .verify_with_provider(&provider, &transcript.length(), &[])
598            .err()
599            .unwrap();
600
601        assert!(matches!(err.kind, ErrorKind::Encoding));
602    }
603
604    #[rstest]
605    fn test_reveal_range_out_of_bounds() {
606        let transcript = Transcript::new(
607            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
608            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
609        );
610        let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
611
612        let err = builder.reveal(&(10..15), Direction::Sent).unwrap_err();
613        assert!(matches!(err.kind, BuilderErrorKind::Index));
614
615        let err = builder
616            .reveal(&(10..15), Direction::Received)
617            .err()
618            .unwrap();
619        assert!(matches!(err.kind, BuilderErrorKind::Index));
620    }
621
622    #[rstest]
623    fn test_reveal_missing_encoding_tree() {
624        let transcript = Transcript::new(
625            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
626            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
627        );
628        let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
629
630        let err = builder.reveal_recv(&(9..11)).unwrap_err();
631        assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
632    }
633
634    #[rstest]
635    fn test_reveal_with_hash_commitment() {
636        let mut rng = rand::rngs::StdRng::seed_from_u64(0);
637        let provider = HashProvider::default();
638        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
639
640        let direction = Direction::Sent;
641        let idx = Idx::new(0..10);
642        let blinder: Blinder = rng.random();
643        let alg = HashAlgId::SHA256;
644        let hasher = provider.get(&alg).unwrap();
645
646        let commitment = PlaintextHash {
647            direction,
648            idx: idx.clone(),
649            hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
650        };
651
652        let secret = PlaintextHashSecret {
653            direction,
654            idx: idx.clone(),
655            alg,
656            blinder,
657        };
658
659        let secrets = vec![TranscriptSecret::Hash(secret)];
660        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
661
662        builder.reveal_sent(&(0..10)).unwrap();
663
664        let transcript_proof = builder.build().unwrap();
665
666        let partial_transcript = transcript_proof
667            .verify_with_provider(
668                &provider,
669                &transcript.length(),
670                &[TranscriptCommitment::Hash(commitment)],
671            )
672            .unwrap();
673
674        assert_eq!(
675            partial_transcript.sent_unsafe()[0..10],
676            transcript.sent()[0..10]
677        );
678    }
679
680    #[rstest]
681    fn test_reveal_with_inconsistent_hash_commitment() {
682        let mut rng = rand::rngs::StdRng::seed_from_u64(0);
683        let provider = HashProvider::default();
684        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
685
686        let direction = Direction::Sent;
687        let idx = Idx::new(0..10);
688        let blinder: Blinder = rng.random();
689        let alg = HashAlgId::SHA256;
690        let hasher = provider.get(&alg).unwrap();
691
692        let commitment = PlaintextHash {
693            direction,
694            idx: idx.clone(),
695            hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
696        };
697
698        let secret = PlaintextHashSecret {
699            direction,
700            idx: idx.clone(),
701            alg,
702            // Use a different blinder to create an inconsistent commitment
703            blinder: rng.random(),
704        };
705
706        let secrets = vec![TranscriptSecret::Hash(secret)];
707        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
708
709        builder.reveal_sent(&(0..10)).unwrap();
710
711        let transcript_proof = builder.build().unwrap();
712
713        let err = transcript_proof
714            .verify_with_provider(
715                &provider,
716                &transcript.length(),
717                &[TranscriptCommitment::Hash(commitment)],
718            )
719            .unwrap_err();
720
721        assert!(matches!(err.kind, ErrorKind::Hash));
722    }
723
724    #[rstest]
725    fn test_set_commitment_kinds_with_duplicates() {
726        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
727        let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
728        builder.commitment_kinds(&[
729            TranscriptCommitmentKind::Hash {
730                alg: HashAlgId::SHA256,
731            },
732            TranscriptCommitmentKind::Encoding,
733            TranscriptCommitmentKind::Hash {
734                alg: HashAlgId::SHA256,
735            },
736            TranscriptCommitmentKind::Hash {
737                alg: HashAlgId::SHA256,
738            },
739            TranscriptCommitmentKind::Encoding,
740        ]);
741
742        assert_eq!(
743            builder.commitment_kinds,
744            vec![
745                TranscriptCommitmentKind::Hash {
746                    alg: HashAlgId::SHA256
747                },
748                TranscriptCommitmentKind::Encoding
749            ]
750        );
751    }
752
753    #[rstest]
754    #[case::reveal_all_rangesets_with_exact_set(
755        vec![RangeSet::from([0..10]), RangeSet::from([12..30]), RangeSet::from([0..5, 15..30]), RangeSet::from([70..75, 85..100])],
756        RangeSet::from([0..10, 12..30]),
757        true,
758    )]
759    #[case::reveal_all_rangesets_with_superset_ranges(
760        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])],
761        RangeSet::from([0..4, 6..9]),
762        true,
763    )]
764    #[case::reveal_all_rangesets_with_superset_range(
765        vec![RangeSet::from([0..1, 2..4]), RangeSet::from([1..3]), RangeSet::from([1..9]), RangeSet::from([2..3])],
766        RangeSet::from([0..4]),
767        true,
768    )]
769    #[case::failed_to_reveal_with_superset_range_missing_within(
770        vec![RangeSet::from([0..20, 45..56]), RangeSet::from([80..120]), RangeSet::from([50..53])],
771        RangeSet::from([0..120]),
772        false,
773    )]
774    #[case::failed_to_reveal_with_superset_range_missing_outside(
775        vec![RangeSet::from([2..20, 45..116]), RangeSet::from([20..45]), RangeSet::from([50..53])],
776        RangeSet::from([0..120]),
777        false,
778    )]
779    #[case::failed_to_reveal_with_superset_ranges_missing_outside(
780        vec![RangeSet::from([1..10]), RangeSet::from([1..20]),  RangeSet::from([15..20, 75..110])],
781        RangeSet::from([0..41, 74..100]),
782        false,
783    )]
784    #[case::failed_to_reveal_as_no_subset_range(
785        vec![RangeSet::from([2..4]), RangeSet::from([1..2]), RangeSet::from([1..9]), RangeSet::from([2..3])],
786        RangeSet::from([0..1]),
787        false,
788    )]
789    #[allow(clippy::single_range_in_vec_init)]
790    fn test_reveal_mutliple_rangesets_with_one_rangeset(
791        #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
792        #[case] reveal_recv_rangeset: RangeSet<usize>,
793        #[case] success: bool,
794    ) {
795        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
796
797        // Encoding commitment kind
798        let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript);
799        for rangeset in commit_recv_rangesets.iter() {
800            transcript_commitment_builder.commit_recv(rangeset).unwrap();
801        }
802
803        let transcripts_commitment_config = transcript_commitment_builder.build().unwrap();
804
805        let encoding_tree = EncodingTree::new(
806            &Blake3::default(),
807            transcripts_commitment_config.iter_encoding(),
808            &encoding_provider(GET_WITH_HEADER, OK_JSON),
809        )
810        .unwrap();
811
812        let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
813        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
814
815        if success {
816            assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok());
817        } else {
818            let err = builder.reveal_recv(&reveal_recv_rangeset).unwrap_err();
819            assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
820        }
821    }
822
823    #[rstest]
824    #[case::cover(
825        vec![RangeSet::from([1..5, 6..10])],
826        vec![RangeSet::from([2..4, 8..10])],
827        RangeSet::from([1..5, 6..10]),
828        RangeSet::from([2..4, 8..10]),
829        RangeSet::default(),
830        RangeSet::default(),
831    )]
832    #[case::failed_to_cover_sent(
833        vec![RangeSet::from([1..5, 6..10])],
834        vec![RangeSet::from([2..4, 8..10])],
835        RangeSet::from([1..5]),
836        RangeSet::from([2..4, 8..10]),
837        RangeSet::from([1..5]),
838        RangeSet::default(),
839    )]
840    #[case::failed_to_cover_recv(
841        vec![RangeSet::from([1..5, 6..10])],
842        vec![RangeSet::from([2..4, 8..10])],
843        RangeSet::from([1..5, 6..10]),
844        RangeSet::from([2..4]),
845        RangeSet::default(),
846        RangeSet::from([2..4]),
847    )]
848    #[case::failed_to_cover_both(
849        vec![RangeSet::from([1..5, 6..10])],
850        vec![RangeSet::from([2..4, 8..10])],
851        RangeSet::from([1..5]),
852        RangeSet::from([2..4]),
853        RangeSet::from([1..5]),
854        RangeSet::from([2..4]),
855    )]
856    #[allow(clippy::single_range_in_vec_init)]
857    fn test_transcript_proof_builder(
858        #[case] commit_sent_rangesets: Vec<RangeSet<usize>>,
859        #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
860        #[case] reveal_sent_rangeset: RangeSet<usize>,
861        #[case] reveal_recv_rangeset: RangeSet<usize>,
862        #[case] uncovered_sent_rangeset: RangeSet<usize>,
863        #[case] uncovered_recv_rangeset: RangeSet<usize>,
864    ) {
865        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
866
867        // Encoding commitment kind
868        let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript);
869        for rangeset in commit_sent_rangesets.iter() {
870            transcript_commitment_builder.commit_sent(rangeset).unwrap();
871        }
872        for rangeset in commit_recv_rangesets.iter() {
873            transcript_commitment_builder.commit_recv(rangeset).unwrap();
874        }
875
876        let transcripts_commitment_config = transcript_commitment_builder.build().unwrap();
877
878        let encoding_tree = EncodingTree::new(
879            &Blake3::default(),
880            transcripts_commitment_config.iter_encoding(),
881            &encoding_provider(GET_WITH_HEADER, OK_JSON),
882        )
883        .unwrap();
884
885        let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
886        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
887        builder.reveal_sent(&reveal_sent_rangeset).unwrap();
888        builder.reveal_recv(&reveal_recv_rangeset).unwrap();
889
890        if uncovered_sent_rangeset.is_empty() && uncovered_recv_rangeset.is_empty() {
891            assert!(builder.build().is_ok());
892        } else {
893            let TranscriptProofBuilderError { kind, .. } = builder.build().unwrap_err();
894            match kind {
895                BuilderErrorKind::Cover { uncovered, .. } => {
896                    if !uncovered_sent_rangeset.is_empty() {
897                        assert_eq!(uncovered.sent, Idx(uncovered_sent_rangeset));
898                    }
899                    if !uncovered_recv_rangeset.is_empty() {
900                        assert_eq!(uncovered.recv, Idx(uncovered_recv_rangeset));
901                    }
902                }
903                _ => panic!("unexpected error kind: {kind:?}"),
904            }
905        }
906    }
907}