use crate::{
simplex::types::{Attributable, AttributableMap, Finalize, Notarize, Nullify, Proposal, Vote},
types::Participant,
};
use commonware_cryptography::{Digest, certificate::Scheme};
enum Phase<T: Attributable> {
Full(AttributableMap<T>),
Compacted,
}
impl<T: Attributable> Phase<T> {
const fn new(participants: usize) -> Self {
Self::Full(AttributableMap::new(participants))
}
fn get(&self, signer: Participant) -> Option<&T> {
match self {
Self::Full(votes) => votes.get(signer),
Self::Compacted => None,
}
}
fn iter(&self) -> impl Iterator<Item = &T> {
match self {
Self::Full(votes) => Some(votes),
Self::Compacted => None,
}
.into_iter()
.flat_map(AttributableMap::iter)
}
fn compact(&mut self) -> Option<AttributableMap<T>> {
match std::mem::replace(self, Self::Compacted) {
Self::Full(votes) => Some(votes),
Self::Compacted => None,
}
}
}
#[derive(Clone, Copy)]
struct PhaseBits {
seen: u8,
has_proposal: u8,
relation: u8,
}
pub(super) struct VoteTracker<S: Scheme, D: Digest> {
participants: usize,
retain_votes_after_certification: bool,
compacted: Vec<u8>,
notarizes: Phase<Notarize<S, D>>,
nullifies: Phase<Nullify<S>>,
finalizes: Phase<Finalize<S, D>>,
}
pub(super) enum Outcome {
Added { retained: bool },
Duplicate { retained: bool },
Conflicting,
}
pub(super) enum ObservedVote<'a, T> {
Retained(&'a T),
Compacted,
}
impl<S: Scheme, D: Digest> VoteTracker<S, D> {
const NOTARIZE: PhaseBits = PhaseBits {
seen: 1 << 0,
has_proposal: 1 << 1,
relation: 1 << 2,
};
const NULLIFY: PhaseBits = PhaseBits {
seen: 1 << 3,
has_proposal: 0,
relation: 0,
};
const FINALIZE: PhaseBits = PhaseBits {
seen: 1 << 4,
has_proposal: 1 << 5,
relation: 1 << 6,
};
pub(super) const fn new(participants: usize, retain_votes_after_certification: bool) -> Self {
Self {
participants,
retain_votes_after_certification,
compacted: Vec::new(),
notarizes: Phase::new(participants),
nullifies: Phase::new(participants),
finalizes: Phase::new(participants),
}
}
pub(super) const fn skipped(participants: usize) -> Self {
Self {
participants,
retain_votes_after_certification: false,
compacted: Vec::new(),
notarizes: Phase::Compacted,
nullifies: Phase::Compacted,
finalizes: Phase::Compacted,
}
}
fn remember(
participants: usize,
compacted: &mut Vec<u8>,
signer: Participant,
bits: PhaseBits,
matches: Option<bool>,
) -> Outcome {
let index = usize::from(signer);
if index >= participants {
return Outcome::Duplicate { retained: false };
}
if compacted.is_empty() {
compacted.resize(participants, 0);
}
let flags = &mut compacted[index];
let previously_seen = *flags & bits.seen != 0;
let proposal_conflict = matches.is_some_and(|matches| {
previously_seen
&& *flags & bits.relation != 0
&& (*flags & bits.has_proposal != 0) != matches
});
*flags |= bits.seen;
if let Some(matches) = matches {
*flags |= bits.relation;
if matches {
*flags |= bits.has_proposal;
}
}
if !previously_seen {
Outcome::Added { retained: false }
} else if proposal_conflict {
Outcome::Conflicting
} else {
Outcome::Duplicate { retained: false }
}
}
fn record_phase<T: Attributable + Clone>(
participants: usize,
compacted: &mut Vec<u8>,
phase: &mut Phase<T>,
vote: &T,
bits: PhaseBits,
matches: Option<bool>,
) -> Outcome {
match phase {
Phase::Full(votes) => {
if votes.insert(vote.clone()) {
Outcome::Added { retained: true }
} else {
Outcome::Duplicate { retained: true }
}
}
Phase::Compacted => {
Self::remember(participants, compacted, vote.signer(), bits, matches)
}
}
}
fn remembered(&self, signer: Participant, flag: u8) -> bool {
self.compacted
.get(usize::from(signer))
.is_some_and(|flags| flags & flag != 0)
}
pub(super) fn record(&mut self, vote: &Vote<S, D>, proposal: Option<&Proposal<D>>) -> Outcome {
match vote {
Vote::Notarize(notarize) => Self::record_phase(
self.participants,
&mut self.compacted,
&mut self.notarizes,
notarize,
Self::NOTARIZE,
proposal.map(|proposal| proposal == ¬arize.proposal),
),
Vote::Nullify(nullify) => Self::record_phase(
self.participants,
&mut self.compacted,
&mut self.nullifies,
nullify,
Self::NULLIFY,
None,
),
Vote::Finalize(finalize) => Self::record_phase(
self.participants,
&mut self.compacted,
&mut self.finalizes,
finalize,
Self::FINALIZE,
proposal.map(|proposal| proposal == &finalize.proposal),
),
}
}
fn release<T: Attributable>(
participants: usize,
compacted: &mut Vec<u8>,
phase: &mut Phase<T>,
bits: PhaseBits,
carries_proposal: impl Fn(&T) -> bool,
) {
let Some(votes) = phase.compact() else {
return;
};
if !votes.is_empty() && compacted.is_empty() {
compacted.resize(participants, 0);
}
for vote in votes.iter() {
let flags = &mut compacted[usize::from(vote.signer())];
*flags |= bits.seen | bits.relation;
if carries_proposal(vote) {
*flags |= bits.has_proposal;
}
}
}
pub(super) fn saw_nullify(&self, signer: Participant) -> Option<ObservedVote<'_, Nullify<S>>> {
self.nullify(signer)
.map(ObservedVote::Retained)
.or_else(|| {
self.remembered(signer, Self::NULLIFY.seen)
.then_some(ObservedVote::Compacted)
})
}
pub(super) fn saw_finalize(
&self,
signer: Participant,
) -> Option<ObservedVote<'_, Finalize<S, D>>> {
self.finalize(signer)
.map(ObservedVote::Retained)
.or_else(|| {
self.remembered(signer, Self::FINALIZE.seen)
.then_some(ObservedVote::Compacted)
})
}
pub(super) fn has_notarize_for(&self, signer: Participant, proposal: &Proposal<D>) -> bool {
self.remembered(signer, Self::NOTARIZE.has_proposal)
|| self
.notarize(signer)
.is_some_and(|vote| &vote.proposal == proposal)
}
pub(super) fn has_finalize_for(&self, signer: Participant, proposal: &Proposal<D>) -> bool {
self.remembered(signer, Self::FINALIZE.has_proposal)
|| self
.finalize(signer)
.is_some_and(|vote| &vote.proposal == proposal)
}
pub(super) fn release_notarizes(&mut self, proposal: &Proposal<D>) {
if self.retain_votes_after_certification {
return;
}
Self::release(
self.participants,
&mut self.compacted,
&mut self.notarizes,
Self::NOTARIZE,
|vote: &Notarize<S, D>| &vote.proposal == proposal,
);
}
pub(super) fn release_nullifies(&mut self) {
if self.retain_votes_after_certification {
return;
}
Self::release(
self.participants,
&mut self.compacted,
&mut self.nullifies,
Self::NULLIFY,
|_| false,
);
}
pub(super) fn release_finalizes(&mut self, proposal: &Proposal<D>) {
if self.retain_votes_after_certification {
return;
}
Self::release(
self.participants,
&mut self.compacted,
&mut self.finalizes,
Self::FINALIZE,
|vote: &Finalize<S, D>| &vote.proposal == proposal,
);
}
pub(super) fn notarize(&self, signer: Participant) -> Option<&Notarize<S, D>> {
self.notarizes.get(signer)
}
pub(super) fn nullify(&self, signer: Participant) -> Option<&Nullify<S>> {
self.nullifies.get(signer)
}
pub(super) fn finalize(&self, signer: Participant) -> Option<&Finalize<S, D>> {
self.finalizes.get(signer)
}
pub(super) fn iter_finalizes(&self) -> impl Iterator<Item = &Finalize<S, D>> {
self.finalizes.iter()
}
pub(super) fn has_nullify(&self, signer: Participant) -> bool {
self.nullify(signer).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
simplex::scheme::ed25519,
types::{Epoch, Round, View},
};
use commonware_cryptography::sha256::Digest as Sha256;
use commonware_utils::test_rng;
const NAMESPACE: &[u8] = b"test";
fn sample_digest(v: u8) -> Sha256 {
Sha256::from([v; 32])
}
#[test]
fn test_vote_tracker_record_and_release() {
let mut rng = test_rng();
let fixture = ed25519::fixture(&mut rng, NAMESPACE, 2);
let round = Round::new(Epoch::new(0), View::new(1));
let proposal = Proposal::new(round, View::zero(), sample_digest(1));
let scheme = &fixture.schemes[0];
let signer = Participant::new(0);
let votes = [
Vote::Notarize(Notarize::sign(scheme, proposal.clone()).unwrap()),
Vote::Nullify(Nullify::sign::<Sha256>(scheme, round).unwrap()),
Vote::Finalize(Finalize::sign(scheme, proposal.clone()).unwrap()),
];
let mut tracker = VoteTracker::new(2, false);
for vote in &votes {
assert!(matches!(
tracker.record(vote, Some(&proposal)),
Outcome::Added { retained: true }
));
}
assert!(tracker.notarize(signer).is_some());
assert!(tracker.has_nullify(signer));
assert!(tracker.finalize(signer).is_some());
tracker.release_notarizes(&proposal);
tracker.release_nullifies();
tracker.release_finalizes(&proposal);
assert!(tracker.notarize(signer).is_none());
assert!(!tracker.has_nullify(signer));
assert!(tracker.finalize(signer).is_none());
assert!(tracker.iter_finalizes().next().is_none());
assert!(tracker.has_notarize_for(signer, &proposal));
assert!(tracker.has_finalize_for(signer, &proposal));
for vote in &votes {
assert!(matches!(
tracker.record(vote, Some(&proposal)),
Outcome::Duplicate { retained: false }
));
}
tracker.release_notarizes(&proposal);
assert!(tracker.has_notarize_for(signer, &proposal));
}
#[test]
fn test_vote_tracker_retention_policy() {
let mut rng = test_rng();
let fixture = ed25519::fixture(&mut rng, NAMESPACE, 2);
let proposal = Proposal::new(
Round::new(Epoch::new(0), View::new(1)),
View::zero(),
sample_digest(1),
);
let notarize = Notarize::sign(&fixture.schemes[0], proposal.clone()).unwrap();
let signer = notarize.signer();
let vote = Vote::Notarize(notarize);
let mut releasing = VoteTracker::new(2, false);
assert!(matches!(
releasing.record(&vote, Some(&proposal)),
Outcome::Added { retained: true }
));
let Phase::Full(votes) = &releasing.notarizes else {
panic!("notarize phase compacted before certification");
};
assert!(votes.capacity() >= 2);
releasing.release_notarizes(&proposal);
assert!(matches!(&releasing.notarizes, Phase::Compacted));
assert!(matches!(
releasing.record(&vote, Some(&proposal)),
Outcome::Duplicate { retained: false }
));
let finalize =
Vote::Finalize(Finalize::sign(&fixture.schemes[0], proposal.clone()).unwrap());
let mut certificate_first = VoteTracker::new(2, false);
certificate_first.release_notarizes(&proposal);
certificate_first.release_finalizes(&proposal);
for vote in [&vote, &finalize] {
assert!(matches!(
certificate_first.record(vote, Some(&proposal)),
Outcome::Added { retained: false }
));
}
assert!(matches!(&certificate_first.notarizes, Phase::Compacted));
assert!(matches!(&certificate_first.finalizes, Phase::Compacted));
assert!(certificate_first.has_notarize_for(signer, &proposal));
assert!(certificate_first.has_finalize_for(signer, &proposal));
for vote in [&vote, &finalize] {
assert!(matches!(
certificate_first.record(vote, Some(&proposal)),
Outcome::Duplicate { retained: false }
));
}
let mut retaining = VoteTracker::new(2, true);
assert!(matches!(
retaining.record(&vote, Some(&proposal)),
Outcome::Added { retained: true }
));
let Phase::Full(votes) = &retaining.notarizes else {
panic!("retained notarize phase compacted");
};
let retained_capacity = votes.capacity();
retaining.release_notarizes(&proposal);
let Phase::Full(votes) = &retaining.notarizes else {
panic!("retained notarize phase compacted");
};
assert_eq!(votes.capacity(), retained_capacity);
assert!(retaining.notarize(signer).is_some());
}
}