diff --git a/crates/example-types/src/node_types.rs b/crates/example-types/src/node_types.rs
index 80b634515c..01160e1cb9 100644
--- a/crates/example-types/src/node_types.rs
+++ b/crates/example-types/src/node_types.rs
@@ -4,9 +4,16 @@
// You should have received a copy of the MIT License
// along with the HotShot repository. If not, see .
+use std::marker::PhantomData;
+
+pub use hotshot::traits::election::helpers::{
+ RandomOverlapQuorumFilterConfig, StableQuorumFilterConfig,
+};
use hotshot::traits::{
election::{
- randomized_committee::RandomizedCommittee, static_committee::StaticCommittee,
+ helpers::QuorumFilterConfig, randomized_committee::RandomizedCommittee,
+ randomized_committee_members::RandomizedCommitteeMembers,
+ static_committee::StaticCommittee,
static_committee_leader_two_views::StaticCommitteeLeaderForTwoViews,
},
implementations::{CombinedNetworks, Libp2pNetwork, MemoryNetwork, PushCdnNetwork},
@@ -87,6 +94,40 @@ impl NodeType for TestTypesRandomizedLeader {
type BuilderSignatureKey = BuilderKey;
}
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ Default,
+ Hash,
+ PartialEq,
+ Eq,
+ PartialOrd,
+ Ord,
+ serde::Serialize,
+ serde::Deserialize,
+)]
+/// filler struct to implement node type and allow us
+/// to select our traits
+pub struct TestTypesRandomizedCommitteeMembers {
+ _pd: PhantomData,
+}
+
+impl NodeType for TestTypesRandomizedCommitteeMembers {
+ type AuctionResult = TestAuctionResult;
+ type View = ViewNumber;
+ type Epoch = EpochNumber;
+ type BlockHeader = TestBlockHeader;
+ type BlockPayload = TestBlockPayload;
+ type SignatureKey = BLSPubKey;
+ type Transaction = TestTransaction;
+ type ValidatedState = TestValidatedState;
+ type InstanceState = TestInstanceState;
+ type Membership =
+ RandomizedCommitteeMembers, CONFIG>;
+ type BuilderSignatureKey = BuilderKey;
+}
+
#[derive(
Copy,
Clone,
@@ -133,7 +174,7 @@ pub struct Libp2pImpl;
#[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)]
pub struct WebImpl;
-/// Combined Network implementation (libp2p + web sever)
+/// Combined Network implementation (libp2p + web server)
#[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)]
pub struct CombinedImpl;
@@ -223,7 +264,7 @@ impl Versions for EpochsTestVersions {
0, 0,
];
- type Marketplace = StaticVersion<0, 3>;
+ type Marketplace = StaticVersion<0, 99>;
type Epochs = StaticVersion<0, 4>;
}
diff --git a/crates/hotshot/src/traits/election/helpers.rs b/crates/hotshot/src/traits/election/helpers.rs
new file mode 100644
index 0000000000..2a2c7fe172
--- /dev/null
+++ b/crates/hotshot/src/traits/election/helpers.rs
@@ -0,0 +1,442 @@
+// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
+// This file is part of the HotShot repository.
+
+// You should have received a copy of the MIT License
+// along with the HotShot repository. If not, see .
+
+use std::{collections::BTreeSet, hash::Hash};
+
+use rand::{rngs::StdRng, Rng, SeedableRng};
+
+/// Helper which allows producing random numbers within a range and preventing duplicates
+/// If consumed as a regular iterator, will return a randomly ordered permutation of all
+/// values from 0..max
+struct NonRepeatValueIterator {
+ /// Random number generator to use
+ rng: StdRng,
+
+ /// Values which have already been emitted, to avoid duplicates
+ values: BTreeSet,
+
+ /// Maximum value, open-ended. Numbers returned will be 0..max
+ max: u64,
+}
+
+impl NonRepeatValueIterator {
+ /// Create a new NonRepeatValueIterator
+ pub fn new(rng: StdRng, max: u64) -> Self {
+ Self {
+ rng,
+ values: BTreeSet::new(),
+ max,
+ }
+ }
+}
+
+impl Iterator for NonRepeatValueIterator {
+ type Item = u64;
+
+ fn next(&mut self) -> Option {
+ if self.values.len() as u64 >= self.max {
+ return None;
+ }
+
+ loop {
+ let v = self.rng.gen_range(0..self.max);
+ if !self.values.contains(&v) {
+ self.values.insert(v);
+ return Some(v);
+ }
+ }
+ }
+}
+
+/// Create a single u64 seed by merging two u64s. Done this way to allow easy seeding of the number generator
+/// from both a stable SOUND as well as a moving value ROUND (typically, epoch). Shift left by 8 to avoid
+/// scenarios where someone manually stepping seeds would pass over the same space of random numbers across
+/// sequential rounds. Doesn't have to be 8, but has to be large enough that it is unlikely that a given
+/// test run will collide; using 8 means that 256 rounds (epochs) would have to happen inside of a test before
+/// the test starts repeating values from SEED+1.
+fn make_seed(seed: u64, round: u64) -> u64 {
+ seed.wrapping_add(round.wrapping_shl(8))
+}
+
+/// Create a pair of PRNGs for the given SEED and ROUND. Prev_rng is the PRNG for the previous ROUND, used to
+/// deterministically replay random numbers generated for the previous ROUND.
+fn make_rngs(seed: u64, round: u64) -> (StdRng, StdRng) {
+ let prev_rng = SeedableRng::seed_from_u64(make_seed(seed, round.wrapping_sub(1)));
+ let this_rng = SeedableRng::seed_from_u64(make_seed(seed, round));
+
+ (prev_rng, this_rng)
+}
+
+/// Iterator which returns odd/even values for a given COUNT of nodes. For OVERLAP=0, this will return
+/// [0, 2, 4, 6, ...] for an even round, and [1, 3, 5, 7, ...] for an odd round. Setting OVERLAP>0 will
+/// randomly introduce OVERLAP elements from the previous round, so an even round with OVERLAP=2 will contain
+/// something like [1, 7, 2, 4, 0, ...]. Note that the total number of nodes will always be COUNT/2, so
+/// for OVERLAP>0 a random number of nodes which would have been in the round for OVERLAP=0 will be dropped.
+/// Ordering of nodes is random. Outputs is deterministic when prev_rng and this_rng are provided by make_rngs
+/// using the same values for SEED and ROUND.
+pub struct StableQuorumIterator {
+ /// PRNG from the previous round
+ prev_rng: NonRepeatValueIterator,
+
+ /// PRNG for the current round
+ this_rng: NonRepeatValueIterator,
+
+ /// Current ROUND
+ round: u64,
+
+ /// Count of nodes in the source quorum being filtered against
+ count: u64,
+
+ /// OVERLAP of nodes to be carried over from the previous round
+ overlap: u64,
+
+ /// The next call to next() will emit the value with this index. Starts at 0 and is incremented for each
+ /// call to next()
+ index: u64,
+}
+
+/// Determines how many possible values can be made for the given odd/even
+/// E.g. if count is 5, then possible values would be [0, 1, 2, 3, 4]
+/// if odd = true, slots = 2 (1 or 3), else slots = 3 (0, 2, 4)
+fn calc_num_slots(count: u64, odd: bool) -> u64 {
+ (count / 2) + if odd { 0 } else { count % 2 }
+}
+
+impl StableQuorumIterator {
+ #[must_use]
+ /// Create a new StableQuorumIterator
+ ///
+ /// # Panics
+ ///
+ /// panics if overlap is greater than half of count
+ pub fn new(seed: u64, round: u64, count: u64, overlap: u64) -> Self {
+ assert!(
+ count / 2 > overlap,
+ "Overlap cannot be greater than the entire set size"
+ );
+
+ let (prev_rng, this_rng) = make_rngs(seed, round);
+
+ Self {
+ prev_rng: NonRepeatValueIterator::new(prev_rng, calc_num_slots(count, round % 2 == 0)),
+ this_rng: NonRepeatValueIterator::new(this_rng, calc_num_slots(count, round % 2 == 1)),
+ round,
+ count,
+ overlap,
+ index: 0,
+ }
+ }
+}
+
+impl Iterator for StableQuorumIterator {
+ type Item = u64;
+
+ fn next(&mut self) -> Option {
+ if self.index >= (self.count / 2) {
+ // Always return exactly half of the possible values. If we have OVERLAP>0 then
+ // we need to return (COUNT/2)-OVERLAP of the current set, even if there are additional
+ // even (or odd) numbers that we can return.
+ None
+ } else if self.index < self.overlap {
+ // Generate enough values for the previous round. If the current round is odd, then
+ // we want to pick even values that were selected from the previous round to create OVERLAP
+ // even values.
+ let v = self.prev_rng.next().unwrap();
+ self.index += 1;
+ Some(v * 2 + (1 - self.round % 2))
+ } else {
+ // Generate new values. If our current round is odd, we'll be creating (COUNT/2)-OVERLAP
+ // odd values here.
+ let v = self.this_rng.next().unwrap();
+ self.index += 1;
+ Some(v * 2 + self.round % 2)
+ }
+ }
+}
+
+#[must_use]
+/// Helper function to convert the arguments to a StableQuorumIterator into an ordered set of values.
+///
+/// # Panics
+///
+/// panics if the arguments are invalid for StableQuorumIterator::new
+pub fn stable_quorum_filter(seed: u64, round: u64, count: usize, overlap: u64) -> BTreeSet {
+ StableQuorumIterator::new(seed, round, count as u64, overlap)
+ // We should never have more than u32_max members in a test
+ .map(|x| usize::try_from(x).unwrap())
+ .collect()
+}
+
+/// Constructs a quorum with a random number of members and overlaps. Functions similar to StableQuorumIterator,
+/// except that the number of MEMBERS and OVERLAP are also (deterministically) random, to allow additional variance
+/// in testing.
+pub struct RandomOverlapQuorumIterator {
+ /// PRNG from the previous round
+ prev_rng: NonRepeatValueIterator,
+
+ /// PRNG for the current round
+ this_rng: NonRepeatValueIterator,
+
+ /// Current ROUND
+ round: u64,
+
+ /// Number of members to emit for the current round
+ members: u64,
+
+ /// OVERLAP of nodes to be carried over from the previous round
+ overlap: u64,
+
+ /// The next call to next() will emit the value with this index. Starts at 0 and is incremented for each
+ /// call to next()
+ index: u64,
+}
+
+impl RandomOverlapQuorumIterator {
+ #[must_use]
+ /// Create a new RandomOverlapQuorumIterator
+ ///
+ /// # Panics
+ ///
+ /// panics if overlap and members can produce invalid results or if ranges are invalid
+ pub fn new(
+ seed: u64,
+ round: u64,
+ count: u64,
+ members_min: u64,
+ members_max: u64,
+ overlap_min: u64,
+ overlap_max: u64,
+ ) -> Self {
+ assert!(
+ members_min <= members_max,
+ "Members_min cannot be greater than members_max"
+ );
+ assert!(
+ overlap_min <= overlap_max,
+ "Overlap_min cannot be greater than overlap_max"
+ );
+ assert!(
+ overlap_max < members_min,
+ "Overlap_max must be less than members_min"
+ );
+ assert!(
+ count / 2 > overlap_max,
+ "Overlap cannot be greater than the entire set size"
+ );
+
+ let (mut prev_rng, mut this_rng) = make_rngs(seed, round);
+
+ // Consume two values from prev_rng to advance it to the same state it was at the beginning of the previous round
+ let _prev_members = prev_rng.gen_range(members_min..=members_max);
+ let _prev_overlap = prev_rng.gen_range(overlap_min..=overlap_max);
+ let this_members = this_rng.gen_range(members_min..=members_max);
+ let this_overlap = this_rng.gen_range(overlap_min..=overlap_max);
+
+ Self {
+ prev_rng: NonRepeatValueIterator::new(prev_rng, calc_num_slots(count, round % 2 == 0)),
+ this_rng: NonRepeatValueIterator::new(this_rng, calc_num_slots(count, round % 2 == 1)),
+ round,
+ members: this_members,
+ overlap: this_overlap,
+ index: 0,
+ }
+ }
+}
+
+impl Iterator for RandomOverlapQuorumIterator {
+ type Item = u64;
+
+ fn next(&mut self) -> Option {
+ if self.index >= self.members {
+ None
+ } else if self.index < self.overlap {
+ // Generate enough values for the previous round
+ let v = self.prev_rng.next().unwrap();
+ self.index += 1;
+ Some(v * 2 + (1 - self.round % 2))
+ } else {
+ // Generate new values
+ let v = self.this_rng.next().unwrap();
+ self.index += 1;
+ Some(v * 2 + self.round % 2)
+ }
+ }
+}
+
+#[must_use]
+/// Helper function to convert the arguments to a StableQuorumIterator into an ordered set of values.
+///
+/// # Panics
+///
+/// panics if the arguments are invalid for RandomOverlapQuorumIterator::new
+pub fn random_overlap_quorum_filter(
+ seed: u64,
+ round: u64,
+ count: usize,
+ members_min: u64,
+ members_max: u64,
+ overlap_min: u64,
+ overlap_max: u64,
+) -> BTreeSet {
+ RandomOverlapQuorumIterator::new(
+ seed,
+ round,
+ count as u64,
+ members_min,
+ members_max,
+ overlap_min,
+ overlap_max,
+ )
+ // We should never have more than u32_max members in a test
+ .map(|x| usize::try_from(x).unwrap())
+ .collect()
+}
+
+/// Trait wrapping a config for quorum filters. This allows selection between either the StableQuorumIterator or the
+/// RandomOverlapQuorumIterator functionality from above
+pub trait QuorumFilterConfig:
+ Copy
+ + Clone
+ + std::fmt::Debug
+ + Default
+ + Send
+ + Sync
+ + Ord
+ + PartialOrd
+ + Eq
+ + PartialEq
+ + Hash
+ + 'static
+{
+ /// Called to run the filter and return a set of indices
+ fn execute(epoch: u64, count: usize) -> BTreeSet;
+}
+
+#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
+/// Provides parameters to use the StableQuorumIterator
+pub struct StableQuorumFilterConfig {}
+
+impl QuorumFilterConfig
+ for StableQuorumFilterConfig
+{
+ fn execute(epoch: u64, count: usize) -> BTreeSet {
+ stable_quorum_filter(SEED, epoch, count, OVERLAP)
+ }
+}
+
+#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
+/// Provides parameters to use the RandomOverlapQuorumIterator
+pub struct RandomOverlapQuorumFilterConfig<
+ const SEED: u64,
+ const MEMBERS_MIN: u64,
+ const MEMBERS_MAX: u64,
+ const OVERLAP_MIN: u64,
+ const OVERLAP_MAX: u64,
+> {}
+
+impl<
+ const SEED: u64,
+ const MEMBERS_MIN: u64,
+ const MEMBERS_MAX: u64,
+ const OVERLAP_MIN: u64,
+ const OVERLAP_MAX: u64,
+ > QuorumFilterConfig
+ for RandomOverlapQuorumFilterConfig
+{
+ fn execute(epoch: u64, count: usize) -> BTreeSet {
+ random_overlap_quorum_filter(
+ SEED,
+ epoch,
+ count,
+ MEMBERS_MIN,
+ MEMBERS_MAX,
+ OVERLAP_MIN,
+ OVERLAP_MAX,
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_stable() {
+ for _ in 0..100 {
+ let seed = rand::random::();
+ let prev_set: Vec = StableQuorumIterator::new(seed, 1, 10, 2).collect();
+ let this_set: Vec = StableQuorumIterator::new(seed, 2, 10, 2).collect();
+
+ // The first two elements from prev_set are from its previous round. But its 2nd and 3rd elements
+ // are new, and should be carried over to become the first two elements from this_set.
+ assert_eq!(
+ prev_set[2..4],
+ this_set[0..2],
+ "prev_set={prev_set:?}, this_set={this_set:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn test_random_overlap() {
+ for _ in 0..100 {
+ let seed = rand::random::();
+ let prev_set: Vec =
+ RandomOverlapQuorumIterator::new(seed, 1, 20, 5, 10, 2, 3).collect();
+ let this_set: Vec =
+ RandomOverlapQuorumIterator::new(seed, 2, 20, 5, 10, 2, 3).collect();
+
+ // Similar to the overlap before, but there are 4 possible cases: the previous set might have had
+ // either 2 or 3 overlaps, meaning we should start with index 2 or 3, and the overlap size might
+ // be either 2 or 3. We'll just check for 2 overlaps, meaning we have two possible overlap cases
+ // to verify.
+ let matched = (prev_set[2..4] == this_set[0..2]) || (prev_set[3..5] == this_set[0..2]);
+ assert!(matched, "prev_set={prev_set:?}, this_set={this_set:?}");
+ }
+ }
+
+ #[test]
+ fn test_odd_even() {
+ for _ in 0..100 {
+ let seed = rand::random::();
+
+ let odd_set: Vec = StableQuorumIterator::new(seed, 1, 10, 2).collect();
+ let even_set: Vec = StableQuorumIterator::new(seed, 2, 10, 2).collect();
+
+ assert!(
+ odd_set[2] % 2 == 1,
+ "odd set non-overlap value should be odd (stable)"
+ );
+ assert!(
+ even_set[2] % 2 == 0,
+ "even set non-overlap value should be even (stable)"
+ );
+
+ let odd_set: Vec =
+ RandomOverlapQuorumIterator::new(seed, 1, 20, 5, 10, 2, 3).collect();
+ let even_set: Vec =
+ RandomOverlapQuorumIterator::new(seed, 2, 20, 5, 10, 2, 3).collect();
+
+ assert!(
+ odd_set[3] % 2 == 1,
+ "odd set non-overlap value should be odd (random overlap)"
+ );
+ assert!(
+ even_set[3] % 2 == 0,
+ "even set non-overlap value should be even (random overlap)"
+ );
+ }
+ }
+
+ #[test]
+ fn calc_num_slots_test() {
+ assert_eq!(calc_num_slots(5, true), 2);
+ assert_eq!(calc_num_slots(5, false), 3);
+
+ assert_eq!(calc_num_slots(6, true), 3);
+ assert_eq!(calc_num_slots(6, false), 3);
+ }
+}
diff --git a/crates/hotshot/src/traits/election.rs b/crates/hotshot/src/traits/election/mod.rs
similarity index 79%
rename from crates/hotshot/src/traits/election.rs
rename to crates/hotshot/src/traits/election/mod.rs
index 4f9212705f..914b9bbb33 100644
--- a/crates/hotshot/src/traits/election.rs
+++ b/crates/hotshot/src/traits/election/mod.rs
@@ -8,7 +8,15 @@
/// leader completely randomized every view
pub mod randomized_committee;
+
+/// quorum randomized every view, with configurable overlap
+pub mod randomized_committee_members;
+
/// static (round robin) committee election
pub mod static_committee;
+
/// static (round robin leader for 2 consecutive views) committee election
pub mod static_committee_leader_two_views;
+
+/// general helpers
+pub mod helpers;
diff --git a/crates/hotshot/src/traits/election/randomized_committee.rs b/crates/hotshot/src/traits/election/randomized_committee.rs
index 2b721a66e0..4046123553 100644
--- a/crates/hotshot/src/traits/election/randomized_committee.rs
+++ b/crates/hotshot/src/traits/election/randomized_committee.rs
@@ -226,22 +226,22 @@ impl Membership for RandomizedCommittee {
self.da_stake_table.len()
}
/// Get the voting success threshold for the committee
- fn success_threshold(&self) -> NonZeroU64 {
+ fn success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}
/// Get the voting success threshold for the committee
- fn da_success_threshold(&self) -> NonZeroU64 {
+ fn da_success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.da_stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}
/// Get the voting failure threshold for the committee
- fn failure_threshold(&self) -> NonZeroU64 {
+ fn failure_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap()
}
/// Get the voting upgrade threshold for the committee
- fn upgrade_threshold(&self) -> NonZeroU64 {
+ fn upgrade_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(max(
(self.stake_table.len() as u64 * 9) / 10,
((self.stake_table.len() as u64 * 2) / 3) + 1,
diff --git a/crates/hotshot/src/traits/election/randomized_committee_members.rs b/crates/hotshot/src/traits/election/randomized_committee_members.rs
new file mode 100644
index 0000000000..5c85ad9c07
--- /dev/null
+++ b/crates/hotshot/src/traits/election/randomized_committee_members.rs
@@ -0,0 +1,353 @@
+// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
+// This file is part of the HotShot repository.
+
+// You should have received a copy of the MIT License
+// along with the HotShot repository. If not, see .
+
+use std::{
+ cmp::max,
+ collections::{BTreeMap, BTreeSet},
+ marker::PhantomData,
+ num::NonZeroU64,
+};
+
+use hotshot_types::{
+ traits::{
+ election::Membership,
+ node_implementation::{ConsensusTime, NodeType},
+ signature_key::{SignatureKey, StakeTableEntryType},
+ },
+ PeerConfig,
+};
+use primitive_types::U256;
+use rand::{rngs::StdRng, Rng};
+use utils::anytrace::Result;
+
+use crate::traits::election::helpers::QuorumFilterConfig;
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+/// The static committee election
+pub struct RandomizedCommitteeMembers {
+ /// The nodes eligible for leadership.
+ /// NOTE: This is currently a hack because the DA leader needs to be the quorum
+ /// leader but without voting rights.
+ eligible_leaders: Vec<::StakeTableEntry>,
+
+ /// The nodes on the committee and their stake
+ stake_table: Vec<::StakeTableEntry>,
+
+ /// The nodes on the da committee and their stake
+ da_stake_table: Vec<::StakeTableEntry>,
+
+ /// The nodes on the committee and their stake, indexed by public key
+ indexed_stake_table:
+ BTreeMap::StakeTableEntry>,
+
+ /// The nodes on the da committee and their stake, indexed by public key
+ indexed_da_stake_table:
+ BTreeMap::StakeTableEntry>,
+
+ /// Phantom
+ _pd: PhantomData,
+}
+
+impl RandomizedCommitteeMembers {
+ /// Creates a set of indices into the stake_table which reference the nodes selected for this epoch's committee
+ fn make_quorum_filter(&self, epoch: ::Epoch) -> BTreeSet {
+ CONFIG::execute(epoch.u64(), self.stake_table.len())
+ }
+
+ /// Creates a set of indices into the da_stake_table which reference the nodes selected for this epoch's da committee
+ fn make_da_quorum_filter(&self, epoch: ::Epoch) -> BTreeSet {
+ CONFIG::execute(epoch.u64(), self.da_stake_table.len())
+ }
+}
+
+impl Membership
+ for RandomizedCommitteeMembers
+{
+ type Error = utils::anytrace::Error;
+
+ /// Create a new election
+ fn new(
+ committee_members: Vec::SignatureKey>>,
+ da_members: Vec::SignatureKey>>,
+ ) -> Self {
+ // For each eligible leader, get the stake table entry
+ let eligible_leaders: Vec<::StakeTableEntry> =
+ committee_members
+ .iter()
+ .map(|member| member.stake_table_entry.clone())
+ .filter(|entry| entry.stake() > U256::zero())
+ .collect();
+
+ // For each member, get the stake table entry
+ let members: Vec<::StakeTableEntry> =
+ committee_members
+ .iter()
+ .map(|member| member.stake_table_entry.clone())
+ .filter(|entry| entry.stake() > U256::zero())
+ .collect();
+
+ // For each da member, get the stake table entry
+ let da_members: Vec<::StakeTableEntry> = da_members
+ .iter()
+ .map(|member| member.stake_table_entry.clone())
+ .filter(|entry| entry.stake() > U256::zero())
+ .collect();
+
+ // Index the stake table by public key
+ let indexed_stake_table: BTreeMap<
+ TYPES::SignatureKey,
+ ::StakeTableEntry,
+ > = members
+ .iter()
+ .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone()))
+ .collect();
+
+ // Index the stake table by public key
+ let indexed_da_stake_table: BTreeMap<
+ TYPES::SignatureKey,
+ ::StakeTableEntry,
+ > = da_members
+ .iter()
+ .map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone()))
+ .collect();
+
+ Self {
+ eligible_leaders,
+ stake_table: members,
+ da_stake_table: da_members,
+ indexed_stake_table,
+ indexed_da_stake_table,
+ _pd: PhantomData,
+ }
+ }
+
+ /// Get the stake table for the current view
+ fn stake_table(
+ &self,
+ epoch: ::Epoch,
+ ) -> Vec<<::SignatureKey as SignatureKey>::StakeTableEntry> {
+ let filter = self.make_quorum_filter(epoch);
+ //self.stake_table.clone()s
+ self.stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| v.clone())
+ .collect()
+ }
+
+ /// Get the da stake table for the current view
+ fn da_stake_table(
+ &self,
+ epoch: ::Epoch,
+ ) -> Vec<<::SignatureKey as SignatureKey>::StakeTableEntry> {
+ let filter = self.make_da_quorum_filter(epoch);
+ //self.stake_table.clone()s
+ self.da_stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| v.clone())
+ .collect()
+ }
+
+ /// Get all members of the committee for the current view
+ fn committee_members(
+ &self,
+ _view_number: ::View,
+ epoch: ::Epoch,
+ ) -> BTreeSet<::SignatureKey> {
+ let filter = self.make_quorum_filter(epoch);
+ self.stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| TYPES::SignatureKey::public_key(v))
+ .collect()
+ }
+
+ /// Get all members of the committee for the current view
+ fn da_committee_members(
+ &self,
+ _view_number: ::View,
+ epoch: ::Epoch,
+ ) -> BTreeSet<::SignatureKey> {
+ let filter = self.make_da_quorum_filter(epoch);
+ self.da_stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| TYPES::SignatureKey::public_key(v))
+ .collect()
+ }
+
+ /// Get all eligible leaders of the committee for the current view
+ fn committee_leaders(
+ &self,
+ view_number: ::View,
+ epoch: ::Epoch,
+ ) -> BTreeSet<::SignatureKey> {
+ self.committee_members(view_number, epoch)
+ }
+
+ /// Get the stake table entry for a public key
+ fn stake(
+ &self,
+ pub_key: &::SignatureKey,
+ epoch: ::Epoch,
+ ) -> Option<::StakeTableEntry> {
+ let filter = self.make_quorum_filter(epoch);
+ let actual_members: BTreeSet<_> = self
+ .stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| TYPES::SignatureKey::public_key(v))
+ .collect();
+
+ if actual_members.contains(pub_key) {
+ // Only return the stake if it is above zero
+ self.indexed_stake_table.get(pub_key).cloned()
+ } else {
+ // Skip members which aren't included based on the quorum filter
+ None
+ }
+ }
+
+ /// Get the da stake table entry for a public key
+ fn da_stake(
+ &self,
+ pub_key: &::SignatureKey,
+ epoch: ::Epoch,
+ ) -> Option<::StakeTableEntry> {
+ let filter = self.make_da_quorum_filter(epoch);
+ let actual_members: BTreeSet<_> = self
+ .da_stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| TYPES::SignatureKey::public_key(v))
+ .collect();
+
+ if actual_members.contains(pub_key) {
+ // Only return the stake if it is above zero
+ self.indexed_da_stake_table.get(pub_key).cloned()
+ } else {
+ // Skip members which aren't included based on the quorum filter
+ None
+ }
+ }
+
+ /// Check if a node has stake in the committee
+ fn has_stake(
+ &self,
+ pub_key: &::SignatureKey,
+ epoch: ::Epoch,
+ ) -> bool {
+ let filter = self.make_quorum_filter(epoch);
+ let actual_members: BTreeSet<_> = self
+ .stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| TYPES::SignatureKey::public_key(v))
+ .collect();
+
+ if actual_members.contains(pub_key) {
+ self.indexed_stake_table
+ .get(pub_key)
+ .is_some_and(|x| x.stake() > U256::zero())
+ } else {
+ // Skip members which aren't included based on the quorum filter
+ false
+ }
+ }
+
+ /// Check if a node has stake in the committee
+ fn has_da_stake(
+ &self,
+ pub_key: &::SignatureKey,
+ epoch: ::Epoch,
+ ) -> bool {
+ let filter = self.make_da_quorum_filter(epoch);
+ let actual_members: BTreeSet<_> = self
+ .da_stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| TYPES::SignatureKey::public_key(v))
+ .collect();
+
+ if actual_members.contains(pub_key) {
+ self.indexed_da_stake_table
+ .get(pub_key)
+ .is_some_and(|x| x.stake() > U256::zero())
+ } else {
+ // Skip members which aren't included based on the quorum filter
+ false
+ }
+ }
+
+ /// Index the vector of public keys with the current view number
+ fn lookup_leader(
+ &self,
+ view_number: TYPES::View,
+ epoch: ::Epoch,
+ ) -> Result {
+ let filter = self.make_quorum_filter(epoch);
+ let leader_vec: Vec<_> = self
+ .stake_table
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| filter.contains(idx))
+ .map(|(_, v)| v.clone())
+ .collect();
+
+ let mut rng: StdRng = rand::SeedableRng::seed_from_u64(*view_number);
+
+ let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX);
+ #[allow(clippy::cast_possible_truncation)]
+ let index = randomized_view_number as usize % leader_vec.len();
+
+ let res = leader_vec[index].clone();
+
+ Ok(TYPES::SignatureKey::public_key(&res))
+ }
+
+ /// Get the total number of nodes in the committee
+ fn total_nodes(&self, epoch: ::Epoch) -> usize {
+ self.make_quorum_filter(epoch).len()
+ }
+
+ /// Get the total number of nodes in the committee
+ fn da_total_nodes(&self, epoch: ::Epoch) -> usize {
+ self.make_da_quorum_filter(epoch).len()
+ }
+
+ /// Get the voting success threshold for the committee
+ fn success_threshold(&self, epoch: ::Epoch) -> NonZeroU64 {
+ let len = self.total_nodes(epoch);
+ NonZeroU64::new(((len as u64 * 2) / 3) + 1).unwrap()
+ }
+
+ /// Get the voting success threshold for the committee
+ fn da_success_threshold(&self, epoch: ::Epoch) -> NonZeroU64 {
+ let len = self.da_total_nodes(epoch);
+ NonZeroU64::new(((len as u64 * 2) / 3) + 1).unwrap()
+ }
+
+ /// Get the voting failure threshold for the committee
+ fn failure_threshold(&self, epoch: ::Epoch) -> NonZeroU64 {
+ let len = self.total_nodes(epoch);
+ NonZeroU64::new(((len as u64) / 3) + 1).unwrap()
+ }
+
+ /// Get the voting upgrade threshold for the committee
+ fn upgrade_threshold(&self, epoch: ::Epoch) -> NonZeroU64 {
+ let len = self.total_nodes(epoch);
+ NonZeroU64::new(max((len as u64 * 9) / 10, ((len as u64 * 2) / 3) + 1)).unwrap()
+ }
+}
diff --git a/crates/hotshot/src/traits/election/static_committee.rs b/crates/hotshot/src/traits/election/static_committee.rs
index fa904c66cf..d2b62f80b7 100644
--- a/crates/hotshot/src/traits/election/static_committee.rs
+++ b/crates/hotshot/src/traits/election/static_committee.rs
@@ -215,26 +215,23 @@ impl Membership for StaticCommittee {
}
/// Get the voting success threshold for the committee
- fn success_threshold(&self) -> NonZeroU64 {
+ fn success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}
/// Get the voting success threshold for the committee
- fn da_success_threshold(&self) -> NonZeroU64 {
+ fn da_success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.da_stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}
/// Get the voting failure threshold for the committee
- fn failure_threshold(&self) -> NonZeroU64 {
+ fn failure_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap()
}
/// Get the voting upgrade threshold for the committee
- fn upgrade_threshold(&self) -> NonZeroU64 {
- NonZeroU64::new(max(
- (self.stake_table.len() as u64 * 9) / 10,
- ((self.stake_table.len() as u64 * 2) / 3) + 1,
- ))
- .unwrap()
+ fn upgrade_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
+ let len = self.stake_table.len();
+ NonZeroU64::new(max((len as u64 * 9) / 10, ((len as u64 * 2) / 3) + 1)).unwrap()
}
}
diff --git a/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs b/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs
index 41ed1d046e..8833d06872 100644
--- a/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs
+++ b/crates/hotshot/src/traits/election/static_committee_leader_two_views.rs
@@ -217,22 +217,22 @@ impl Membership for StaticCommitteeLeaderForTwoViews NonZeroU64 {
+ fn success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}
/// Get the voting success threshold for the committee
- fn da_success_threshold(&self) -> NonZeroU64 {
+ fn da_success_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.da_stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}
/// Get the voting failure threshold for the committee
- fn failure_threshold(&self) -> NonZeroU64 {
+ fn failure_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64) / 3) + 1).unwrap()
}
/// Get the voting upgrade threshold for the committee
- fn upgrade_threshold(&self) -> NonZeroU64 {
+ fn upgrade_threshold(&self, _epoch: ::Epoch) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64 * 9) / 10) + 1).unwrap()
}
}
diff --git a/crates/hotshot/src/traits/networking/memory_network.rs b/crates/hotshot/src/traits/networking/memory_network.rs
index d48f6f5f79..5925a85eff 100644
--- a/crates/hotshot/src/traits/networking/memory_network.rs
+++ b/crates/hotshot/src/traits/networking/memory_network.rs
@@ -88,7 +88,7 @@ struct MemoryNetworkInner {
/// This provides an in memory simulation of a networking implementation, allowing nodes running on
/// the same machine to mock networking while testing other functionality.
///
-/// Under the hood, this simply maintains mpmc channels to every other `MemoryNetwork` insane of the
+/// Under the hood, this simply maintains mpmc channels to every other `MemoryNetwork` instance of the
/// same group.
#[derive(Clone)]
pub struct MemoryNetwork {
@@ -297,22 +297,53 @@ impl ConnectedNetwork for MemoryNetwork {
&self,
message: Vec,
recipients: Vec,
- broadcast_delay: BroadcastDelay,
+ _broadcast_delay: BroadcastDelay,
) -> Result<(), NetworkError> {
- // Iterate over all topics, compare to recipients, and get the `Topic`
- let topic = self
+ trace!(?message, "Broadcasting message to DA");
+ for node in self
.inner
.master_map
.subscribed_map
+ .entry(Topic::Da)
+ .or_default()
.iter()
- .find(|v| v.value().iter().all(|(k, _)| recipients.contains(k)))
- .map(|v| v.key().clone())
- .ok_or(NetworkError::MessageSendError(
- "no topic found for recipients".to_string(),
- ))?;
-
- self.broadcast_message(message, topic, broadcast_delay)
- .await
+ {
+ if !recipients.contains(&node.0) {
+ tracing::error!("Skipping node because not in recipient list: {:?}", &node.0);
+ continue;
+ }
+ // TODO delay/drop etc here
+ let (key, node) = node;
+ trace!(?key, "Sending message to node");
+ if let Some(ref config) = &self.inner.reliability_config {
+ {
+ let node2 = node.clone();
+ let fut = config.chaos_send_msg(
+ message.clone(),
+ Arc::new(move |msg: Vec| {
+ let node3 = (node2).clone();
+ boxed_sync(async move {
+ let _res = node3.input(msg).await;
+ // NOTE we're dropping metrics here but this is only for testing
+ // purposes. I think that should be okay
+ })
+ }),
+ );
+ spawn(fut);
+ }
+ } else {
+ let res = node.input(message.clone()).await;
+ match res {
+ Ok(()) => {
+ trace!(?key, "Delivered message to remote");
+ }
+ Err(e) => {
+ warn!(?e, ?key, "Error sending broadcast message to node");
+ }
+ }
+ }
+ }
+ Ok(())
}
#[instrument(name = "MemoryNetwork::direct_message")]
diff --git a/crates/hotshot/src/types/handle.rs b/crates/hotshot/src/types/handle.rs
index 7b1fd5a424..9ea46b34d7 100644
--- a/crates/hotshot/src/types/handle.rs
+++ b/crates/hotshot/src/types/handle.rs
@@ -184,7 +184,6 @@ impl + 'static, V: Versions>
.ok_or(anyhow!("Event dependency failed to get event"))?;
// Then, if it's `Some`, make sure that the data is correct
-
if let HotShotEvent::QuorumProposalResponseRecv(quorum_proposal) = hs_event.as_ref()
{
// Make sure that the quorum_proposal is valid
diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs
index e1920bf4fa..80f49e1d6a 100644
--- a/crates/macros/src/lib.rs
+++ b/crates/macros/src/lib.rs
@@ -11,25 +11,41 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseStream, Result},
- parse_macro_input, Expr, ExprArray, ExprPath, ExprTuple, Ident, LitBool, Token,
+ parse_macro_input,
+ punctuated::Punctuated,
+ Expr, ExprArray, ExprPath, ExprTuple, Ident, LitBool, PathArguments, Token, TypePath,
};
+/// Bracketed types, e.g. [A, B, C]
+/// These types can have generic parameters, whereas ExprArray items must be Expr.
+#[derive(derive_builder::Builder, Debug, Clone)]
+struct TypePathBracketedArray {
+ /// elems
+ pub elems: Punctuated,
+}
+
/// description of a crosstest
#[derive(derive_builder::Builder, Debug, Clone)]
struct CrossTestData {
/// imlementations
impls: ExprArray,
+
/// builder impl
#[builder(default = "syn::parse_str(\"[SimpleBuilderImplementation]\").unwrap()")]
builder_impls: ExprArray,
+
/// versions
versions: ExprArray,
+
/// types
- types: ExprArray,
+ types: TypePathBracketedArray,
+
/// name of the test
test_name: Ident,
+
/// test description/spec
metadata: Expr,
+
/// whether or not to ignore
ignore: LitBool,
}
@@ -51,17 +67,23 @@ impl CrossTestDataBuilder {
#[derive(derive_builder::Builder, Debug, Clone)]
struct TestData {
/// type
- ty: ExprPath,
+ ty: TypePath,
+
/// impl
imply: ExprPath,
+
/// builder implementation
builder_impl: ExprPath,
+
/// impl
version: ExprPath,
+
/// name of test
test_name: Ident,
+
/// test description
metadata: Expr,
+
/// whether or not to ignore the test
ignore: LitBool,
}
@@ -86,6 +108,58 @@ impl ToLowerSnakeStr for ExprPath {
}
}
+impl ToLowerSnakeStr for syn::GenericArgument {
+ /// allow panic because this is a compiler error
+ #[allow(clippy::panic)]
+ fn to_lower_snake_str(&self) -> String {
+ match self {
+ syn::GenericArgument::Lifetime(l) => l.ident.to_string().to_lowercase(),
+ syn::GenericArgument::Type(t) => match t {
+ syn::Type::Path(p) => p.to_lower_snake_str(),
+ _ => {
+ panic!("Unexpected type for GenericArgument::Type: {t:?}");
+ }
+ },
+ syn::GenericArgument::Const(c) => match c {
+ syn::Expr::Lit(l) => match &l.lit {
+ syn::Lit::Str(v) => format!("{}_", v.value().to_lowercase()),
+ syn::Lit::Int(v) => format!("{}_", v.base10_digits()),
+ _ => {
+ panic!("Unexpected type for GenericArgument::Const::Lit: {l:?}");
+ }
+ },
+ _ => {
+ panic!("Unexpected type for GenericArgument::Const: {c:?}");
+ }
+ },
+ _ => {
+ panic!("Unexpected type for GenericArgument: {self:?}");
+ }
+ }
+ }
+}
+
+impl ToLowerSnakeStr for TypePath {
+ fn to_lower_snake_str(&self) -> String {
+ self.path
+ .segments
+ .iter()
+ .fold(String::new(), |mut acc, s| {
+ acc.push_str(&s.ident.to_string().to_lowercase());
+ if let PathArguments::AngleBracketed(a) = &s.arguments {
+ acc.push('_');
+ for arg in &a.args {
+ acc.push_str(&arg.to_lower_snake_str());
+ }
+ }
+
+ acc.push('_');
+ acc
+ })
+ .to_lowercase()
+ }
+}
+
impl ToLowerSnakeStr for ExprTuple {
/// allow panic because this is a compiler error
#[allow(clippy::panic)]
@@ -149,6 +223,28 @@ mod keywords {
syn::custom_keyword!(Versions);
}
+impl Parse for TypePathBracketedArray {
+ /// allow panic because this is a compiler error
+ #[allow(clippy::panic)]
+ fn parse(input: ParseStream<'_>) -> Result {
+ let content;
+ syn::bracketed!(content in input);
+ let mut elems = Punctuated::new();
+
+ while !content.is_empty() {
+ let first: TypePath = content.parse()?;
+ elems.push_value(first);
+ if content.is_empty() {
+ break;
+ }
+ let punct = content.parse()?;
+ elems.push_punct(punct);
+ }
+
+ Ok(Self { elems })
+ }
+}
+
impl Parse for CrossTestData {
/// allow panic because this is a compiler error
#[allow(clippy::panic)]
@@ -159,7 +255,7 @@ impl Parse for CrossTestData {
if input.peek(keywords::Types) {
let _ = input.parse::()?;
input.parse::()?;
- let types = input.parse::()?;
+ let types = input.parse::()?; //ExprArray>()?;
description.types(types);
} else if input.peek(keywords::Impls) {
let _ = input.parse::()?;
@@ -216,13 +312,8 @@ fn cross_tests_internal(test_spec: CrossTestData) -> TokenStream {
};
p
});
- //
- let types = test_spec.types.elems.iter().map(|t| {
- let Expr::Path(p) = t else {
- panic!("Expected Path for Type! Got {t:?}");
- };
- p
- });
+
+ let types = test_spec.types.elems.iter();
let versions = test_spec.versions.elems.iter().map(|t| {
let Expr::Path(p) = t else {
diff --git a/crates/task-impls/src/consensus/handlers.rs b/crates/task-impls/src/consensus/handlers.rs
index 6dab4f938c..27fc0a7b43 100644
--- a/crates/task-impls/src/consensus/handlers.rs
+++ b/crates/task-impls/src/consensus/handlers.rs
@@ -286,7 +286,10 @@ pub(crate) async fn handle_timeout
task_state
.membership
.has_stake(&task_state.public_key, task_state.cur_epoch),
- debug!("We were not chosen for the consensus committee for view {view_number:?}")
+ debug!(
+ "We were not chosen for the consensus committee for view {:?}",
+ view_number
+ )
);
let vote = TimeoutVote::create_signed_vote(
diff --git a/crates/task-impls/src/da.rs b/crates/task-impls/src/da.rs
index 68df39136f..2c503d2b13 100644
--- a/crates/task-impls/src/da.rs
+++ b/crates/task-impls/src/da.rs
@@ -120,7 +120,6 @@ impl, V: Versions> DaTaskState(
if !justify_qc
.is_valid_cert(
quorum_membership.stake_table(cur_epoch),
- quorum_membership.success_threshold(),
+ quorum_membership.success_threshold(cur_epoch),
upgrade_lock,
)
.await
@@ -686,7 +686,9 @@ pub(crate) async fn validate_proposal_view_and_certs<
validation_info
.quorum_membership
.stake_table(validation_info.cur_epoch),
- validation_info.quorum_membership.success_threshold(),
+ validation_info
+ .quorum_membership
+ .success_threshold(validation_info.cur_epoch),
&validation_info.upgrade_lock
)
.await,
@@ -709,7 +711,9 @@ pub(crate) async fn validate_proposal_view_and_certs<
validation_info
.quorum_membership
.stake_table(validation_info.cur_epoch),
- validation_info.quorum_membership.success_threshold(),
+ validation_info
+ .quorum_membership
+ .success_threshold(validation_info.cur_epoch),
&validation_info.upgrade_lock
)
.await,
diff --git a/crates/task-impls/src/quorum_proposal/handlers.rs b/crates/task-impls/src/quorum_proposal/handlers.rs
index 55f283bcd8..b1cc8e36fa 100644
--- a/crates/task-impls/src/quorum_proposal/handlers.rs
+++ b/crates/task-impls/src/quorum_proposal/handlers.rs
@@ -128,7 +128,8 @@ impl ProposalDependencyHandle {
// TODO take epoch from `qc`
// https://github.com/EspressoSystems/HotShot/issues/3917
self.quorum_membership.stake_table(TYPES::Epoch::new(0)),
- self.quorum_membership.success_threshold(),
+ self.quorum_membership
+ .success_threshold(TYPES::Epoch::new(0)),
&self.upgrade_lock,
)
.await
diff --git a/crates/task-impls/src/quorum_proposal/mod.rs b/crates/task-impls/src/quorum_proposal/mod.rs
index 034db12812..06150dff97 100644
--- a/crates/task-impls/src/quorum_proposal/mod.rs
+++ b/crates/task-impls/src/quorum_proposal/mod.rs
@@ -444,7 +444,7 @@ impl, V: Versions>
certificate
.is_valid_cert(
self.quorum_membership.stake_table(epoch_number),
- self.quorum_membership.success_threshold(),
+ self.quorum_membership.success_threshold(epoch_number),
&self.upgrade_lock
)
.await,
@@ -508,11 +508,11 @@ impl, V: Versions>
ensure!(
qc.is_valid_cert(
self.quorum_membership.stake_table(epoch_number),
- self.quorum_membership.success_threshold(),
+ self.quorum_membership.success_threshold(epoch_number),
&self.upgrade_lock
)
.await,
- warn!("Qurom certificate {:?} was invalid", qc.data())
+ warn!("Quorum certificate {:?} was invalid", qc.data())
);
self.highest_qc = qc.clone();
}
diff --git a/crates/task-impls/src/quorum_proposal_recv/handlers.rs b/crates/task-impls/src/quorum_proposal_recv/handlers.rs
index 73f6addef0..3d5c010058 100644
--- a/crates/task-impls/src/quorum_proposal_recv/handlers.rs
+++ b/crates/task-impls/src/quorum_proposal_recv/handlers.rs
@@ -157,7 +157,9 @@ pub(crate) async fn handle_quorum_proposal_recv<
validation_info
.quorum_membership
.stake_table(validation_info.cur_epoch),
- validation_info.quorum_membership.success_threshold(),
+ validation_info
+ .quorum_membership
+ .success_threshold(validation_info.cur_epoch),
&validation_info.upgrade_lock,
)
.await
diff --git a/crates/task-impls/src/quorum_vote/mod.rs b/crates/task-impls/src/quorum_vote/mod.rs
index 0567329788..7f4178171d 100644
--- a/crates/task-impls/src/quorum_vote/mod.rs
+++ b/crates/task-impls/src/quorum_vote/mod.rs
@@ -482,7 +482,7 @@ impl, V: Versions> QuorumVoteTaskS
ensure!(
cert.is_valid_cert(
self.membership.da_stake_table(cur_epoch),
- self.membership.da_success_threshold(),
+ self.membership.da_success_threshold(cur_epoch),
&self.upgrade_lock
)
.await,
diff --git a/crates/task-impls/src/view_sync.rs b/crates/task-impls/src/view_sync.rs
index efb87f9cd5..6bb83dfc53 100644
--- a/crates/task-impls/src/view_sync.rs
+++ b/crates/task-impls/src/view_sync.rs
@@ -535,7 +535,7 @@ impl ViewSyncReplicaTaskState {
if !certificate
.is_valid_cert(
self.membership.stake_table(self.cur_epoch),
- self.membership.failure_threshold(),
+ self.membership.failure_threshold(self.cur_epoch),
&self.upgrade_lock,
)
.await
@@ -621,7 +621,7 @@ impl ViewSyncReplicaTaskState {
if !certificate
.is_valid_cert(
self.membership.stake_table(self.cur_epoch),
- self.membership.success_threshold(),
+ self.membership.success_threshold(self.cur_epoch),
&self.upgrade_lock,
)
.await
@@ -718,7 +718,7 @@ impl ViewSyncReplicaTaskState {
if !certificate
.is_valid_cert(
self.membership.stake_table(self.cur_epoch),
- self.membership.success_threshold(),
+ self.membership.success_threshold(self.cur_epoch),
&self.upgrade_lock,
)
.await
diff --git a/crates/testing/src/helpers.rs b/crates/testing/src/helpers.rs
index 6759d17e69..cc468a1859 100644
--- a/crates/testing/src/helpers.rs
+++ b/crates/testing/src/helpers.rs
@@ -140,7 +140,7 @@ pub async fn build_cert<
CERT: Certificate,
>(
data: DATAType,
- membership: &TYPES::Membership,
+ da_membership: &TYPES::Membership,
view: TYPES::View,
epoch: TYPES::Epoch,
public_key: &TYPES::SignatureKey,
@@ -149,7 +149,7 @@ pub async fn build_cert<
) -> CERT {
let real_qc_sig = build_assembled_sig::(
&data,
- membership,
+ da_membership,
view,
epoch,
upgrade_lock,
@@ -215,7 +215,7 @@ pub async fn build_assembled_sig<
let real_qc_pp: ::QcParams =
::public_parameter(
stake_table.clone(),
- U256::from(CERT::threshold(membership)),
+ U256::from(CERT::threshold(membership, epoch)),
);
let total_nodes = stake_table.len();
let signers = bitvec![1; total_nodes];
diff --git a/crates/testing/src/test_runner.rs b/crates/testing/src/test_runner.rs
index 414e6e0f4b..ffee9b39e5 100644
--- a/crates/testing/src/test_runner.rs
+++ b/crates/testing/src/test_runner.rs
@@ -308,7 +308,7 @@ where
for node in &mut *nodes {
node.handle.shut_down().await;
}
- tracing::info!("Nodes shtudown");
+ tracing::info!("Nodes shutdown");
completion_handle.abort();
diff --git a/crates/testing/tests/tests_1/block_builder.rs b/crates/testing/tests/tests_1/block_builder.rs
index 5b0a6cf5c2..fc29b1c01d 100644
--- a/crates/testing/tests/tests_1/block_builder.rs
+++ b/crates/testing/tests/tests_1/block_builder.rs
@@ -12,7 +12,7 @@ use std::{
use hotshot_builder_api::v0_1::block_info::AvailableBlockData;
use hotshot_example_types::{
block_types::{TestBlockPayload, TestMetadata, TestTransaction},
- node_types::{TestTypes, TestVersions},
+ node_types::TestTypes,
};
use hotshot_task_impls::builder::{BuilderClient, BuilderClientError};
use hotshot_testing::block_builder::{
@@ -21,14 +21,13 @@ use hotshot_testing::block_builder::{
use hotshot_types::{
network::RandomBuilderConfig,
traits::{
- block_contents::vid_commitment,
- node_implementation::{NodeType, Versions},
- signature_key::SignatureKey,
+ block_contents::vid_commitment, node_implementation::NodeType, signature_key::SignatureKey,
BlockPayload,
},
};
use tide_disco::Url;
use tokio::time::sleep;
+use vbs::version::StaticVersion;
#[cfg(test)]
#[tokio::test(flavor = "multi_thread")]
@@ -50,8 +49,7 @@ async fn test_random_block_builder() {
let builder_started = Instant::now();
- let client: BuilderClient::Base> =
- BuilderClient::new(api_url);
+ let client: BuilderClient> = BuilderClient::new(api_url);
assert!(client.connect(Duration::from_millis(100)).await);
let (pub_key, private_key) =
diff --git a/crates/testing/tests/tests_1/message.rs b/crates/testing/tests/tests_1/message.rs
index 3778aa4737..9536cf0f22 100644
--- a/crates/testing/tests/tests_1/message.rs
+++ b/crates/testing/tests/tests_1/message.rs
@@ -104,7 +104,7 @@ async fn test_certificate2_validity() {
assert!(
qc.is_valid_cert(
membership.stake_table(EpochNumber::new(0)),
- membership.success_threshold(),
+ membership.success_threshold(EpochNumber::new(0)),
&handle.hotshot.upgrade_lock
)
.await
@@ -113,7 +113,7 @@ async fn test_certificate2_validity() {
assert!(
qc2.is_valid_cert(
membership.stake_table(EpochNumber::new(0)),
- membership.success_threshold(),
+ membership.success_threshold(EpochNumber::new(0)),
&handle.hotshot.upgrade_lock
)
.await
diff --git a/crates/testing/tests/tests_1/test_success.rs b/crates/testing/tests/tests_1/test_success.rs
index e81060aedb..982b7018f6 100644
--- a/crates/testing/tests/tests_1/test_success.rs
+++ b/crates/testing/tests/tests_1/test_success.rs
@@ -41,6 +41,25 @@ cross_tests!(
},
);
+// cross_tests!(
+// TestName: test_epoch_success,
+// Impls: [MemoryImpl, Libp2pImpl, PushCdnImpl],
+// Types: [TestTypes, TestTypesRandomizedLeader, TestTypesRandomizedCommitteeMembers>, TestTypesRandomizedCommitteeMembers>],
+// Versions: [EpochsTestVersions],
+// Ignore: false,
+// Metadata: {
+// TestDescription {
+// // allow more time to pass in CI
+// completion_task_description: CompletionTaskDescription::TimeBasedCompletionTaskBuilder(
+// TimeBasedCompletionTaskDescription {
+// duration: Duration::from_secs(60),
+// },
+// ),
+// ..TestDescription::default()
+// }
+// },
+// );
+
cross_tests!(
TestName: test_success_with_async_delay,
Impls: [MemoryImpl, Libp2pImpl, PushCdnImpl],
diff --git a/crates/types/src/simple_certificate.rs b/crates/types/src/simple_certificate.rs
index 271d5b0729..f2cc8cd689 100644
--- a/crates/types/src/simple_certificate.rs
+++ b/crates/types/src/simple_certificate.rs
@@ -24,7 +24,7 @@ use crate::{
data::serialize_signature2,
message::UpgradeLock,
simple_vote::{
- DaData, QuorumData, QuorumData2, QuorumMaker, TimeoutData, UpgradeProposalData,
+ DaData, QuorumData, QuorumData2, QuorumMarker, TimeoutData, UpgradeProposalData,
VersionedVoteData, ViewSyncCommitData, ViewSyncFinalizeData, ViewSyncPreCommitData,
Voteable,
},
@@ -39,7 +39,10 @@ use crate::{
/// Trait which allows use to inject different threshold calculations into a Certificate type
pub trait Threshold {
/// Calculate a threshold based on the membership
- fn threshold>(membership: &MEMBERSHIP) -> u64;
+ fn threshold>(
+ membership: &MEMBERSHIP,
+ epoch: ::Epoch,
+ ) -> u64;
}
/// Defines a threshold which is 2f + 1 (Amount needed for Quorum)
@@ -47,8 +50,11 @@ pub trait Threshold {
pub struct SuccessThreshold {}
impl Threshold for SuccessThreshold {
- fn threshold>(membership: &MEMBERSHIP) -> u64 {
- membership.success_threshold().into()
+ fn threshold>(
+ membership: &MEMBERSHIP,
+ epoch: ::Epoch,
+ ) -> u64 {
+ membership.success_threshold(epoch).into()
}
}
@@ -57,8 +63,11 @@ impl Threshold for SuccessThreshold {
pub struct OneHonestThreshold {}
impl Threshold for OneHonestThreshold {
- fn threshold>(membership: &MEMBERSHIP) -> u64 {
- membership.failure_threshold().into()
+ fn threshold>(
+ membership: &MEMBERSHIP,
+ epoch: ::Epoch,
+ ) -> u64 {
+ membership.failure_threshold(epoch).into()
}
}
@@ -67,8 +76,11 @@ impl Threshold for OneHonestThreshold {
pub struct UpgradeThreshold {}
impl Threshold for UpgradeThreshold {
- fn threshold>(membership: &MEMBERSHIP) -> u64 {
- membership.upgrade_threshold().into()
+ fn threshold>(
+ membership: &MEMBERSHIP,
+ epoch: ::Epoch,
+ ) -> u64 {
+ membership.upgrade_threshold(epoch).into()
}
}
@@ -192,8 +204,11 @@ impl> Certificate
) -> usize {
membership.da_total_nodes(epoch)
}
- fn threshold>(membership: &MEMBERSHIP) -> u64 {
- membership.da_success_threshold().into()
+ fn threshold>(
+ membership: &MEMBERSHIP,
+ epoch: ::Epoch,
+ ) -> u64 {
+ membership.da_success_threshold(epoch).into()
}
fn data(&self) -> &Self::Voteable {
&self.data
@@ -210,7 +225,7 @@ impl> Certificate
}
}
-impl>
+impl>
Certificate for SimpleCertificate
{
type Voteable = VOTEABLE;
@@ -254,8 +269,11 @@ impl>(membership: &MEMBERSHIP) -> u64 {
- THRESHOLD::threshold(membership)
+ fn threshold>(
+ membership: &MEMBERSHIP,
+ epoch: ::Epoch,
+ ) -> u64 {
+ THRESHOLD::threshold(membership, epoch)
}
fn stake_table_entry>(
@@ -345,7 +363,7 @@ impl UpgradeCertificate {
ensure!(
cert.is_valid_cert(
quorum_membership.stake_table(epoch),
- quorum_membership.upgrade_threshold(),
+ quorum_membership.upgrade_threshold(epoch),
upgrade_lock
)
.await,
diff --git a/crates/types/src/simple_vote.rs b/crates/types/src/simple_vote.rs
index 35997d3725..138f730fb9 100644
--- a/crates/types/src/simple_vote.rs
+++ b/crates/types/src/simple_vote.rs
@@ -25,7 +25,7 @@ use crate::{
};
/// Marker that data should use the quorum cert type
-pub(crate) trait QuorumMaker {}
+pub(crate) trait QuorumMarker {}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)]
/// Data used for a yes vote.
@@ -117,13 +117,13 @@ mod sealed {
impl Sealed for C {}
}
-impl QuorumMaker for QuorumData {}
-impl QuorumMaker for QuorumData2 {}
-impl QuorumMaker for TimeoutData {}
-impl QuorumMaker for ViewSyncPreCommitData {}
-impl QuorumMaker for ViewSyncCommitData {}
-impl QuorumMaker for ViewSyncFinalizeData {}
-impl QuorumMaker for UpgradeProposalData {}
+impl QuorumMarker for QuorumData {}
+impl QuorumMarker for QuorumData2 {}
+impl QuorumMarker for TimeoutData {}
+impl QuorumMarker for ViewSyncPreCommitData {}
+impl QuorumMarker for ViewSyncCommitData {}
+impl QuorumMarker for ViewSyncFinalizeData {}
+impl QuorumMarker for UpgradeProposalData {}
/// A simple yes vote over some votable type.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)]
diff --git a/crates/types/src/traits/election.rs b/crates/types/src/traits/election.rs
index 04aa76ccb4..5b72ea4f84 100644
--- a/crates/types/src/traits/election.rs
+++ b/crates/types/src/traits/election.rs
@@ -115,14 +115,14 @@ pub trait Membership: Clone + Debug + Send + Sync {
fn da_total_nodes(&self, epoch: TYPES::Epoch) -> usize;
/// Returns the threshold for a specific `Membership` implementation
- fn success_threshold(&self) -> NonZeroU64;
+ fn success_threshold(&self, epoch: TYPES::Epoch) -> NonZeroU64;
/// Returns the DA threshold for a specific `Membership` implementation
- fn da_success_threshold(&self) -> NonZeroU64;
+ fn da_success_threshold(&self, epoch: TYPES::Epoch) -> NonZeroU64;
/// Returns the threshold for a specific `Membership` implementation
- fn failure_threshold(&self) -> NonZeroU64;
+ fn failure_threshold(&self, epoch: TYPES::Epoch) -> NonZeroU64;
/// Returns the threshold required to upgrade the network protocol
- fn upgrade_threshold(&self) -> NonZeroU64;
+ fn upgrade_threshold(&self, epoch: TYPES::Epoch) -> NonZeroU64;
}
diff --git a/crates/types/src/traits/metrics.rs b/crates/types/src/traits/metrics.rs
index 38d9a8d6ea..cd29c75da5 100644
--- a/crates/types/src/traits/metrics.rs
+++ b/crates/types/src/traits/metrics.rs
@@ -212,13 +212,14 @@ pub trait Counter: Send + Sync + Debug + DynClone {
/// Add a value to the counter
fn add(&self, amount: usize);
}
+
/// A gauge that stores the latest value.
pub trait Gauge: Send + Sync + Debug + DynClone {
/// Set the gauge value
fn set(&self, amount: usize);
/// Update the gauge value
- fn update(&self, delts: i64);
+ fn update(&self, delta: i64);
}
/// A histogram which will record a series of points.
diff --git a/crates/types/src/vote.rs b/crates/types/src/vote.rs
index bdff9d4bb5..13112afa12 100644
--- a/crates/types/src/vote.rs
+++ b/crates/types/src/vote.rs
@@ -81,7 +81,10 @@ pub trait Certificate: HasViewNumber {
) -> impl std::future::Future