From 6611a50007eeb76f3da1930a67872e8cac9afd9d Mon Sep 17 00:00:00 2001 From: MozirDmitriy Date: Sun, 14 Sep 2025 14:58:36 +0300 Subject: [PATCH 1/5] fix: guard leader selection against empty sets in randomized committee --- .../election/randomized_committee_members.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/hotshot/hotshot/src/traits/election/randomized_committee_members.rs b/crates/hotshot/hotshot/src/traits/election/randomized_committee_members.rs index e9c244c8ddb..683d751226c 100644 --- a/crates/hotshot/hotshot/src/traits/election/randomized_committee_members.rs +++ b/crates/hotshot/hotshot/src/traits/election/randomized_committee_members.rs @@ -21,6 +21,7 @@ use hotshot_types::{ PeerConfig, }; use hotshot_utils::anytrace::Result; +use hotshot_utils::anytrace::{Error as AnyError, Level as AnyLevel}; use rand::{rngs::StdRng, Rng}; use tracing::error; @@ -386,6 +387,13 @@ impl .map(|(idx, v)| (idx, v.clone())) .collect(); + if leader_vec.is_empty() { + return Err(AnyError { + level: AnyLevel::Unspecified, + message: format!("No eligible leaders after quorum filter for epoch {epoch}"), + }); + } + let mut rng: StdRng = rand::SeedableRng::seed_from_u64(*view_number); let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX); @@ -406,6 +414,12 @@ impl let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX); #[allow(clippy::cast_possible_truncation)] + if self.eligible_leaders.is_empty() { + return Err(AnyError { + level: AnyLevel::Unspecified, + message: "No eligible leaders configured".to_string(), + }); + } let index = randomized_view_number as usize % self.eligible_leaders.len(); let res = self.eligible_leaders[index].clone(); From 7ac93c470b5aed4df81e72f3afa27de8ad762ed3 Mon Sep 17 00:00:00 2001 From: MozirDmitriy Date: Sun, 14 Sep 2025 15:03:09 +0300 Subject: [PATCH 2/5] Update static_committee.rs --- .../hotshot/hotshot/src/traits/election/static_committee.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/hotshot/hotshot/src/traits/election/static_committee.rs b/crates/hotshot/hotshot/src/traits/election/static_committee.rs index 7bddcdfc50d..87836b95508 100644 --- a/crates/hotshot/hotshot/src/traits/election/static_committee.rs +++ b/crates/hotshot/hotshot/src/traits/election/static_committee.rs @@ -217,6 +217,12 @@ impl Membership for StaticCommittee { epoch: Option<::Epoch>, ) -> Result { self.check_first_epoch(epoch); + if self.eligible_leaders.is_empty() { + return Err(Error { + level: Level::Unspecified, + message: "No eligible leaders configured".to_string(), + }); + } #[allow(clippy::cast_possible_truncation)] let index = *view_number as usize % self.eligible_leaders.len(); let res = self.eligible_leaders[index].clone(); From 07cee6f25eba3651e0abd76178c4d401ad65ee68 Mon Sep 17 00:00:00 2001 From: MozirDmitriy Date: Sun, 14 Sep 2025 15:03:30 +0300 Subject: [PATCH 3/5] Update static_committee_leader_two_views.rs --- .../traits/election/static_committee_leader_two_views.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/hotshot/hotshot/src/traits/election/static_committee_leader_two_views.rs b/crates/hotshot/hotshot/src/traits/election/static_committee_leader_two_views.rs index bfd7d826c16..46e0d46b705 100644 --- a/crates/hotshot/hotshot/src/traits/election/static_committee_leader_two_views.rs +++ b/crates/hotshot/hotshot/src/traits/election/static_committee_leader_two_views.rs @@ -187,6 +187,12 @@ impl Membership for StaticCommitteeLeaderForTwoViews::View, _epoch: Option<::Epoch>, ) -> Result { + if self.eligible_leaders.is_empty() { + return Err(hotshot_utils::anytrace::Error { + level: hotshot_utils::anytrace::Level::Unspecified, + message: "No eligible leaders configured".to_string(), + }); + } let index = usize::try_from((*view_number / 2) % self.eligible_leaders.len() as u64).unwrap(); let res = self.eligible_leaders[index].clone(); From cb76cd6a341ef9af97dc0c561e401a23b7fdcef2 Mon Sep 17 00:00:00 2001 From: MozirDmitriy Date: Sun, 14 Sep 2025 15:03:43 +0300 Subject: [PATCH 4/5] Update two_static_committees.rs --- .../src/traits/election/two_static_committees.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/hotshot/hotshot/src/traits/election/two_static_committees.rs b/crates/hotshot/hotshot/src/traits/election/two_static_committees.rs index b1074b9834f..6e7df720ae0 100644 --- a/crates/hotshot/hotshot/src/traits/election/two_static_committees.rs +++ b/crates/hotshot/hotshot/src/traits/election/two_static_committees.rs @@ -337,11 +337,23 @@ impl Membership for TwoStaticCommittees { ) -> Result { let epoch = epoch.expect("epochs cannot be disabled with TwoStaticCommittees"); if *epoch != 0 && *epoch % 2 == 0 { + if self.eligible_leaders.0.is_empty() { + return Err(hotshot_utils::anytrace::Error { + level: hotshot_utils::anytrace::Level::Unspecified, + message: "No eligible leaders configured for committee 0".to_string(), + }); + } #[allow(clippy::cast_possible_truncation)] let index = *view_number as usize % self.eligible_leaders.0.len(); let res = self.eligible_leaders.0[index].clone(); Ok(TYPES::SignatureKey::public_key(&res.stake_table_entry)) } else { + if self.eligible_leaders.1.is_empty() { + return Err(hotshot_utils::anytrace::Error { + level: hotshot_utils::anytrace::Level::Unspecified, + message: "No eligible leaders configured for committee 1".to_string(), + }); + } #[allow(clippy::cast_possible_truncation)] let index = *view_number as usize % self.eligible_leaders.1.len(); let res = self.eligible_leaders.1[index].clone(); From eb4b630294b2f4628be5f0dbd5a395cb8c1314c6 Mon Sep 17 00:00:00 2001 From: MozirDmitriy Date: Sun, 14 Sep 2025 15:05:29 +0300 Subject: [PATCH 5/5] Update stake_table.rs --- types/src/v0/impls/stake_table.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/types/src/v0/impls/stake_table.rs b/types/src/v0/impls/stake_table.rs index a1022459bd9..f7492d4adfa 100644 --- a/types/src/v0/impls/stake_table.rs +++ b/types/src/v0/impls/stake_table.rs @@ -2218,7 +2218,9 @@ impl Membership for EpochCommittees { }, (_, None) => { let leaders = &self.non_epoch_committee.eligible_leaders; - + if leaders.is_empty() { + return Err(LeaderLookupError); + } let index = *view_number as usize % leaders.len(); let res = leaders[index].clone(); Ok(PubKey::public_key(&res.stake_table_entry))