use super::{Error, Signature, VerificationKey, VerificationKeyBytes};
use crate::transcript::{Summary, Transcript, Version};
use ahash::RandomState;
#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
use commonware_codec::{Encode, EncodeSize, Write, varint::UInt};
use commonware_math::algebra::Random;
use commonware_parallel::Strategy;
use commonware_utils::Widen;
use core::{iter::once, num::NonZeroUsize};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_POINT as B,
edwards::{CompressedEdwardsY, EdwardsPoint},
scalar::Scalar,
traits::{IsIdentity, VartimeMultiscalarMul},
};
use hashbrown::HashMap;
use rand_core::CryptoRng;
use sha2::{Sha512, digest::Update};
const NOISE_BATCH_VERIFY: &[u8] = b"batch_verify";
fn gen_u128<R: CryptoRng>(mut rng: R) -> u128 {
let mut bytes = [0u8; 16];
rng.fill_bytes(&mut bytes[..]);
u128::from_le_bytes(bytes)
}
pub fn verify_projected<'a, R, T, F>(
rng: &mut R,
items: &'a [T],
project: F,
strategy: &impl Strategy,
) -> Result<(), Error>
where
R: CryptoRng,
T: Sync,
F: Fn(usize, &'a T) -> (&'a VerificationKey, Signature, Option<&'a [u8]>, &'a [u8]) + Sync,
{
if items.is_empty() {
return Err(Error::InvalidSignature);
}
let total = items.len();
let seed = Summary::random(&mut *rng);
strategy.try_run_batches(total, NonZeroUsize::MIN, 1, |batches| {
if batches.is_whole() {
return verify_shard(
items.iter().enumerate().map(|(i, item)| project(i, item)),
total,
seed,
0,
);
}
let order = partition(items, |i, item| project(i, item).0);
batches
.try_map_collect_vec(
|ranges| ranges.into_iter().enumerate(),
|(batch, range)| {
let shard = &order[range];
verify_shard(
shard.iter().map(|&idx| project(idx, &items[idx])),
shard.len(),
seed,
Widen::widen(batch),
)
},
)
.map(|_| ())
})
}
fn partition<'a, T>(
signatures: &'a [T],
key: impl Fn(usize, &'a T) -> &'a VerificationKey,
) -> Vec<usize> {
let mut counts = [0; 256];
for (i, item) in signatures.iter().enumerate() {
let vk = key(i, item);
counts[vk.as_bytes()[0] as usize] += 1;
}
let mut offsets = [0; 256];
let mut acc = 0;
for (offset, count) in offsets.iter_mut().zip(counts) {
*offset = acc;
acc += count;
}
let mut order = vec![0; signatures.len()];
for (i, item) in signatures.iter().enumerate() {
let vk = key(i, item);
let bucket = vk.as_bytes()[0] as usize;
order[offsets[bucket]] = i;
offsets[bucket] += 1;
}
order
}
#[allow(non_snake_case)]
fn verify_shard<'a>(
items: impl Iterator<Item = (&'a VerificationKey, Signature, Option<&'a [u8]>, &'a [u8])>,
n: usize,
seed: Summary,
batch: u64,
) -> Result<(), Error> {
let mut rng = Transcript::resume(seed, Version::V1)
.commit(batch.encode())
.noise(NOISE_BATCH_VERIFY);
let mut key_indices: HashMap<&VerificationKeyBytes, usize, RandomState> =
HashMap::with_capacity_and_hasher(n, RandomState::default());
let mut A_terms: Vec<(Scalar, &EdwardsPoint)> = Vec::with_capacity(n);
let mut R_coeffs = Vec::with_capacity(n);
let mut Rs = Vec::with_capacity(n);
let mut B_coeff = Scalar::ZERO;
for (vk, sig, namespace, message) in items {
let mut hash = Sha512::default()
.chain(&sig.R_bytes[..])
.chain(vk.as_bytes());
if let Some(namespace) = namespace {
let len = UInt(u32::try_from(namespace.len()).map_err(|_| Error::InvalidSignature)?);
let mut prefix = [0u8; 5];
len.write(&mut prefix.as_mut_slice());
hash.update(&prefix[..len.encode_size()]);
hash.update(namespace);
}
hash.update(message);
let k = Scalar::from_hash(hash);
let R = CompressedEdwardsY(sig.R_bytes)
.decompress()
.ok_or(Error::InvalidSignature)?;
let s = Scalar::from_canonical_bytes(sig.s_bytes)
.into_option()
.ok_or(Error::InvalidSignature)?;
let z = Scalar::from(gen_u128(&mut rng));
B_coeff -= z * s;
Rs.push(R);
R_coeffs.push(z);
let index = *key_indices.entry(&vk.A_bytes).or_insert_with(|| {
A_terms.push((Scalar::ZERO, &vk.minus_A));
A_terms.len() - 1
});
A_terms[index].0 += z * k;
}
let check = EdwardsPoint::vartime_multiscalar_mul(
once(&B_coeff)
.chain(A_terms.iter().map(|(coeff, _)| coeff))
.chain(R_coeffs.iter()),
once(B)
.chain(A_terms.iter().map(|(_, point)| -*point))
.chain(Rs.iter().copied()),
);
if check.mul_by_cofactor().is_identity() {
Ok(())
} else {
Err(Error::InvalidSignature)
}
}
#[cfg(test)]
mod tests {
use super::{super::SigningKey, *};
use commonware_parallel::mocks::explore;
use commonware_utils::test_rng;
use rand::RngExt as _;
fn signatures(
signers: usize,
per_signer: usize,
) -> Vec<(VerificationKey, Signature, [u8; 32])> {
let mut rng = test_rng();
let mut items = Vec::with_capacity(signers * per_signer);
for _ in 0..signers {
let sk = SigningKey::new(&mut rng);
let vk = sk.verification_key();
for _ in 0..per_signer {
let mut msg = [0u8; 32];
rng.fill(&mut msg);
items.push((vk, sk.sign(&msg), msg));
}
}
items
}
fn verify(items: &[(VerificationKey, Signature, [u8; 32])]) -> bool {
explore(|strategy| {
verify_projected(
&mut test_rng(),
items,
|_, (vk, sig, message)| (vk, *sig, None, message.as_slice()),
strategy,
)
.is_ok()
})
}
#[test]
fn test_verify_deferred_hashing() {
let mut items = signatures(4, 3);
assert!(verify(&items));
items[7].2[0] ^= 1;
assert!(!verify(&items));
}
#[test]
fn test_verify_interleaved_duplicate_keys() {
let grouped = signatures(2, 6);
let mut items = Vec::with_capacity(grouped.len());
for i in 0..6 {
items.push(grouped[i]);
items.push(grouped[6 + i]);
}
assert!(verify(&items));
items[5].2[0] ^= 1;
assert!(!verify(&items));
}
#[test]
fn test_verify_rejects_each_altered_item_in_every_shape() {
for signers in 1..=3 {
for per_signer in 1..=3 {
let valid = signatures(signers, per_signer);
assert!(verify(&valid));
for altered in 0..valid.len() {
let mut items = valid.clone();
items[altered].2[0] ^= 1;
let total = items.len();
assert!(
!verify(&items),
"verification must reject altered item {altered} of {total}"
);
}
}
}
}
}