fix(crypto): return None from shamir_threshold for an empty committee

This commit is contained in:
CleverWild
2026-09-07 13:03:57 +02:00
parent f32da65467
commit d916997ef3
4 changed files with 83 additions and 11 deletions

View File

@@ -277,7 +277,11 @@ impl ProposalManager {
let threshold: i64 = if requires_full_quorum {
total_eligible
} else {
crate::crypto::shamir::shamir_threshold(tally.total_ordinary as usize) as i64
match crate::crypto::shamir::shamir_threshold(tally.total_ordinary as usize) {
Some(threshold) => threshold as i64,
// No ordinary operators means no electorate: nothing can settle.
None => return VoteOutcome::Pending,
}
};
if tally.approve >= threshold {

View File

@@ -52,6 +52,8 @@ pub enum Error {
TwoOperatorsRequireRecovery,
#[error("Broken database")]
BrokenDatabase,
#[error("A committee must have at least one ordinary operator")]
EmptyCommittee,
}
// Passphrases stored as plain Vec<u8> (not SafeCell) so CoordinatorState is Sync.
@@ -153,7 +155,7 @@ async fn finalize_bootstrap(
let ordinary_count = ordinary_passphrases.len();
let recovery_count = recovery_passphrases.len();
let total = ordinary_count + recovery_count;
let threshold = shamir_threshold(ordinary_count);
let threshold = shamir_threshold(ordinary_count).ok_or(Error::EmptyCommittee)?;
let mut seal_key_bytes = [0u8; 32];
OsRng.fill_bytes(&mut seal_key_bytes);
@@ -232,7 +234,8 @@ async fn finalize_unseal(
.count()
.get_result(&mut conn)
.await?;
let threshold = shamir_threshold(ordinary_operator_count as usize);
let threshold =
shamir_threshold(ordinary_operator_count as usize).ok_or(Error::EmptyCommittee)?;
let mut shares: Vec<Vec<u8>> = Vec::new();
@@ -314,7 +317,7 @@ async fn finalize_rekey(
let ordinary_count = ordinary_passphrases.len();
let recovery_count = recovery_passphrases.len();
let total = ordinary_count + recovery_count;
let threshold = shamir_threshold(ordinary_count);
let threshold = shamir_threshold(ordinary_count).ok_or(Error::EmptyCommittee)?;
let mut new_seal_key_bytes = [0u8; 32];
OsRng.fill_bytes(&mut new_seal_key_bytes);
@@ -398,6 +401,9 @@ impl VaultCoordinator {
if !matches!(self.state, CoordinatorState::Idle) {
return Err(Error::AlreadyBootstrapping);
}
if declared_count == 0 {
return Err(Error::EmptyCommittee);
}
if declared_count == 2 && recovery_count == 0 {
return Err(Error::TwoOperatorsRequireRecovery);
}
@@ -584,7 +590,8 @@ impl VaultCoordinator {
.count()
.get_result(&mut conn)
.await?;
let threshold = shamir_threshold(usize::try_from(ordinary_count).unwrap_or_default());
let threshold = shamir_threshold(usize::try_from(ordinary_count).unwrap_or_default())
.ok_or(Error::EmptyCommittee)?;
self.state = CoordinatorState::Unsealing {
threshold,
ordinary_passphrases: HashMap::new(),