use super::harness::{ CommitteeSchedule, Registration, RegistrationRole, ValidatorState, final_height, }; use crate::{ dkg::{ ReshareBlock, types::{EpochOutcome, Payload}, }, simulate::{ exit::ExitCondition, processed::ProcessedHeight, property::Property, tracker::ProgressTracker, }, }; use commonware_codec::{Encode as _, FixedSize}; use commonware_consensus::types::{Epoch, Height}; use commonware_cryptography::{bls12381::primitives::variant::MinPk, ed25519, transcript::Summary}; use commonware_utils::sync::Mutex; use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc}; #[derive(Clone)] pub(super) struct AllActiveProcessedHeight { required: Height, participants: usize, } impl AllActiveProcessedHeight { pub(super) const fn new(required: Height, participants: usize) -> Self { Self { required, participants, } } } impl ExitCondition for AllActiveProcessedHeight { fn name(&self) -> &str { "all_active_processed_height" } fn requires_polling(&self) -> bool { true } fn reached<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], _target_count: usize, ) -> Pin> + Send + 'a>> { Box::pin(async move { if states.len() != self.participants { return Ok(false); } for state in states { if state.processed_height().await < self.required.get() { return Ok(false); } } Ok(true) }) } } #[derive(Clone)] pub(super) struct SignerRegistered; impl Property for SignerRegistered { fn name(&self) -> &str { "signer_registered" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { let found = states.iter().any(|state| { state .registrations() .iter() .any(|registration| registration.role == RegistrationRole::Signer) }); if found { Ok(()) } else { Err("no node registered a signing scheme".to_string()) } }) } } #[derive(Clone)] pub(super) struct BoundaryEpochInfos { epochs: u64, no_reveals: bool, min_successes: u64, expected_failures: Vec, } impl BoundaryEpochInfos { pub(super) const fn new(epochs: u64) -> Self { Self { epochs, no_reveals: false, min_successes: epochs, expected_failures: Vec::new(), } } pub(super) const fn with_no_reveals(mut self) -> Self { self.no_reveals = true; self } pub(super) const fn with_min_successes(mut self, min_successes: u64) -> Self { self.min_successes = min_successes; self } pub(super) fn with_expected_failures( mut self, failures: impl IntoIterator, ) -> Self { self.expected_failures = failures.into_iter().collect(); self } } impl Property for BoundaryEpochInfos { fn name(&self) -> &str { "boundary_epoch_infos" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { for state in states { let mut checked = 0; let mut successes = 0; let mut expected_failures = 0; let state_sync_height = state.state_sync_height(); for epoch in 0..self.epochs { let height = final_height(epoch); if state_sync_height.is_some_and(|synced| height.get() < synced) { continue; } checked += 1; let expect_failure = self.expected_failures.contains(&epoch); if expect_failure { expected_failures += 1; } let Some(block) = state.marshal.get_block(height).await else { return Err(format!( "missing finalized boundary block at height {height}" )); }; match block.payload() { Some(Payload::EpochInfo(info)) if info.epoch == Epoch::new(epoch + 1) => { if info.outcome == EpochOutcome::Success { if expect_failure { return Err(format!( "boundary at height {height} succeeded, expected failure" )); } successes += 1; if self.no_reveals && !info.output.revealed().is_empty() { return Err(format!( "epoch {epoch} revealed {} shares", info.output.revealed().len() )); } continue; } if expect_failure { continue; } if self.min_successes == self.epochs { return Err(format!( "boundary at height {height} carried epoch info {:?}", info.outcome )); } } Some(Payload::EpochInfo(info)) => { return Err(format!( "boundary at height {height} carried epoch info for {}, expected {}", info.epoch, Epoch::new(epoch + 1) )); } Some(_) => { return Err(format!( "boundary at height {height} carried non-epoch-info DKG payload" )); } None => { return Err(format!( "boundary at height {height} carried no DKG payload" )); } } } let required = if !self.expected_failures.is_empty() { checked - expected_failures } else if self.min_successes == self.epochs { checked } else { self.min_successes.min(checked) }; if successes < required { return Err(format!( "observed {successes} successful epochs, expected at least {required}" )); } } Ok(()) }) } } #[derive(Clone)] pub(super) struct EpochInfoContinuity { epochs: u64, schedule: Arc, } impl EpochInfoContinuity { pub(super) const fn new(epochs: u64, schedule: Arc) -> Self { Self { epochs, schedule } } } impl Property for EpochInfoContinuity { fn name(&self) -> &str { "epoch_info_continuity" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { for epoch in 0..self.epochs { let previous_height = Epoch::new(epoch) .previous() .map(|epoch| final_height(epoch.get())) .unwrap_or(Height::zero()); let previous = boundary_info(states, previous_height).await?; let height = final_height(epoch); let info = boundary_info(states, height).await?; let expected_epoch = Epoch::new(epoch + 1); if info.epoch != expected_epoch { return Err(format!( "boundary at height {height} carried epoch info for {}, expected {expected_epoch}", info.epoch )); } if info.players != previous.next_players { return Err(format!( "boundary at height {height} players did not match previous next players" )); } let expected_next_players = self.schedule.players(expected_epoch.next()); if info.next_players != expected_next_players { return Err(format!( "boundary at height {height} next players did not match schedule for {}", expected_epoch.next() )); } match info.outcome { EpochOutcome::Success => { if info.output.players() != &previous.players { return Err(format!( "successful boundary at height {height} output players did not match previous players" )); } } EpochOutcome::Failure => { if info.output != previous.output { return Err(format!( "failed boundary at height {height} did not carry forward output" )); } } } } Ok(()) }) } } async fn boundary_info( states: &[&ValidatorState], height: Height, ) -> Result< crate::dkg::types::EpochInfo, String, > { for state in states { let Some(block) = state.marshal.get_block(height).await else { continue; }; let Some(Payload::EpochInfo(info)) = block.payload() else { return Err(format!( "boundary at height {height} did not carry epoch info" )); }; return Ok(info); } Err(format!( "missing finalized boundary block at height {height}" )) } #[derive(Clone)] pub(super) struct BoundaryOutputMode { epoch: Epoch, mode: u8, } impl BoundaryOutputMode { pub(super) const fn new(epoch: Epoch, mode: u8) -> Self { Self { epoch, mode } } } impl Property for BoundaryOutputMode { fn name(&self) -> &str { "boundary_output_mode" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { let height = final_height(self.epoch.get()); let info = boundary_info(states, height).await?; let encoded = info.output.encode(); let Some(mode) = encoded.get(::SIZE).copied() else { return Err("encoded output missing sharing mode".to_string()); }; if mode != self.mode { return Err(format!( "boundary at height {height} used sharing mode {mode}, expected {}", self.mode )); } Ok(()) }) } } #[derive(Clone)] pub(super) struct FailedCeremonyCarryOver { epoch: Epoch, schedule: CommitteeSchedule, } impl FailedCeremonyCarryOver { pub(super) const fn new(epoch: Epoch, schedule: CommitteeSchedule) -> Self { Self { epoch, schedule } } } impl Property for FailedCeremonyCarryOver { fn name(&self) -> &str { "failed_ceremony_carry_over" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { // The previous boundary may predate a node's state-sync floor and // be absent from its marshal, so read it from whichever node // retains it. let previous_height = self .epoch .previous() .map(|epoch| final_height(epoch.get())) .unwrap_or(Height::zero()); let previous = boundary_info(states, previous_height).await?; // The carry-over boundary itself must be visible to a state-synced // node whose floor covers it: demand that node's view of the // boundary rather than falling back to a peer that retains it. let height = final_height(self.epoch.get()); let synced = states.iter().find(|state| { state .state_sync_height() .is_some_and(|floor| floor <= height.get()) }); let info = match synced { Some(synced) => { let Some(block) = synced.marshal.get_block(height).await else { return Err(format!( "state-synced node missing boundary block at height {height}" )); }; let Some(Payload::EpochInfo(info)) = block.payload() else { return Err(format!( "boundary at height {height} did not carry epoch info" )); }; info } None => boundary_info(states, height).await?, }; let expected_epoch = self.epoch.next(); if info.epoch != expected_epoch { return Err(format!( "boundary at height {height} carried epoch info for {}, expected {expected_epoch}", info.epoch )); } if info.outcome != EpochOutcome::Failure { return Err(format!( "boundary at height {height} carried {:?}, expected failure", info.outcome )); } if info.output != previous.output { return Err("failed ceremony did not carry forward output".to_string()); } if info.players != previous.next_players { return Err("failed ceremony did not advance to previous next players".to_string()); } let expected_next_players = self.schedule.players(expected_epoch.next()); if info.next_players != expected_next_players { return Err(format!( "failed ceremony did not refresh next players for {}", expected_epoch.next() )); } for state in states { let expected = if previous .output .players() .position(state.public_key()) .is_some() { RegistrationRole::Signer } else { RegistrationRole::Verifier }; let registrations = state.registrations(); let registered = registrations.iter().any(|registration| { registration.epoch == expected_epoch && registration.role == expected }); if !registered { return Err(format!( "node {} did not register {expected:?} for carried epoch {expected_epoch}: {registrations:?}", state.public_key() )); } } Ok(()) }) } } #[derive(Clone)] pub(super) struct AllNodesRecovered { public_keys: Vec, } impl AllNodesRecovered { pub(super) fn new(public_keys: Vec) -> Self { Self { public_keys } } } impl Property for AllNodesRecovered { fn name(&self) -> &str { "all_nodes_recovered" } fn check<'a>( &'a self, tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { if states.len() != self.public_keys.len() { return Err(format!( "active states {}, expected {}", states.len(), self.public_keys.len() )); } if tracker.tracked_count() != self.public_keys.len() { return Err(format!( "tracker saw {} nodes, expected {}", tracker.tracked_count(), self.public_keys.len() )); } for public_key in &self.public_keys { let recovered = states.iter().any(|state| state.public_key() == public_key); if !recovered { return Err(format!("node {public_key} was not active at shutdown")); } } Ok(()) }) } } #[derive(Clone)] pub(super) struct SchemesRegistered { public_keys: Vec, epoch: Epoch, } impl SchemesRegistered { pub(super) fn new(public_keys: Vec, epoch: Epoch) -> Self { Self { public_keys, epoch } } } impl Property for SchemesRegistered { fn name(&self) -> &str { "schemes_registered" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { let Some(ceremony_epoch) = self.epoch.previous() else { return Err("epoch zero has no reshare output".to_string()); }; let height = final_height(ceremony_epoch.get()); let Some(reference) = states.first() else { return Err("no active validator states".to_string()); }; let Some(block) = reference.marshal.get_block(height).await else { return Err(format!( "missing finalized boundary block at height {height}" )); }; let Some(Payload::EpochInfo(info)) = block.payload() else { return Err(format!( "boundary at height {height} did not carry epoch info" )); }; if info.epoch != self.epoch { return Err(format!( "boundary at height {height} carried epoch info for {}, expected {}", info.epoch, self.epoch )); } for public_key in &self.public_keys { let expected = if info.output.players().position(public_key).is_some() { RegistrationRole::Signer } else { RegistrationRole::Verifier }; let Some(registrations) = states .iter() .find(|state| state.public_key() == public_key) .map(|state| state.registrations()) else { return Err(format!("node {public_key} was not active at shutdown")); }; let registered = registrations.iter().any(|registration| { registration.epoch == self.epoch && registration.role == expected }); if !registered { return Err(format!( "node {public_key} did not register {expected:?} for epoch {}: {registrations:?}", self.epoch )); } } Ok(()) }) } } #[derive(Clone)] pub(super) struct StateSyncedSigner { public_key: ed25519::PublicKey, min_epoch: Epoch, registrations: Arc>>>, state_syncs: Arc>>, } impl StateSyncedSigner { pub(super) fn new( public_key: ed25519::PublicKey, min_epoch: Epoch, registrations: Arc>>>, state_syncs: Arc>>, ) -> Self { Self { public_key, min_epoch, registrations, state_syncs, } } } impl Property for StateSyncedSigner { fn name(&self) -> &str { "state_synced_signer" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, _states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { let state_syncs = self.state_syncs.lock(); let Some(height) = state_syncs.get(&self.public_key).copied() else { let keys = state_syncs.keys().cloned().collect::>(); return Err(format!( "node {} did not state sync, recorded syncs: {keys:?}", self.public_key )); }; let signed = self .registrations .lock() .get(&self.public_key) .into_iter() .flatten() .any(|registration| { registration.role == RegistrationRole::Signer && registration.epoch >= self.min_epoch }); if signed { Ok(()) } else { Err(format!( "node {} state synced at height {height} but never registered as signer at or after epoch {}", self.public_key, self.min_epoch )) } }) } } #[derive(Clone)] pub(super) struct StateSyncedAtHeight { public_key: ed25519::PublicKey, min_height: Height, max_height: Height, state_syncs: Arc>>, } impl StateSyncedAtHeight { pub(super) const fn new( public_key: ed25519::PublicKey, min_height: Height, max_height: Height, state_syncs: Arc>>, ) -> Self { Self { public_key, min_height, max_height, state_syncs, } } } impl Property for StateSyncedAtHeight { fn name(&self) -> &str { "state_synced_at_height" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, _states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { let state_syncs = self.state_syncs.lock(); let Some(height) = state_syncs.get(&self.public_key).copied() else { return Err(format!("node {} did not state sync", self.public_key)); }; if height < self.min_height.get() || height > self.max_height.get() { return Err(format!( "node {} state synced at height {height}, expected {}..={}", self.public_key, self.min_height, self.max_height )); } Ok(()) }) } } #[derive(Clone)] pub(super) struct StateSyncMembership { schedule: Arc, public_key: ed25519::PublicKey, next_player_epoch: Epoch, } impl StateSyncMembership { pub(super) fn new( schedule: Arc, public_key: ed25519::PublicKey, next_player_epoch: Epoch, ) -> Self { Self { schedule, public_key, next_player_epoch, } } } impl Property for StateSyncMembership { fn name(&self) -> &str { "state_sync_membership" } fn check<'a>( &'a self, _tracker: &'a ProgressTracker, _states: &'a [&'a ValidatorState], ) -> Pin> + Send + 'a>> { Box::pin(async move { let player_epoch = self.next_player_epoch.next(); if let Some(previous) = self.next_player_epoch.previous() && self .schedule .players(previous) .position(&self.public_key) .is_some() { return Err(format!( "node {} was present before state-sync epoch {}", self.public_key, self.next_player_epoch )); } if self .schedule .players(self.next_player_epoch) .position(&self.public_key) .is_some() { return Err(format!( "node {} was a player in state-sync epoch {}", self.public_key, self.next_player_epoch )); } if self .schedule .players(player_epoch) .position(&self.public_key) .is_none() { return Err(format!( "node {} was not a next player in epoch {}", self.public_key, self.next_player_epoch )); } Ok(()) }) } }