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