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