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