Skip to main content

commonware_cryptography/
transcript.rs

1//! This module provides a [Transcript] abstraction.
2//!
3//! This is useful for hashing data, committing to it, and extracting secure
4//! randomness from it. The API evades common footguns when doing these things
5//! in an ad hoc way.
6use crate::{BatchVerifier, Signer, Verifier};
7use blake3::BLOCK_LEN;
8use bytes::Buf;
9use commonware_codec::{
10    EncodeSize, FixedArray, FixedSize, Read, ReadExt, Write,
11    varint::{MAX_U64_VARINT_SIZE, UInt},
12};
13use commonware_math::algebra::Random;
14#[commonware_macros::stability(ALPHA)]
15use commonware_utils::NZU64;
16use commonware_utils::{Array, Span};
17#[commonware_macros::stability(ALPHA)]
18use core::num::NonZeroU64;
19use core::{convert::Infallible, fmt::Display, ops::Deref};
20use rand_core::{CryptoRng, TryCryptoRng, TryRng};
21use zeroize::ZeroizeOnDrop;
22
23/// Provides an implementation of [CryptoRng].
24///
25/// We intentionally don't expose this struct, to make the impl returned by
26/// [Transcript::noise] completely opaque.
27#[derive(ZeroizeOnDrop)]
28struct Rng {
29    inner: blake3::OutputReader,
30    buf: [u8; BLOCK_LEN],
31    start: usize,
32}
33
34impl Rng {
35    const fn new(inner: blake3::OutputReader) -> Self {
36        Self {
37            inner,
38            buf: [0u8; BLOCK_LEN],
39            start: BLOCK_LEN,
40        }
41    }
42}
43
44impl TryRng for Rng {
45    type Error = Infallible;
46
47    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
48        let mut bytes = [0u8; 4];
49        self.try_fill_bytes(&mut bytes)?;
50        Ok(u32::from_le_bytes(bytes))
51    }
52
53    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
54        let mut bytes = [0u8; 8];
55        self.try_fill_bytes(&mut bytes)?;
56        Ok(u64::from_le_bytes(bytes))
57    }
58
59    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
60        let dest_len = dest.len();
61        let remaining = &self.buf[self.start..];
62        if remaining.len() >= dest_len {
63            dest.copy_from_slice(&remaining[..dest_len]);
64            self.start += dest_len;
65            return Ok(());
66        }
67
68        let (start, mut dest) = dest.split_at_mut(remaining.len());
69        start.copy_from_slice(remaining);
70        self.start = BLOCK_LEN;
71
72        while dest.len() >= BLOCK_LEN {
73            let (block, rest) = dest.split_at_mut(BLOCK_LEN);
74            self.inner.fill(block);
75            dest = rest;
76        }
77
78        let dest_len = dest.len();
79        if dest_len > 0 {
80            self.inner.fill(&mut self.buf[..]);
81            dest.copy_from_slice(&self.buf[..dest_len]);
82            self.start = dest_len;
83        }
84
85        Ok(())
86    }
87}
88
89impl TryCryptoRng for Rng {}
90
91fn flush(hasher: &mut blake3::Hasher, pending: u64, version: Version) {
92    let mut pending_bytes = [0u8; MAX_U64_VARINT_SIZE];
93    let pending = UInt(pending);
94    pending.write(&mut &mut pending_bytes[..]);
95    let length = &mut pending_bytes[..pending.encode_size()];
96    version.frame_length(length);
97    hasher.update(length);
98}
99
100/// Domain-separates transcript construction operations.
101#[repr(u8)]
102#[derive(Clone, Copy)]
103enum StartTag {
104    New = 0,
105    Resume = 1,
106    Fork = 2,
107    Noise = 3,
108}
109
110/// The packet framing used by a [`Transcript`].
111///
112/// The version is an immutable part of a protocol's definition.
113/// [`Version::V0`] uses schema-dependent framing and requires the protocol's complete
114/// packet-history language to be uniquely decodable. [`Version::V1`] provides injective framing
115/// for arbitrary byte packets.
116#[non_exhaustive]
117#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
118pub enum Version {
119    /// Use schema-dependent suffix-length framing.
120    ///
121    /// V0 commits `data || varint(length(data))`. This suffix is ambiguous for unrestricted
122    /// packet histories because packet data can imitate an earlier packet's length.
123    ///
124    /// # Safety
125    ///
126    /// A protocol may use V0 only when its complete set of accepted histories is uniquely
127    /// decodable. One sufficient condition is:
128    ///
129    /// - the namespace is one fixed value;
130    /// - every history has a fixed packet count and order; and
131    /// - the payload language accepted at each position is prefix-free, such as one fixed-size
132    ///   value or a canonical self-delimiting encoding.
133    ///
134    /// A history containing only one arbitrary packet is also unambiguous because
135    /// `n + varint_size(n)` is strictly increasing. This is why a one-packet namespace may be
136    /// summarized before a fixed-schema protocol continues from the resulting summary.
137    ///
138    /// The proof applies to the complete packet schema, not to each payload in isolation.
139    /// Fixed-size encodings alone do not make optional, repeated, or reordered packets safe.
140    /// Changing a packet's encoding, when it may appear, or how often it may repeat requires
141    /// checking unique decodability for the full set of accepted histories again.
142    ///
143    /// This fixed schema is safe: every accepted history contains the same namespace, one 8-byte
144    /// round, and one 32-byte public key.
145    ///
146    /// ```
147    /// # use commonware_cryptography::transcript::{Summary, Transcript, Version};
148    /// fn summarize(round: u64, public_key: [u8; 32]) -> Summary {
149    ///     let round = round.to_be_bytes();
150    ///     Transcript::new(b"_COMMONWARE_CRYPTOGRAPHY_TRANSCRIPT_V0_FIXED", Version::V0)
151    ///         .commit(round.as_slice())
152    ///         .commit(public_key.as_slice())
153    ///         .summarize()
154    /// }
155    /// assert_ne!(summarize(7, [1; 32]), summarize(8, [1; 32]));
156    /// ```
157    ///
158    /// By contrast, unrestricted packet boundaries are unsafe. These distinct V0 histories commit
159    /// the same bytes.
160    ///
161    /// ```
162    /// # use commonware_cryptography::transcript::{Transcript, Version};
163    /// let zeros = [0u8; 127];
164    /// let split = Transcript::new(b"", Version::V0)
165    ///     .commit(zeros.as_slice())
166    ///     .commit([0x80].as_slice())
167    ///     .summarize();
168    ///
169    /// let mut merged = zeros.to_vec();
170    /// merged.push(0x7f);
171    /// let merged = Transcript::new(b"", Version::V0)
172    ///     .commit(merged.as_slice())
173    ///     .summarize();
174    ///
175    /// assert_eq!(split, merged);
176    /// ```
177    V0,
178    /// Use injective packet framing for arbitrary packet contents.
179    ///
180    /// V1 commits `data || reverse(varint(length(data)))`. Canonical varints are prefix-free, so
181    /// their reversals are suffix-free. Starting at the end of a history, the final length and then
182    /// its exact payload can be recovered repeatedly. Packet data cannot alter those boundaries.
183    /// Empty packets remain distinct from no packet, and [`Transcript::append`] retains constant
184    /// framing memory because only the pending length is stored.
185    ///
186    /// # Safety
187    ///
188    /// V1 is safe for arbitrary byte packets, variable packet lengths, optional or repeated
189    /// packets, and schemas that evolve to include them. It binds byte packets and their
190    /// boundaries; it cannot repair a non-injective application encoding where two semantic values
191    /// already produce the same packet bytes.
192    ///
193    /// The V0 collision above is separated under V1:
194    ///
195    /// ```
196    /// # use commonware_cryptography::transcript::{Transcript, Version};
197    /// let zeros = [0u8; 127];
198    /// let split = Transcript::new(b"", Version::V1)
199    ///     .commit(zeros.as_slice())
200    ///     .commit([0x80].as_slice())
201    ///     .summarize();
202    ///
203    /// let mut merged = zeros.to_vec();
204    /// merged.push(0x7f);
205    /// let merged = Transcript::new(b"", Version::V1)
206    ///     .commit(merged.as_slice())
207    ///     .summarize();
208    ///
209    /// assert_ne!(split, merged);
210    /// ```
211    V1,
212}
213
214impl Version {
215    /// Transform a canonical varint into the version's suffix framing.
216    const fn frame_length(self, length: &mut [u8]) {
217        match self {
218            Self::V0 => {}
219            Self::V1 => length.reverse(),
220        }
221    }
222}
223
224/// Provides a convenient abstraction over hashing data and deriving randomness.
225///
226/// It automatically takes care of details like:
227/// - segmenting packets according to the selected framing scheme,
228/// - domain separating different uses of tags and randomness,
229/// - making sure that secret state is zeroized as necessary.
230#[derive(ZeroizeOnDrop)]
231pub struct Transcript {
232    hasher: blake3::Hasher,
233    pending: u64,
234    #[zeroize(skip)]
235    version: Version,
236}
237
238impl Transcript {
239    fn start(tag: StartTag, summary: Option<Summary>, version: Version) -> Self {
240        // By starting with an optional key, we basically get to hash in 32 bytes
241        // for free, since they won't affect the number of bytes we can process without
242        // a call to the compression function. So, in many cases where we want to
243        // link a new transcript to a previous history, we take an optional summary.
244        let mut hasher = summary.map_or_else(blake3::Hasher::new, |s| {
245            blake3::Hasher::new_keyed(s.hash.as_bytes())
246        });
247        hasher.update(&[tag as u8]);
248        Self {
249            hasher,
250            pending: 0,
251            version,
252        }
253    }
254
255    fn flush(&mut self) {
256        flush(&mut self.hasher, self.pending, self.version);
257        self.pending = 0;
258    }
259
260    const fn unflushed(&self) -> bool {
261        self.pending != 0
262    }
263}
264
265impl Transcript {
266    /// Create a new transcript.
267    ///
268    /// The namespace serves to disambiguate two transcripts, so that even if they record
269    /// the same information, the results will be different:
270    /// ```
271    /// # use commonware_cryptography::transcript::{Transcript, Version};
272    /// let s1 = Transcript::new(b"n1", Version::V1).commit(b"A".as_slice()).summarize();
273    /// let s2 = Transcript::new(b"n2", Version::V1).commit(b"A".as_slice()).summarize();
274    /// assert_ne!(s1, s2);
275    /// ```
276    pub fn new(namespace: &[u8], version: Version) -> Self {
277        let mut out = Self::start(StartTag::New, None, version);
278        out.commit(namespace);
279        out
280    }
281
282    /// Start a transcript from a summary.
283    ///
284    /// Note that this will not produce the same result as if the transcript
285    /// were never summarized to begin with.
286    /// ```
287    /// # use commonware_cryptography::transcript::{Transcript, Version};
288    /// let s1 = Transcript::new(b"test", Version::V1).commit(b"A".as_slice()).summarize();
289    /// let s2 = Transcript::resume(s1.clone(), Version::V1).summarize();
290    /// assert_ne!(s1, s2);
291    /// ```
292    pub fn resume(summary: Summary, version: Version) -> Self {
293        Self::start(StartTag::Resume, Some(summary), version)
294    }
295
296    /// Record data in this transcript.
297    ///
298    /// Consecutive calls are treated as separate packets:
299    /// ```
300    /// # use commonware_cryptography::transcript::{Transcript, Version};
301    /// let s1 = Transcript::new(b"test", Version::V1).commit(b"A".as_slice()).commit(b"B".as_slice()).summarize();
302    /// let s2 = Transcript::new(b"test", Version::V1).commit(b"AB".as_slice()).summarize();
303    /// assert_ne!(s1, s2);
304    /// ```
305    ///
306    /// In particular, even a call with an empty string matters:
307    /// ```
308    /// # use commonware_cryptography::transcript::{Transcript, Version};
309    /// let s1 = Transcript::new(b"test", Version::V1).summarize();
310    /// let s2 = Transcript::new(b"testt", Version::V1).commit(b"".as_slice()).summarize();
311    /// assert_ne!(s1, s2);
312    /// ```
313    ///
314    /// If you want to provide data incrementally, use [Self::append].
315    pub fn commit(&mut self, data: impl Buf) -> &mut Self {
316        self.append(data);
317        self.flush();
318        self
319    }
320
321    /// Like [Self::commit], except that subsequent calls to [Self::append] or [Self::commit] are
322    /// considered part of the same message.
323    ///
324    /// [Self::commit] needs to be called before calling any other method, besides [Self::append],
325    /// in order to avoid having uncommitted data.
326    ///
327    /// The packet length is checked before any bytes are hashed. This method panics without
328    /// changing the transcript if the pending packet would exceed `u64::MAX` bytes.
329    ///
330    /// ```
331    /// # use commonware_cryptography::transcript::{Transcript, Version};
332    /// let s1 = Transcript::new(b"test", Version::V1).append(b"A".as_slice()).commit(b"B".as_slice()).summarize();
333    /// let s2 = Transcript::new(b"test", Version::V1).commit(b"AB".as_slice()).summarize();
334    /// assert_eq!(s1, s2);
335    /// ```
336    pub fn append(&mut self, mut data: impl Buf) -> &mut Self {
337        let length =
338            u64::try_from(data.remaining()).expect("transcript packet length does not fit in u64");
339        let pending = self
340            .pending
341            .checked_add(length)
342            .expect("transcript packet exceeds u64::MAX bytes");
343
344        while data.has_remaining() {
345            let chunk = data.chunk();
346            self.hasher.update(chunk);
347            data.advance(chunk.len());
348        }
349        self.pending = pending;
350        self
351    }
352
353    /// Create a new instance sharing the same history.
354    ///
355    /// This instance will commit to the same data, but it will produce a different
356    /// summary and noise:
357    /// ```
358    /// # use commonware_cryptography::transcript::{Transcript, Version};
359    /// let t = Transcript::new(b"test", Version::V1);
360    /// assert_ne!(t.summarize(), t.fork(b"A").summarize());
361    /// assert_ne!(t.fork(b"A").summarize(), t.fork(b"B").summarize());
362    /// ```
363    pub fn fork(&self, label: &'static [u8]) -> Self {
364        let mut out = Self::start(StartTag::Fork, Some(self.summarize()), self.version);
365        out.commit(label);
366        out
367    }
368
369    /// Pull out some noise from this transript.
370    ///
371    /// This noise will depend on all of the messages committed to the transcript
372    /// so far, and can be used as a secure source of randomness, for generating
373    /// keys, and other things.
374    ///
375    /// The label will also affect the noise. Changing the label will change
376    /// the stream of bytes generated.
377    pub fn noise(&self, label: &'static [u8]) -> impl CryptoRng + use<> {
378        let mut out = Self::start(StartTag::Noise, Some(self.summarize()), self.version);
379        out.commit(label);
380        Rng::new(out.hasher.finalize_xof())
381    }
382
383    /// Shuffle a slice deterministically, based on this transcript.
384    ///
385    /// The permutation will depend on all of the messages committed to the
386    /// transcript so far. This is a Fisher-Yates shuffle over [Transcript::noise].
387    ///
388    /// The label will also affect the permutation. Changing the label will
389    /// change the resulting order:
390    /// ```
391    /// # use commonware_cryptography::transcript::{Transcript, Version};
392    /// let t = Transcript::new(b"test", Version::V1);
393    /// let mut a = [0u32, 1, 2, 3, 4, 5, 6, 7];
394    /// let mut b = a;
395    /// t.shuffle(b"A", &mut a);
396    /// t.shuffle(b"B", &mut b);
397    /// assert_ne!(a, b);
398    /// ```
399    #[commonware_macros::stability(ALPHA)]
400    pub fn shuffle<T>(&self, label: &'static [u8], items: &mut [T]) {
401        let mut rng = self.noise(label);
402        for i in (1..items.len()).rev() {
403            let j = sample(&mut rng, NZU64!(i as u64 + 1));
404            items.swap(i, j as usize);
405        }
406    }
407
408    /// Sample a uniform value in `0..bound`, based on this transcript.
409    ///
410    /// The value is unbiased, and will depend on all of the messages committed
411    /// to the transcript so far. The label will also affect the value:
412    /// ```
413    /// # use commonware_cryptography::transcript::{Transcript, Version};
414    /// # use commonware_utils::NZU64;
415    /// let t = Transcript::new(b"test", Version::V1);
416    /// assert_eq!(t.sample(b"A", NZU64!(100)), t.sample(b"A", NZU64!(100)));
417    /// assert!(t.sample(b"A", NZU64!(100)) < 100);
418    /// ```
419    #[commonware_macros::stability(ALPHA)]
420    pub fn sample(&self, label: &'static [u8], bound: NonZeroU64) -> u64 {
421        sample(self.noise(label), bound)
422    }
423
424    /// Extract a compact summary from this transcript.
425    ///
426    /// This can be used to compare transcripts for equality:
427    /// ```
428    /// # use commonware_cryptography::transcript::{Transcript, Version};
429    /// let s1 = Transcript::new(b"test", Version::V1).commit(b"DATA".as_slice()).summarize();
430    /// let s2 = Transcript::new(b"test", Version::V1).commit(b"DATA".as_slice()).summarize();
431    /// assert_eq!(s1, s2);
432    /// ```
433    pub fn summarize(&self) -> Summary {
434        let hash = if self.unflushed() {
435            let mut hasher = self.hasher.clone();
436            flush(&mut hasher, self.pending, self.version);
437            hasher.finalize()
438        } else {
439            self.hasher.finalize()
440        };
441        Summary { hash }
442    }
443}
444
445/// Sample a uniform value in `0..bound` from an infallible RNG.
446#[commonware_macros::stability(ALPHA)]
447fn sample(mut rng: impl CryptoRng, bound: NonZeroU64) -> u64 {
448    let bound = bound.get();
449
450    // Accept only draws below the largest multiple of `bound`, so that the
451    // modulo is unbiased. Fewer than two draws are needed on average.
452    let zone = bound * (u64::MAX / bound);
453    loop {
454        let v = rng.next_u64();
455        if v < zone {
456            return v % bound;
457        }
458    }
459}
460
461// Utility methods which can be created using the other methods.
462impl Transcript {
463    /// Use a signer to create a signature over this transcript.
464    ///
465    /// Conceptually, this is the same as:
466    /// - signing the operations that have been performed on the transcript,
467    /// - or, equivalently, signing randomness or a summary extracted from the transcript.
468    pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
469        self.summarize().sign(s)
470    }
471
472    /// Verify a signature produced by [Transcript::sign].
473    pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
474        self.summarize().verify(v, sig)
475    }
476
477    /// Append a signature produced by [Transcript::sign] to a batch verifier.
478    pub fn add_to_batch<B: BatchVerifier>(
479        &self,
480        batch: &mut B,
481        public_key: &B::PublicKey,
482        signature: &<B::PublicKey as Verifier>::Signature,
483    ) -> bool {
484        self.summarize().add_to_batch(batch, public_key, signature)
485    }
486}
487
488impl Summary {
489    /// Use a signer to create a signature over this summary.
490    pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
491        // Note: We pass an empty namespace here, since the namespace may be included
492        // within the transcript summary already via `Transcript::new`.
493        s.sign(b"", self.as_ref())
494    }
495
496    /// Verify a signature produced by [Summary::sign].
497    pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
498        // Note: We pass an empty namespace here, since the namespace may be included
499        // within the transcript summary already via `Transcript::new`.
500        v.verify(b"", self.as_ref(), sig)
501    }
502
503    /// Append a signature produced by [Summary::sign] to a batch verifier.
504    pub fn add_to_batch<B: BatchVerifier>(
505        &self,
506        batch: &mut B,
507        public_key: &B::PublicKey,
508        signature: &<B::PublicKey as Verifier>::Signature,
509    ) -> bool {
510        // Note: We pass an empty namespace here, since the namespace may be included
511        // within the transcript summary already via `Transcript::new`.
512        batch.add(b"", self.as_ref(), public_key, signature)
513    }
514}
515
516/// Represents a summary of a transcript.
517///
518/// This is the primary way to compare two transcripts for equality.
519/// You can think of this as a hash over the transcript, providing a commitment
520/// to the data it recorded.
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FixedArray)]
522pub struct Summary {
523    hash: blake3::Hash,
524}
525
526impl FixedSize for Summary {
527    const SIZE: usize = blake3::OUT_LEN;
528}
529
530impl Write for Summary {
531    fn write(&self, buf: &mut impl bytes::BufMut) {
532        self.hash.as_bytes().write(buf)
533    }
534}
535
536impl Read for Summary {
537    type Cfg = ();
538
539    fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
540        Ok(Self {
541            hash: blake3::Hash::from_bytes(ReadExt::read(buf)?),
542        })
543    }
544}
545
546impl AsRef<[u8]> for Summary {
547    fn as_ref(&self) -> &[u8] {
548        self.hash.as_bytes().as_slice()
549    }
550}
551
552impl Deref for Summary {
553    type Target = [u8];
554
555    fn deref(&self) -> &Self::Target {
556        self.as_ref()
557    }
558}
559
560impl PartialOrd for Summary {
561    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
562        Some(self.cmp(other))
563    }
564}
565
566impl Ord for Summary {
567    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
568        self.as_ref().cmp(other.as_ref())
569    }
570}
571
572impl Display for Summary {
573    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
574        write!(f, "{}", commonware_formatting::Hex(self.as_ref()))
575    }
576}
577
578impl Span for Summary {}
579
580impl Array for Summary {}
581
582impl crate::Digest for Summary {
583    const EMPTY: Self = Self {
584        hash: blake3::Hash::from_bytes([0u8; blake3::OUT_LEN]),
585    };
586}
587
588impl Random for Summary {
589    fn random(mut rng: impl CryptoRng) -> Self {
590        let mut bytes = [0u8; blake3::OUT_LEN];
591        rng.fill_bytes(&mut bytes[..]);
592        Self {
593            hash: blake3::Hash::from_bytes(bytes),
594        }
595    }
596}
597
598#[cfg(any(test, feature = "arbitrary"))]
599impl arbitrary::Arbitrary<'_> for Summary {
600    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
601        let bytes: [u8; blake3::OUT_LEN] = u.arbitrary()?;
602        Ok(Self {
603            hash: blake3::Hash::from_bytes(bytes),
604        })
605    }
606}
607
608#[cfg(test)]
609mod test {
610    use super::*;
611    use crate::ed25519;
612    use commonware_codec::{DecodeExt as _, Encode};
613    use commonware_parallel::Sequential;
614    use commonware_utils::test_rng;
615    use rand_core::Rng;
616
617    const V0_VERSION: Version = Version::V0;
618
619    fn v0(namespace: &[u8]) -> Transcript {
620        Transcript::new(namespace, V0_VERSION)
621    }
622
623    fn v1(namespace: &[u8]) -> Transcript {
624        Transcript::new(namespace, Version::V1)
625    }
626
627    #[test]
628    fn test_namespace_affects_summary() {
629        let s1 = v0(b"Test-A").summarize();
630        let s2 = v0(b"Test-B").summarize();
631        assert_ne!(s1, s2);
632    }
633
634    #[test]
635    fn test_namespace_doesnt_leak_into_data() {
636        let s1 = v0(b"Test-A").summarize();
637        let s2 = v0(b"Test-").commit(b"".as_slice()).summarize();
638        assert_ne!(s1, s2);
639    }
640
641    #[test]
642    fn test_commit_separates_data() {
643        let s1 = v0(b"").commit(b"AB".as_slice()).summarize();
644        let s2 = v0(b"")
645            .commit(b"A".as_slice())
646            .commit(b"B".as_slice())
647            .summarize();
648        assert_ne!(s1, s2);
649    }
650
651    #[test]
652    fn test_v1_separates_adversarial_data() {
653        let zeros = [0u8; 127];
654        let split_v0 = v0(b"")
655            .commit(zeros.as_slice())
656            .commit([0x80].as_slice())
657            .summarize();
658
659        let mut merged = zeros.to_vec();
660        merged.push(0x7f);
661        let merged_v0 = v0(b"").commit(merged.as_slice()).summarize();
662        assert_eq!(split_v0, merged_v0);
663
664        let split_v1 = v1(b"")
665            .commit(zeros.as_slice())
666            .commit([0x80].as_slice())
667            .summarize();
668        let merged_v1 = v1(b"").commit(merged.as_slice()).summarize();
669
670        assert_ne!(split_v1, merged_v1);
671    }
672
673    #[test]
674    fn test_flush_supports_maximum_packet_length() {
675        for version in [V0_VERSION, Version::V1] {
676            let mut actual = blake3::Hasher::new();
677            flush(&mut actual, u64::MAX, version);
678
679            let mut length = [0u8; MAX_U64_VARINT_SIZE];
680            let pending = UInt(u64::MAX);
681            pending.write(&mut &mut length[..]);
682            let length = &mut length[..pending.encode_size()];
683            match version {
684                Version::V0 => {}
685                Version::V1 => length.reverse(),
686            }
687
688            let mut expected = blake3::Hasher::new();
689            expected.update(length);
690            assert_eq!(actual.finalize(), expected.finalize());
691        }
692    }
693
694    #[test]
695    fn test_start_tags() {
696        for (tag, expected) in [
697            (StartTag::New, 0),
698            (StartTag::Resume, 1),
699            (StartTag::Fork, 2),
700            (StartTag::Noise, 3),
701        ] {
702            assert_eq!(tag as u8, expected);
703        }
704    }
705
706    #[test]
707    fn test_versions_match_for_single_byte_lengths() {
708        let v0 = v0(b"test");
709        let v1 = v1(b"test");
710        assert_eq!(v0.summarize(), v1.summarize());
711        assert_eq!(v0.fork(b"fork").summarize(), v1.fork(b"fork").summarize());
712
713        let summary = v0.summarize();
714        let resumed_v0 = Transcript::resume(summary, Version::V0)
715            .commit(b"x".as_slice())
716            .summarize();
717        let resumed_v1 = Transcript::resume(summary, Version::V1)
718            .commit(b"x".as_slice())
719            .summarize();
720        assert_eq!(resumed_v0, resumed_v1);
721
722        let mut noise_v0 = [0u8; 32];
723        let mut noise_v1 = [0u8; 32];
724        v0.noise(b"noise").fill_bytes(&mut noise_v0);
725        v1.noise(b"noise").fill_bytes(&mut noise_v1);
726        assert_eq!(noise_v0, noise_v1);
727    }
728
729    #[test]
730    fn test_version_frames_derived_labels() {
731        const LONG_LABEL: &[u8] = &[0; 128];
732
733        let v0 = v0(b"test");
734        let v1 = v1(b"test");
735        assert_ne!(
736            v0.fork(LONG_LABEL).summarize(),
737            v1.fork(LONG_LABEL).summarize()
738        );
739
740        let mut noise_v0 = [0u8; 32];
741        let mut noise_v1 = [0u8; 32];
742        v0.noise(LONG_LABEL).fill_bytes(&mut noise_v0);
743        v1.noise(LONG_LABEL).fill_bytes(&mut noise_v1);
744        assert_ne!(noise_v0, noise_v1);
745    }
746
747    #[test]
748    fn test_append_overflow_does_not_hash() {
749        let mut transcript = Transcript::start(StartTag::New, None, Version::V1);
750        transcript.pending = u64::MAX - 1;
751        let before = transcript.hasher.finalize();
752
753        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
754            transcript.append(b"A".as_slice().chain(b"B".as_slice()));
755        }));
756
757        assert!(result.is_err());
758        assert_eq!(transcript.pending, u64::MAX - 1);
759        assert_eq!(transcript.hasher.finalize(), before);
760    }
761
762    #[test]
763    fn test_append_commit_works() {
764        let s1 = v0(b"")
765            .append(b"A".as_slice())
766            .commit(b"B".as_slice())
767            .summarize();
768        let s2 = v0(b"").commit(b"AB".as_slice()).summarize();
769        assert_eq!(s1, s2);
770    }
771
772    #[test]
773    fn test_fork_returns_different_result() {
774        let t1 = v0(b"");
775        let t2 = t1.fork(b"");
776        assert_ne!(t1.summarize(), t2.summarize());
777    }
778
779    #[test]
780    fn test_fork_label_matters() {
781        let t1 = v0(b"");
782        let t2 = t1.fork(b"A");
783        let t3 = t2.fork(b"B");
784        assert_ne!(t2.summarize(), t3.summarize());
785    }
786
787    #[test]
788    fn test_noise_and_summarize_are_different() {
789        let t1 = v0(b"");
790        let mut s1_bytes = [0u8; 32];
791        t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
792        let s1 = Summary {
793            hash: blake3::Hash::from_bytes(s1_bytes),
794        };
795        let s2 = t1.summarize();
796        assert_ne!(s1, s2);
797    }
798
799    #[test]
800    fn test_noise_stream_chunking_doesnt_matter() {
801        let mut s = [0u8; 2 * BLOCK_LEN];
802        v0(b"test").noise(b"NOISE").fill_bytes(&mut s[..]);
803        // Split up the bytes into two chunks
804        for i in 0..s.len() {
805            let mut s_prime = [0u8; 2 * BLOCK_LEN];
806            let mut noise = v0(b"test").noise(b"NOISE");
807            noise.fill_bytes(&mut s_prime[..i]);
808            noise.fill_bytes(&mut s_prime[i..]);
809            assert_eq!(s, s_prime);
810        }
811    }
812
813    #[test]
814    fn test_noise_label_matters() {
815        let mut s1 = [0u8; 32];
816        let mut s2 = [0u8; 32];
817        let t1 = v0(b"test");
818        t1.noise(b"A").fill_bytes(&mut s1);
819        t1.noise(b"B").fill_bytes(&mut s2);
820        assert_ne!(s1, s2);
821    }
822
823    #[test]
824    fn test_summarize_resume_is_different_than_new() {
825        let s = v0(b"test").summarize();
826        let s1 = v0(s.hash.as_bytes()).summarize();
827        let s2 = Transcript::resume(s, V0_VERSION).summarize();
828        assert_ne!(s1, s2);
829    }
830
831    #[test]
832    fn test_summary_encode_roundtrip() {
833        let s = v0(b"test").summarize();
834        assert_eq!(&s, &Summary::decode(s.encode()).unwrap());
835    }
836
837    #[test]
838    fn test_summary_sign_verify_matches_transcript() {
839        let sk = ed25519::PrivateKey::from_seed(7);
840        let pk = sk.public_key();
841        let mut transcript = v0(b"test");
842        transcript.commit(b"DATA".as_slice());
843        let summary = transcript.summarize();
844
845        let sig = summary.sign(&sk);
846        assert_eq!(sig, transcript.sign(&sk));
847        assert!(summary.verify(&pk, &sig));
848        assert!(transcript.verify(&pk, &sig));
849    }
850
851    #[test]
852    fn test_summary_add_to_batch_matches_transcript() {
853        let sk = ed25519::PrivateKey::from_seed(7);
854        let pk = sk.public_key();
855        let mut transcript = v0(b"test");
856        transcript.commit(b"DATA".as_slice());
857        let summary = transcript.summarize();
858        let sig = transcript.sign(&sk);
859
860        let mut summary_batch = ed25519::Batch::new(1);
861        assert!(summary.add_to_batch(&mut summary_batch, &pk, &sig));
862        let mut transcript_batch = ed25519::Batch::new(1);
863        assert!(transcript.add_to_batch(&mut transcript_batch, &pk, &sig));
864
865        assert!(summary_batch.verify(&mut test_rng(), &Sequential));
866        assert!(transcript_batch.verify(&mut test_rng(), &Sequential));
867    }
868
869    #[test]
870    fn test_shuffle_is_permutation() {
871        let t = v0(b"test");
872        let mut items: Vec<u32> = (0..1000).collect();
873        t.shuffle(b"shuffle", &mut items);
874        assert_ne!(items, (0..1000).collect::<Vec<_>>());
875        items.sort_unstable();
876        assert_eq!(items, (0..1000).collect::<Vec<_>>());
877    }
878
879    #[test]
880    fn test_shuffle_is_deterministic() {
881        let mut t = v0(b"test");
882        t.commit(b"DATA".as_slice());
883        let mut s1: Vec<u32> = (0..100).collect();
884        let mut s2 = s1.clone();
885        t.shuffle(b"shuffle", &mut s1);
886        t.shuffle(b"shuffle", &mut s2);
887        assert_eq!(s1, s2);
888    }
889
890    #[test]
891    fn test_shuffle_label_and_history_matter() {
892        let t1 = v0(b"test");
893        let mut t2 = v0(b"test");
894        t2.commit(b"DATA".as_slice());
895        let mut base: Vec<u32> = (0..100).collect();
896        let (mut a, mut b, mut c) = (base.clone(), base.clone(), base.clone());
897        t1.shuffle(b"A", &mut a);
898        t1.shuffle(b"B", &mut b);
899        t2.shuffle(b"A", &mut c);
900        base.clear();
901        assert_ne!(a, b);
902        assert_ne!(a, c);
903    }
904
905    #[test]
906    fn test_sample_within_bound() {
907        let t = v0(b"test");
908        let mut rng = t.noise(b"sample");
909        for bound in [1, 2, 3, 7, 100, 1 << 40, u64::MAX] {
910            assert!(sample(&mut rng, NZU64!(bound)) < bound);
911        }
912        assert_eq!(t.sample(b"sample", NZU64!(1)), 0);
913        assert_eq!(
914            t.sample(b"one shot", NZU64!(1000)),
915            sample(t.noise(b"one shot"), NZU64!(1000))
916        );
917    }
918
919    #[test]
920    fn test_missing_append() {
921        let s1 = v0(b"foo").append(b"AB".as_slice()).summarize();
922        let s2 = v0(b"foo")
923            .append(b"A".as_slice())
924            .commit(b"B".as_slice())
925            .summarize();
926        assert_eq!(s1, s2)
927    }
928
929    #[cfg(feature = "arbitrary")]
930    mod conformance {
931        use super::*;
932        use commonware_codec::conformance::CodecConformance;
933        use commonware_conformance::Conformance;
934
935        #[allow(clippy::unused_async)]
936        async fn transcript_ops(seed: u64, version: Version) -> Vec<u8> {
937            let seed_bytes = seed.to_le_bytes();
938            let namespace = seed_bytes[..(seed as usize % seed_bytes.len()) + 1].to_vec();
939            let data: Vec<_> = (0..seed as usize % 256)
940                .map(|i| (seed as u8).wrapping_add((3 * i) as u8))
941                .collect();
942            let split = data.len() / 2;
943
944            let mut transcript = Transcript::new(&namespace, version);
945            transcript.append(&data[..split]);
946            transcript.commit(&data[split..]);
947
948            let mut log = transcript.summarize().encode().to_vec();
949            log.extend(
950                Transcript::new(&namespace, version)
951                    .commit(&data[..split])
952                    .commit(&data[split..])
953                    .summarize()
954                    .encode(),
955            );
956            log.extend(
957                Transcript::new(&namespace, version)
958                    .append(data.as_slice())
959                    .commit([].as_slice())
960                    .summarize()
961                    .encode(),
962            );
963            let resumed = Transcript::resume(transcript.summarize(), version);
964            log.extend(resumed.summarize().encode());
965            log.extend(transcript.fork(b"left").summarize().encode());
966            log.extend(transcript.fork(b"right").summarize().encode());
967
968            let mut noise = [0u8; 80];
969            let mut rng = transcript.noise(b"noise");
970            log.extend(rng.next_u32().encode());
971            log.extend(rng.next_u64().encode());
972            rng.fill_bytes(&mut noise[..31]);
973            rng.fill_bytes(&mut noise[31..]);
974            log.extend(noise);
975
976            let mut indices: Vec<u32> = (0..(seed % 100) as u32).collect();
977            transcript.shuffle(b"shuffle", &mut indices);
978            for index in &indices {
979                log.extend(index.encode());
980            }
981            log.extend(transcript.sample(b"sample", NZU64!(seed | 1)).encode());
982
983            let private_key = ed25519::PrivateKey::from_seed(seed);
984            let public_key = private_key.public_key();
985            let summary = transcript.summarize();
986            let summary_sig = summary.sign(&private_key);
987            let transcript_sig = transcript.sign(&private_key);
988            log.extend(summary_sig.encode());
989            log.extend(transcript_sig.encode());
990            log.extend(summary.verify(&public_key, &summary_sig).encode());
991            log.extend(transcript.verify(&public_key, &transcript_sig).encode());
992
993            let mut summary_batch = ed25519::Batch::new(1);
994            log.extend(
995                summary
996                    .add_to_batch(&mut summary_batch, &public_key, &summary_sig)
997                    .encode(),
998            );
999            log.extend(
1000                summary_batch
1001                    .verify(&mut transcript.noise(b"summary batch"), &Sequential)
1002                    .encode(),
1003            );
1004
1005            let mut transcript_batch = ed25519::Batch::new(1);
1006            log.extend(
1007                transcript
1008                    .add_to_batch(&mut transcript_batch, &public_key, &transcript_sig)
1009                    .encode(),
1010            );
1011            log.extend(
1012                transcript_batch
1013                    .verify(&mut transcript.noise(b"transcript batch"), &Sequential)
1014                    .encode(),
1015            );
1016
1017            let mut pending = Transcript::new(&namespace, version);
1018            pending.append(data.as_slice());
1019            let pending_summary = pending.summarize();
1020            log.extend(pending_summary.encode());
1021            log.extend(pending.fork(b"pending fork").summarize().encode());
1022
1023            let mut pending_noise = [0u8; 37];
1024            pending
1025                .noise(b"pending noise")
1026                .fill_bytes(&mut pending_noise);
1027            log.extend(pending_noise);
1028
1029            let pending_sig = pending.sign(&private_key);
1030            log.extend(pending_sig.encode());
1031            log.extend(pending.verify(&public_key, &pending_sig).encode());
1032
1033            log
1034        }
1035
1036        struct TranscriptV0Ops;
1037
1038        impl Conformance for TranscriptV0Ops {
1039            async fn commit(seed: u64) -> Vec<u8> {
1040                transcript_ops(seed, V0_VERSION).await
1041            }
1042        }
1043
1044        struct TranscriptV1Ops;
1045
1046        impl Conformance for TranscriptV1Ops {
1047            async fn commit(seed: u64) -> Vec<u8> {
1048                transcript_ops(seed, Version::V1).await
1049            }
1050        }
1051
1052        commonware_conformance::conformance_tests! {
1053            TranscriptV0Ops => 4096,
1054            TranscriptV1Ops => 4096,
1055            CodecConformance<Summary>,
1056        }
1057    }
1058}