extern crate alloc;
use alloc::collections::{BTreeMap, BTreeSet};
use super::super::stability::Stability;
use super::gate::EpochAddress;
use super::vouched::Vouched;
use super::{Epochs, SealedEpoch};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Admission {
base: EpochAddress,
joiners: BTreeSet<u32>,
}
impl Admission {
pub fn new(
base: EpochAddress,
joiners: impl IntoIterator<Item = u32>,
) -> Result<Self, ArrivalRefusal> {
let joiners: BTreeSet<u32> = joiners.into_iter().collect();
if joiners.is_empty() {
return Err(ArrivalRefusal::EmptyAdmission);
}
Ok(Self { base, joiners })
}
#[must_use]
pub const fn base(&self) -> EpochAddress {
self.base
}
pub fn joiners(&self) -> impl Iterator<Item = u32> + '_ {
self.joiners.iter().copied()
}
#[must_use]
pub fn admits(&self, station: u32) -> bool {
self.joiners.contains(&station)
}
pub(super) const fn joiner_set(&self) -> &BTreeSet<u32> {
&self.joiners
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Endorsement {
admission: Admission,
commitment: [u8; 32],
}
impl Endorsement {
#[must_use]
pub const fn from_parts(admission: Admission, commitment: [u8; 32]) -> Self {
Self {
admission,
commitment,
}
}
#[must_use]
pub const fn admission(&self) -> &Admission {
&self.admission
}
#[must_use]
pub const fn commitment(&self) -> &[u8; 32] {
&self.commitment
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArrivalRound {
pub(super) admission: Admission,
pub(super) commitment: [u8; 32],
pub(super) slots: BTreeMap<u32, bool>,
pub(super) own: u32,
}
impl ArrivalRound {
#[must_use]
pub const fn admission(&self) -> &Admission {
&self.admission
}
#[must_use]
pub const fn own(&self) -> u32 {
self.own
}
pub fn family(&self) -> impl Iterator<Item = u32> + '_ {
self.slots.keys().copied()
}
pub fn pending(&self) -> impl Iterator<Item = u32> + '_ {
self.slots
.iter()
.filter(|&(_, &endorsed)| !endorsed)
.map(|(&station, _)| station)
}
#[must_use]
pub fn complete(&self) -> bool {
self.slots.values().all(|&endorsed| endorsed)
}
#[must_use]
pub const fn commitment(&self) -> &[u8; 32] {
&self.commitment
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ArrivalRefusal {
#[error("empty admission: a boundary must admit at least one station")]
EmptyAdmission,
#[error("already present: station {station} is on the roster")]
AlreadyPresent {
station: u32,
},
#[error("unknown station: {station} is not in the round's family")]
UnknownStation {
station: u32,
},
#[error("impersonated: a report may not fill this replica's own slot ({station})")]
Impersonated {
station: u32,
},
#[error("premature: base generation {base} is ahead of newest sealed {newest}")]
Premature {
base: u64,
newest: u64,
},
#[error("elapsed: base generation {base} is behind newest sealed {newest}")]
Elapsed {
base: u64,
newest: u64,
},
#[error("unfounded: no sealed epoch exists to found a boundary on")]
Unfounded,
#[error("discordant base: held {held:?}, offered {offered:?}")]
DiscordantBase {
held: EpochAddress,
offered: EpochAddress,
},
#[error("discordant round: a round is open over base {held:?}, offered {offered:?}")]
DiscordantRound {
held: EpochAddress,
offered: EpochAddress,
},
#[error("foreign boundary: the endorsement names another joiner set over base {base:?}")]
ForeignBoundary {
base: EpochAddress,
},
#[error("no round: no arrival round is open")]
NoRound,
#[error("commitment discord over base {base:?}")]
CommitmentDiscord {
base: EpochAddress,
},
#[error("discordant voice: the round is held by {held}, offered by {offered}")]
DiscordantVoice {
held: u32,
offered: u32,
},
#[error("attested departure: station {station} left and its words are not spendable")]
AttestedDeparture {
station: u32,
},
}
impl Epochs {
pub fn open_arrival(
&mut self,
admission: Admission,
commitment: [u8; 32],
own: u32,
stability: &Stability,
) -> Result<Endorsement, ArrivalRefusal> {
if let Some(&station) = admission
.joiner_set()
.iter()
.find(|station| self.roster.contains(station))
{
return Err(ArrivalRefusal::AlreadyPresent { station });
}
let family: BTreeSet<u32> = stability
.surviving_family()
.filter(|station| self.roster.contains(station))
.collect();
if !family.contains(&own) {
return Err(ArrivalRefusal::UnknownStation { station: own });
}
self.locate_base(admission.base())?;
if let Some(round) = &self.arrival {
if round.own != own {
return Err(ArrivalRefusal::DiscordantVoice {
held: round.own,
offered: own,
});
}
if *round.admission() != admission {
if round.admission().base() == admission.base() {
return Err(ArrivalRefusal::ForeignBoundary {
base: admission.base(),
});
}
return Err(ArrivalRefusal::DiscordantRound {
held: round.admission().base(),
offered: admission.base(),
});
}
if round.commitment != commitment {
return Err(ArrivalRefusal::CommitmentDiscord {
base: admission.base(),
});
}
return Ok(Endorsement {
admission,
commitment,
});
}
let slots = family
.iter()
.map(|&station| (station, station == own))
.collect();
self.arrival = Some(ArrivalRound {
admission: admission.clone(),
commitment,
slots,
own,
});
Ok(Endorsement {
admission,
commitment,
})
}
pub fn fold_endorsement(
&mut self,
endorsement: &Vouched<Endorsement>,
stability: &Stability,
) -> Result<(), ArrivalRefusal> {
let by = endorsement.by();
let offered = endorsement.claim().admission();
if stability.attested(by).is_some() {
return Err(ArrivalRefusal::AttestedDeparture { station: by });
}
let Some(round) = &mut self.arrival else {
return Err(ArrivalRefusal::NoRound);
};
if offered.base() != round.admission.base() {
return Err(ArrivalRefusal::DiscordantRound {
held: round.admission.base(),
offered: offered.base(),
});
}
if *offered != round.admission {
return Err(ArrivalRefusal::ForeignBoundary {
base: round.admission.base(),
});
}
if *endorsement.claim().commitment() != round.commitment {
return Err(ArrivalRefusal::CommitmentDiscord {
base: round.admission.base(),
});
}
if by == round.own {
return Err(ArrivalRefusal::Impersonated { station: by });
}
let Some(slot) = round.slots.get_mut(&by) else {
return Err(ArrivalRefusal::UnknownStation { station: by });
};
*slot = true;
Ok(())
}
#[must_use]
pub const fn arrival(&self) -> Option<&ArrivalRound> {
self.arrival.as_ref()
}
fn locate_base(&self, base: EpochAddress) -> Result<(), ArrivalRefusal> {
let Some(newest) = self.lineage.back().map(SealedEpoch::declaration) else {
return Err(ArrivalRefusal::Unfounded);
};
if base == newest {
return Ok(());
}
if base.generation() > newest.generation() {
return Err(ArrivalRefusal::Premature {
base: base.generation(),
newest: newest.generation(),
});
}
if base.generation() == newest.generation() {
return Err(ArrivalRefusal::DiscordantBase {
held: newest,
offered: base,
});
}
Err(ArrivalRefusal::Elapsed {
base: base.generation(),
newest: newest.generation(),
})
}
}