# proof.rs - source
Source of the Rust file `crates/core/src/transcript/proof.rs`.

Skip to main content

#
tlsn_core/transcript/
proof.rs

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