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 {:?} commitments: {uncovered}",
550                kinds
551            ))?,
552            BuilderErrorKind::NotSupported => f.write_str("not supported")?,
553        }
554
555        if let Some(source) = &self.source {
556            write!(f, " caused by: {}", source)?;
557        }
558
559        Ok(())
560    }
561}
562
563#[allow(clippy::single_range_in_vec_init)]
564#[cfg(test)]
565mod tests {
566    use rand::{Rng, SeedableRng};
567    use rangeset::RangeSet;
568    use rstest::rstest;
569    use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
570
571    use crate::{
572        fixtures::{encoding_provider, request_fixture, ConnectionFixture, RequestFixture},
573        hash::{Blake3, Blinder, HashAlgId},
574        transcript::TranscriptCommitConfigBuilder,
575    };
576
577    use super::*;
578
579    #[rstest]
580    fn test_verify_missing_encoding_commitment_root() {
581        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
582        let connection = ConnectionFixture::tlsnotary(transcript.length());
583
584        let RequestFixture { encoding_tree, .. } = request_fixture(
585            transcript.clone(),
586            encoding_provider(GET_WITH_HEADER, OK_JSON),
587            connection.clone(),
588            Blake3::default(),
589            Vec::new(),
590        );
591
592        let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
593        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
594
595        builder.reveal_recv(&(0..transcript.len().1)).unwrap();
596
597        let transcript_proof = builder.build().unwrap();
598
599        let provider = CryptoProvider::default();
600        let err = transcript_proof
601            .verify_with_provider(&provider, &transcript.length(), &[])
602            .err()
603            .unwrap();
604
605        assert!(matches!(err.kind, ErrorKind::Encoding));
606    }
607
608    #[rstest]
609    fn test_reveal_range_out_of_bounds() {
610        let transcript = Transcript::new(
611            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
612            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
613        );
614        let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
615
616        let err = builder.reveal(&(10..15), Direction::Sent).unwrap_err();
617        assert!(matches!(err.kind, BuilderErrorKind::Index));
618
619        let err = builder
620            .reveal(&(10..15), Direction::Received)
621            .err()
622            .unwrap();
623        assert!(matches!(err.kind, BuilderErrorKind::Index));
624    }
625
626    #[rstest]
627    fn test_reveal_missing_encoding_tree() {
628        let transcript = Transcript::new(
629            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
630            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
631        );
632        let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
633
634        let err = builder.reveal_recv(&(9..11)).unwrap_err();
635        assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
636    }
637
638    #[rstest]
639    fn test_reveal_with_hash_commitment() {
640        let mut rng = rand::rngs::StdRng::seed_from_u64(0);
641        let provider = CryptoProvider::default();
642        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
643
644        let direction = Direction::Sent;
645        let idx = Idx::new(0..10);
646        let blinder: Blinder = rng.random();
647        let alg = HashAlgId::SHA256;
648        let hasher = provider.hash.get(&alg).unwrap();
649
650        let commitment = PlaintextHash {
651            direction,
652            idx: idx.clone(),
653            hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
654        };
655
656        let secret = PlaintextHashSecret {
657            direction,
658            idx: idx.clone(),
659            alg,
660            blinder,
661        };
662
663        let secrets = vec![TranscriptSecret::Hash(secret)];
664        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
665
666        builder.reveal_sent(&(0..10)).unwrap();
667
668        let transcript_proof = builder.build().unwrap();
669
670        let partial_transcript = transcript_proof
671            .verify_with_provider(
672                &provider,
673                &transcript.length(),
674                &[TranscriptCommitment::Hash(commitment)],
675            )
676            .unwrap();
677
678        assert_eq!(
679            partial_transcript.sent_unsafe()[0..10],
680            transcript.sent()[0..10]
681        );
682    }
683
684    #[rstest]
685    fn test_reveal_with_inconsistent_hash_commitment() {
686        let mut rng = rand::rngs::StdRng::seed_from_u64(0);
687        let provider = CryptoProvider::default();
688        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
689
690        let direction = Direction::Sent;
691        let idx = Idx::new(0..10);
692        let blinder: Blinder = rng.random();
693        let alg = HashAlgId::SHA256;
694        let hasher = provider.hash.get(&alg).unwrap();
695
696        let commitment = PlaintextHash {
697            direction,
698            idx: idx.clone(),
699            hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
700        };
701
702        let secret = PlaintextHashSecret {
703            direction,
704            idx: idx.clone(),
705            alg,
706            // Use a different blinder to create an inconsistent commitment
707            blinder: rng.random(),
708        };
709
710        let secrets = vec![TranscriptSecret::Hash(secret)];
711        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
712
713        builder.reveal_sent(&(0..10)).unwrap();
714
715        let transcript_proof = builder.build().unwrap();
716
717        let err = transcript_proof
718            .verify_with_provider(
719                &provider,
720                &transcript.length(),
721                &[TranscriptCommitment::Hash(commitment)],
722            )
723            .unwrap_err();
724
725        assert!(matches!(err.kind, ErrorKind::Hash));
726    }
727
728    #[rstest]
729    fn test_set_commitment_kinds_with_duplicates() {
730        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
731        let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
732        builder.commitment_kinds(&[
733            TranscriptCommitmentKind::Hash {
734                alg: HashAlgId::SHA256,
735            },
736            TranscriptCommitmentKind::Encoding,
737            TranscriptCommitmentKind::Hash {
738                alg: HashAlgId::SHA256,
739            },
740            TranscriptCommitmentKind::Hash {
741                alg: HashAlgId::SHA256,
742            },
743            TranscriptCommitmentKind::Encoding,
744        ]);
745
746        assert_eq!(
747            builder.commitment_kinds,
748            vec![
749                TranscriptCommitmentKind::Hash {
750                    alg: HashAlgId::SHA256
751                },
752                TranscriptCommitmentKind::Encoding
753            ]
754        );
755    }
756
757    #[rstest]
758    #[case::reveal_all_rangesets_with_exact_set(
759        vec![RangeSet::from([0..10]), RangeSet::from([12..30]), RangeSet::from([0..5, 15..30]), RangeSet::from([70..75, 85..100])],
760        RangeSet::from([0..10, 12..30]),
761        true,
762    )]
763    #[case::reveal_all_rangesets_with_superset_ranges(
764        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])],
765        RangeSet::from([0..4, 6..9]),
766        true,
767    )]
768    #[case::reveal_all_rangesets_with_superset_range(
769        vec![RangeSet::from([0..1, 2..4]), RangeSet::from([1..3]), RangeSet::from([1..9]), RangeSet::from([2..3])],
770        RangeSet::from([0..4]),
771        true,
772    )]
773    #[case::failed_to_reveal_with_superset_range_missing_within(
774        vec![RangeSet::from([0..20, 45..56]), RangeSet::from([80..120]), RangeSet::from([50..53])],
775        RangeSet::from([0..120]),
776        false,
777    )]
778    #[case::failed_to_reveal_with_superset_range_missing_outside(
779        vec![RangeSet::from([2..20, 45..116]), RangeSet::from([20..45]), RangeSet::from([50..53])],
780        RangeSet::from([0..120]),
781        false,
782    )]
783    #[case::failed_to_reveal_with_superset_ranges_missing_outside(
784        vec![RangeSet::from([1..10]), RangeSet::from([1..20]),  RangeSet::from([15..20, 75..110])],
785        RangeSet::from([0..41, 74..100]),
786        false,
787    )]
788    #[case::failed_to_reveal_as_no_subset_range(
789        vec![RangeSet::from([2..4]), RangeSet::from([1..2]), RangeSet::from([1..9]), RangeSet::from([2..3])],
790        RangeSet::from([0..1]),
791        false,
792    )]
793    #[allow(clippy::single_range_in_vec_init)]
794    fn test_reveal_mutliple_rangesets_with_one_rangeset(
795        #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
796        #[case] reveal_recv_rangeset: RangeSet<usize>,
797        #[case] success: bool,
798    ) {
799        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
800
801        // Encoding commitment kind
802        let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript);
803        for rangeset in commit_recv_rangesets.iter() {
804            transcript_commitment_builder.commit_recv(rangeset).unwrap();
805        }
806
807        let transcripts_commitment_config = transcript_commitment_builder.build().unwrap();
808
809        let encoding_tree = EncodingTree::new(
810            &Blake3::default(),
811            transcripts_commitment_config.iter_encoding(),
812            &encoding_provider(GET_WITH_HEADER, OK_JSON),
813        )
814        .unwrap();
815
816        let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
817        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
818
819        if success {
820            assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok());
821        } else {
822            let err = builder.reveal_recv(&reveal_recv_rangeset).unwrap_err();
823            assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
824        }
825    }
826
827    #[rstest]
828    #[case::cover(
829        vec![RangeSet::from([1..5, 6..10])],
830        vec![RangeSet::from([2..4, 8..10])],
831        RangeSet::from([1..5, 6..10]),
832        RangeSet::from([2..4, 8..10]),
833        RangeSet::default(),
834        RangeSet::default(),
835    )]
836    #[case::failed_to_cover_sent(
837        vec![RangeSet::from([1..5, 6..10])],
838        vec![RangeSet::from([2..4, 8..10])],
839        RangeSet::from([1..5]),
840        RangeSet::from([2..4, 8..10]),
841        RangeSet::from([1..5]),
842        RangeSet::default(),
843    )]
844    #[case::failed_to_cover_recv(
845        vec![RangeSet::from([1..5, 6..10])],
846        vec![RangeSet::from([2..4, 8..10])],
847        RangeSet::from([1..5, 6..10]),
848        RangeSet::from([2..4]),
849        RangeSet::default(),
850        RangeSet::from([2..4]),
851    )]
852    #[case::failed_to_cover_both(
853        vec![RangeSet::from([1..5, 6..10])],
854        vec![RangeSet::from([2..4, 8..10])],
855        RangeSet::from([1..5]),
856        RangeSet::from([2..4]),
857        RangeSet::from([1..5]),
858        RangeSet::from([2..4]),
859    )]
860    #[allow(clippy::single_range_in_vec_init)]
861    fn test_transcript_proof_builder(
862        #[case] commit_sent_rangesets: Vec<RangeSet<usize>>,
863        #[case] commit_recv_rangesets: Vec<RangeSet<usize>>,
864        #[case] reveal_sent_rangeset: RangeSet<usize>,
865        #[case] reveal_recv_rangeset: RangeSet<usize>,
866        #[case] uncovered_sent_rangeset: RangeSet<usize>,
867        #[case] uncovered_recv_rangeset: RangeSet<usize>,
868    ) {
869        let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
870
871        // Encoding commitment kind
872        let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript);
873        for rangeset in commit_sent_rangesets.iter() {
874            transcript_commitment_builder.commit_sent(rangeset).unwrap();
875        }
876        for rangeset in commit_recv_rangesets.iter() {
877            transcript_commitment_builder.commit_recv(rangeset).unwrap();
878        }
879
880        let transcripts_commitment_config = transcript_commitment_builder.build().unwrap();
881
882        let encoding_tree = EncodingTree::new(
883            &Blake3::default(),
884            transcripts_commitment_config.iter_encoding(),
885            &encoding_provider(GET_WITH_HEADER, OK_JSON),
886        )
887        .unwrap();
888
889        let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
890        let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
891        builder.reveal_sent(&reveal_sent_rangeset).unwrap();
892        builder.reveal_recv(&reveal_recv_rangeset).unwrap();
893
894        if uncovered_sent_rangeset.is_empty() && uncovered_recv_rangeset.is_empty() {
895            assert!(builder.build().is_ok());
896        } else {
897            let TranscriptProofBuilderError { kind, .. } = builder.build().unwrap_err();
898            match kind {
899                BuilderErrorKind::Cover { uncovered, .. } => {
900                    if !uncovered_sent_rangeset.is_empty() {
901                        assert_eq!(uncovered.sent, Idx(uncovered_sent_rangeset));
902                    }
903                    if !uncovered_recv_rangeset.is_empty() {
904                        assert_eq!(uncovered.recv, Idx(uncovered_recv_rangeset));
905                    }
906                }
907                _ => panic!("unexpected error kind: {:?}", kind),
908            }
909        }
910    }
911}