use crate::{BatchVerifier, Signer, Verifier};
use blake3::BLOCK_LEN;
use bytes::Buf;
use commonware_codec::{varint::UInt, EncodeSize, FixedArray, FixedSize, Read, ReadExt, Write};
use commonware_math::algebra::Random;
#[commonware_macros::stability(ALPHA)]
use commonware_utils::NZU64;
use commonware_utils::{Array, Span};
#[commonware_macros::stability(ALPHA)]
use core::num::NonZeroU64;
use core::{convert::Infallible, fmt::Display, ops::Deref};
use rand_core::{CryptoRng, TryCryptoRng, TryRng};
use zeroize::ZeroizeOnDrop;
#[derive(ZeroizeOnDrop)]
struct Rng {
inner: blake3::OutputReader,
buf: [u8; BLOCK_LEN],
start: usize,
}
impl Rng {
const fn new(inner: blake3::OutputReader) -> Self {
Self {
inner,
buf: [0u8; BLOCK_LEN],
start: BLOCK_LEN,
}
}
}
impl TryRng for Rng {
type Error = Infallible;
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
let mut bytes = [0u8; 4];
self.try_fill_bytes(&mut bytes)?;
Ok(u32::from_le_bytes(bytes))
}
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
let mut bytes = [0u8; 8];
self.try_fill_bytes(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
let dest_len = dest.len();
let remaining = &self.buf[self.start..];
if remaining.len() >= dest_len {
dest.copy_from_slice(&remaining[..dest_len]);
self.start += dest_len;
return Ok(());
}
let (start, mut dest) = dest.split_at_mut(remaining.len());
start.copy_from_slice(remaining);
self.start = BLOCK_LEN;
while dest.len() >= BLOCK_LEN {
let (block, rest) = dest.split_at_mut(BLOCK_LEN);
self.inner.fill(block);
dest = rest;
}
let dest_len = dest.len();
if dest_len > 0 {
self.inner.fill(&mut self.buf[..]);
dest.copy_from_slice(&self.buf[..dest_len]);
self.start = dest_len;
}
Ok(())
}
}
impl TryCryptoRng for Rng {}
fn flush(hasher: &mut blake3::Hasher, pending: u64) {
let mut pending_bytes = [0u8; 9];
let pending = UInt(pending);
pending.write(&mut &mut pending_bytes[..]);
hasher.update(&pending_bytes[..pending.encode_size()]);
}
#[repr(u8)]
enum StartTag {
New = 0,
Resume = 1,
Fork = 2,
Noise = 3,
}
#[derive(ZeroizeOnDrop)]
pub struct Transcript {
hasher: blake3::Hasher,
pending: u64,
}
impl Transcript {
fn start(tag: StartTag, summary: Option<Summary>) -> Self {
let mut hasher = summary.map_or_else(blake3::Hasher::new, |s| {
blake3::Hasher::new_keyed(s.hash.as_bytes())
});
hasher.update(&[tag as u8]);
Self { hasher, pending: 0 }
}
fn flush(&mut self) {
flush(&mut self.hasher, self.pending);
self.pending = 0;
}
fn do_append(&mut self, data: &[u8]) {
self.hasher.update(data);
self.pending += data.len() as u64;
}
const fn unflushed(&self) -> bool {
self.pending != 0
}
}
impl Transcript {
pub fn new(namespace: &[u8]) -> Self {
let mut out = Self::start(StartTag::New, None);
out.commit(namespace);
out
}
pub fn resume(summary: Summary) -> Self {
Self::start(StartTag::Resume, Some(summary))
}
pub fn commit(&mut self, data: impl Buf) -> &mut Self {
self.append(data);
self.flush();
self
}
pub fn append(&mut self, mut data: impl Buf) -> &mut Self {
while data.has_remaining() {
let chunk = data.chunk();
self.do_append(chunk);
data.advance(chunk.len());
}
self
}
pub fn fork(&self, label: &'static [u8]) -> Self {
let mut out = Self::start(StartTag::Fork, Some(self.summarize()));
out.commit(label);
out
}
pub fn noise(&self, label: &'static [u8]) -> impl CryptoRng {
let mut out = Self::start(StartTag::Noise, Some(self.summarize()));
out.commit(label);
Rng::new(out.hasher.finalize_xof())
}
#[commonware_macros::stability(ALPHA)]
pub fn shuffle<T>(&self, label: &'static [u8], items: &mut [T]) {
let mut rng = self.noise(label);
for i in (1..items.len()).rev() {
let j = sample(&mut rng, NZU64!(i as u64 + 1));
items.swap(i, j as usize);
}
}
#[commonware_macros::stability(ALPHA)]
pub fn sample(&self, label: &'static [u8], bound: NonZeroU64) -> u64 {
sample(self.noise(label), bound)
}
pub fn summarize(&self) -> Summary {
let hash = if self.unflushed() {
let mut hasher = self.hasher.clone();
flush(&mut hasher, self.pending);
hasher.finalize()
} else {
self.hasher.finalize()
};
Summary { hash }
}
}
#[commonware_macros::stability(ALPHA)]
fn sample(mut rng: impl CryptoRng, bound: NonZeroU64) -> u64 {
let bound = bound.get();
let zone = bound * (u64::MAX / bound);
loop {
let v = rng.next_u64();
if v < zone {
return v % bound;
}
}
}
impl Transcript {
pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
self.summarize().sign(s)
}
pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
self.summarize().verify(v, sig)
}
pub fn add_to_batch<B: BatchVerifier>(
&self,
batch: &mut B,
public_key: &B::PublicKey,
signature: &<B::PublicKey as Verifier>::Signature,
) -> bool {
self.summarize().add_to_batch(batch, public_key, signature)
}
}
impl Summary {
pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
s.sign(b"", self.as_ref())
}
pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
v.verify(b"", self.as_ref(), sig)
}
pub fn add_to_batch<B: BatchVerifier>(
&self,
batch: &mut B,
public_key: &B::PublicKey,
signature: &<B::PublicKey as Verifier>::Signature,
) -> bool {
batch.add(b"", self.as_ref(), public_key, signature)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FixedArray)]
pub struct Summary {
hash: blake3::Hash,
}
impl FixedSize for Summary {
const SIZE: usize = blake3::OUT_LEN;
}
impl Write for Summary {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.hash.as_bytes().write(buf)
}
}
impl Read for Summary {
type Cfg = ();
fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
Ok(Self {
hash: blake3::Hash::from_bytes(ReadExt::read(buf)?),
})
}
}
impl AsRef<[u8]> for Summary {
fn as_ref(&self) -> &[u8] {
self.hash.as_bytes().as_slice()
}
}
impl Deref for Summary {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl PartialOrd for Summary {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Summary {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.as_ref().cmp(other.as_ref())
}
}
impl Display for Summary {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", commonware_formatting::Hex(self.as_ref()))
}
}
impl Span for Summary {}
impl Array for Summary {}
impl crate::Digest for Summary {
const EMPTY: Self = Self {
hash: blake3::Hash::from_bytes([0u8; blake3::OUT_LEN]),
};
}
impl Random for Summary {
fn random(mut rng: impl CryptoRng) -> Self {
let mut bytes = [0u8; blake3::OUT_LEN];
rng.fill_bytes(&mut bytes[..]);
Self {
hash: blake3::Hash::from_bytes(bytes),
}
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl arbitrary::Arbitrary<'_> for Summary {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let bytes: [u8; blake3::OUT_LEN] = u.arbitrary()?;
Ok(Self {
hash: blake3::Hash::from_bytes(bytes),
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ed25519;
use commonware_codec::{DecodeExt as _, Encode};
use commonware_parallel::Sequential;
use commonware_utils::test_rng;
use rand_core::Rng;
#[test]
fn test_namespace_affects_summary() {
let s1 = Transcript::new(b"Test-A").summarize();
let s2 = Transcript::new(b"Test-B").summarize();
assert_ne!(s1, s2);
}
#[test]
fn test_namespace_doesnt_leak_into_data() {
let s1 = Transcript::new(b"Test-A").summarize();
let s2 = Transcript::new(b"Test-").commit(b"".as_slice()).summarize();
assert_ne!(s1, s2);
}
#[test]
fn test_commit_separates_data() {
let s1 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
let s2 = Transcript::new(b"")
.commit(b"A".as_slice())
.commit(b"B".as_slice())
.summarize();
assert_ne!(s1, s2);
}
#[test]
fn test_append_commit_works() {
let s1 = Transcript::new(b"")
.append(b"A".as_slice())
.commit(b"B".as_slice())
.summarize();
let s2 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
assert_eq!(s1, s2);
}
#[test]
fn test_fork_returns_different_result() {
let t1 = Transcript::new(b"");
let t2 = t1.fork(b"");
assert_ne!(t1.summarize(), t2.summarize());
}
#[test]
fn test_fork_label_matters() {
let t1 = Transcript::new(b"");
let t2 = t1.fork(b"A");
let t3 = t2.fork(b"B");
assert_ne!(t2.summarize(), t3.summarize());
}
#[test]
fn test_noise_and_summarize_are_different() {
let t1 = Transcript::new(b"");
let mut s1_bytes = [0u8; 32];
t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
let s1 = Summary {
hash: blake3::Hash::from_bytes(s1_bytes),
};
let s2 = t1.summarize();
assert_ne!(s1, s2);
}
#[test]
fn test_noise_stream_chunking_doesnt_matter() {
let mut s = [0u8; 2 * BLOCK_LEN];
Transcript::new(b"test")
.noise(b"NOISE")
.fill_bytes(&mut s[..]);
for i in 0..s.len() {
let mut s_prime = [0u8; 2 * BLOCK_LEN];
let mut noise = Transcript::new(b"test").noise(b"NOISE");
noise.fill_bytes(&mut s_prime[..i]);
noise.fill_bytes(&mut s_prime[i..]);
assert_eq!(s, s_prime);
}
}
#[test]
fn test_noise_label_matters() {
let mut s1 = [0u8; 32];
let mut s2 = [0u8; 32];
let t1 = Transcript::new(b"test");
t1.noise(b"A").fill_bytes(&mut s1);
t1.noise(b"B").fill_bytes(&mut s2);
assert_ne!(s1, s2);
}
#[test]
fn test_summarize_resume_is_different_than_new() {
let s = Transcript::new(b"test").summarize();
let s1 = Transcript::new(s.hash.as_bytes()).summarize();
let s2 = Transcript::resume(s).summarize();
assert_ne!(s1, s2);
}
#[test]
fn test_summary_encode_roundtrip() {
let s = Transcript::new(b"test").summarize();
assert_eq!(&s, &Summary::decode(s.encode()).unwrap());
}
#[test]
fn test_summary_sign_verify_matches_transcript() {
let sk = ed25519::PrivateKey::from_seed(7);
let pk = sk.public_key();
let mut transcript = Transcript::new(b"test");
transcript.commit(b"DATA".as_slice());
let summary = transcript.summarize();
let sig = summary.sign(&sk);
assert_eq!(sig, transcript.sign(&sk));
assert!(summary.verify(&pk, &sig));
assert!(transcript.verify(&pk, &sig));
}
#[test]
fn test_summary_add_to_batch_matches_transcript() {
let sk = ed25519::PrivateKey::from_seed(7);
let pk = sk.public_key();
let mut transcript = Transcript::new(b"test");
transcript.commit(b"DATA".as_slice());
let summary = transcript.summarize();
let sig = transcript.sign(&sk);
let mut summary_batch = ed25519::Batch::new(1);
assert!(summary.add_to_batch(&mut summary_batch, &pk, &sig));
let mut transcript_batch = ed25519::Batch::new(1);
assert!(transcript.add_to_batch(&mut transcript_batch, &pk, &sig));
assert!(summary_batch.verify(&mut test_rng(), &Sequential));
assert!(transcript_batch.verify(&mut test_rng(), &Sequential));
}
#[test]
fn test_shuffle_is_permutation() {
let t = Transcript::new(b"test");
let mut items: Vec<u32> = (0..1000).collect();
t.shuffle(b"shuffle", &mut items);
assert_ne!(items, (0..1000).collect::<Vec<_>>());
items.sort_unstable();
assert_eq!(items, (0..1000).collect::<Vec<_>>());
}
#[test]
fn test_shuffle_is_deterministic() {
let mut t = Transcript::new(b"test");
t.commit(b"DATA".as_slice());
let mut s1: Vec<u32> = (0..100).collect();
let mut s2 = s1.clone();
t.shuffle(b"shuffle", &mut s1);
t.shuffle(b"shuffle", &mut s2);
assert_eq!(s1, s2);
}
#[test]
fn test_shuffle_label_and_history_matter() {
let t1 = Transcript::new(b"test");
let mut t2 = Transcript::new(b"test");
t2.commit(b"DATA".as_slice());
let mut base: Vec<u32> = (0..100).collect();
let (mut a, mut b, mut c) = (base.clone(), base.clone(), base.clone());
t1.shuffle(b"A", &mut a);
t1.shuffle(b"B", &mut b);
t2.shuffle(b"A", &mut c);
base.clear();
assert_ne!(a, b);
assert_ne!(a, c);
}
#[test]
fn test_sample_within_bound() {
let t = Transcript::new(b"test");
let mut rng = t.noise(b"sample");
for bound in [1, 2, 3, 7, 100, 1 << 40, u64::MAX] {
assert!(sample(&mut rng, NZU64!(bound)) < bound);
}
assert_eq!(t.sample(b"sample", NZU64!(1)), 0);
assert_eq!(
t.sample(b"one shot", NZU64!(1000)),
sample(t.noise(b"one shot"), NZU64!(1000))
);
}
#[test]
fn test_missing_append() {
let s1 = Transcript::new(b"foo").append(b"AB".as_slice()).summarize();
let s2 = Transcript::new(b"foo")
.append(b"A".as_slice())
.commit(b"B".as_slice())
.summarize();
assert_eq!(s1, s2)
}
#[cfg(feature = "arbitrary")]
mod conformance {
use super::*;
use commonware_codec::conformance::CodecConformance;
use commonware_conformance::Conformance;
struct TranscriptOps;
impl Conformance for TranscriptOps {
async fn commit(seed: u64) -> Vec<u8> {
let seed_bytes = seed.to_le_bytes();
let namespace = seed_bytes[..(seed as usize % seed_bytes.len()) + 1].to_vec();
let data: Vec<_> = (0..seed as usize % 256)
.map(|i| (seed as u8).wrapping_add((3 * i) as u8))
.collect();
let split = data.len() / 2;
let mut transcript = Transcript::new(&namespace);
transcript.append(&data[..split]);
transcript.commit(&data[split..]);
let mut log = transcript.summarize().encode().to_vec();
log.extend(
Transcript::new(&namespace)
.commit(&data[..split])
.commit(&data[split..])
.summarize()
.encode(),
);
log.extend(
Transcript::new(&namespace)
.append(data.as_slice())
.commit([].as_slice())
.summarize()
.encode(),
);
let resumed = Transcript::resume(transcript.summarize());
log.extend(resumed.summarize().encode());
log.extend(transcript.fork(b"left").summarize().encode());
log.extend(transcript.fork(b"right").summarize().encode());
let mut noise = [0u8; 80];
let mut rng = transcript.noise(b"noise");
log.extend(rng.next_u32().encode());
log.extend(rng.next_u64().encode());
rng.fill_bytes(&mut noise[..31]);
rng.fill_bytes(&mut noise[31..]);
log.extend(noise);
let mut indices: Vec<u32> = (0..(seed % 100) as u32).collect();
transcript.shuffle(b"shuffle", &mut indices);
for index in &indices {
log.extend(index.encode());
}
log.extend(transcript.sample(b"sample", NZU64!(seed | 1)).encode());
let private_key = ed25519::PrivateKey::from_seed(seed);
let public_key = private_key.public_key();
let summary = transcript.summarize();
let summary_sig = summary.sign(&private_key);
let transcript_sig = transcript.sign(&private_key);
log.extend(summary_sig.encode());
log.extend(transcript_sig.encode());
log.extend(summary.verify(&public_key, &summary_sig).encode());
log.extend(transcript.verify(&public_key, &transcript_sig).encode());
let mut summary_batch = ed25519::Batch::new(1);
log.extend(
summary
.add_to_batch(&mut summary_batch, &public_key, &summary_sig)
.encode(),
);
log.extend(
summary_batch
.verify(&mut transcript.noise(b"summary batch"), &Sequential)
.encode(),
);
let mut transcript_batch = ed25519::Batch::new(1);
log.extend(
transcript
.add_to_batch(&mut transcript_batch, &public_key, &transcript_sig)
.encode(),
);
log.extend(
transcript_batch
.verify(&mut transcript.noise(b"transcript batch"), &Sequential)
.encode(),
);
let mut pending = Transcript::new(&namespace);
pending.append(data.as_slice());
let pending_summary = pending.summarize();
log.extend(pending_summary.encode());
log.extend(pending.fork(b"pending fork").summarize().encode());
let mut pending_noise = [0u8; 37];
pending
.noise(b"pending noise")
.fill_bytes(&mut pending_noise);
log.extend(pending_noise);
let pending_sig = pending.sign(&private_key);
log.extend(pending_sig.encode());
log.extend(pending.verify(&public_key, &pending_sig).encode());
log
}
}
commonware_conformance::conformance_tests! {
TranscriptOps => 4096,
CodecConformance<Summary>,
}
}
}