use crate::{
simplex::elector::{Config as ElectorConfig, Elector as Elected},
types::{Participant, Round, View},
};
use commonware_cryptography::certificate::Scheme;
use commonware_p2p::simulated::SplitTarget;
use commonware_utils::ordered::Set;
use rand::{seq::SliceRandom, Rng, RngExt as _};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
const MAX_PARTICIPANTS: usize = u64::BITS as usize;
type Cells = Vec<usize>;
type Parts = Vec<usize>;
type ParticipantIndex = usize;
type Compromised = Vec<ParticipantIndex>;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RoundScenario {
leader: ParticipantIndex,
primary_mask: u64,
secondary_mask: u64,
}
impl RoundScenario {
pub const fn leader(&self) -> usize {
self.leader
}
fn partitions<P: Clone>(&self, participants: &[P]) -> (Vec<P>, Vec<P>) {
let primary = partition_for_mask(self.primary_mask, participants);
let secondary = partition_for_mask(self.secondary_mask, participants);
(primary, secondary)
}
fn route<P: PartialEq>(&self, sender: &P, participants: &[P]) -> SplitTarget {
let sender_idx = participants
.iter()
.position(|participant| participant == sender)
.expect("sender missing from runtime participant list");
let in_primary = mask_contains(self.primary_mask, sender_idx);
let in_secondary = mask_contains(self.secondary_mask, sender_idx);
match (in_primary, in_secondary) {
(true, true) => SplitTarget::Both,
(true, false) => SplitTarget::Primary,
(false, true) => SplitTarget::Secondary,
(false, false) => SplitTarget::None,
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Scenario {
rounds: Vec<RoundScenario>,
}
impl Scenario {
pub fn rounds(&self) -> &[RoundScenario] {
&self.rounds
}
pub fn partitions<P: Clone>(&self, view: View, participants: &[P]) -> (Vec<P>, Vec<P>) {
let idx = view.get().saturating_sub(1) as usize;
if let Some(round) = self.rounds.get(idx) {
return round.partitions(participants);
}
(participants.to_vec(), participants.to_vec())
}
pub fn route<P: PartialEq>(&self, view: View, sender: &P, participants: &[P]) -> SplitTarget {
let idx = view.get().saturating_sub(1) as usize;
if let Some(round) = self.rounds.get(idx) {
return round.route(sender, participants);
}
SplitTarget::Both
}
}
fn route_with_groups<P: PartialEq>(sender: &P, primary: &[P], secondary: &[P]) -> SplitTarget {
let in_primary = primary.contains(sender);
let in_secondary = secondary.contains(sender);
match (in_primary, in_secondary) {
(true, true) => SplitTarget::Both,
(true, false) => SplitTarget::Primary,
(false, true) => SplitTarget::Secondary,
(false, false) => panic!("sender not in any partition"),
}
}
pub fn view_partitions<P: Clone>(view: View, participants: &[P]) -> (Vec<P>, Vec<P>) {
let split = (view.get() as usize) % participants.len();
let (primary, secondary) = participants.split_at(split);
(primary.to_vec(), secondary.to_vec())
}
pub fn view_route<P: Clone + PartialEq>(view: View, sender: &P, participants: &[P]) -> SplitTarget {
let (primary, secondary) = view_partitions(view, participants);
route_with_groups(sender, &primary, &secondary)
}
#[derive(Clone, Debug)]
pub struct Elector<C> {
fallback: C,
round_leaders: Arc<[Participant]>,
}
impl<C: Default> Default for Elector<C> {
fn default() -> Self {
Self {
fallback: C::default(),
round_leaders: Arc::from(Vec::new()),
}
}
}
impl<C> Elector<C> {
pub fn new(fallback: C, scenario: &Scenario, participants: usize) -> Self {
let round_leaders: Vec<_> = scenario
.rounds()
.iter()
.map(|round| {
assert!(
round.leader() < participants,
"scenario leader out of bounds"
);
Participant::from_usize(round.leader())
})
.collect();
Self {
fallback,
round_leaders: Arc::from(round_leaders),
}
}
}
#[derive(Clone, Debug)]
pub struct ElectorState<E> {
fallback: E,
round_leaders: Arc<[Participant]>,
}
impl<S, C> ElectorConfig<S> for Elector<C>
where
S: Scheme,
C: ElectorConfig<S>,
{
type Elector = ElectorState<C::Elector>;
fn build(self, participants: &Set<S::PublicKey>) -> Self::Elector {
ElectorState {
fallback: self.fallback.build(participants),
round_leaders: self.round_leaders,
}
}
}
impl<S, E> Elected<S> for ElectorState<E>
where
S: Scheme,
E: Elected<S>,
{
fn elect(&self, round: Round, certificate: Option<&S::Certificate>) -> Participant {
let idx = round.view().get().saturating_sub(1) as usize;
if let Some(&leader) = self.round_leaders.get(idx) {
return leader;
}
self.fallback.elect(round, certificate)
}
}
#[derive(Clone, Copy, Debug)]
pub enum Mode {
Sampled,
Sustained,
}
#[derive(Clone, Copy, Debug)]
pub struct Framework {
pub participants: usize,
pub faults: usize,
pub rounds: usize,
pub mode: Mode,
pub max_cases: usize,
}
#[derive(Clone, Debug)]
pub struct Case {
pub compromised: Compromised,
pub scenario: Scenario,
}
pub fn cases(rng: &mut impl Rng, framework: Framework) -> Vec<Case> {
assert!(framework.participants > 1, "participants must be > 1");
assert!(
framework.participants <= MAX_PARTICIPANTS,
"participants must be <= {MAX_PARTICIPANTS}"
);
assert!(framework.faults > 0, "faults must be > 0");
assert!(
framework.faults < framework.participants,
"faults must be less than participants"
);
assert!(framework.rounds > 0, "rounds must be > 0");
assert!(framework.max_cases > 0, "max_cases must be > 0");
let mut generator = ScenarioGenerator::new(framework.participants);
let scenarios = match framework.mode {
Mode::Sampled => generator.generate(rng, framework.rounds, framework.max_cases),
Mode::Sustained => {
let single_round = generator.generate(rng, 1, framework.max_cases);
single_round
.into_iter()
.map(|(s, cells)| {
let scenario = Scenario {
rounds: vec![s.rounds[0]; framework.rounds],
};
(scenario, cells)
})
.collect()
}
};
let mut result: Vec<Case> = scenarios
.iter()
.flat_map(|(scenario, residual_cells)| {
compromised_sets_for_cells(residual_cells, framework.faults)
.into_iter()
.map(move |compromised| Case {
compromised,
scenario: scenario.clone(),
})
})
.collect();
result.shuffle(rng);
result.truncate(framework.max_cases);
result
}
fn partition_for_mask<P: Clone>(mask: u64, participants: &[P]) -> Vec<P> {
let mut group = Vec::new();
for (idx, participant) in participants.iter().enumerate() {
if mask_contains(mask, idx) {
group.push(participant.clone());
}
}
group
}
#[inline]
const fn set_mask_bit(mask: &mut u64, idx: usize) {
*mask |= 1u64 << idx;
}
#[inline]
const fn set_mask_range(mask: &mut u64, start: usize, len: usize) {
if len == 0 {
return;
}
let range = if len == u64::BITS as usize {
u64::MAX
} else {
((1u64 << len) - 1) << start
};
*mask |= range;
}
#[inline]
const fn mask_contains(mask: u64, idx: usize) -> bool {
idx < MAX_PARTICIPANTS && (mask & (1u64 << idx)) != 0
}
fn cells_to_ranges(cells: &[usize]) -> Vec<(usize, usize)> {
let mut start = 0usize;
cells
.iter()
.copied()
.map(|size| {
let range = (start, size);
start += size;
range
})
.collect()
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct CellState {
boundaries: u64,
}
impl CellState {
fn initial(n: usize) -> Self {
assert!(
n <= MAX_PARTICIPANTS,
"participants must be <= {MAX_PARTICIPANTS}"
);
Self { boundaries: 0 }
}
#[cfg(test)]
fn from_cells(cells: &[usize]) -> Self {
let n = cells.iter().sum();
let mut state = Self::initial(n);
let mut end = 0usize;
for size in cells.iter().take(cells.len().saturating_sub(1)) {
end += size;
set_mask_bit(&mut state.boundaries, end - 1);
}
state
}
const fn boundary_after(&self, idx: usize) -> bool {
mask_contains(self.boundaries, idx)
}
fn to_cells(self, n: usize) -> Cells {
self.ranges(n).into_iter().map(|(_, len)| len).collect()
}
fn ranges(&self, n: usize) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut start = 0usize;
for idx in 0..n.saturating_sub(1) {
if self.boundary_after(idx) {
ranges.push((start, idx + 1 - start));
start = idx + 1;
}
}
ranges.push((start, n - start));
ranges
}
fn parts_in_cell(&self, start: usize, size: usize) -> Parts {
let end = start + size;
let mut parts = Vec::new();
let mut part_start = start;
for idx in start..end {
if idx + 1 == end || self.boundary_after(idx) {
parts.push(idx + 1 - part_start);
part_start = idx + 1;
}
}
parts
}
fn add_local_boundaries(&mut self, start: usize, n: usize, parts: &[usize]) {
let mut offset = 0usize;
for part in parts {
offset += part;
let end = start + offset;
if end < n {
set_mask_bit(&mut self.boundaries, end - 1);
}
}
}
}
#[derive(Clone, Copy, Debug)]
struct LocalRefinement {
parts: [usize; 5],
len: usize,
multiplicity: u128,
}
impl LocalRefinement {
fn new(parts: &[usize], multiplicity: u128) -> Self {
let mut stored = [0usize; 5];
stored[..parts.len()].copy_from_slice(parts);
Self {
parts: stored,
len: parts.len(),
multiplicity,
}
}
fn parts(&self) -> &[usize] {
&self.parts[..self.len]
}
}
#[derive(Clone, Copy, Debug)]
struct TransitionEdge {
next: CellState,
leader_cell: usize,
multiplicity: u128,
}
#[derive(Clone, Debug, Default)]
struct TransitionSet {
edges: Vec<TransitionEdge>,
overflowed: bool,
}
fn build_transitions(
ranges: &[(usize, usize)],
refinements_by_cell: &[Vec<LocalRefinement>],
leader_cell: usize,
cell_idx: usize,
next_state: CellState,
multiplicity: u128,
transitions: &mut TransitionSet,
) {
if cell_idx == ranges.len() {
transitions.edges.push(TransitionEdge {
next: next_state,
leader_cell,
multiplicity,
});
return;
}
let (start, _) = ranges[cell_idx];
let n = ranges
.last()
.map(|(start, len)| start + len)
.expect("transition ranges must be non-empty");
for refinement in refinements_by_cell[cell_idx].iter().copied() {
let Some(multiplicity) = multiplicity.checked_mul(refinement.multiplicity) else {
transitions.overflowed = true;
continue;
};
let mut next_state = next_state;
next_state.add_local_boundaries(start, n, refinement.parts());
build_transitions(
ranges,
refinements_by_cell,
leader_cell,
cell_idx + 1,
next_state,
multiplicity,
transitions,
);
}
}
#[derive(Clone, Copy, Debug)]
enum Role {
Outside,
Both,
Secondary,
Primary,
}
struct ScenarioGenerator {
n: usize,
transition_memo: HashMap<CellState, TransitionSet>,
local_memo: HashMap<(usize, bool), Vec<LocalRefinement>>,
}
impl ScenarioGenerator {
fn new(n: usize) -> Self {
Self {
n,
transition_memo: HashMap::new(),
local_memo: HashMap::new(),
}
}
fn generate(
&mut self,
rng: &mut impl Rng,
rounds: usize,
max_scenarios: usize,
) -> Vec<(Scenario, Cells)> {
let initial = CellState::initial(self.n);
let counts = self
.counts(&initial, rounds)
.expect("scenario space overflows u128; reduce rounds or participants");
let total = counts[rounds][&initial];
if total <= max_scenarios as u128 {
return (0..total)
.map(|idx| self.scenario_from_rank(&initial, rounds, idx, &counts))
.collect();
}
sample_unique_indices(rng, total, max_scenarios)
.into_iter()
.map(|idx| self.scenario_from_rank(&initial, rounds, idx, &counts))
.collect()
}
fn counts(
&mut self,
initial: &CellState,
rounds: usize,
) -> Option<Vec<HashMap<CellState, u128>>> {
let mut layers = Vec::with_capacity(rounds + 1);
layers.push(HashSet::from([*initial]));
for depth in 0..rounds {
let mut next_layer = HashSet::new();
for state in layers[depth].iter() {
self.ensure_transitions(state);
let transitions = self.transitions(state);
if transitions.overflowed {
return None;
}
for edge in &transitions.edges {
next_layer.insert(edge.next);
}
}
layers.push(next_layer);
}
let mut counts = vec![HashMap::new(); rounds + 1];
for state in layers[rounds].iter() {
counts[0].insert(*state, 1);
}
for remaining in 1..=rounds {
let depth = rounds - remaining;
for state in layers[depth].iter() {
self.ensure_transitions(state);
let transitions = self.transitions(state);
if transitions.overflowed {
return None;
}
let mut total = 0u128;
for edge in &transitions.edges {
let suffix = counts[remaining - 1]
.get(&edge.next)
.copied()
.expect("next state missing from count layer");
let edge_count = edge.multiplicity.checked_mul(suffix)?;
total = total.checked_add(edge_count)?;
}
counts[remaining].insert(*state, total);
}
}
Some(counts)
}
fn scenario_from_rank(
&mut self,
initial: &CellState,
rounds: usize,
mut rank: u128,
counts: &[HashMap<CellState, u128>],
) -> (Scenario, Cells) {
let mut scenario = Vec::with_capacity(rounds);
let mut state = *initial;
for remaining in (1..=rounds).rev() {
self.ensure_transitions(&state);
let transitions = self.transitions(&state);
assert!(
!transitions.overflowed,
"transition multiplicity should fit in u128"
);
let mut selected = false;
for edge in &transitions.edges {
let suffix = counts[remaining - 1]
.get(&edge.next)
.copied()
.expect("next state missing from count layer");
let subtree_count = edge
.multiplicity
.checked_mul(suffix)
.expect("canonical scenario count should fit in u128");
if rank < subtree_count {
let transition_rank = rank / suffix;
scenario.push(self.round_from_edge(&state, edge, transition_rank));
state = edge.next;
rank %= suffix;
selected = true;
break;
}
rank -= subtree_count;
}
assert!(selected, "canonical scenario rank out of bounds");
}
(Scenario { rounds: scenario }, state.to_cells(self.n))
}
fn ensure_transitions(&mut self, state: &CellState) {
if self.transition_memo.contains_key(state) {
return;
}
let ranges = state.ranges(self.n);
let mut transitions = TransitionSet::default();
for leader_cell in 0..ranges.len() {
let refinements: Vec<_> = ranges
.iter()
.enumerate()
.map(|(cell_idx, (_, size))| {
self.local_refinements(*size, cell_idx == leader_cell)
.to_vec()
})
.collect();
build_transitions(
&ranges,
&refinements,
leader_cell,
0,
CellState::initial(self.n),
1,
&mut transitions,
);
}
self.transition_memo.insert(*state, transitions);
}
fn transitions(&self, state: &CellState) -> &TransitionSet {
self.transition_memo
.get(state)
.expect("transition set should be memoized")
}
fn local_refinements(&mut self, size: usize, is_leader: bool) -> &[LocalRefinement] {
self.local_memo
.entry((size, is_leader))
.or_insert_with(|| build_local_refinements(size, is_leader))
.as_slice()
}
fn round_from_edge(
&self,
state: &CellState,
edge: &TransitionEdge,
mut rank: u128,
) -> RoundScenario {
let mut primary_mask = 0u64;
let mut secondary_mask = 0u64;
let mut leader = None;
for (cell_idx, (start, size)) in state.ranges(self.n).into_iter().enumerate() {
let parts = edge.next.parts_in_cell(start, size);
if cell_idx == edge.leader_cell {
let non_leader_len = parts.len() - 1;
let role_count = role_subset_count(non_leader_len);
let local_space = role_count * 3;
let local_rank = rank % local_space;
rank /= local_space;
let roles = roles_from_rank(non_leader_len, local_rank / 3);
apply_roles(
start,
&parts[..non_leader_len],
&roles[..non_leader_len],
&mut primary_mask,
&mut secondary_mask,
);
let leader_idx = start + size - 1;
match local_rank % 3 {
0 => set_mask_bit(&mut primary_mask, leader_idx),
1 => set_mask_bit(&mut secondary_mask, leader_idx),
2 => {
set_mask_bit(&mut primary_mask, leader_idx);
set_mask_bit(&mut secondary_mask, leader_idx);
}
_ => unreachable!("leader placement rank out of bounds"),
}
leader = Some(leader_idx);
} else {
let role_count = role_subset_count(parts.len());
let local_rank = rank % role_count;
rank /= role_count;
let roles = roles_from_rank(parts.len(), local_rank);
apply_roles(
start,
&parts,
&roles[..parts.len()],
&mut primary_mask,
&mut secondary_mask,
);
}
}
assert_eq!(rank, 0, "transition rank should be fully consumed");
RoundScenario {
leader: leader.expect("leader cell should assign a leader"),
primary_mask,
secondary_mask,
}
}
}
const fn role_subset_count(len: usize) -> u128 {
match len {
0 => 1,
1 => 4,
2 => 6,
3 => 4,
4 => 1,
_ => panic!("role bucket count exceeds role space"),
}
}
fn build_local_refinements(size: usize, is_leader: bool) -> Vec<LocalRefinement> {
let mut refinements = Vec::new();
let available = if is_leader { size - 1 } else { size };
let mut prefix = Vec::new();
for_each_composition(available, 4, &mut prefix, &mut |parts| {
let role_choices = role_subset_count(parts.len());
let mut local = Vec::with_capacity(parts.len() + usize::from(is_leader));
local.extend_from_slice(parts);
let multiplicity = if is_leader {
local.push(1);
role_choices * 3
} else {
role_choices
};
refinements.push(LocalRefinement::new(&local, multiplicity));
});
refinements
}
fn for_each_composition<F: FnMut(&[usize])>(
total: usize,
max_parts: usize,
current: &mut Parts,
f: &mut F,
) {
if total == 0 {
f(current);
return;
}
for parts in 1..=total.min(max_parts) {
for_each_composition_with_len(total, parts, current, f);
}
}
fn for_each_composition_with_len<F: FnMut(&[usize])>(
remaining: usize,
parts_left: usize,
current: &mut Parts,
f: &mut F,
) {
if parts_left == 0 {
if remaining == 0 {
f(current);
}
return;
}
if parts_left == 1 {
current.push(remaining);
f(current);
current.pop();
return;
}
let max_first = remaining - (parts_left - 1);
for first in 1..=max_first {
current.push(first);
for_each_composition_with_len(remaining - first, parts_left - 1, current, f);
current.pop();
}
}
fn roles_from_rank(len: usize, rank: u128) -> [Role; 4] {
let mut seen = 0u128;
for mask in 0u8..16 {
if mask.count_ones() as usize != len {
continue;
}
if seen == rank {
let mut roles = [Role::Outside; 4];
let mut next = 0usize;
for (bit, role) in [Role::Outside, Role::Both, Role::Secondary, Role::Primary]
.into_iter()
.enumerate()
{
if (mask & (1u8 << bit)) != 0 {
roles[next] = role;
next += 1;
}
}
return roles;
}
seen += 1;
}
unreachable!("role subset rank out of bounds")
}
fn apply_roles(
start: usize,
parts: &[usize],
roles: &[Role],
primary_mask: &mut u64,
secondary_mask: &mut u64,
) {
let mut offset = 0usize;
for (part, role) in parts.iter().zip(roles) {
let range_start = start + offset;
match role {
Role::Outside => {}
Role::Both => {
set_mask_range(primary_mask, range_start, *part);
set_mask_range(secondary_mask, range_start, *part);
}
Role::Secondary => set_mask_range(secondary_mask, range_start, *part),
Role::Primary => set_mask_range(primary_mask, range_start, *part),
}
offset += part;
}
}
fn compromised_sets_for_cells(cells: &[usize], faults: usize) -> Vec<Compromised> {
let ranges = cells_to_ranges(cells);
let mut result = Vec::new();
let mut current = Vec::new();
allocate_faults(&ranges, 0, faults, &mut current, &mut result);
result
}
fn allocate_faults(
ranges: &[(usize, usize)],
cell_idx: usize,
remaining: usize,
current: &mut Compromised,
result: &mut Vec<Compromised>,
) {
if remaining == 0 {
result.push(current.clone());
return;
}
if cell_idx >= ranges.len() {
return;
}
let (start, size) = ranges[cell_idx];
let remaining_capacity: usize = ranges[cell_idx..].iter().map(|(_, s)| *s).sum();
if remaining > remaining_capacity {
return;
}
let max_from_this = remaining.min(size);
for take in 0..=max_from_this {
for i in 0..take {
current.push(start + i);
}
allocate_faults(ranges, cell_idx + 1, remaining - take, current, result);
for _ in 0..take {
current.pop();
}
}
}
fn sample_unique_indices(rng: &mut impl Rng, total: u128, samples: usize) -> Vec<u128> {
assert!(
(samples as u128) <= total,
"cannot sample more unique indices than total domain size"
);
if samples == 0 {
return Vec::new();
}
let mut sampled = Vec::with_capacity(samples);
let mut seen = HashSet::with_capacity(samples);
for idx in (total - samples as u128)..total {
let candidate = rng.random_range(0..=idx);
if seen.insert(candidate) {
sampled.push(candidate);
} else {
assert!(
seen.insert(idx),
"tail index should be unique in Floyd sampling"
);
sampled.push(idx);
}
}
sampled
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
simplex::{
elector::{Config as ElectorConfig, RoundRobin},
scheme::ed25519,
},
types::Epoch,
};
use commonware_cryptography::{ed25519::PrivateKey, Sha256, Signer};
use commonware_utils::{ordered::Set, test_rng, TestRng};
use std::collections::HashSet;
fn round(_: usize, leader: usize, primary_mask: u64, secondary_mask: u64) -> RoundScenario {
RoundScenario {
leader,
primary_mask,
secondary_mask,
}
}
fn range_mask(start: usize, len: usize) -> u64 {
if len == 0 {
return 0;
}
(((1u128 << len) - 1) << start) as u64
}
fn reference_next_round_transitions(cells: &[usize]) -> Vec<(RoundScenario, Cells)> {
let n = cells.iter().sum();
#[derive(Clone, Copy)]
struct PrimaryState {
primary_mask: u64,
leader: Option<usize>,
}
#[derive(Clone, Copy)]
struct SecondaryState {
primary_mask: u64,
secondary_mask: u64,
leader: Option<usize>,
}
const LEADER_PLACEMENTS: &[(bool, bool)] = &[(true, false), (false, true), (true, true)];
fn push_nonzero_cells(next_cells: &mut Cells, sizes: [usize; 4]) {
for size in sizes {
if size > 0 {
next_cells.push(size);
}
}
}
fn no_secondary(
n: usize,
ranges: &[(usize, usize)],
leader_cell: usize,
cell_idx: usize,
state: PrimaryState,
next_cells: &mut Cells,
out: &mut Vec<(RoundScenario, Cells)>,
) {
if cell_idx == ranges.len() {
out.push((
round(
n,
state.leader.expect("leader cell should assign a leader"),
state.primary_mask,
state.primary_mask,
),
next_cells.clone(),
));
return;
}
let (start, size) = ranges[cell_idx];
let max_outside = if cell_idx == leader_cell {
size - 1
} else {
size
};
for outside in 0..=max_outside {
let both = if cell_idx == leader_cell {
size - outside - 1
} else {
size - outside
};
if outside > 0 {
next_cells.push(outside);
}
if both > 0 {
next_cells.push(both);
}
let mut next_primary = state.primary_mask | range_mask(start + outside, both);
let mut next_leader = state.leader;
if cell_idx == leader_cell {
let leader_idx = start + outside + both;
next_primary |= 1u64 << leader_idx;
next_cells.push(1);
next_leader = Some(leader_idx);
}
no_secondary(
n,
ranges,
leader_cell,
cell_idx + 1,
PrimaryState {
primary_mask: next_primary,
leader: next_leader,
},
next_cells,
out,
);
if cell_idx == leader_cell {
next_cells.pop();
}
if both > 0 {
next_cells.pop();
}
if outside > 0 {
next_cells.pop();
}
}
}
fn with_secondary(
n: usize,
ranges: &[(usize, usize)],
leader_cell: usize,
cell_idx: usize,
state: SecondaryState,
next_cells: &mut Cells,
out: &mut Vec<(RoundScenario, Cells)>,
) {
if cell_idx == ranges.len() {
if state.primary_mask == state.secondary_mask {
return;
}
out.push((
round(
n,
state.leader.expect("leader cell should assign a leader"),
state.primary_mask,
state.secondary_mask,
),
next_cells.clone(),
));
return;
}
let (start, size) = ranges[cell_idx];
let has_leader = cell_idx == leader_cell;
let available = if has_leader { size - 1 } else { size };
for outside in 0..=available {
let remaining_after_outside = available - outside;
for both in 0..=remaining_after_outside {
let remaining = remaining_after_outside - both;
for secondary in 0..=remaining {
let primary = remaining - secondary;
let checkpoint = next_cells.len();
push_nonzero_cells(next_cells, [outside, both, secondary, primary]);
let shared_mask = range_mask(start + outside, both);
let next_secondary = state.secondary_mask
| shared_mask
| range_mask(start + outside + both, secondary);
let next_primary = state.primary_mask
| shared_mask
| range_mask(start + outside + both + secondary, primary);
if has_leader {
let leader_idx = start + outside + both + secondary + primary;
next_cells.push(1);
for &(leader_in_primary, leader_in_secondary) in LEADER_PLACEMENTS {
let leader_bit = 1u64 << leader_idx;
with_secondary(
n,
ranges,
leader_cell,
cell_idx + 1,
SecondaryState {
primary_mask: if leader_in_primary {
next_primary | leader_bit
} else {
next_primary
},
secondary_mask: if leader_in_secondary {
next_secondary | leader_bit
} else {
next_secondary
},
leader: Some(leader_idx),
},
next_cells,
out,
);
}
} else {
with_secondary(
n,
ranges,
leader_cell,
cell_idx + 1,
SecondaryState {
primary_mask: next_primary,
secondary_mask: next_secondary,
leader: state.leader,
},
next_cells,
out,
);
}
next_cells.truncate(checkpoint);
}
}
}
}
let ranges = cells_to_ranges(cells);
let mut out = Vec::new();
for leader_cell in 0..cells.len() {
let mut next_cells = Vec::new();
no_secondary(
n,
&ranges,
leader_cell,
0,
PrimaryState {
primary_mask: 0,
leader: None,
},
&mut next_cells,
&mut out,
);
let mut next_cells = Vec::new();
with_secondary(
n,
&ranges,
leader_cell,
0,
SecondaryState {
primary_mask: 0,
secondary_mask: 0,
leader: None,
},
&mut next_cells,
&mut out,
);
}
out
}
fn reference_scenario_count(
cells: &[usize],
rounds: usize,
memo: &mut HashMap<(Cells, usize), Option<u128>>,
) -> Option<u128> {
if rounds == 0 {
return Some(1);
}
let key = (cells.to_vec(), rounds);
if let Some(cached) = memo.get(&key) {
return *cached;
}
let result = (|| {
let mut total = 0u128;
for (_, next_cells) in reference_next_round_transitions(cells) {
let suffix = reference_scenario_count(&next_cells, rounds - 1, memo)?;
total = total.checked_add(suffix)?;
}
Some(total)
})();
memo.insert(key, result);
result
}
fn reference_scenarios(cells: &[usize], rounds: usize) -> HashSet<(Scenario, Cells)> {
if rounds == 0 {
return HashSet::from([(Scenario { rounds: Vec::new() }, cells.to_vec())]);
}
let mut out = HashSet::new();
for (round, next_cells) in reference_next_round_transitions(cells) {
for (mut scenario, residual) in reference_scenarios(&next_cells, rounds - 1) {
scenario.rounds.insert(0, round);
out.insert((scenario, residual));
}
}
out
}
fn expected_single_round_transitions(n: usize) -> HashSet<(RoundScenario, Cells)> {
let mut out = HashSet::new();
for outside in 0..n {
let both = n - outside - 1;
let leader = outside + both;
let leader_bit = 1u64 << leader;
let mask = range_mask(outside, both) | leader_bit;
let mut cells = Vec::new();
if outside > 0 {
cells.push(outside);
}
if both > 0 {
cells.push(both);
}
cells.push(1);
out.insert((round(n, leader, mask, mask), cells));
}
for outside in 0..n {
let remaining_after_outside = n - outside - 1;
for both in 0..=remaining_after_outside {
let remaining = remaining_after_outside - both;
for secondary in 0..=remaining {
let primary = remaining - secondary;
let leader = outside + both + secondary + primary;
let leader_bit = 1u64 << leader;
let shared_mask = range_mask(outside, both);
let secondary_mask = shared_mask | range_mask(outside + both, secondary);
let primary_mask =
shared_mask | range_mask(outside + both + secondary, primary);
let mut cells = Vec::new();
if outside > 0 {
cells.push(outside);
}
if both > 0 {
cells.push(both);
}
if secondary > 0 {
cells.push(secondary);
}
if primary > 0 {
cells.push(primary);
}
cells.push(1);
out.insert((
round(n, leader, primary_mask | leader_bit, secondary_mask),
cells.clone(),
));
out.insert((
round(n, leader, primary_mask, secondary_mask | leader_bit),
cells.clone(),
));
out.insert((
round(
n,
leader,
primary_mask | leader_bit,
secondary_mask | leader_bit,
),
cells,
));
}
}
}
out
}
#[test]
fn generated_cases_are_deterministic() {
let framework = Framework {
participants: 5,
faults: 1,
rounds: 3,
mode: Mode::Sampled,
max_cases: 50,
};
let first = cases(&mut test_rng(), framework);
let second = cases(&mut test_rng(), framework);
assert_eq!(first.len(), second.len());
for (a, b) in first.iter().zip(second.iter()) {
assert_eq!(a.compromised, b.compromised);
assert_eq!(a.scenario, b.scenario);
}
}
#[test]
fn sampled_cases_are_unique() {
let framework = Framework {
participants: 7,
faults: 2,
rounds: 3,
mode: Mode::Sampled,
max_cases: 64,
};
let generated = cases(&mut test_rng(), framework);
assert_eq!(generated.len(), framework.max_cases);
let mut seen = HashSet::new();
for case in generated {
assert!(
seen.insert((case.scenario, case.compromised)),
"duplicate generated case"
);
}
}
#[test]
fn generated_single_round_scenarios_match_expected_canonical_space() {
for n in 3..=5 {
let mut generator = ScenarioGenerator::new(n);
let generated: HashSet<_> = generator
.generate(&mut test_rng(), 1, usize::MAX)
.into_iter()
.map(|(scenario, cells)| (scenario.rounds[0], cells))
.collect();
assert_eq!(generated, expected_single_round_transitions(n));
}
}
#[test]
fn cell_state_roundtrips_cell_partitions() {
for cells in [
vec![5],
vec![1, 4],
vec![2, 1, 2],
vec![1, 1, 1, 1],
vec![3, 2, 1],
] {
let n = cells.iter().sum();
let state = CellState::from_cells(&cells);
let ranges = cells_to_ranges(&cells);
assert_eq!(state.to_cells(n), cells);
assert_eq!(state.ranges(n), ranges);
}
}
#[test]
fn compressed_counts_match_reference_enumeration() {
for n in 2..=4 {
for rounds in 1..=3 {
let initial_cells = vec![n];
let mut reference_memo = HashMap::new();
let reference =
reference_scenario_count(&initial_cells, rounds, &mut reference_memo);
let initial = CellState::from_cells(&initial_cells);
let mut generator = ScenarioGenerator::new(n);
let counts = generator.counts(&initial, rounds);
let compressed = counts.as_ref().map(|counts| counts[rounds][&initial]);
assert_eq!(
compressed, reference,
"count mismatch for n={n} rounds={rounds}"
);
}
}
}
#[test]
fn compressed_unranking_matches_reference_space() {
for (n, rounds) in [(2usize, 3usize), (3, 2)] {
let mut generator = ScenarioGenerator::new(n);
let generated: HashSet<_> = generator
.generate(&mut test_rng(), rounds, usize::MAX)
.into_iter()
.collect();
assert_eq!(
generated,
reference_scenarios(&[n], rounds),
"mismatch for n={n} rounds={rounds}"
);
}
}
#[test]
fn scripted_partitions_preserve_participant_order_and_overlap() {
let scenario = Scenario {
rounds: vec![round(4, 3, 0b1011, 0b0110)],
};
let participants: Vec<u32> = (0..4).collect();
let (primary, secondary) = scenario.partitions(View::new(1), &participants);
assert_eq!(primary, vec![0, 1, 3]);
assert_eq!(secondary, vec![1, 2]);
}
#[test]
fn route_with_groups_covers_reachable_split_targets() {
let primary = vec![1u32, 2];
let secondary = vec![2u32, 3];
assert_eq!(
route_with_groups(&2, &primary, &secondary),
SplitTarget::Both
);
assert_eq!(
route_with_groups(&1, &primary, &secondary),
SplitTarget::Primary
);
assert_eq!(
route_with_groups(&3, &primary, &secondary),
SplitTarget::Secondary
);
}
#[test]
#[should_panic(expected = "sender not in any partition")]
fn route_with_groups_panics_when_sender_is_missing() {
let primary = vec![1u32, 2];
let secondary = vec![2u32, 3];
let _ = route_with_groups(&0, &primary, &secondary);
}
#[test]
fn view_helpers_follow_modulo_split() {
let participants: Vec<u32> = (0..4).collect();
assert_eq!(
view_partitions(View::new(1), &participants),
(vec![0], vec![1, 2, 3])
);
assert_eq!(
view_partitions(View::new(4), &participants),
(Vec::<u32>::new(), participants.clone())
);
assert_eq!(
view_route(View::new(1), &0, &participants),
SplitTarget::Primary
);
assert_eq!(
view_route(View::new(1), &3, &participants),
SplitTarget::Secondary
);
assert_eq!(
view_route(View::new(4), &0, &participants),
SplitTarget::Secondary
);
}
#[test]
fn route_supports_shared_non_leaders_in_split_rounds() {
let scenario = Scenario {
rounds: vec![round(5, 4, 0b11010, 0b00110)],
};
let participants: Vec<u32> = (0..5).collect();
assert_eq!(
scenario.route(View::new(1), &0, &participants),
SplitTarget::None
);
assert_eq!(
scenario.route(View::new(1), &1, &participants),
SplitTarget::Both
);
assert_eq!(
scenario.route(View::new(1), &2, &participants),
SplitTarget::Secondary
);
assert_eq!(
scenario.route(View::new(1), &3, &participants),
SplitTarget::Primary
);
assert_eq!(
scenario.route(View::new(1), &4, &participants),
SplitTarget::Primary
);
}
#[test]
fn leader_visible_to_both_halves_preserves_partitions_and_route() {
let scenario = Scenario {
rounds: vec![round(4, 3, 0b1010, 0b1100)],
};
let participants: Vec<u32> = (0..4).collect();
let (primary, secondary) = scenario.partitions(View::new(1), &participants);
assert_eq!(primary, vec![1, 3]);
assert_eq!(secondary, vec![2, 3]);
assert_eq!(
scenario.route(View::new(1), &3, &participants),
SplitTarget::Both
);
}
#[test]
fn generated_split_round_leaders_belong_to_at_least_one_half() {
let mut generator = ScenarioGenerator::new(4);
let scenarios = generator.generate(&mut test_rng(), 2, usize::MAX);
for (scenario, _) in scenarios {
for round in scenario.rounds {
if round.primary_mask == round.secondary_mask {
continue;
}
let leader_in_primary = mask_contains(round.primary_mask, round.leader);
let leader_in_secondary = mask_contains(round.secondary_mask, round.leader);
assert!(leader_in_primary || leader_in_secondary);
}
}
}
#[test]
fn route_selects_correct_half() {
let scenario = Scenario {
rounds: vec![round(4, 0, 0b0011, 0b1100)],
};
let participants: Vec<u32> = (0..4).collect();
assert_eq!(
scenario.route(View::new(1), &0, &participants),
SplitTarget::Primary
);
assert_eq!(
scenario.route(View::new(1), &1, &participants),
SplitTarget::Primary
);
assert_eq!(
scenario.route(View::new(1), &2, &participants),
SplitTarget::Secondary
);
assert_eq!(
scenario.route(View::new(1), &3, &participants),
SplitTarget::Secondary
);
}
#[test]
fn route_returns_both_after_scripted_rounds() {
let scenario = Scenario {
rounds: vec![round(4, 0, 0b0011, 0b1100)],
};
let participants: Vec<u32> = (0..4).collect();
for participant in &participants {
assert_eq!(
scenario.route(View::new(2), participant, &participants),
SplitTarget::Both
);
}
}
#[test]
fn scenarios_fall_back_to_synchrony_after_attack_rounds() {
let scenario = Scenario {
rounds: vec![round(4, 0, 0b0011, 0b1100)],
};
let participants: Vec<u32> = (0..4).collect();
let (primary, secondary) = scenario.partitions(View::new(2), &participants);
assert_eq!(primary, participants);
assert_eq!(secondary, participants);
}
#[test]
fn canonical_scenario_count_reports_overflow() {
let initial = CellState::from_cells(&[4]);
let mut generator = ScenarioGenerator::new(4);
assert_eq!(generator.counts(&initial, 40), None);
}
#[test]
fn unique_index_sampling_handles_near_full_ranges() {
let total = 100_000u128;
let samples = 99_999usize;
let mut rng = TestRng::new(9);
let sampled = sample_unique_indices(&mut rng, total, samples);
assert_eq!(sampled.len(), samples);
assert_eq!(
sampled.iter().copied().collect::<HashSet<_>>().len(),
samples
);
assert!(sampled.into_iter().all(|idx| idx < total));
}
#[test]
fn sustained_cases_repeat_single_round() {
let framework = Framework {
participants: 5,
faults: 1,
rounds: 3,
mode: Mode::Sustained,
max_cases: 50,
};
let all = cases(&mut test_rng(), framework);
assert!(!all.is_empty());
for case in &all {
let rounds = case.scenario.rounds();
assert_eq!(rounds.len(), 3);
assert_eq!(rounds[0], rounds[1]);
assert_eq!(rounds[1], rounds[2]);
}
}
#[test]
#[should_panic(expected = "scenario space overflows u128")]
fn cases_panic_on_scenario_overflow() {
let _ = cases(
&mut test_rng(),
Framework {
participants: 4,
faults: 1,
rounds: 40,
mode: Mode::Sampled,
max_cases: 1,
},
);
}
#[test]
fn pruned_scenarios_vary_across_round_positions() {
let mut generator = ScenarioGenerator::new(5);
let scenarios = generator.generate(&mut test_rng(), 3, 32);
for round in 0..3 {
let unique: HashSet<_> = scenarios
.iter()
.map(|(scenario, _)| scenario.rounds[round])
.collect();
assert!(
unique.len() > 1,
"round index {round} should vary under deterministic pruning"
);
}
}
#[test]
fn compromised_sets_collapse_symmetry_cells() {
assert_eq!(compromised_sets_for_cells(&[3], 1).len(), 1);
assert_eq!(compromised_sets_for_cells(&[3], 1), vec![vec![0]]);
let sets = compromised_sets_for_cells(&[2, 1], 1);
assert_eq!(sets, vec![vec![2], vec![0]]);
let sets = compromised_sets_for_cells(&[1, 1, 1], 1);
assert_eq!(sets, vec![vec![2], vec![1], vec![0]]);
}
#[test]
fn compromised_sets_multi_fault() {
let sets = compromised_sets_for_cells(&[3, 2], 2);
assert_eq!(sets, vec![vec![3, 4], vec![0, 3], vec![0, 1]]);
}
#[test]
fn cases_fewer_than_naive_cross_product() {
let framework = Framework {
participants: 5,
faults: 1,
rounds: 1,
mode: Mode::Sampled,
max_cases: usize::MAX,
};
let all_cases = cases(&mut test_rng(), framework);
let mut generator = ScenarioGenerator::new(5);
let scenarios = generator.generate(&mut test_rng(), 1, usize::MAX);
let naive = scenarios.len() * 5;
assert!(
all_cases.len() < naive,
"got {} cases but naive would be {naive}",
all_cases.len()
);
}
#[test]
fn cases_reject_more_than_64_participants() {
let result = std::panic::catch_unwind(|| {
cases(
&mut test_rng(),
Framework {
participants: MAX_PARTICIPANTS + 1,
faults: 1,
rounds: 1,
mode: Mode::Sampled,
max_cases: 1,
},
)
});
assert!(result.is_err());
}
#[test]
fn mask_helpers_set_boundary_ranges() {
let mut mask = 0u64;
set_mask_bit(&mut mask, 63);
assert!(!mask_contains(mask, 62));
assert!(mask_contains(mask, 63));
let mut mask = 0u64;
set_mask_range(&mut mask, 60, 4);
for idx in 60..=63 {
assert!(mask_contains(mask, idx), "bit {idx} should be set");
}
assert!(!mask_contains(mask, 59));
}
#[test]
fn scenario_masks_route_within_one_word() {
let mut primary_mask = 0u64;
set_mask_range(&mut primary_mask, 60, 4);
let mut secondary_mask = 0u64;
set_mask_bit(&mut secondary_mask, 61);
set_mask_bit(&mut secondary_mask, 63);
let scenario = Scenario {
rounds: vec![RoundScenario {
leader: 63,
primary_mask,
secondary_mask,
}],
};
let participants: Vec<_> = (0..64).collect();
let (primary, secondary) = scenario.partitions(View::new(1), &participants);
assert_eq!(primary, vec![60, 61, 62, 63]);
assert_eq!(secondary, vec![61, 63]);
assert_eq!(
scenario.route(View::new(1), &60, &participants),
SplitTarget::Primary
);
assert_eq!(
scenario.route(View::new(1), &61, &participants),
SplitTarget::Both
);
assert_eq!(
scenario.route(View::new(1), &63, &participants),
SplitTarget::Both
);
assert_eq!(
scenario.route(View::new(1), &0, &participants),
SplitTarget::None
);
}
#[test]
fn cases_support_64_participants() {
let generated = cases(
&mut test_rng(),
Framework {
participants: MAX_PARTICIPANTS,
faults: 1,
rounds: 1,
mode: Mode::Sampled,
max_cases: 1,
},
);
assert_eq!(generated.len(), 1);
}
#[test]
fn route_returns_none_for_participants_outside_selected_partitions() {
let scenario = Scenario {
rounds: vec![round(4, 0, 0b0001, 0b0010)],
};
let participants: Vec<u32> = (0..4).collect();
assert_eq!(
scenario.route(View::new(1), &2, &participants),
SplitTarget::None
);
}
#[test]
#[should_panic(expected = "sender missing from runtime participant list")]
fn route_panics_when_sender_is_missing_from_participants() {
let scenario = Scenario {
rounds: vec![round(4, 0, 0b0001, 0b0010)],
};
let participants: Vec<u32> = (0..4).collect();
let _ = scenario.route(View::new(1), &99, &participants);
}
#[test]
fn twins_elector_uses_scenario_leaders_then_fallback_suffix() {
let framework = Framework {
participants: 5,
faults: 1,
rounds: 3,
mode: Mode::Sampled,
max_cases: 1,
};
let case = cases(&mut test_rng(), framework)
.into_iter()
.next()
.expect("expected at least one generated twins case");
let participants: Vec<_> = (0..framework.participants as u64)
.map(|seed| PrivateKey::from_seed(seed).public_key())
.collect();
let participants = Set::try_from(participants).expect("participants should be unique");
let twins = <Elector<RoundRobin<Sha256>> as ElectorConfig<ed25519::Scheme>>::build(
Elector::new(
RoundRobin::<Sha256>::default(),
&case.scenario,
framework.participants,
),
&participants,
);
let fallback = <RoundRobin<Sha256> as ElectorConfig<ed25519::Scheme>>::build(
RoundRobin::<Sha256>::default(),
&participants,
);
for (round_idx, round_scenario) in case.scenario.rounds().iter().enumerate() {
let round = Round::new(Epoch::new(0), View::new((round_idx as u64) + 1));
assert_eq!(
twins.elect(round, None),
Participant::from_usize(round_scenario.leader()),
"unexpected leader in scripted attack round"
);
}
for view in (framework.rounds as u64 + 1)..=20 {
let round = Round::new(Epoch::new(333), View::new(view));
assert_eq!(twins.elect(round, None), fallback.elect(round, None));
}
}
}