tlsn_core/transcript/
proof.rs

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