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