Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Randomize leader election #3693

Merged
merged 11 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
rob-maron marked this conversation as resolved.
Show resolved Hide resolved

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
Loading