Skip to main content

tlsn_core/transcript/
commit.rs

1//! Transcript commitments.
2
3use std::{collections::HashSet, fmt};
4
5use rangeset::iter::{FromRangeIterator, IntoRangeIterator};
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    hash::HashAlgId,
10    transcript::{
11        hash::{PlaintextHash, PlaintextHashSecret},
12        Direction, RangeSet, Transcript,
13    },
14};
15
16/// Kind of transcript commitment.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[non_exhaustive]
19pub enum TranscriptCommitmentKind {
20    /// A hash commitment to plaintext in the transcript.
21    Hash {
22        /// The hash algorithm used.
23        alg: HashAlgId,
24    },
25}
26
27impl fmt::Display for TranscriptCommitmentKind {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::Hash { alg } => write!(f, "hash ({alg})"),
31        }
32    }
33}
34
35/// Transcript commitment.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[non_exhaustive]
38pub enum TranscriptCommitment {
39    /// Plaintext hash commitment.
40    Hash(PlaintextHash),
41}
42
43/// Secret for a transcript commitment.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[non_exhaustive]
46pub enum TranscriptSecret {
47    /// Plaintext hash secret.
48    Hash(PlaintextHashSecret),
49}
50
51/// Configuration for transcript commitments.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TranscriptCommitConfig {
54    commits: Vec<((Direction, RangeSet<usize>), TranscriptCommitmentKind)>,
55}
56
57impl TranscriptCommitConfig {
58    /// Creates a new commit config builder.
59    pub fn builder(transcript: &Transcript) -> TranscriptCommitConfigBuilder<'_> {
60        TranscriptCommitConfigBuilder::new(transcript)
61    }
62
63    /// Returns `true` if the configuration has any hash commitments.
64    pub fn has_hash(&self) -> bool {
65        self.commits
66            .iter()
67            .any(|(_, kind)| matches!(kind, TranscriptCommitmentKind::Hash { .. }))
68    }
69
70    /// Returns an iterator over the hash commitment indices.
71    pub fn iter_hash(&self) -> impl Iterator<Item = (&(Direction, RangeSet<usize>), &HashAlgId)> {
72        self.commits.iter().map(|(idx, kind)| match kind {
73            TranscriptCommitmentKind::Hash { alg } => (idx, alg),
74        })
75    }
76
77    /// Returns a request for the transcript commitments.
78    pub fn to_request(&self) -> TranscriptCommitRequest {
79        TranscriptCommitRequest {
80            hash: self
81                .iter_hash()
82                .map(|((dir, idx), alg)| (*dir, idx.clone(), *alg))
83                .collect(),
84        }
85    }
86}
87
88/// A builder for [`TranscriptCommitConfig`].
89#[derive(Debug)]
90pub struct TranscriptCommitConfigBuilder<'a> {
91    transcript: &'a Transcript,
92    default_kind: TranscriptCommitmentKind,
93    commits: HashSet<((Direction, RangeSet<usize>), TranscriptCommitmentKind)>,
94}
95
96impl<'a> TranscriptCommitConfigBuilder<'a> {
97    /// Creates a new commit config builder.
98    pub fn new(transcript: &'a Transcript) -> Self {
99        Self {
100            transcript,
101            default_kind: TranscriptCommitmentKind::Hash {
102                alg: HashAlgId::BLAKE3,
103            },
104            commits: HashSet::default(),
105        }
106    }
107
108    /// Sets the default kind of commitment to use.
109    pub fn default_kind(&mut self, default_kind: TranscriptCommitmentKind) -> &mut Self {
110        self.default_kind = default_kind;
111        self
112    }
113
114    /// Adds a commitment.
115    ///
116    /// # Arguments
117    ///
118    /// * `ranges` - The ranges of the commitment.
119    /// * `direction` - The direction of the transcript.
120    /// * `kind` - The kind of commitment.
121    pub fn commit_with_kind(
122        &mut self,
123        ranges: impl IntoRangeIterator<usize>,
124        direction: Direction,
125        kind: TranscriptCommitmentKind,
126    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
127        self.commit_with_kind_inner(RangeSet::from_range_iter(ranges), direction, kind)
128    }
129
130    fn commit_with_kind_inner(
131        &mut self,
132        idx: RangeSet<usize>,
133        direction: Direction,
134        kind: TranscriptCommitmentKind,
135    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
136        if idx.end().unwrap_or(0) > 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().unwrap_or(0),
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: impl IntoRangeIterator<usize>,
162        direction: Direction,
163    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
164        self.commit_with_kind_inner(
165            RangeSet::from_range_iter(ranges),
166            direction,
167            self.default_kind,
168        )
169    }
170
171    /// Adds a commitment with the default kind to the sent data transcript.
172    ///
173    /// # Arguments
174    ///
175    /// * `ranges` - The ranges of the commitment.
176    pub fn commit_sent(
177        &mut self,
178        ranges: impl IntoRangeIterator<usize>,
179    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
180        self.commit_with_kind_inner(
181            RangeSet::from_range_iter(ranges),
182            Direction::Sent,
183            self.default_kind,
184        )
185    }
186
187    /// Adds a commitment with the default kind to the received data transcript.
188    ///
189    /// # Arguments
190    ///
191    /// * `ranges` - The ranges of the commitment.
192    pub fn commit_recv(
193        &mut self,
194        ranges: impl IntoRangeIterator<usize>,
195    ) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
196        self.commit_with_kind_inner(
197            RangeSet::from_range_iter(ranges),
198            Direction::Received,
199            self.default_kind,
200        )
201    }
202
203    /// Builds the configuration.
204    pub fn build(self) -> Result<TranscriptCommitConfig, TranscriptCommitConfigBuilderError> {
205        Ok(TranscriptCommitConfig {
206            commits: Vec::from_iter(self.commits),
207        })
208    }
209}
210
211/// Error for [`TranscriptCommitConfigBuilder`].
212#[derive(Debug, thiserror::Error)]
213pub struct TranscriptCommitConfigBuilderError {
214    kind: ErrorKind,
215    source: Option<Box<dyn std::error::Error + Send + Sync>>,
216}
217
218impl TranscriptCommitConfigBuilderError {
219    fn new<E>(kind: ErrorKind, source: E) -> Self
220    where
221        E: Into<Box<dyn std::error::Error + Send + Sync>>,
222    {
223        Self {
224            kind,
225            source: Some(source.into()),
226        }
227    }
228}
229
230#[derive(Debug)]
231enum ErrorKind {
232    Index,
233}
234
235impl fmt::Display for TranscriptCommitConfigBuilderError {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        match self.kind {
238            ErrorKind::Index => f.write_str("index error")?,
239        }
240
241        if let Some(source) = &self.source {
242            write!(f, " caused by: {source}")?;
243        }
244
245        Ok(())
246    }
247}
248
249/// Request to compute transcript commitments.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct TranscriptCommitRequest {
252    hash: Vec<(Direction, RangeSet<usize>, HashAlgId)>,
253}
254
255impl TranscriptCommitRequest {
256    /// Returns `true` if a hash commitment is requested.
257    pub fn has_hash(&self) -> bool {
258        !self.hash.is_empty()
259    }
260
261    /// Returns an iterator over the hash commitments.
262    pub fn iter_hash(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>, HashAlgId)> {
263        self.hash.iter()
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_range_out_of_bounds() {
273        let transcript = Transcript::new(
274            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
275            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
276        );
277        let mut builder = TranscriptCommitConfigBuilder::new(&transcript);
278
279        assert!(builder.commit_sent(&(10..15)).is_err());
280        assert!(builder.commit_recv(&(10..15)).is_err());
281    }
282}