use core::{hash::Hash, fmt::Debug, future::Future, num::NonZero};
use crate::{BlockNumber, RoundNumber};
pub trait Validator: Clone + Copy + PartialEq + Eq + Hash + Debug {}
impl<V: Clone + Copy + PartialEq + Eq + Hash + Debug> Validator for V {}
pub trait Signature: Clone + AsRef<[u8]> {}
impl<S: Clone + AsRef<[u8]>> Signature for S {}
pub trait AggregateSignature: Clone + AsRef<[u8]> {}
impl<S: Clone + AsRef<[u8]>> AggregateSignature for S {}
#[derive(Debug)]
pub struct InvalidAggregateSignature;
pub trait Signer {
type Validator: Validator;
type Signature: Signature;
fn validator(&self) -> Self::Validator;
type SignFuture: Future<Output = Self::Signature>;
fn sign(&self, message: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Self::SignFuture;
}
impl<S: ?Sized + Signer> Signer for &S {
type Validator = S::Validator;
type Signature = S::Signature;
fn validator(&self) -> Self::Validator {
S::validator(self)
}
type SignFuture = S::SignFuture;
fn sign(&self, message: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Self::SignFuture {
S::sign(self, message)
}
}
pub trait SignatureScheme {
type Validator: Validator;
type Signature: Signature;
type AggregateSignature: AggregateSignature;
#[must_use]
fn verify(
&self,
validator: &Self::Validator,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
signature: &Self::Signature,
) -> bool;
#[must_use]
fn aggregate<'sig>(
&self,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
signatures: impl IntoIterator<Item = (&'sig Self::Validator, &'sig Self::Signature)>,
) -> Self::AggregateSignature
where
Self::Validator: 'sig,
Self::Signature: 'sig;
fn verify_aggregate(
&self,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
aggregate_signature: &Self::AggregateSignature,
) -> Result<impl IntoIterator<Item = Self::Validator>, InvalidAggregateSignature>;
}
impl<S: ?Sized + SignatureScheme> SignatureScheme for &S {
type Validator = S::Validator;
type Signature = S::Signature;
type AggregateSignature = S::AggregateSignature;
fn verify(
&self,
validator: &Self::Validator,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
signature: &Self::Signature,
) -> bool {
S::verify(self, validator, message, signature)
}
fn aggregate<'sig>(
&self,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
signatures: impl IntoIterator<Item = (&'sig Self::Validator, &'sig Self::Signature)>,
) -> Self::AggregateSignature
where
Self::Validator: 'sig,
Self::Signature: 'sig,
{
S::aggregate(self, message, signatures)
}
fn verify_aggregate(
&self,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
aggregate_signature: &Self::AggregateSignature,
) -> Result<impl IntoIterator<Item = Self::Validator>, InvalidAggregateSignature> {
S::verify_aggregate(self, message, aggregate_signature)
}
}
pub trait ValidatorSet {
type Validator: Validator;
fn total_weight(&self) -> NonZero<u16>;
fn validators(&self) -> impl IntoIterator<Item = &Self::Validator>;
fn weight(&self, validator: &Self::Validator) -> Option<NonZero<u16>>;
fn threshold(&self) -> u16 {
u16::try_from(((u32::from(u16::from(self.total_weight())) * 2) / 3) + 1)
.expect("threshold is less than or equal to the total weight, which is a `u16`")
}
fn fault_threshold(&self) -> u16 {
u16::from(self.total_weight()) - self.threshold()
}
fn proposer(&self, block_number: BlockNumber, round_number: RoundNumber) -> Self::Validator;
}
impl<V: ?Sized + ValidatorSet> ValidatorSet for &V {
type Validator = V::Validator;
fn total_weight(&self) -> NonZero<u16> {
V::total_weight(self)
}
fn validators(&self) -> impl IntoIterator<Item = &Self::Validator> {
V::validators(self)
}
fn weight(&self, validator: &Self::Validator) -> Option<NonZero<u16>> {
V::weight(self, validator)
}
fn proposer(&self, block_number: BlockNumber, round_number: RoundNumber) -> Self::Validator {
V::proposer(self, block_number, round_number)
}
}
macro_rules! map {
($feature: literal, $map: path, $($generics: tt)*) => {
#[cfg(feature = $feature)]
impl<$($generics)*> ValidatorSet for $map {
type Validator = V;
fn total_weight(&self) -> NonZero<u16> {
NonZero::new(
self
.values()
.try_fold(0u16, |accum, value| accum.checked_add(u16::from(*value)))
.unwrap(),
)
.unwrap()
}
fn validators(&self) -> impl IntoIterator<Item = &Self::Validator> {
self.keys()
}
fn weight(&self, validator: &Self::Validator) -> Option<NonZero<u16>> {
self.get(validator).copied()
}
fn proposer(&self, block_number: BlockNumber, round_number: RoundNumber) -> Self::Validator {
const {
assert!(usize::BITS > 16);
}
use alloc::vec::Vec;
let total_weight = u16::from(self.total_weight());
let i = usize::from({
let block_i = (u64::from(block_number) - 1) % u64::from(total_weight);
let round_i = (u64::from(round_number) - 1) % u64::from(total_weight);
let i = (block_i + round_i) % u64::from(total_weight);
u16::try_from(i).expect(
"`i` indexes a validator by weight, where the weight is representable in a `u16`",
)
});
let mut all_keys = Vec::with_capacity((i + 2).min(usize::from(total_weight)));
for (key, value) in self {
let mut insert_at = 0;
for weight_i in 0 .. u16::from(*value) {
match all_keys[insert_at ..].binary_search_by(
|(existing_weight, existing_key): &(u16, &V)| {
existing_weight.cmp(&weight_i).then((*existing_key).cmp(key))
},
) {
Ok(_index) => {
panic!("`binary_search_by` located element despite not being already present")
}
Err(insert_at_in_slice) => insert_at += insert_at_in_slice,
}
if insert_at > i {
break;
}
all_keys.insert(insert_at, (weight_i, key));
insert_at += 1;
all_keys.truncate(i + 1);
}
}
*all_keys[i].1
}
}
};
}
#[cfg(not(target_pointer_width = "16"))]
map!(
"alloc",
alloc::collections::BTreeMap<V, NonZero<u16>>,
V: PartialOrd + Ord + Validator
);
#[cfg(not(target_pointer_width = "16"))]
map!(
"std",
std::collections::HashMap<V, NonZero<u16>, H>,
V: PartialOrd + Ord + Validator, H: core::hash::BuildHasher
);
#[cfg(test)]
mod tests {
use super::*;
pub(crate) struct TestSignatureScheme(u64, u64);
impl TestSignatureScheme {
pub(crate) fn new() -> Self {
use rand_core::{TryRngCore as _, OsRng};
Self(OsRng.try_next_u64().unwrap(), OsRng.try_next_u64().unwrap())
}
}
impl TestSignatureScheme {
fn hash(&self, message: impl IntoIterator<Item = impl AsRef<[u8]>>) -> [u8; 8] {
#[expect(deprecated)]
use core::hash::{Hasher as _, SipHasher};
#[expect(deprecated)]
let mut hasher = SipHasher::new_with_keys(self.0, self.1);
for chunk in message {
for byte in chunk.as_ref() {
hasher.write_u8(*byte);
}
}
hasher.finish().to_le_bytes()
}
}
impl SignatureScheme for TestSignatureScheme {
type Validator = u8;
type Signature = [u8; 1 + 8];
type AggregateSignature = [u8; 32 + 8];
fn verify(
&self,
validator: &<Self as SignatureScheme>::Validator,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
signature: &<Self as SignatureScheme>::Signature,
) -> bool {
(validator == &signature[0]) && (self.hash(message) == signature[1 ..])
}
fn aggregate<'sig>(
&self,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
signatures: impl IntoIterator<
Item = (
&'sig <Self as SignatureScheme>::Validator,
&'sig <Self as SignatureScheme>::Signature,
),
>,
) -> <Self as SignatureScheme>::AggregateSignature
where
<Self as SignatureScheme>::Validator: 'sig,
<Self as SignatureScheme>::Signature: 'sig,
{
let hash = self.hash(message);
let mut aggregate_signature = [0; 32 + 8];
for (validator, signature) in signatures {
assert!((validator == &signature[0]) && (hash == signature[1 ..]));
aggregate_signature[usize::from(validator / 8)] |= 1 << (validator % 8);
}
aggregate_signature[32 ..].copy_from_slice(&hash);
aggregate_signature
}
fn verify_aggregate(
&self,
message: impl IntoIterator<Item = impl AsRef<[u8]>>,
aggregate_signature: &<Self as SignatureScheme>::AggregateSignature,
) -> Result<
impl IntoIterator<Item = <Self as SignatureScheme>::Validator>,
InvalidAggregateSignature,
> {
let hash = self.hash(message);
if aggregate_signature[32 ..] != hash {
Err(InvalidAggregateSignature)?;
}
Ok(
aggregate_signature[.. 32]
.iter()
.enumerate()
.flat_map(|(i, b)| {
let i = 8 * u8::try_from(i).unwrap();
core::array::from_fn::<Option<u8>, 8, _>(|j| {
((b & (1 << (j % 8))) != 0)
.then_some(i.checked_add(u8::try_from(j).unwrap()).unwrap())
})
})
.flatten(),
)
}
}
pub(crate) struct TestSigner {
signature_scheme: TestSignatureScheme,
validator: u8,
}
impl TestSignatureScheme {
pub(crate) fn signer(&self, validator: u8) -> TestSigner {
TestSigner { signature_scheme: TestSignatureScheme(self.0, self.1), validator }
}
}
impl Signer for TestSigner {
type Validator = u8;
type Signature = [u8; 1 + 8];
fn validator(&self) -> <Self as Signer>::Validator {
self.validator
}
type SignFuture = core::future::Ready<Self::Signature>;
fn sign(&self, message: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Self::SignFuture {
let mut result = [0; 1 + 8];
result[0] = self.validator;
result[1 ..].copy_from_slice(&self.signature_scheme.hash(message));
core::future::ready(result)
}
}
#[test]
fn signature_scheme() {
use core::{
pin::pin,
task::{Poll, Waker, Context},
};
let mut context = Context::from_waker(Waker::noop());
let message = [[12].as_slice(), &[2], &[3]];
let other_message = [[].as_slice()];
let signature_scheme = loop {
let signature_scheme = TestSignatureScheme::new();
if signature_scheme.hash(message) == signature_scheme.hash(other_message) {
continue;
}
break signature_scheme;
};
let aggregated = [3, 101, 135];
let mut signatures = [None; 3];
for validator in 0 ..= u8::MAX {
let Poll::Ready(signature) =
pin!(signature_scheme.signer(validator).sign(message)).poll(&mut context)
else {
panic!("`TestSignatureScheme::sign` returned `Poll::Pending`")
};
assert!(signature_scheme.verify(&validator, message, &signature));
assert!(!signature_scheme.verify(&validator.wrapping_add(1), message, &signature));
assert!(!signature_scheme.verify(&validator, other_message, &signature));
if let Some(i) =
aggregated.iter().position(|validator_in_list| *validator_in_list == validator)
{
signatures[i] = Some((validator, signature));
}
}
let aggregate_signature = signature_scheme.aggregate(
message,
signatures.iter().map(|signature| {
let (validator, signature) = signature.as_ref().unwrap();
(validator, signature)
}),
);
let mut yielded_aggregated =
signature_scheme.verify_aggregate(message, &aggregate_signature).unwrap().into_iter();
let mut aggregated = aggregated.into_iter();
for (i, j) in (&mut yielded_aggregated).zip(&mut aggregated) {
assert_eq!(i, j);
}
assert!(yielded_aggregated.next().is_none());
assert!(aggregated.next().is_none());
}
}
#[cfg(test)]
pub(crate) use tests::*;
#[cfg(all(test, any(feature = "alloc", feature = "std")))]
fn test_map<M: FromIterator<(u16, NonZero<u16>)> + ValidatorSet<Validator = u16>>() {
let test = |pairs: &[(u16, NonZero<u16>)]| {
let map = pairs.iter().copied().collect::<M>();
assert_eq!(
u16::from(map.total_weight()),
pairs.iter().map(|(_validator, weight)| u16::from(*weight)).sum::<u16>()
);
assert_eq!(
map.validators().into_iter().copied().collect::<alloc::collections::BTreeSet<_>>(),
pairs
.iter()
.map(|(validator, _weight)| validator)
.copied()
.collect::<alloc::collections::BTreeSet<_>>()
);
let proposers = {
let mut proposers = alloc::vec::Vec::new();
for (validator, weight) in pairs.iter().copied() {
let weight = u16::from(weight);
for w in 0 .. weight {
proposers.push((validator, w));
}
}
proposers.sort_by(|(a_validator, a_weight), (b_validator, b_weight)| {
a_weight.cmp(b_weight).then_with(|| a_validator.cmp(b_validator))
});
proposers.into_iter().map(|(validator, _weight)| validator).collect::<alloc::vec::Vec<_>>()
};
assert_eq!(map.proposer(BlockNumber::ONE, RoundNumber::ONE), proposers[0]);
for i in 0 .. (2 * proposers.len()) {
assert_eq!(
map.proposer(
BlockNumber::from(NonZero::new(1 + u64::try_from(i).unwrap()).unwrap()),
RoundNumber::ONE
),
proposers[i % proposers.len()]
);
assert_eq!(
map.proposer(
BlockNumber::ONE,
RoundNumber(NonZero::new(1 + u64::try_from(i).unwrap()).unwrap())
),
proposers[i % proposers.len()]
);
}
assert_eq!(
map.proposer(
BlockNumber::from(NonZero::new(u64::MAX).unwrap()),
RoundNumber(NonZero::new(u64::MAX).unwrap())
),
proposers[usize::try_from(
(2 * u128::from(u64::MAX - 1)) % u128::try_from(proposers.len()).unwrap()
)
.unwrap()]
);
};
for weighted in [false, true] {
for len in 1 .. 128 {
test(
&(1 ..= len)
.map(|i| {
let weight = if weighted {
use rand_core::{TryRngCore as _, OsRng};
NonZero::new(u16::try_from(1 + (OsRng.try_next_u64().unwrap() % 16)).unwrap())
.unwrap()
} else {
NonZero::new(1).unwrap()
};
(i, weight)
})
.collect::<alloc::vec::Vec<_>>(),
);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn test_btree_map() {
test_map::<alloc::collections::BTreeMap<u16, NonZero<u16>>>();
}
#[cfg(feature = "std")]
#[test]
fn test_hash_map() {
test_map::<std::collections::HashMap<u16, NonZero<u16>>>();
}