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

Seed leader rotation from QC #3743

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 3 additions & 1 deletion crates/hotshot/src/traits/election.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

//! elections used for consensus

/// leader completely randomized every view
/// leader completely randomized every view, seeded by QC signatures
pub mod randomized_committee;
/// leader completely randomized every view, using a shared seed at startup
pub mod seeded_randomized_committee;
/// static (round robin) committee election
pub mod static_committee;
/// static (round robin leader for 2 consecutive views) committee election
Expand Down
25 changes: 20 additions & 5 deletions crates/hotshot/src/traits/election/randomized_committee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

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

use anyhow::Result;
use async_lock::RwLock;
use ethereum_types::U256;
use hotshot_types::{
traits::{
Expand All @@ -18,10 +20,8 @@ use hotshot_types::{
};
use rand::{rngs::StdRng, Rng};

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

/// The static committee election

#[derive(Debug)]
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
Expand All @@ -37,8 +37,18 @@ pub struct RandomizedCommittee<T: NodeType> {

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

/// Seeds indexed by block height, used to randomize the stake table.
seeds: RwLock<BTreeMap<u64, [u8; 32]>>,
}

impl<T: NodeType> Clone for RandomizedCommittee<T> {
fn clone(&self) -> Self {
todo!()
}
}

#[async_trait::async_trait]
impl<TYPES: NodeType> Membership<TYPES> for RandomizedCommittee<TYPES> {
/// Create a new election
fn new(
Expand Down Expand Up @@ -75,6 +85,7 @@ impl<TYPES: NodeType> Membership<TYPES> for RandomizedCommittee<TYPES> {
eligible_leaders,
stake_table: members,
indexed_stake_table,
seeds: RwLock::new(BTreeMap::new()),
committee_topic,
}
}
Expand Down Expand Up @@ -108,6 +119,10 @@ impl<TYPES: NodeType> Membership<TYPES> for RandomizedCommittee<TYPES> {
.collect()
}

async fn add_seed(&self, block_height: u64, seed: [u8; 32]) {
self.seeds.write().await.insert(block_height, seed);
}

/// Get the stake table entry for a public key
fn stake(
&self,
Expand All @@ -130,7 +145,7 @@ impl<TYPES: NodeType> Membership<TYPES> for RandomizedCommittee<TYPES> {
}

/// Index the vector of public keys with the current view number
fn leader(&self, view_number: TYPES::Time) -> TYPES::SignatureKey {
async fn leader(&self, view_number: TYPES::Time) -> Result<TYPES::SignatureKey> {
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(*view_number);

let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX);
Expand All @@ -139,7 +154,7 @@ impl<TYPES: NodeType> Membership<TYPES> for RandomizedCommittee<TYPES> {

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

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

/// Get the total number of nodes in the committee
Expand Down
176 changes: 176 additions & 0 deletions crates/hotshot/src/traits/election/seeded_randomized_committee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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 anyhow::Result;
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};

/// The static committee election
#[derive(Debug)]
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<T: NodeType> Clone for RandomizedCommittee<T> {
fn clone(&self) -> Self {
todo!()
}
}

#[async_trait::async_trait]
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()
}

async fn add_seed(&self, _block_height: u64, _seed: [u8; 32]) {}

/// 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
async fn leader(&self, view_number: TYPES::Time) -> Result<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();

Ok(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()
}
}
8 changes: 6 additions & 2 deletions crates/hotshot/src/traits/election/static_committee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

use anyhow::Result;
use ethereum_types::U256;
use hotshot_types::{
traits::{
Expand Down Expand Up @@ -37,6 +38,7 @@ pub struct StaticCommittee<T: NodeType> {
committee_topic: Topic,
}

#[async_trait::async_trait]
impl<TYPES: NodeType> Membership<TYPES> for StaticCommittee<TYPES> {
/// Create a new election
fn new(
Expand Down Expand Up @@ -106,6 +108,8 @@ impl<TYPES: NodeType> Membership<TYPES> for StaticCommittee<TYPES> {
.collect()
}

async fn add_seed(&self, _block_height: u64, _seed: [u8; 32]) {}

/// Get the stake table entry for a public key
fn stake(
&self,
Expand All @@ -128,11 +132,11 @@ impl<TYPES: NodeType> Membership<TYPES> for StaticCommittee<TYPES> {
}

/// Index the vector of public keys with the current view number
fn leader(&self, view_number: TYPES::Time) -> TYPES::SignatureKey {
async fn leader(&self, view_number: TYPES::Time) -> Result<TYPES::SignatureKey> {
#[allow(clippy::cast_possible_truncation)]
let index = *view_number as usize % self.eligible_leaders.len();
let res = self.eligible_leaders[index].clone();
TYPES::SignatureKey::public_key(&res)
Ok(TYPES::SignatureKey::public_key(&res))
}

/// Get the total number of nodes in the committee
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

use anyhow::Result;
use ethereum_types::U256;
use hotshot_types::{
traits::{
Expand Down Expand Up @@ -37,6 +38,7 @@ pub struct StaticCommitteeLeaderForTwoViews<T: NodeType> {
committee_topic: Topic,
}

#[async_trait::async_trait]
impl<TYPES: NodeType> Membership<TYPES> for StaticCommitteeLeaderForTwoViews<TYPES> {
/// Create a new election
fn new(
Expand Down Expand Up @@ -106,6 +108,8 @@ impl<TYPES: NodeType> Membership<TYPES> for StaticCommitteeLeaderForTwoViews<TYP
.collect()
}

async fn add_seed(&self, _block_height: u64, _seed: [u8; 32]) {}

/// Get the stake table entry for a public key
fn stake(
&self,
Expand All @@ -128,11 +132,12 @@ impl<TYPES: NodeType> Membership<TYPES> for StaticCommitteeLeaderForTwoViews<TYP
}

/// Index the vector of public keys with the current view number
fn leader(&self, view_number: TYPES::Time) -> TYPES::SignatureKey {
async fn leader(&self, view_number: TYPES::Time) -> Result<TYPES::SignatureKey> {
let index =
usize::try_from((*view_number / 2) % self.eligible_leaders.len() as u64).unwrap();
let res = self.eligible_leaders[index].clone();
TYPES::SignatureKey::public_key(&res)

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

/// Get the total number of nodes in the committee
Expand Down
10 changes: 9 additions & 1 deletion crates/hotshot/src/traits/networking/libp2p_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,15 @@ impl<K: SignatureKey + 'static> ConnectedNetwork<K> for Libp2pNetwork<K> {
TYPES: NodeType<SignatureKey = K> + 'a,
{
let future_view = <TYPES as NodeType>::Time::new(view) + LOOK_AHEAD;
let future_leader = membership.leader(future_view);
let future_leader = match membership.leader(future_view).await {
Ok(l) => l,
Err(e) => {
return tracing::info!(
"Failed to calculate leader for view {:?}: {e}",
future_view
);
}
};

let _ = self
.queue_node_lookup(ViewNumber::new(*future_view), future_leader)
Expand Down
7 changes: 6 additions & 1 deletion crates/hotshot/src/types/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use std::sync::Arc;

use anyhow::Result;
use async_broadcast::{InactiveReceiver, Receiver, Sender};
use async_lock::RwLock;
use futures::Stream;
Expand Down Expand Up @@ -189,12 +190,16 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES> + 'static, V: Versions>
}

/// Wrapper for `HotShotConsensusApi`'s `leader` function
///
/// # Errors
/// Returns an error if the leader cannot be calculated
#[allow(clippy::unused_async)] // async for API compatibility reasons
pub async fn leader(&self, view_number: TYPES::Time) -> TYPES::SignatureKey {
pub async fn leader(&self, view_number: TYPES::Time) -> Result<TYPES::SignatureKey> {
self.hotshot
.memberships
.quorum_membership
.leader(view_number)
.await
}

// Below is for testing only:
Expand Down
Loading