Skip to content

Commit

Permalink
Randomize leader election (#3693)
Browse files Browse the repository at this point in the history
  • Loading branch information
ss-es authored Oct 1, 2024
1 parent 9d6286b commit f010b3b
Show file tree
Hide file tree
Showing 19 changed files with 461 additions and 412 deletions.
33 changes: 31 additions & 2 deletions crates/example-types/src/node_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use hotshot::traits::{
election::{
static_committee::{GeneralStaticCommittee, StaticCommittee},
randomized_committee::RandomizedCommittee, static_committee::StaticCommittee,
static_committee_leader_two_views::StaticCommitteeLeaderForTwoViews,
},
implementations::{CombinedNetworks, Libp2pNetwork, MemoryNetwork, PushCdnNetwork},
Expand Down Expand Up @@ -52,7 +52,36 @@ impl NodeType for TestTypes {
type Transaction = TestTransaction;
type ValidatedState = TestValidatedState;
type InstanceState = TestInstanceState;
type Membership = GeneralStaticCommittee<TestTypes>;
type Membership = StaticCommittee<TestTypes>;
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 TestTypesRandomizedLeader;
impl NodeType for TestTypesRandomizedLeader {
type AuctionResult = TestAuctionResult;
type Time = ViewNumber;
type BlockHeader = TestBlockHeader;
type BlockPayload = TestBlockPayload;
type SignatureKey = BLSPubKey;
type Transaction = TestTransaction;
type ValidatedState = TestValidatedState;
type InstanceState = TestInstanceState;
type Membership = RandomizedCommittee<TestTypesRandomizedLeader>;
type BuilderSignatureKey = BuilderKey;
}

Expand Down
1 change: 0 additions & 1 deletion crates/examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ bin-orchestrator = ["clap"]
docs = []
doc-images = []
hotshot-testing = ["hotshot/hotshot-testing"]
randomized-leader-election = []
fixed-leader-election = []

# Common
Expand Down
33 changes: 23 additions & 10 deletions crates/examples/infra/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,27 +381,29 @@ pub trait RunDa<

let network = self.network();

let all_nodes = config.config.known_nodes_with_stake.clone();
let all_nodes = if cfg!(feature = "fixed-leader-election") {
let mut vec = config.config.known_nodes_with_stake.clone();

vec.truncate(config.config.fixed_leader_for_gpuvid);

vec
} else {
config.config.known_nodes_with_stake.clone()
};

let da_nodes = config.config.known_da_nodes.clone();

// Create the quorum membership from all nodes
let quorum_membership = <TYPES as NodeType>::Membership::new(
all_nodes.clone(),
all_nodes.clone(),
Topic::Global,
#[cfg(feature = "fixed-leader-election")]
config.config.fixed_leader_for_gpuvid,
);

// Create the quorum membership from all nodes, specifying the committee
// as the known da nodes
let da_membership = <TYPES as NodeType>::Membership::new(
all_nodes.clone(),
da_nodes,
Topic::Da,
#[cfg(feature = "fixed-leader-election")]
config.config.fixed_leader_for_gpuvid,
);
let da_membership =
<TYPES as NodeType>::Membership::new(all_nodes.clone(), da_nodes, Topic::Da);

let memberships = Memberships {
quorum_membership: quorum_membership.clone(),
Expand Down Expand Up @@ -557,6 +559,12 @@ pub trait RunDa<
}
let consensus_lock = context.hotshot.consensus();
let consensus = consensus_lock.read().await;
let num_eligible_leaders = context
.hotshot
.memberships
.quorum_membership
.committee_leaders(TYPES::Time::genesis())
.len();
let total_num_views = usize::try_from(consensus.locked_view().u64()).unwrap();
// `failed_num_views` could include uncommitted views
let failed_num_views = total_num_views - num_successful_commits;
Expand All @@ -574,6 +582,7 @@ pub trait RunDa<
/ total_time_elapsed_sec;
let avg_latency_in_sec = total_latency / num_latency;
println!("[{node_index}]: throughput: {throughput_bytes_per_sec} bytes/sec, avg_latency: {avg_latency_in_sec} sec.");

BenchResults {
partial_results: "Unset".to_string(),
avg_latency_in_sec,
Expand All @@ -586,6 +595,10 @@ pub trait RunDa<
total_time_elapsed_in_sec: total_time_elapsed.as_secs(),
total_num_views,
failed_num_views,
committee_type: format!(
"{} with {num_eligible_leaders} eligible leaders",
std::any::type_name::<TYPES::Membership>()
),
}
} else {
// all values with zero
Expand Down
2 changes: 0 additions & 2 deletions crates/hotshot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ bin-orchestrator = ["clap"]
docs = []
doc-images = []
hotshot-testing = []
randomized-leader-election = []
fixed-leader-election = []

[dependencies]
anyhow = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/hotshot/src/traits/election.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

//! elections used for consensus
/// leader completely randomized every view
pub mod randomized_committee;
/// static (round robin) committee election
pub mod static_committee;
/// static (round robin leader for 2 consecutive views) committee election
Expand Down
168 changes: 168 additions & 0 deletions crates/hotshot/src/traits/election/randomized_committee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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 <https://mit-license.org/>.

use std::{cmp::max, collections::BTreeMap, num::NonZeroU64};

use ethereum_types::U256;
use hotshot_types::{
traits::{
election::Membership,
network::Topic,
node_implementation::NodeType,
signature_key::{SignatureKey, StakeTableEntryType},
},
PeerConfig,
};
use rand::{rngs::StdRng, Rng};

#[derive(Clone, Debug, Eq, PartialEq, Hash)]

/// The static committee election
pub struct RandomizedCommittee<T: NodeType> {
/// 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<<T::SignatureKey as SignatureKey>::StakeTableEntry>,

/// The nodes on the committee and their stake
stake_table: Vec<<T::SignatureKey as SignatureKey>::StakeTableEntry>,

/// The nodes on the committee and their stake, indexed by public key
indexed_stake_table:
BTreeMap<T::SignatureKey, <T::SignatureKey as SignatureKey>::StakeTableEntry>,

/// The network topic of the committee
committee_topic: Topic,
}

impl<TYPES: NodeType> Membership<TYPES> for RandomizedCommittee<TYPES> {
/// Create a new election
fn new(
eligible_leaders: Vec<PeerConfig<<TYPES as NodeType>::SignatureKey>>,
committee_members: Vec<PeerConfig<<TYPES as NodeType>::SignatureKey>>,
committee_topic: Topic,
) -> Self {
// For each eligible leader, get the stake table entry
let eligible_leaders: Vec<<TYPES::SignatureKey as SignatureKey>::StakeTableEntry> =
eligible_leaders
.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<<TYPES::SignatureKey as SignatureKey>::StakeTableEntry> =
committee_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,
<TYPES::SignatureKey as SignatureKey>::StakeTableEntry,
> = members
.iter()
.map(|entry| (TYPES::SignatureKey::public_key(entry), entry.clone()))
.collect();

Self {
eligible_leaders,
stake_table: members,
indexed_stake_table,
committee_topic,
}
}

/// Get the stake table for the current view
fn stake_table(
&self,
) -> Vec<<<TYPES as NodeType>::SignatureKey as SignatureKey>::StakeTableEntry> {
self.stake_table.clone()
}

/// Get all members of the committee for the current view
fn committee_members(
&self,
_view_number: <TYPES as NodeType>::Time,
) -> std::collections::BTreeSet<<TYPES as NodeType>::SignatureKey> {
self.stake_table
.iter()
.map(TYPES::SignatureKey::public_key)
.collect()
}

/// Get all eligible leaders of the committee for the current view
fn committee_leaders(
&self,
_view_number: <TYPES as NodeType>::Time,
) -> std::collections::BTreeSet<<TYPES as NodeType>::SignatureKey> {
self.eligible_leaders
.iter()
.map(TYPES::SignatureKey::public_key)
.collect()
}

/// Get the stake table entry for a public key
fn stake(
&self,
pub_key: &<TYPES as NodeType>::SignatureKey,
) -> Option<<TYPES::SignatureKey as SignatureKey>::StakeTableEntry> {
// Only return the stake if it is above zero
self.indexed_stake_table.get(pub_key).cloned()
}

/// Check if a node has stake in the committee
fn has_stake(&self, pub_key: &<TYPES as NodeType>::SignatureKey) -> bool {
self.indexed_stake_table
.get(pub_key)
.is_some_and(|x| x.stake() > U256::zero())
}

/// Get the network topic for the committee
fn committee_topic(&self) -> Topic {
self.committee_topic.clone()
}

/// Index the vector of public keys with the current view number
fn leader(&self, view_number: TYPES::Time) -> TYPES::SignatureKey {
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 % self.eligible_leaders.len();

let res = self.eligible_leaders[index].clone();

TYPES::SignatureKey::public_key(&res)
}

/// Get the total number of nodes in the committee
fn total_nodes(&self) -> usize {
self.stake_table.len()
}

/// Get the voting success threshold for the committee
fn success_threshold(&self) -> NonZeroU64 {
NonZeroU64::new(((self.stake_table.len() as u64 * 2) / 3) + 1).unwrap()
}

/// Get the voting failure threshold for the committee
fn failure_threshold(&self) -> 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()
}
}
Loading

0 comments on commit f010b3b

Please sign in to comment.