use core::{num::NonZero, iter::Flatten};
use std::collections::{
hash_map::{Entry, Values as HashMapValues},
HashMap,
};
use crate::{
RoundNumber, ValidatorSet, Signature, AggregateSignature, SignatureScheme, BlockHash, Block,
Data, SlashReason, Blockchain, MessageFor,
};
#[must_use]
pub(crate) struct RoundMessages<B: Blockchain> {
proposal: Option<MessageFor<B>>,
prevote: Option<MessageFor<B>>,
precommit: Option<MessageFor<B>>,
}
impl<B: Blockchain> Clone for RoundMessages<B> {
fn clone(&self) -> Self {
let Self { proposal, prevote, precommit } = self;
Self { proposal: proposal.clone(), prevote: prevote.clone(), precommit: precommit.clone() }
}
}
impl<B: Blockchain> IntoIterator for RoundMessages<B> {
type IntoIter = Flatten<<[Option<MessageFor<B>>; 3] as IntoIterator>::IntoIter>;
type Item = MessageFor<B>;
fn into_iter(self) -> Self::IntoIter {
let Self { proposal, prevote, precommit } = self;
[proposal, prevote, precommit].into_iter().flatten()
}
}
impl<'messages, B: Blockchain> IntoIterator for &'messages RoundMessages<B> {
type IntoIter = Flatten<<[&'messages Option<MessageFor<B>>; 3] as IntoIterator>::IntoIter>;
type Item = &'messages MessageFor<B>;
fn into_iter(self) -> Self::IntoIter {
let RoundMessages { proposal, prevote, precommit } = self;
[proposal, prevote, precommit].into_iter().flatten()
}
}
impl<B: Blockchain> RoundMessages<B> {
pub(super) const NONE: Self = RoundMessages { proposal: None, prevote: None, precommit: None };
#[must_use]
fn round_number(&self) -> Option<RoundNumber> {
self
.proposal
.as_ref()
.or(self.prevote.as_ref())
.or(self.precommit.as_ref())
.map(|message| message.round_number)
}
#[must_use]
fn slot(&mut self, message: &MessageFor<B>) -> &mut Option<MessageFor<B>> {
match message.data {
Data::Proposal { .. } => &mut self.proposal,
Data::Prevote { .. } => &mut self.prevote,
Data::Precommit { .. } => &mut self.precommit,
}
}
pub(super) fn insert(&mut self, message: Option<MessageFor<B>>) {
let Some(message) = message else { return };
let slot = self.slot(&message);
debug_assert!(slot.is_none());
*slot = Some(message);
}
#[must_use]
fn maybe_insert(&mut self, message: &MessageFor<B>) -> Option<&MessageFor<B>> {
let slot = self.slot(message);
if let Some(existing_message) = slot {
return Some(existing_message);
}
*slot = Some(message.clone());
None
}
}
#[doc(hidden)]
pub(super) struct NRoundMessages<B: Blockchain, const N: usize>([RoundMessages<B>; N]);
impl<B: Blockchain, const N: usize> NRoundMessages<B, N> {
const NONE: Self = Self([RoundMessages::NONE; N]);
#[must_use]
fn highest_round_number(&self) -> Option<RoundNumber> {
self.0.iter().filter_map(RoundMessages::round_number).max()
}
#[must_use]
fn get(&self, round_number: RoundNumber) -> Option<&RoundMessages<B>> {
self.0.iter().find(|round_messages| round_messages.round_number() == Some(round_number))
}
fn retain<'rounds>(
&'rounds mut self,
tracked_rounds: &'rounds [Option<RoundNumber>; N],
) -> impl use<'rounds, B, N> + Iterator<Item = RoundNumber> {
self.0.iter_mut().filter_map(|round_messages| {
let round_number = round_messages.round_number()?;
if tracked_rounds.contains(&Some(round_number)) {
None?;
}
*round_messages = RoundMessages::NONE;
Some(round_number)
})
}
}
pub(super) struct TrackedRounds<B: Blockchain> {
messages: HashMap<B::Validator, NRoundMessages<B, 3>>,
round_tallies: HashMap<NonZero<u64>, u16>,
}
#[doc(hidden)]
pub(super) struct NRoundMessagesToRoundMessages<I> {
round_number: RoundNumber,
iterator: I,
}
impl<
'messages,
B: 'messages + Blockchain,
const N: usize,
I: Iterator<Item = &'messages NRoundMessages<B, N>>,
> Iterator for NRoundMessagesToRoundMessages<I>
{
type Item = &'messages RoundMessages<B>;
fn next(&mut self) -> Option<Self::Item> {
self.iterator.next().and_then(|messages| messages.get(self.round_number))
}
}
pub(super) enum Updated<S: Signature, A: AggregateSignature, H: BlockHash> {
Fresh,
NotTracked,
AlreadyHandled,
Equivocation(SlashReason<S, A, H>),
}
type UpdatedFor<B> = Updated<
<<B as Blockchain>::SignatureScheme as SignatureScheme>::Signature,
<<B as Blockchain>::SignatureScheme as SignatureScheme>::AggregateSignature,
<<B as Blockchain>::Block as Block>::Hash,
>;
type MessagesForRound<'messages, B> = Flatten<
NRoundMessagesToRoundMessages<
HashMapValues<'messages, <B as Blockchain>::Validator, NRoundMessages<B, 3>>,
>,
>;
impl<B: Blockchain> TrackedRounds<B> {
#[must_use]
pub(super) fn new(capacity: usize) -> Self {
Self {
messages: HashMap::with_capacity(capacity),
round_tallies: HashMap::with_capacity(3 * capacity),
}
}
pub(super) fn reset(&mut self) {
let Self { messages, round_tallies } = self;
messages.clear();
round_tallies.clear();
}
#[must_use]
pub(super) fn update(
&mut self,
validator_set: &(impl ?Sized + ValidatorSet<Validator = B::Validator>),
validator: B::Validator,
current_round_number: RoundNumber,
message: &MessageFor<B>,
) -> UpdatedFor<B> {
let all_round_messages = self.messages.entry(validator).or_insert(NRoundMessages::NONE);
let tracked_rounds = {
let highest_observed_round_number = all_round_messages
.highest_round_number()
.unwrap_or(message.round_number)
.max(message.round_number);
let prior_observed_round_number = u64::from(highest_observed_round_number)
.checked_sub(1)
.and_then(NonZero::new)
.map(RoundNumber);
[Some(highest_observed_round_number), prior_observed_round_number, Some(current_round_number)]
};
let validator_weight = validator_set.weight(&validator).map(u16::from).unwrap_or(0);
for pruned_round in all_round_messages.retain(&tracked_rounds) {
let Entry::Occupied(mut entry) = self.round_tallies.entry(pruned_round.0) else {
panic!("round present in messages but no corresponding tally?")
};
*entry.get_mut() -= validator_weight;
if (*entry.get()) == 0 {
entry.remove();
}
}
if !tracked_rounds.contains(&Some(message.round_number)) {
return Updated::NotTracked;
}
match all_round_messages
.0
.iter_mut()
.find(|round_messages| round_messages.round_number() == Some(message.round_number))
{
Some(round_messages) => {
if let Some(existing_message) = round_messages.maybe_insert(message) {
if existing_message == message {
return Updated::AlreadyHandled;
}
return Updated::Equivocation(
SlashReason::equivocation(existing_message.clone(), message.clone())
.expect("`RoundMessages` had a mismatch between message and slot"),
);
}
}
None => {
let round_messages = all_round_messages
.0
.iter_mut()
.find(|round_messages| round_messages.round_number().is_none())
.expect(
"`round_number` in retained `tracked_rounds` but no corresponding or empty slot?",
);
let existing_message = round_messages.maybe_insert(message);
debug_assert!(existing_message.is_none());
self
.round_tallies
.entry(message.round_number.0)
.and_modify(|weight| *weight += validator_weight)
.or_insert(validator_weight);
debug_assert!(self.round_tallies.len() <= (3 * self.messages.len()));
}
}
Updated::Fresh
}
#[must_use]
pub(super) fn should_jump_ahead(
&self,
validator_set: &(impl ?Sized + ValidatorSet<Validator = B::Validator>),
round_number: RoundNumber,
) -> bool {
self
.round_tallies
.get(&round_number.0)
.is_some_and(|weight| (*weight) > validator_set.fault_threshold())
}
pub(super) fn messages_for_round(&self, round_number: RoundNumber) -> MessagesForRound<'_, B> {
(NRoundMessagesToRoundMessages { round_number, iterator: self.messages.values() }).flatten()
}
}