use super::round::Round;
use crate::{
simplex::{
elector::{Config as ElectorConfig, Elector},
interesting, min_active,
scheme::Scheme,
types::{
Artifact, Certificate, Context, Finalization, Finalize, Notarization, Notarize,
Nullification, Nullify, Proposal,
},
},
types::{Epoch, Participant, Round as Rnd, View, ViewDelta},
Viewable,
};
use commonware_cryptography::{certificate, Digest};
use commonware_runtime::{telemetry::metrics::status::GaugeExt, Clock, Metrics};
use commonware_utils::futures::Aborter;
use prometheus_client::metrics::{counter::Counter, gauge::Gauge};
use rand_core::CryptoRngCore;
use std::{
collections::{BTreeMap, BTreeSet},
mem::{replace, take},
sync::atomic::AtomicI64,
time::{Duration, SystemTime},
};
use tracing::{debug, warn};
const GENESIS_VIEW: View = View::zero();
pub struct Config<S: certificate::Scheme, L: ElectorConfig<S>> {
pub scheme: S,
pub elector: L,
pub epoch: Epoch,
pub activity_timeout: ViewDelta,
pub leader_timeout: Duration,
pub notarization_timeout: Duration,
pub nullify_retry: Duration,
}
pub struct State<E: Clock + CryptoRngCore + Metrics, S: Scheme<D>, L: ElectorConfig<S>, D: Digest> {
context: E,
scheme: S,
elector: L::Elector,
epoch: Epoch,
activity_timeout: ViewDelta,
leader_timeout: Duration,
notarization_timeout: Duration,
nullify_retry: Duration,
view: View,
last_finalized: View,
genesis: Option<D>,
views: BTreeMap<View, Round<S, D>>,
certification_candidates: BTreeSet<View>,
outstanding_certifications: BTreeSet<View>,
current_view: Gauge,
tracked_views: Gauge,
skipped_views: Counter,
}
impl<E: Clock + CryptoRngCore + Metrics, S: Scheme<D>, L: ElectorConfig<S>, D: Digest>
State<E, S, L, D>
{
pub fn new(context: E, cfg: Config<S, L>) -> Self {
let current_view = Gauge::<i64, AtomicI64>::default();
let tracked_views = Gauge::<i64, AtomicI64>::default();
let skipped_views = Counter::default();
context.register("current_view", "current view", current_view.clone());
context.register("tracked_views", "tracked views", tracked_views.clone());
context.register("skipped_views", "skipped views", skipped_views.clone());
let elector = cfg.elector.build(cfg.scheme.participants());
Self {
context,
scheme: cfg.scheme,
elector,
epoch: cfg.epoch,
activity_timeout: cfg.activity_timeout,
leader_timeout: cfg.leader_timeout,
notarization_timeout: cfg.notarization_timeout,
nullify_retry: cfg.nullify_retry,
view: GENESIS_VIEW,
last_finalized: GENESIS_VIEW,
genesis: None,
views: BTreeMap::new(),
certification_candidates: BTreeSet::new(),
outstanding_certifications: BTreeSet::new(),
current_view,
tracked_views,
skipped_views,
}
}
pub fn set_genesis(&mut self, genesis: D) {
self.genesis = Some(genesis);
self.enter_view(GENESIS_VIEW.next());
self.set_leader(GENESIS_VIEW.next(), None);
}
pub const fn epoch(&self) -> Epoch {
self.epoch
}
pub const fn current_view(&self) -> View {
self.view
}
pub const fn last_finalized(&self) -> View {
self.last_finalized
}
pub const fn min_active(&self) -> View {
min_active(self.activity_timeout, self.last_finalized)
}
pub fn is_interesting(&self, pending: View, allow_future: bool) -> bool {
interesting(
self.activity_timeout,
self.last_finalized,
self.view,
pending,
allow_future,
)
}
pub fn is_me(&self, idx: Participant) -> bool {
self.scheme.me().is_some_and(|me| me == idx)
}
fn enter_view(&mut self, view: View) -> bool {
if view <= self.view {
return false;
}
let now = self.context.current();
let leader_deadline = now + self.leader_timeout;
let advance_deadline = now + self.notarization_timeout;
let round = self.create_round(view);
round.set_deadlines(leader_deadline, advance_deadline);
self.view = view;
let _ = self.current_view.try_set(view.get());
true
}
fn set_leader(&mut self, view: View, certificate: Option<&S::Certificate>) {
let leader = self.elector.elect(Rnd::new(self.epoch, view), certificate);
let round = self.create_round(view);
if round.leader().is_some() {
return;
}
round.set_leader(leader);
}
fn create_round(&mut self, view: View) -> &mut Round<S, D> {
self.views.entry(view).or_insert_with(|| {
Round::new(
self.scheme.clone(),
Rnd::new(self.epoch, view),
self.context.current(),
)
})
}
pub fn next_timeout_deadline(&mut self) -> SystemTime {
let now = self.context.current();
let nullify_retry = self.nullify_retry;
let round = self.create_round(self.view);
round.next_timeout_deadline(now, nullify_retry)
}
pub fn handle_timeout(&mut self) -> (bool, Option<Nullify<S>>, Option<Certificate<S, D>>) {
let view = self.view;
let Some(retry) = self.create_round(view).construct_nullify() else {
return (false, None, None);
};
let nullify = Nullify::sign::<D>(&self.scheme, Rnd::new(self.epoch, view));
let entry_view = view.previous().unwrap_or(GENESIS_VIEW);
if !retry || entry_view == GENESIS_VIEW {
return (retry, nullify, None);
}
#[allow(clippy::option_if_let_else)]
let cert = if let Some(finalization) = self.finalization(entry_view).cloned() {
Some(Certificate::Finalization(finalization))
} else if let Some(nullification) = self.nullification(entry_view).cloned() {
Some(Certificate::Nullification(nullification))
} else if let Some(notarization) = self.notarization(entry_view).cloned() {
Some(Certificate::Notarization(notarization))
} else {
warn!(%entry_view, "entry certificate not found during timeout");
None
};
(retry, nullify, cert)
}
pub fn add_notarization(
&mut self,
notarization: Notarization<S, D>,
) -> (bool, Option<S::PublicKey>) {
let view = notarization.view();
self.set_leader(view.next(), Some(¬arization.certificate));
let result = self.create_round(view).add_notarization(notarization);
if result.0 && view > self.last_finalized {
self.certification_candidates.insert(view);
}
result
}
pub fn add_nullification(&mut self, nullification: Nullification<S>) -> bool {
let view = nullification.view();
self.enter_view(view.next());
self.set_leader(view.next(), Some(&nullification.certificate));
self.create_round(view).add_nullification(nullification)
}
pub fn add_finalization(
&mut self,
finalization: Finalization<S, D>,
) -> (bool, Option<S::PublicKey>) {
let view = finalization.view();
if view > self.last_finalized {
self.last_finalized = view;
self.certification_candidates.retain(|v| *v > view);
let keep = self.outstanding_certifications.split_off(&view.next());
for v in replace(&mut self.outstanding_certifications, keep) {
if let Some(round) = self.views.get_mut(&v) {
round.abort_certify();
}
}
}
self.enter_view(view.next());
self.set_leader(view.next(), Some(&finalization.certificate));
self.create_round(view).add_finalization(finalization)
}
pub fn construct_notarize(&mut self, view: View) -> Option<Notarize<S, D>> {
let candidate = self
.views
.get_mut(&view)
.and_then(|round| round.construct_notarize().cloned())?;
Notarize::sign(&self.scheme, candidate)
}
pub fn construct_finalize(&mut self, view: View) -> Option<Finalize<S, D>> {
let candidate = self
.views
.get_mut(&view)
.and_then(|round| round.construct_finalize().cloned())?;
Finalize::sign(&self.scheme, candidate)
}
pub fn broadcast_notarization(&mut self, view: View) -> Option<Notarization<S, D>> {
self.views
.get_mut(&view)
.and_then(|round| round.broadcast_notarization())
}
pub fn notarization(&self, view: View) -> Option<&Notarization<S, D>> {
self.views.get(&view).and_then(|round| round.notarization())
}
pub fn nullification(&self, view: View) -> Option<&Nullification<S>> {
self.views
.get(&view)
.and_then(|round| round.nullification())
}
pub fn finalization(&self, view: View) -> Option<&Finalization<S, D>> {
self.views.get(&view).and_then(|round| round.finalization())
}
pub fn broadcast_nullification(&mut self, view: View) -> Option<Nullification<S>> {
self.views
.get_mut(&view)
.and_then(|round| round.broadcast_nullification())
}
pub fn broadcast_finalization(&mut self, view: View) -> Option<Finalization<S, D>> {
self.views
.get_mut(&view)
.and_then(|round| round.broadcast_finalization())
}
pub fn replay(&mut self, artifact: &Artifact<S, D>) {
self.create_round(artifact.view()).replay(artifact);
}
pub fn leader_index(&self, view: View) -> Option<Participant> {
self.views
.get(&view)
.and_then(|round| round.leader().map(|leader| leader.idx))
}
pub fn elapsed_since_start(&self, view: View) -> Option<Duration> {
let now = self.context.current();
self.views
.get(&view)
.map(|round| round.elapsed_since_start(now))
}
pub fn expire_round(&mut self, view: View) {
let now = self.context.current();
self.create_round(view).set_deadlines(now, now);
self.skipped_views.inc();
}
pub fn try_propose(&mut self) -> Option<Context<D, S::PublicKey>> {
let view = self.view;
if view == GENESIS_VIEW {
return None;
}
if !self
.views
.get_mut(&view)
.expect("view must exist")
.should_propose()
{
return None;
}
let parent = self.find_parent(view);
let (parent_view, parent_payload) = match parent {
Ok(parent) => parent,
Err(missing) => {
debug!(%view, %missing, "missing parent during proposal");
return None;
}
};
let leader = self
.views
.get_mut(&view)
.expect("view must exist")
.try_propose()?;
Some(Context {
round: Rnd::new(self.epoch, view),
leader: leader.key,
parent: (parent_view, parent_payload),
})
}
pub fn proposed(&mut self, proposal: Proposal<D>) -> bool {
self.views
.get_mut(&proposal.view())
.map(|round| round.proposed(proposal))
.unwrap_or(false)
}
pub fn set_proposal(&mut self, view: View, proposal: Proposal<D>) -> bool {
self.create_round(view).set_proposal(proposal)
}
#[allow(clippy::type_complexity)]
pub fn try_verify(&mut self) -> Option<(Context<D, S::PublicKey>, Proposal<D>)> {
let view = self.view;
let (leader, proposal) = self.views.get(&view)?.should_verify()?;
let parent_payload = self.parent_payload(&proposal)?;
if !self.views.get_mut(&view)?.try_verify() {
return None;
}
let context = Context {
round: proposal.round,
leader: leader.key,
parent: (proposal.parent, parent_payload),
};
Some((context, proposal))
}
pub fn verified(&mut self, view: View) -> bool {
self.views
.get_mut(&view)
.map(|round| round.verified())
.unwrap_or(false)
}
pub fn set_certify_handle(&mut self, view: View, handle: Aborter) {
let Some(round) = self.views.get_mut(&view) else {
return;
};
round.set_certify_handle(handle);
self.outstanding_certifications.insert(view);
}
pub fn certify_candidates(&mut self) -> Vec<Proposal<D>> {
let candidates = take(&mut self.certification_candidates);
candidates
.into_iter()
.filter_map(|view| {
if view <= self.last_finalized {
return None;
}
self.views.get_mut(&view)?.try_certify()
})
.collect()
}
pub fn certified(&mut self, view: View, is_success: bool) -> Option<Notarization<S, D>> {
let round = self.views.get_mut(&view)?;
round.certified(is_success);
self.outstanding_certifications.remove(&view);
let notarization = round
.notarization()
.cloned()
.expect("notarization must exist for certified view");
if is_success {
self.enter_view(view.next());
} else {
self.expire_round(view);
}
Some(notarization)
}
pub fn prune(&mut self) -> Vec<View> {
let min = self.min_active();
let kept = self.views.split_off(&min);
let removed = replace(&mut self.views, kept).into_keys().collect();
let _ = self.tracked_views.try_set(self.views.len());
removed
}
fn is_certified(&self, view: View) -> Option<&D> {
if view == GENESIS_VIEW {
return Some(self.genesis.as_ref().expect("genesis must be present"));
}
let round = self.views.get(&view)?;
if round.finalization().is_some() || round.is_certified() {
return Some(&round.proposal().expect("proposal must exist").payload);
}
None
}
fn is_nullified(&self, view: View) -> bool {
if view == GENESIS_VIEW {
return false;
}
let round = match self.views.get(&view) {
Some(round) => round,
None => return false,
};
round.nullification().is_some()
}
#[cfg(test)]
pub fn is_certify_aborted(&self, view: View) -> bool {
self.views
.get(&view)
.is_some_and(|round| round.is_certify_aborted())
}
fn find_parent(&self, view: View) -> Result<(View, D), View> {
let mut cursor = view.previous().unwrap_or(GENESIS_VIEW);
loop {
if let Some(parent) = self.is_certified(cursor) {
return Ok((cursor, *parent));
}
if !self.is_nullified(cursor) {
return Err(cursor);
}
cursor = cursor.previous().expect("cursor must not wrap");
}
}
fn parent_payload(&self, proposal: &Proposal<D>) -> Option<D> {
let (view, parent) = (proposal.view(), proposal.parent);
if view <= parent {
return None;
}
if parent < self.last_finalized {
return None;
}
if !View::range(parent.next(), view).all(|v| self.is_nullified(v)) {
return None;
}
self.is_certified(parent).copied()
}
pub fn parent_certificate(&mut self, view: View) -> Option<Certificate<S, D>> {
let parent = {
let view = self.views.get(&view)?.proposal()?.parent;
self.views.get(&view)?
};
if let Some(f) = parent.finalization().cloned() {
return Some(Certificate::Finalization(f));
}
if let Some(n) = parent.notarization().cloned() {
return Some(Certificate::Notarization(n));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simplex::{
elector::RoundRobin,
scheme::ed25519,
types::{Finalization, Finalize, Notarization, Notarize, Nullification, Nullify, Proposal},
};
use commonware_cryptography::{certificate::mocks::Fixture, sha256::Digest as Sha256Digest};
use commonware_parallel::Sequential;
use commonware_runtime::{deterministic, Runner};
use commonware_utils::futures::AbortablePool;
use std::time::Duration;
fn test_genesis() -> Sha256Digest {
Sha256Digest::from([0u8; 32])
}
#[test]
fn certificate_candidates_respect_force_flag() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let mut state = State::new(
context,
Config {
scheme: verifier.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(11),
activity_timeout: ViewDelta::new(6),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
},
);
state.set_genesis(test_genesis());
let notarize_view = View::new(3);
let notarize_round = Rnd::new(Epoch::new(11), notarize_view);
let notarize_proposal =
Proposal::new(notarize_round, GENESIS_VIEW, Sha256Digest::from([50u8; 32]));
let notarize_votes: Vec<_> = schemes
.iter()
.map(|scheme| Notarize::sign(scheme, notarize_proposal.clone()).unwrap())
.collect();
let notarization =
Notarization::from_notarizes(&verifier, notarize_votes.iter(), &Sequential)
.expect("notarization");
state.add_notarization(notarization);
assert!(state.broadcast_notarization(notarize_view).is_some());
assert!(state.broadcast_notarization(notarize_view).is_none());
assert!(state.notarization(notarize_view).is_some());
let nullify_view = View::new(4);
let nullify_round = Rnd::new(Epoch::new(11), nullify_view);
let nullify_votes: Vec<_> = schemes
.iter()
.map(|scheme| {
Nullify::sign::<Sha256Digest>(scheme, nullify_round).expect("nullify")
})
.collect();
let nullification =
Nullification::from_nullifies(&verifier, &nullify_votes, &Sequential)
.expect("nullification");
state.add_nullification(nullification);
assert!(state.broadcast_nullification(nullify_view).is_some());
assert!(state.broadcast_nullification(nullify_view).is_none());
assert!(state.nullification(nullify_view).is_some());
let finalize_view = View::new(5);
let finalize_round = Rnd::new(Epoch::new(11), finalize_view);
let finalize_proposal =
Proposal::new(finalize_round, GENESIS_VIEW, Sha256Digest::from([51u8; 32]));
let finalize_votes: Vec<_> = schemes
.iter()
.map(|scheme| Finalize::sign(scheme, finalize_proposal.clone()).unwrap())
.collect();
let finalization =
Finalization::from_finalizes(&verifier, finalize_votes.iter(), &Sequential)
.expect("finalization");
state.add_finalization(finalization);
assert!(state.broadcast_finalization(finalize_view).is_some());
assert!(state.broadcast_finalization(finalize_view).is_none());
assert!(state.finalization(finalize_view).is_some());
});
}
#[test]
fn timeout_helpers_reuse_and_reset_deadlines() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture { schemes, .. } = ed25519::fixture(&mut context, &namespace, 4);
let local_scheme = schemes[0].clone(); let retry = Duration::from_secs(3);
let cfg = Config {
scheme: local_scheme.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(4),
activity_timeout: ViewDelta::new(2),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: retry,
};
let mut state = State::new(context.clone(), cfg);
state.set_genesis(test_genesis());
let first = state.next_timeout_deadline();
let second = state.next_timeout_deadline();
assert_eq!(first, second, "cached deadline should be reused");
let (was_retry, _, _) = state.handle_timeout();
assert!(!was_retry, "first timeout is not a retry");
context.sleep(Duration::from_secs(2)).await;
let later = context.current();
let third = state.next_timeout_deadline();
assert_eq!(third, later + retry, "new retry scheduled after timeout");
let fourth = state.next_timeout_deadline();
assert_eq!(fourth, third, "retry deadline should be set");
context.sleep(Duration::from_secs(10)).await;
let fifth = state.next_timeout_deadline();
assert_eq!(fifth, later + retry, "retry deadline should be set");
let (was_retry, _, _) = state.handle_timeout();
assert!(was_retry, "subsequent timeout should be treated as retry");
let sixth = state.next_timeout_deadline();
let later = context.current();
assert_eq!(sixth, later + retry, "retry deadline should be set");
});
}
#[test]
fn round_prunes_with_min_active() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let cfg = Config {
scheme: schemes[0].clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(7),
activity_timeout: ViewDelta::new(10),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
for view in 0..5 {
state.create_round(View::new(view));
}
let proposal_a = Proposal::new(
Rnd::new(Epoch::new(1), View::new(20)),
GENESIS_VIEW,
Sha256Digest::from([1u8; 32]),
);
let finalization_votes: Vec<_> = schemes
.iter()
.map(|scheme| Finalize::sign(scheme, proposal_a.clone()).unwrap())
.collect();
let finalization =
Finalization::from_finalizes(&verifier, finalization_votes.iter(), &Sequential)
.expect("finalization");
state.add_finalization(finalization);
let removed = state.prune();
assert_eq!(
removed,
vec![
View::new(0),
View::new(1),
View::new(2),
View::new(3),
View::new(4)
]
);
assert_eq!(state.views.len(), 2); });
}
#[test]
fn parent_payload_returns_parent_digest() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let local_scheme = schemes[2].clone(); let cfg = Config {
scheme: local_scheme,
elector: <RoundRobin>::default(),
epoch: Epoch::new(4),
activity_timeout: ViewDelta::new(2),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let parent_view = View::new(1);
let parent_payload = Sha256Digest::from([1u8; 32]);
let parent_proposal = Proposal::new(
Rnd::new(Epoch::new(1), parent_view),
GENESIS_VIEW,
parent_payload,
);
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), View::new(2)),
parent_view,
Sha256Digest::from([9u8; 32]),
);
assert!(state.parent_payload(&proposal).is_none());
let notarization_votes: Vec<_> = schemes
.iter()
.map(|scheme| Notarize::sign(scheme, parent_proposal.clone()).unwrap())
.collect();
let notarization =
Notarization::from_notarizes(&verifier, notarization_votes.iter(), &Sequential)
.unwrap();
state.add_notarization(notarization);
assert!(state.parent_payload(&proposal).is_none());
let mut pool = AbortablePool::<()>::default();
let handle = pool.push(futures::future::pending());
state.set_certify_handle(parent_view, handle);
state.certified(parent_view, true);
let digest = state.parent_payload(&proposal).expect("parent payload");
assert_eq!(digest, parent_payload);
});
}
#[test]
fn parent_certificate_prefers_finalization() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let local_scheme = schemes[1].clone(); let cfg = Config {
scheme: local_scheme,
elector: <RoundRobin>::default(),
epoch: Epoch::new(7),
activity_timeout: ViewDelta::new(3),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let parent_round = Rnd::new(state.epoch(), View::new(1));
let parent_proposal =
Proposal::new(parent_round, GENESIS_VIEW, Sha256Digest::from([11u8; 32]));
let notarize_votes: Vec<_> = schemes
.iter()
.map(|scheme| Notarize::sign(scheme, parent_proposal.clone()).unwrap())
.collect();
let notarization =
Notarization::from_notarizes(&verifier, notarize_votes.iter(), &Sequential)
.expect("notarization");
state.add_notarization(notarization.clone());
let proposal = Proposal::new(
Rnd::new(state.epoch(), View::new(2)),
View::new(1),
Sha256Digest::from([22u8; 32]),
);
state.proposed(proposal);
let cert = state.parent_certificate(View::new(2)).unwrap();
assert!(matches!(cert, Certificate::Notarization(n) if n == notarization));
let finalize_votes: Vec<_> = schemes
.iter()
.map(|scheme| Finalize::sign(scheme, parent_proposal.clone()).unwrap())
.collect();
let finalization =
Finalization::from_finalizes(&verifier, finalize_votes.iter(), &Sequential)
.expect("finalization");
state.add_finalization(finalization.clone());
let cert = state.parent_certificate(View::new(2)).unwrap();
assert!(matches!(cert, Certificate::Finalization(f) if f == finalization));
});
}
#[test]
fn parent_payload_errors_without_nullification() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let cfg = Config {
scheme: verifier.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
activity_timeout: ViewDelta::new(5),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let parent_view = View::new(1);
let parent_proposal = Proposal::new(
Rnd::new(Epoch::new(1), parent_view),
GENESIS_VIEW,
Sha256Digest::from([2u8; 32]),
);
let notarization_votes: Vec<_> = schemes
.iter()
.map(|scheme| Notarize::sign(scheme, parent_proposal.clone()).unwrap())
.collect();
let notarization =
Notarization::from_notarizes(&verifier, notarization_votes.iter(), &Sequential)
.unwrap();
state.add_notarization(notarization);
state.create_round(View::new(2));
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), View::new(3)),
parent_view,
Sha256Digest::from([3u8; 32]),
);
assert!(state.parent_payload(&proposal).is_none());
});
}
#[test]
fn parent_payload_returns_genesis_payload() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let cfg = Config {
scheme: verifier.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
activity_timeout: ViewDelta::new(5),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let nullify_votes: Vec<_> = schemes
.iter()
.map(|scheme| {
Nullify::sign::<Sha256Digest>(scheme, Rnd::new(Epoch::new(1), View::new(1)))
.unwrap()
})
.collect();
let nullification =
Nullification::from_nullifies(&verifier, &nullify_votes, &Sequential).unwrap();
state.add_nullification(nullification);
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), View::new(2)),
GENESIS_VIEW,
Sha256Digest::from([8u8; 32]),
);
let genesis = Sha256Digest::from([0u8; 32]);
let digest = state.parent_payload(&proposal).expect("genesis payload");
assert_eq!(digest, genesis);
});
}
#[test]
fn parent_payload_rejects_parent_before_finalized() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let cfg = Config {
scheme: verifier.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
activity_timeout: ViewDelta::new(5),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let proposal_a = Proposal::new(
Rnd::new(Epoch::new(1), View::new(3)),
GENESIS_VIEW,
Sha256Digest::from([1u8; 32]),
);
let finalization_votes: Vec<_> = schemes
.iter()
.map(|scheme| Finalize::sign(scheme, proposal_a.clone()).unwrap())
.collect();
let finalization =
Finalization::from_finalizes(&verifier, finalization_votes.iter(), &Sequential)
.expect("finalization");
state.add_finalization(finalization);
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), View::new(4)),
View::new(2),
Sha256Digest::from([6u8; 32]),
);
assert!(state.parent_payload(&proposal).is_none());
});
}
#[test]
fn replay_restores_conflict_state() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let mut scheme_iter = schemes.into_iter();
let local_scheme = scheme_iter.next().unwrap();
let other_schemes: Vec<_> = scheme_iter.collect();
let epoch: Epoch = Epoch::new(3);
let mut state = State::new(
context.clone(),
Config {
scheme: local_scheme.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
activity_timeout: ViewDelta::new(5),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
},
);
state.set_genesis(test_genesis());
let view = View::new(4);
let round = Rnd::new(epoch, view);
let proposal_a = Proposal::new(round, GENESIS_VIEW, Sha256Digest::from([21u8; 32]));
let proposal_b = Proposal::new(round, GENESIS_VIEW, Sha256Digest::from([22u8; 32]));
let local_vote = Notarize::sign(&local_scheme, proposal_a).unwrap();
state.replay(&Artifact::Notarize(local_vote.clone()));
let votes_b: Vec<_> = other_schemes
.iter()
.take(3)
.map(|scheme| Notarize::sign(scheme, proposal_b.clone()).unwrap())
.collect();
let conflicting = Notarization::from_notarizes(&verifier, votes_b.iter(), &Sequential)
.expect("certificate");
state.add_notarization(conflicting.clone());
state.replay(&Artifact::Notarization(conflicting.clone()));
assert!(state.construct_finalize(view).is_none());
let mut restarted = State::new(
context,
Config {
scheme: local_scheme,
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
activity_timeout: ViewDelta::new(5),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
},
);
restarted.set_genesis(test_genesis());
restarted.replay(&Artifact::Notarize(local_vote));
restarted.add_notarization(conflicting.clone());
restarted.replay(&Artifact::Notarization(conflicting));
assert!(restarted.construct_finalize(view).is_none());
});
}
#[test]
fn certification_lifecycle() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut context, &namespace, 4);
let cfg = Config {
scheme: verifier.clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
activity_timeout: ViewDelta::new(10),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let make_notarization = |view: View| {
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), view),
GENESIS_VIEW,
Sha256Digest::from([view.get() as u8; 32]),
);
let votes: Vec<_> = schemes
.iter()
.map(|s| Notarize::sign(s, proposal.clone()).unwrap())
.collect();
Notarization::from_notarizes(&verifier, votes.iter(), &Sequential).unwrap()
};
let make_finalization = |view: View| {
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), view),
GENESIS_VIEW,
Sha256Digest::from([view.get() as u8; 32]),
);
let votes: Vec<_> = schemes
.iter()
.map(|s| Finalize::sign(s, proposal.clone()).unwrap())
.collect();
Finalization::from_finalizes(&verifier, votes.iter(), &Sequential).unwrap()
};
let mut pool = AbortablePool::<()>::default();
for i in 3..=8u64 {
state.add_notarization(make_notarization(View::new(i)));
}
let candidates = state.certify_candidates();
assert_eq!(candidates.len(), 6);
for i in [3u64, 4, 5, 7] {
let handle = pool.push(futures::future::pending());
state.set_certify_handle(View::new(i), handle);
}
assert!(state.certify_candidates().is_empty());
let notarization = state.certified(View::new(7), true);
assert!(notarization.is_some());
assert!(!state.is_certify_aborted(View::new(7)));
state.add_finalization(make_finalization(View::new(5)));
assert!(state.is_certify_aborted(View::new(3)));
assert!(state.is_certify_aborted(View::new(4)));
assert!(state.is_certify_aborted(View::new(5)));
assert!(!state.is_certify_aborted(View::new(7)));
assert!(!state.is_certify_aborted(View::new(6)));
assert!(!state.is_certify_aborted(View::new(8)));
assert!(state.certify_candidates().is_empty());
state.add_notarization(make_notarization(View::new(9)));
let candidates = state.certify_candidates();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].round.view(), View::new(9));
let handle9 = pool.push(futures::future::pending());
state.set_certify_handle(View::new(9), handle9);
state.add_notarization(make_notarization(View::new(10)));
let candidates = state.certify_candidates();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].round.view(), View::new(10));
state.add_finalization(make_finalization(View::new(9)));
assert!(state.is_certify_aborted(View::new(9)));
state.add_notarization(make_notarization(View::new(11)));
let candidates = state.certify_candidates();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].round.view(), View::new(11));
});
}
#[test]
fn only_notarize_before_nullify() {
let runtime = deterministic::Runner::default();
runtime.start(|mut context| async move {
let namespace = b"ns".to_vec();
let Fixture { schemes, .. } = ed25519::fixture(&mut context, &namespace, 4);
let cfg = Config {
scheme: schemes[0].clone(),
elector: <RoundRobin>::default(),
epoch: Epoch::new(1),
activity_timeout: ViewDelta::new(5),
leader_timeout: Duration::from_secs(1),
notarization_timeout: Duration::from_secs(2),
nullify_retry: Duration::from_secs(3),
};
let mut state = State::new(context, cfg);
state.set_genesis(test_genesis());
let view = state.current_view();
let proposal = Proposal::new(
Rnd::new(Epoch::new(1), view),
GENESIS_VIEW,
Sha256Digest::from([1u8; 32]),
);
state.set_proposal(view, proposal);
assert!(state.try_verify().is_some());
assert!(state.verified(view));
assert!(!state.handle_timeout().0);
assert!(state.construct_notarize(view).is_none());
});
}
}