tlsn_core/transcript/
commit.rs

1//! Transcript commitments.
2
3use std::{collections::HashSet, fmt};
4
5use rangeset::ToRangeSet;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    hash::HashAlgId,
10    transcript::{Direction, Idx, Transcript},
11};
12
13/// The maximum allowed total bytelength of committed data for a single
14/// commitment kind. Used to prevent DoS during verification. (May cause the
15/// verifier to hash up to a max of 1GB * 128 = 128GB of data for certain kinds
16/// of encoding commitments.)
17///
18/// This value must not exceed bcs's MAX_SEQUENCE_LENGTH limit (which is (1 <<
19/// 31) - 1 by default)
20pub(crate) const MAX_TOTAL_COMMITTED_DATA: usize = 1_000_000_000;
21
22/// Kind of transcript commitment.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[non_exhaustive]
25pub enum TranscriptCommitmentKind {
26    /// A commitment to encodings of the transcript.
27    Encoding,
28    /// A hash commitment to plaintext in the transcript.
29    Hash {
30        /// The hash algorithm used.
31        alg: HashAlgId,
32    },
33}
34
35impl fmt::Display for TranscriptCommitmentKind {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::Encoding => f.write_str("encoding"),
39            Self::Hash { alg } => write!(f, "hash ({alg})"),
40        }
41    }
42}
43
44/// Configuration for transcript commitments.
45#[derive(Debug, Clone)]
46pub struct TranscriptCommitConfig {
47    encoding_hash_alg: HashAlgId,
48    commits: Vec<((Direction, Idx), TranscriptCommitmentKind)>,
49}
50
51impl TranscriptCommitConfig {
52    /// Creates a new commit config builder.
53    pub fn builder(transcript: &Transcript) -> TranscriptCommitConfigBuilder {
54        TranscriptCommitConfigBuilder::new(transcript)
55    }
56
57    /// Returns the hash algorithm to use for encoding commitments.
58    pub fn encoding_hash_alg(&self) -> &HashAlgId {
59        &self.encoding_hash_alg
60    }
61
62    /// Returns whether the configuration has any encoding commitments.
63    pub fn has_encoding(&self) -> bool {
64        self.commits
65            .iter()
66            .any(|(_, kind)| matches!(kind, TranscriptCommitmentKind::Encoding))
67    }
68
69    /// Returns an iterator over the encoding commitment indices.
70    pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, Idx)> {
71        self.commits.iter().filter_map(|(idx, kind)| match kind {
72            TranscriptCommitmentKind::Encoding => Some(idx),
73            _ => None,
74        })
75    }
76
77    /// Returns an iterator over the hash commitment indices.
78    pub fn iter_hash(&self) -> impl Iterator<Item = (&(Direction, Idx), &HashAlgId)> {
79        self.commits.iter().filter_map(|(idx, kind)| match kind {
80            TranscriptCommitmentKind::Hash { alg } => Some((idx, alg)),
81            _ => None,
82        })
83    }
84}
85
86/// A builder for [`TranscriptCommitConfig`].
87///
88/// The default hash algorithm is [`HashAlgId::BLAKE3`] and the default kind
89/// is [`TranscriptCommitmentKind::Encoding`].
90#[derive(Debug)]
91pub struct TranscriptCommitConfigBuilder<'a> {
92    transcript: &'a Transcript,
93    encoding_hash_alg: HashAlgId,
94    default_kind: TranscriptCommitmentKind,
95    commits: HashSet<((Direction, Idx), TranscriptCommitmentKind)>,
96}
97
98impl<'a> TranscriptCommitConfigBuilder<'a> {
99    /// Creates a new commit config builder.
100    pub fn new(transcript: &'a Transcript) -> Self {
101        Self {
102            transcript,
103            encoding_hash_alg: HashAlgId::BLAKE3,
104            default_kind: TranscriptCommitmentKind::Encoding,
105            commits: HashSet::default(),
106        }
107    }
108
109    /// Sets the hash algorithm to use for encoding commitments.
110    pub fn encoding_hash_alg(&mut self, alg: HashAlgId) -> &mut Self {
111        self.encoding_hash_alg = alg;
112        self
113    }
114
115    /// Sets the default kind of commitment to use.
116    pub fn default_kind(&mut self, default_kind: TranscriptCommitmentKind) -> &mut Self {
117        self.default_kind = default_kind;
118        self
119    }
120
121    /// Adds a commitment.
122    ///
123    /// # Arguments
124    ///
125    /// * `ranges` - The ranges of the commitment.
126    /// * `direction` - The direction of the transcript.
127    /// * `kind` - The kind of commitment.
128    pub fn commit_with_kind(
129        &mut self,
130        ranges: &dyn ToRangeSet<usize>,
131        direction: Direction,
132        kind: TranscriptCommitmentKind,
133    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
134        let idx = Idx::new(ranges.to_range_set());
135
136        if idx.end() > self.transcript.len_of_direction(direction) {
137            return Err(TranscriptCommitConfigBuilderError::new(
138                ErrorKind::Index,
139                format!(
140                    "range is out of bounds of the transcript ({}): {} > {}",
141                    direction,
142                    idx.end(),
143                    self.transcript.len_of_direction(direction)
144                ),
145            ));
146        }
147
148        self.commits.insert(((direction, idx), kind));
149
150        Ok(self)
151    }
152
153    /// Adds a commitment with the default kind.
154    ///
155    /// # Arguments
156    ///
157    /// * `ranges` - The ranges of the commitment.
158    /// * `direction` - The direction of the transcript.
159    pub fn commit(
160        &mut self,
161        ranges: &dyn ToRangeSet<usize>,
162        direction: Direction,
163    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
164        self.commit_with_kind(ranges, direction, self.default_kind)
165    }
166
167    /// Adds a commitment with the default kind to the sent data transcript.
168    ///
169    /// # Arguments
170    ///
171    /// * `ranges` - The ranges of the commitment.
172    pub fn commit_sent(
173        &mut self,
174        ranges: &dyn ToRangeSet<usize>,
175    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
176        self.commit(ranges, Direction::Sent)
177    }
178
179    /// Adds a commitment with the default kind to the received data transcript.
180    ///
181    /// # Arguments
182    ///
183    /// * `ranges` - The ranges of the commitment.
184    pub fn commit_recv(
185        &mut self,
186        ranges: &dyn ToRangeSet<usize>,
187    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
188        self.commit(ranges, Direction::Received)
189    }
190
191    /// Builds the configuration.
192    pub fn build(self) -> Result<TranscriptCommitConfig, TranscriptCommitConfigBuilderError> {
193        Ok(TranscriptCommitConfig {
194            encoding_hash_alg: self.encoding_hash_alg,
195            commits: Vec::from_iter(self.commits),
196        })
197    }
198}
199
200/// Error for [`TranscriptCommitConfigBuilder`].
201#[derive(Debug, thiserror::Error)]
202pub struct TranscriptCommitConfigBuilderError {
203    kind: ErrorKind,
204    source: Option<Box<dyn std::error::Error + Send + Sync>>,
205}
206
207impl TranscriptCommitConfigBuilderError {
208    fn new<E>(kind: ErrorKind, source: E) -> Self
209    where
210        E: Into<Box<dyn std::error::Error + Send + Sync>>,
211    {
212        Self {
213            kind,
214            source: Some(source.into()),
215        }
216    }
217}
218
219#[derive(Debug)]
220enum ErrorKind {
221    Index,
222}
223
224impl fmt::Display for TranscriptCommitConfigBuilderError {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        match self.kind {
227            ErrorKind::Index => f.write_str("index error")?,
228        }
229
230        if let Some(source) = &self.source {
231            write!(f, " caused by: {}", source)?;
232        }
233
234        Ok(())
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_range_out_of_bounds() {
244        let transcript = Transcript::new(
245            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
246            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
247        );
248        let mut builder = TranscriptCommitConfigBuilder::new(&transcript);
249
250        assert!(builder.commit_sent(&(10..15)).is_err());
251        assert!(builder.commit_recv(&(10..15)).is_err());
252    }
253}