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