tlsn_core/transcript/
proof.rs

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