From 66eed6e58c723ff4bf9079ba17743faed8bc7144 Mon Sep 17 00:00:00 2001
From: Wen <113942165+wen-coding@users.noreply.github.com>
Date: Sun, 21 Sep 2025 14:36:27 -0700
Subject: [PATCH 1/6] Allow creating v4 account in genesis, upstream Consensus
Pool.
---
Cargo.lock | 1 +
local-cluster/src/local_cluster.rs | 1 +
program-test/src/lib.rs | 1 +
programs/sbf/Cargo.lock | 1 +
programs/sbf/tests/programs.rs | 1 +
programs/stake/src/stake_state.rs | 22 +-
runtime/Cargo.toml | 1 +
runtime/src/genesis_utils.rs | 88 +-
test-validator/src/lib.rs | 1 +
votor/src/commitment.rs | 7 +
votor/src/consensus_pool.rs | 2261 +++++++++++++++++++++++++++-
votor/src/lib.rs | 3 +
12 files changed, 2369 insertions(+), 19 deletions(-)
create mode 100644 votor/src/commitment.rs
diff --git a/Cargo.lock b/Cargo.lock
index 9e71c7b43ac1eb..9b45fb961bbb7e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10391,6 +10391,7 @@ dependencies = [
"solana-vote",
"solana-vote-interface",
"solana-vote-program",
+ "solana-votor-messages",
"spl-generic-token",
"static_assertions",
"strum",
diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs
index 646aca0e8e8896..5557a61c0ffdd3 100644
--- a/local-cluster/src/local_cluster.rs
+++ b/local-cluster/src/local_cluster.rs
@@ -312,6 +312,7 @@ impl LocalCluster {
&keys_in_genesis,
stakes_in_genesis,
config.cluster_type,
+ false,
);
genesis_config.accounts.extend(
config
diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs
index f5be6aeef061f1..4e178729c568c5 100644
--- a/program-test/src/lib.rs
+++ b/program-test/src/lib.rs
@@ -805,6 +805,7 @@ impl ProgramTest {
&bootstrap_validator_pubkey,
&voting_keypair.pubkey(),
&Pubkey::new_unique(),
+ None,
bootstrap_validator_stake_lamports,
42,
fee_rate_governor,
diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock
index 2de0f24444df0f..c0b99faf6571d4 100644
--- a/programs/sbf/Cargo.lock
+++ b/programs/sbf/Cargo.lock
@@ -8176,6 +8176,7 @@ dependencies = [
"solana-vote",
"solana-vote-interface",
"solana-vote-program",
+ "solana-votor-messages",
"spl-generic-token",
"static_assertions",
"strum",
diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs
index 33646aa2710590..391e114251f3a7 100644
--- a/programs/sbf/tests/programs.rs
+++ b/programs/sbf/tests/programs.rs
@@ -1595,6 +1595,7 @@ fn get_stable_genesis_config() -> GenesisConfigInfo {
&validator_pubkey,
&voting_keypair.pubkey(),
&stake_pubkey,
+ None,
bootstrap_validator_stake_lamports(),
42,
FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees
diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs
index 447afecf788fac..15b236a90a1f04 100644
--- a/programs/stake/src/stake_state.rs
+++ b/programs/stake/src/stake_state.rs
@@ -12,7 +12,7 @@ use {
solana_rent::Rent,
solana_sdk_ids::stake::id,
solana_stake_interface::stake_flags::StakeFlags,
- solana_vote_interface::state::VoteStateV3,
+ solana_vote_interface::state::{VoteStateV3, VoteStateV4},
};
// utility function, used by Stakes, tests
@@ -40,15 +40,10 @@ pub fn meta_from(account: &AccountSharedData) -> Option {
from(account).and_then(|state: StakeStateV2| state.meta())
}
-fn new_stake(
- stake: u64,
- voter_pubkey: &Pubkey,
- vote_state: &VoteStateV3,
- activation_epoch: Epoch,
-) -> Stake {
+fn new_stake(stake: u64, voter_pubkey: &Pubkey, credits: u64, activation_epoch: Epoch) -> Stake {
Stake {
delegation: Delegation::new(voter_pubkey, stake, activation_epoch),
- credits_observed: vote_state.credits(),
+ credits_observed: credits,
}
}
@@ -106,7 +101,14 @@ fn do_create_account(
) -> AccountSharedData {
let mut stake_account = AccountSharedData::new(lamports, StakeStateV2::size_of(), &id());
- let vote_state = VoteStateV3::deserialize(vote_account.data()).expect("vote_state");
+ let credits = if let Ok(vote_state_v3) = VoteStateV3::deserialize(vote_account.data()) {
+ vote_state_v3.credits()
+ } else {
+ match VoteStateV4::deserialize(vote_account.data(), voter_pubkey) {
+ Ok(vote_state_v4) => vote_state_v4.epoch_credits.last().map_or(0, |(_, c, _)| *c),
+ Err(e) => panic!("Invalid vote account state data: {e}"),
+ }
+ };
let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len());
@@ -120,7 +122,7 @@ fn do_create_account(
new_stake(
lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve);
voter_pubkey,
- &vote_state,
+ credits,
activation_epoch,
),
StakeFlags::empty(),
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 09f6ce39a4717b..b8663178f71455 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -176,6 +176,7 @@ solana-version = { workspace = true }
solana-vote = { workspace = true }
solana-vote-interface = { workspace = true }
solana-vote-program = { workspace = true }
+solana-votor-messages = { workspace = true }
spl-generic-token = { workspace = true }
static_assertions = { workspace = true }
strum = { workspace = true, features = ["derive"] }
diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs
index 31c24b3906686a..2d71c2c0bfb9f0 100644
--- a/runtime/src/genesis_utils.rs
+++ b/runtime/src/genesis_utils.rs
@@ -2,6 +2,10 @@ use {
agave_feature_set::{FeatureSet, FEATURE_NAMES},
log::*,
solana_account::{Account, AccountSharedData},
+ solana_bls_signatures::{
+ keypair::Keypair as BLSKeypair, pubkey::PubkeyCompressed as BLSPubkeyCompressed,
+ Pubkey as BLSPubkey,
+ },
solana_cluster_type::ClusterType,
solana_feature_gate_interface::{self as feature, Feature},
solana_fee_calculator::FeeRateGovernor,
@@ -15,7 +19,9 @@ use {
solana_stake_interface::state::StakeStateV2,
solana_stake_program::stake_state,
solana_system_interface::program as system_program,
+ solana_vote_interface::state::BLS_PUBLIC_KEY_COMPRESSED_SIZE,
solana_vote_program::vote_state,
+ solana_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED,
std::borrow::Borrow,
};
@@ -99,6 +105,21 @@ pub fn create_genesis_config_with_vote_accounts(
voting_keypairs,
stakes,
ClusterType::Development,
+ false,
+ )
+}
+
+pub fn create_genesis_config_with_alpenglow_vote_accounts(
+ mint_lamports: u64,
+ voting_keypairs: &[impl Borrow],
+ stakes: Vec,
+) -> GenesisConfigInfo {
+ create_genesis_config_with_vote_accounts_and_cluster_type(
+ mint_lamports,
+ voting_keypairs,
+ stakes,
+ ClusterType::Development,
+ true,
)
}
@@ -107,6 +128,7 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type(
voting_keypairs: &[impl Borrow],
stakes: Vec,
cluster_type: ClusterType,
+ is_alpenglow: bool,
) -> GenesisConfigInfo {
assert!(!voting_keypairs.is_empty());
assert_eq!(voting_keypairs.len(), stakes.len());
@@ -115,12 +137,23 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type(
let voting_keypair = voting_keypairs[0].borrow().vote_keypair.insecure_clone();
let validator_pubkey = voting_keypairs[0].borrow().node_keypair.pubkey();
+ let validator_bls_pubkey = if is_alpenglow {
+ let bls_keypair = BLSKeypair::derive_from_signer(
+ &voting_keypairs[0].borrow().vote_keypair,
+ BLS_KEYPAIR_DERIVE_SEED,
+ )
+ .unwrap();
+ Some(bls_pubkey_to_compressed_bytes(&bls_keypair.public))
+ } else {
+ None
+ };
let genesis_config = create_genesis_config_with_leader_ex(
mint_lamports,
&mint_keypair.pubkey(),
&validator_pubkey,
&voting_keypairs[0].borrow().vote_keypair.pubkey(),
&voting_keypairs[0].borrow().stake_keypair.pubkey(),
+ validator_bls_pubkey,
stakes[0],
VALIDATOR_LAMPORTS,
FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees
@@ -143,7 +176,24 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type(
// Create accounts
let node_account = Account::new(VALIDATOR_LAMPORTS, 0, &system_program::id());
- let vote_account = vote_state::create_account(&vote_pubkey, &node_pubkey, 0, *stake);
+ let vote_account = if is_alpenglow {
+ let bls_keypair = BLSKeypair::derive_from_signer(
+ &voting_keypairs[0].borrow().vote_keypair,
+ BLS_KEYPAIR_DERIVE_SEED,
+ )
+ .unwrap();
+ let bls_pubkey_compressed = bls_pubkey_to_compressed_bytes(&bls_keypair.public);
+ vote_state::create_v4_account_with_authorized(
+ &node_pubkey,
+ &vote_pubkey,
+ &vote_pubkey,
+ Some(bls_pubkey_compressed),
+ 0,
+ *stake,
+ )
+ } else {
+ vote_state::create_account(&vote_pubkey, &node_pubkey, 0, *stake)
+ };
let stake_account = Account::from(stake_state::create_account(
&stake_pubkey,
&vote_pubkey,
@@ -204,6 +254,7 @@ pub fn create_genesis_config_with_leader_with_mint_keypair(
validator_pubkey,
&voting_keypair.pubkey(),
&Pubkey::new_unique(),
+ None,
validator_stake_lamports,
VALIDATOR_LAMPORTS,
FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees
@@ -228,7 +279,7 @@ pub fn activate_all_features(genesis_config: &mut GenesisConfig) {
do_activate_all_features::(genesis_config);
}
-fn do_activate_all_features(genesis_config: &mut GenesisConfig) {
+pub fn do_activate_all_features(genesis_config: &mut GenesisConfig) {
// Activate all features at genesis in development mode
for feature_id in FeatureSet::default().inactive() {
if IS_ALPENGLOW || *feature_id != agave_feature_set::alpenglow::id() {
@@ -266,6 +317,13 @@ pub fn activate_feature(genesis_config: &mut GenesisConfig, feature_id: Pubkey)
);
}
+pub fn bls_pubkey_to_compressed_bytes(
+ bls_pubkey: &BLSPubkey,
+) -> [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE] {
+ let key = BLSPubkeyCompressed::try_from(bls_pubkey).unwrap();
+ bincode::serialize(&key).unwrap().try_into().unwrap()
+}
+
#[allow(clippy::too_many_arguments)]
pub fn create_genesis_config_with_leader_ex_no_features(
mint_lamports: u64,
@@ -273,6 +331,7 @@ pub fn create_genesis_config_with_leader_ex_no_features(
validator_pubkey: &Pubkey,
validator_vote_account_pubkey: &Pubkey,
validator_stake_account_pubkey: &Pubkey,
+ validator_bls_pubkey: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>,
validator_stake_lamports: u64,
validator_lamports: u64,
fee_rate_governor: FeeRateGovernor,
@@ -280,12 +339,23 @@ pub fn create_genesis_config_with_leader_ex_no_features(
cluster_type: ClusterType,
mut initial_accounts: Vec<(Pubkey, AccountSharedData)>,
) -> GenesisConfig {
- let validator_vote_account = vote_state::create_account(
- validator_vote_account_pubkey,
- validator_pubkey,
- 0,
- validator_stake_lamports,
- );
+ let validator_vote_account = if let Some(bls_pubkey_compressed) = validator_bls_pubkey {
+ vote_state::create_v4_account_with_authorized(
+ validator_pubkey,
+ validator_vote_account_pubkey,
+ validator_vote_account_pubkey,
+ Some(bls_pubkey_compressed),
+ 0,
+ validator_stake_lamports,
+ )
+ } else {
+ vote_state::create_account(
+ validator_vote_account_pubkey,
+ validator_pubkey,
+ 0,
+ validator_stake_lamports,
+ )
+ };
let validator_stake_account = stake_state::create_account(
validator_stake_account_pubkey,
@@ -342,6 +412,7 @@ pub fn create_genesis_config_with_leader_ex(
validator_pubkey: &Pubkey,
validator_vote_account_pubkey: &Pubkey,
validator_stake_account_pubkey: &Pubkey,
+ validator_bls_pubkey: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>,
validator_stake_lamports: u64,
validator_lamports: u64,
fee_rate_governor: FeeRateGovernor,
@@ -355,6 +426,7 @@ pub fn create_genesis_config_with_leader_ex(
validator_pubkey,
validator_vote_account_pubkey,
validator_stake_account_pubkey,
+ validator_bls_pubkey,
validator_stake_lamports,
validator_lamports,
fee_rate_governor,
diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs
index fc8cf7a7a13ac4..ed813cda6afa07 100644
--- a/test-validator/src/lib.rs
+++ b/test-validator/src/lib.rs
@@ -927,6 +927,7 @@ impl TestValidator {
&validator_identity.pubkey(),
&validator_vote_account.pubkey(),
&validator_stake_account.pubkey(),
+ None,
validator_stake_lamports,
validator_identity_lamports,
config.fee_rate_governor.clone(),
diff --git a/votor/src/commitment.rs b/votor/src/commitment.rs
new file mode 100644
index 00000000000000..493ec8538d4c7e
--- /dev/null
+++ b/votor/src/commitment.rs
@@ -0,0 +1,7 @@
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum AlpenglowCommitmentError {
+ #[error("Failed to send commitment data, channel disconnected")]
+ ChannelDisconnected,
+}
diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs
index a1035ade74dc5b..b0d777e2910545 100644
--- a/votor/src/consensus_pool.rs
+++ b/votor/src/consensus_pool.rs
@@ -1,5 +1,2264 @@
+use {
+ crate::{
+ commitment::AlpenglowCommitmentError,
+ common::{
+ certificate_limits_and_vote_types, conflicting_types, vote_to_certificate_ids, Stake,
+ VoteType, MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE,
+ MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES,
+ },
+ consensus_pool::{
+ parent_ready_tracker::ParentReadyTracker,
+ slot_stake_counters::SlotStakeCounters,
+ stats::ConsensusPoolStats,
+ vote_certificate_builder::{CertificateError, VoteCertificateBuilder},
+ vote_pool::{DuplicateBlockVotePool, SimpleVotePool, VotePool, VotePoolType},
+ },
+ event::VotorEvent,
+ },
+ log::{error, trace},
+ solana_clock::{Epoch, Slot},
+ solana_epoch_schedule::EpochSchedule,
+ solana_hash::Hash,
+ solana_pubkey::Pubkey,
+ solana_runtime::{bank::Bank, epoch_stakes::VersionedEpochStakes},
+ solana_votor_messages::{
+ consensus_message::{
+ Block, Certificate, CertificateMessage, CertificateType, ConsensusMessage, VoteMessage,
+ },
+ vote::Vote,
+ },
+ std::{
+ cmp::Ordering,
+ collections::{BTreeMap, HashMap},
+ sync::Arc,
+ },
+ thiserror::Error,
+};
+
pub mod parent_ready_tracker;
-pub mod slot_stake_counters;
+mod slot_stake_counters;
mod stats;
mod vote_certificate_builder;
mod vote_pool;
+
+pub type PoolId = (Slot, VoteType);
+
+#[derive(Debug, Error, PartialEq)]
+pub enum AddVoteError {
+ #[error("Conflicting vote type: {0:?} vs existing {1:?} for slot: {2} pubkey: {3}")]
+ ConflictingVoteType(VoteType, VoteType, Slot, Pubkey),
+
+ #[error("Epoch stakes missing for epoch: {0}")]
+ EpochStakesNotFound(Epoch),
+
+ #[error("Unrooted slot")]
+ UnrootedSlot,
+
+ #[error("Slot in the future")]
+ SlotInFuture,
+
+ #[error("Certificate error: {0}")]
+ Certificate(#[from] CertificateError),
+
+ #[error("{0} channel disconnected")]
+ ChannelDisconnected(String),
+
+ #[error("Voting Service queue full")]
+ VotingServiceQueueFull,
+
+ #[error("Invalid rank: {0}")]
+ InvalidRank(u16),
+}
+
+impl From for AddVoteError {
+ fn from(_: AlpenglowCommitmentError) -> Self {
+ AddVoteError::ChannelDisconnected("CommitmentSender".to_string())
+ }
+}
+
+fn get_key_and_stakes(
+ epoch_schedule: &EpochSchedule,
+ epoch_stakes_map: &HashMap,
+ slot: Slot,
+ rank: u16,
+) -> Result<(Pubkey, Stake, Stake), AddVoteError> {
+ let epoch = epoch_schedule.get_epoch(slot);
+ let epoch_stakes = epoch_stakes_map
+ .get(&epoch)
+ .ok_or(AddVoteError::EpochStakesNotFound(epoch))?;
+ let Some((vote_key, _)) = epoch_stakes
+ .bls_pubkey_to_rank_map()
+ .get_pubkey(rank as usize)
+ else {
+ return Err(AddVoteError::InvalidRank(rank));
+ };
+ let stake = epoch_stakes.vote_account_stake(vote_key);
+ if stake == 0 {
+ // Since we have a valid rank, this should never happen, there is no rank for zero stake.
+ panic!("Validator stake is zero for pubkey: {vote_key}");
+ }
+ Ok((*vote_key, stake, epoch_stakes.total_stake()))
+}
+
+pub struct ConsensusPool {
+ my_pubkey: Pubkey,
+ // Vote pools to do bean counting for votes.
+ vote_pools: BTreeMap,
+ /// Completed certificates
+ completed_certificates: BTreeMap>,
+ /// Tracks slots which have reached the parent ready condition:
+ /// - They have a potential parent block with a NotarizeFallback certificate
+ /// - All slots from the parent have a Skip certificate
+ pub parent_ready_tracker: ParentReadyTracker,
+ /// Highest block that has a NotarizeFallback certificate, for use in producing our leader window
+ highest_notarized_fallback: Option<(Slot, Hash)>,
+ /// Highest slot that has a Finalized variant certificate
+ highest_finalized_slot: Option,
+ /// Highest slot that has Finalize+Notarize or FinalizeFast, for use in standstill
+ /// Also add a bool to indicate whether this slot has FinalizeFast certificate
+ highest_finalized_with_notarize: Option<(Slot, bool)>,
+ /// Stats for the certificate pool
+ stats: ConsensusPoolStats,
+ /// Slot stake counters, used to calculate safe_to_notar and safe_to_skip
+ slot_stake_counters_map: BTreeMap,
+}
+
+impl ConsensusPool {
+ pub fn new_from_root_bank(my_pubkey: Pubkey, bank: &Bank) -> Self {
+ // To account for genesis and snapshots we allow default block id until
+ // block id can be serialized as part of the snapshot
+ let root_block = (bank.slot(), bank.block_id().unwrap_or_default());
+ let parent_ready_tracker = ParentReadyTracker::new(my_pubkey, root_block);
+
+ Self {
+ my_pubkey,
+ vote_pools: BTreeMap::new(),
+ completed_certificates: BTreeMap::new(),
+ highest_notarized_fallback: None,
+ highest_finalized_slot: None,
+ highest_finalized_with_notarize: None,
+ parent_ready_tracker,
+ stats: ConsensusPoolStats::new(),
+ slot_stake_counters_map: BTreeMap::new(),
+ }
+ }
+
+ fn new_vote_pool(vote_type: VoteType) -> VotePoolType {
+ match vote_type {
+ VoteType::NotarizeFallback => VotePoolType::DuplicateBlockVotePool(
+ DuplicateBlockVotePool::new(MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE),
+ ),
+ VoteType::Notarize => VotePoolType::DuplicateBlockVotePool(
+ DuplicateBlockVotePool::new(MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES),
+ ),
+ _ => VotePoolType::SimpleVotePool(SimpleVotePool::new()),
+ }
+ }
+
+ fn update_vote_pool(
+ &mut self,
+ slot: Slot,
+ vote_type: VoteType,
+ block_id: Option,
+ transaction: &VoteMessage,
+ validator_vote_key: &Pubkey,
+ validator_stake: Stake,
+ ) -> Option {
+ let pool = self
+ .vote_pools
+ .entry((slot, vote_type))
+ .or_insert_with(|| Self::new_vote_pool(vote_type));
+ match pool {
+ VotePoolType::SimpleVotePool(pool) => {
+ pool.add_vote(validator_vote_key, validator_stake, transaction)
+ }
+ VotePoolType::DuplicateBlockVotePool(pool) => pool.add_vote(
+ validator_vote_key,
+ block_id.expect("Duplicate block pool expects a block id"),
+ transaction,
+ validator_stake,
+ ),
+ }
+ }
+
+ /// For a new vote `slot` , `vote_type` checks if any
+ /// of the related certificates are newly complete.
+ /// For each newly constructed certificate
+ /// - Insert it into `self.certificates`
+ /// - Potentially update `self.highest_notarized_fallback`,
+ /// - Potentially update `self.highest_finalized_slot`,
+ /// - If we have a new highest finalized slot, return it
+ /// - update any newly created events
+ fn update_certificates(
+ &mut self,
+ vote: &Vote,
+ block_id: Option,
+ events: &mut Vec,
+ total_stake: Stake,
+ ) -> Result>, AddVoteError> {
+ let slot = vote.slot();
+ let mut new_certificates_to_send = Vec::new();
+ for cert_id in vote_to_certificate_ids(vote) {
+ // If the certificate is already complete, skip it
+ if self.completed_certificates.contains_key(&cert_id) {
+ continue;
+ }
+ // Otherwise check whether the certificate is complete
+ let (limit, vote_types) = certificate_limits_and_vote_types(cert_id);
+ let accumulated_stake = vote_types
+ .iter()
+ .filter_map(|vote_type| {
+ Some(match self.vote_pools.get(&(slot, *vote_type))? {
+ VotePoolType::SimpleVotePool(pool) => pool.total_stake(),
+ VotePoolType::DuplicateBlockVotePool(pool) => {
+ pool.total_stake_by_block_id(block_id.as_ref().expect(
+ "Duplicate block pool for {vote_type:?} expects a block id for \
+ certificate {cert_id:?}",
+ ))
+ }
+ })
+ })
+ .sum::();
+ if accumulated_stake as f64 / (total_stake as f64) < limit {
+ continue;
+ }
+ let mut vote_certificate_builder = VoteCertificateBuilder::new(cert_id);
+ vote_types.iter().for_each(|vote_type| {
+ if let Some(vote_pool) = self.vote_pools.get(&(slot, *vote_type)) {
+ match vote_pool {
+ VotePoolType::SimpleVotePool(pool) => {
+ pool.add_to_certificate(&mut vote_certificate_builder)
+ }
+ VotePoolType::DuplicateBlockVotePool(pool) => pool.add_to_certificate(
+ block_id.as_ref().expect(
+ "Duplicate block pool for {vote_type:?} expects a block id for \
+ certificate {cert_id:?}",
+ ),
+ &mut vote_certificate_builder,
+ ),
+ };
+ }
+ });
+ let new_cert = Arc::new(vote_certificate_builder.build()?);
+ self.insert_certificate(cert_id, new_cert.clone(), events);
+ self.stats
+ .incr_cert_type(new_cert.certificate.certificate_type(), true);
+ new_certificates_to_send.push(new_cert);
+ }
+ Ok(new_certificates_to_send)
+ }
+
+ fn has_conflicting_vote(
+ &self,
+ slot: Slot,
+ vote_type: VoteType,
+ validator_vote_key: &Pubkey,
+ block_id: &Option,
+ ) -> Option {
+ for conflicting_type in conflicting_types(vote_type) {
+ if let Some(pool) = self.vote_pools.get(&(slot, *conflicting_type)) {
+ let is_conflicting = match pool {
+ // In a simple vote pool, just check if the validator previously voted at all. If so, that's a conflict
+ VotePoolType::SimpleVotePool(pool) => {
+ pool.has_prev_validator_vote(validator_vote_key)
+ }
+ // In a duplicate block vote pool, because some conflicts between things like Notarize and NotarizeFallback
+ // for different blocks are allowed, we need a more specific check.
+ // TODO: This can be made much cleaner/safer if VoteType carried the bank hash, block id so we
+ // could check which exact VoteType(blockid, bankhash) was the source of the conflict.
+ VotePoolType::DuplicateBlockVotePool(pool) => {
+ if let Some(block_id) = &block_id {
+ // Reject votes for the same block with a conflicting type, i.e.
+ // a NotarizeFallback vote for the same block as a Notarize vote.
+ pool.has_prev_validator_vote_for_block(validator_vote_key, block_id)
+ } else {
+ pool.has_prev_validator_vote(validator_vote_key)
+ }
+ }
+ };
+ if is_conflicting {
+ return Some(*conflicting_type);
+ }
+ }
+ }
+ None
+ }
+
+ fn insert_certificate(
+ &mut self,
+ cert_id: Certificate,
+ cert: Arc,
+ events: &mut Vec,
+ ) {
+ trace!("{}: Inserting certificate {:?}", self.my_pubkey, cert_id);
+ self.completed_certificates.insert(cert_id, cert);
+ match cert_id {
+ Certificate::NotarizeFallback(slot, block_id) => {
+ self.parent_ready_tracker
+ .add_new_notar_fallback_or_stronger((slot, block_id), events);
+ if self
+ .highest_notarized_fallback
+ .is_none_or(|(s, _)| s < slot)
+ {
+ self.highest_notarized_fallback = Some((slot, block_id));
+ }
+ }
+ Certificate::Skip(slot) => self.parent_ready_tracker.add_new_skip(slot, events),
+ Certificate::Notarize(slot, block_id) => {
+ events.push(VotorEvent::BlockNotarized((slot, block_id)));
+ self.parent_ready_tracker
+ .add_new_notar_fallback_or_stronger((slot, block_id), events);
+ if self.is_finalized(slot) {
+ // It's fine to set FastFinalization to false here, because
+ // we will report correctly as long as we have FastFinalization cert.
+ events.push(VotorEvent::Finalized((slot, block_id), false));
+ if self
+ .highest_finalized_with_notarize
+ .is_none_or(|(s, _)| s < slot)
+ {
+ self.highest_finalized_with_notarize = Some((slot, false));
+ }
+ }
+ }
+ Certificate::Finalize(slot) => {
+ if let Some(block) = self.get_notarized_block(slot) {
+ events.push(VotorEvent::Finalized(block, false));
+ if self
+ .highest_finalized_with_notarize
+ .is_none_or(|(s, _)| s < slot)
+ {
+ self.highest_finalized_with_notarize = Some((slot, false));
+ }
+ }
+ if self.highest_finalized_slot.is_none_or(|s| s < slot) {
+ self.highest_finalized_slot = Some(slot);
+ }
+ }
+ Certificate::FinalizeFast(slot, block_id) => {
+ events.push(VotorEvent::Finalized((slot, block_id), true));
+ self.parent_ready_tracker
+ .add_new_notar_fallback_or_stronger((slot, block_id), events);
+ if self.highest_finalized_slot.is_none_or(|s| s < slot) {
+ self.highest_finalized_slot = Some(slot);
+ }
+ if self
+ .highest_finalized_with_notarize
+ .is_none_or(|(s, _)| s <= slot)
+ {
+ self.highest_finalized_with_notarize = Some((slot, true));
+ }
+ }
+ }
+ }
+
+ /// Adds the new vote the the certificate pool. If a new certificate is created
+ /// as a result of this, send it via the `self.certificate_sender`
+ ///
+ /// Any new votor events that are a result of adding this new vote will be added
+ /// to `events`.
+ ///
+ /// If this resulted in a new highest Finalize or FastFinalize certificate,
+ /// return the slot
+ pub fn add_message(
+ &mut self,
+ epoch_schedule: &EpochSchedule,
+ epoch_stakes_map: &HashMap,
+ root_slot: Slot,
+ my_vote_pubkey: &Pubkey,
+ message: &ConsensusMessage,
+ events: &mut Vec,
+ ) -> Result<(Option, Vec>), AddVoteError> {
+ let current_highest_finalized_slot = self.highest_finalized_slot;
+ let new_certficates_to_send = match message {
+ ConsensusMessage::Vote(vote_message) => self.add_vote(
+ epoch_schedule,
+ epoch_stakes_map,
+ root_slot,
+ my_vote_pubkey,
+ vote_message,
+ events,
+ )?,
+ ConsensusMessage::Certificate(certificate_message) => {
+ self.add_certificate(root_slot, certificate_message, events)?
+ }
+ };
+ // If we have a new highest finalized slot, return it
+ let new_finalized_slot = if self.highest_finalized_slot > current_highest_finalized_slot {
+ self.highest_finalized_slot
+ } else {
+ None
+ };
+ Ok((new_finalized_slot, new_certficates_to_send))
+ }
+
+ fn add_vote(
+ &mut self,
+ epoch_schedule: &EpochSchedule,
+ epoch_stakes_map: &HashMap,
+ root_slot: Slot,
+ my_vote_pubkey: &Pubkey,
+ vote_message: &VoteMessage,
+ events: &mut Vec,
+ ) -> Result>, AddVoteError> {
+ let vote = &vote_message.vote;
+ let rank = vote_message.rank;
+ let vote_slot = vote.slot();
+ let (validator_vote_key, validator_stake, total_stake) =
+ get_key_and_stakes(epoch_schedule, epoch_stakes_map, vote_slot, rank)?;
+
+ // Since we have a valid rank, this should never happen, there is no rank for zero stake.
+ assert_ne!(
+ validator_stake, 0,
+ "Validator stake is zero for pubkey: {validator_vote_key}"
+ );
+
+ self.stats.incoming_votes = self.stats.incoming_votes.saturating_add(1);
+ if vote_slot < root_slot {
+ self.stats.out_of_range_votes = self.stats.out_of_range_votes.saturating_add(1);
+ return Err(AddVoteError::UnrootedSlot);
+ }
+ let block_id = vote.block_id().map(|block_id| {
+ if !matches!(vote, Vote::Notarize(_) | Vote::NotarizeFallback(_)) {
+ panic!("expected Notarize or NotarizeFallback vote");
+ }
+ *block_id
+ });
+ let vote_type = VoteType::get_type(vote);
+ if let Some(conflicting_type) =
+ self.has_conflicting_vote(vote_slot, vote_type, &validator_vote_key, &block_id)
+ {
+ self.stats.conflicting_votes = self.stats.conflicting_votes.saturating_add(1);
+ return Err(AddVoteError::ConflictingVoteType(
+ vote_type,
+ conflicting_type,
+ vote_slot,
+ validator_vote_key,
+ ));
+ }
+ match self.update_vote_pool(
+ vote_slot,
+ vote_type,
+ block_id,
+ vote_message,
+ &validator_vote_key,
+ validator_stake,
+ ) {
+ None => {
+ // No new vote pool entry was created, just return empty vec
+ self.stats.exist_votes = self.stats.exist_votes.saturating_add(1);
+ return Ok(vec![]);
+ }
+ Some(entry_stake) => {
+ let fallback_vote_counters = self
+ .slot_stake_counters_map
+ .entry(vote_slot)
+ .or_insert_with(|| SlotStakeCounters::new(total_stake));
+ fallback_vote_counters.add_vote(
+ vote,
+ entry_stake,
+ my_vote_pubkey == &validator_vote_key,
+ events,
+ &mut self.stats,
+ );
+ }
+ }
+ self.stats.incr_ingested_vote_type(vote_type);
+
+ self.update_certificates(vote, block_id, events, total_stake)
+ }
+
+ fn add_certificate(
+ &mut self,
+ root_slot: Slot,
+ certificate_message: &CertificateMessage,
+ events: &mut Vec,
+ ) -> Result>, AddVoteError> {
+ let certificate_id = certificate_message.certificate;
+ self.stats.incoming_certs = self.stats.incoming_certs.saturating_add(1);
+ if certificate_id.slot() < root_slot {
+ self.stats.out_of_range_certs = self.stats.out_of_range_certs.saturating_add(1);
+ return Err(AddVoteError::UnrootedSlot);
+ }
+ if self.completed_certificates.contains_key(&certificate_id) {
+ self.stats.exist_certs = self.stats.exist_certs.saturating_add(1);
+ return Ok(vec![]);
+ }
+ let new_certificate = Arc::new(certificate_message.clone());
+ self.insert_certificate(certificate_id, new_certificate.clone(), events);
+
+ self.stats
+ .incr_cert_type(certificate_id.certificate_type(), false);
+
+ Ok(vec![new_certificate])
+ }
+
+ /// The highest notarized fallback slot, for use as the parent slot in leader window
+ pub fn highest_notarized_fallback(&self) -> Option<(Slot, Hash)> {
+ self.highest_notarized_fallback
+ }
+
+ /// Get the notarized block in `slot`
+ pub fn get_notarized_block(&self, slot: Slot) -> Option {
+ self.completed_certificates
+ .iter()
+ .find_map(|(cert_id, _)| match cert_id {
+ Certificate::Notarize(s, block_id) if slot == *s => Some((*s, *block_id)),
+ _ => None,
+ })
+ }
+
+ #[cfg(test)]
+ fn highest_notarized_slot(&self) -> Slot {
+ // Return the max of CertificateType::Notarize and CertificateType::NotarizeFallback
+ self.completed_certificates
+ .iter()
+ .filter_map(|(cert_id, _)| match cert_id {
+ Certificate::Notarize(s, _) => Some(s),
+ Certificate::NotarizeFallback(s, _) => Some(s),
+ _ => None,
+ })
+ .max()
+ .copied()
+ .unwrap_or(0)
+ }
+
+ #[cfg(test)]
+ fn highest_skip_slot(&self) -> Slot {
+ self.completed_certificates
+ .iter()
+ .filter_map(|(cert_id, _)| match cert_id {
+ Certificate::Skip(s) => Some(s),
+ _ => None,
+ })
+ .max()
+ .copied()
+ .unwrap_or(0)
+ }
+
+ pub fn highest_finalized_slot(&self) -> Slot {
+ self.completed_certificates
+ .iter()
+ .filter_map(|(cert_id, _)| match cert_id {
+ Certificate::Finalize(s) => Some(s),
+ Certificate::FinalizeFast(s, _) => Some(s),
+ _ => None,
+ })
+ .max()
+ .copied()
+ .unwrap_or(0)
+ }
+
+ pub fn highest_fast_finalized_block(&self) -> Option {
+ self.completed_certificates
+ .iter()
+ .filter_map(|(cert_id, _)| match cert_id {
+ Certificate::FinalizeFast(s, bid) => Some((*s, *bid)),
+ _ => None,
+ })
+ .max()
+ }
+
+ /// Checks if any block in the slot `s` is finalized
+ pub fn is_finalized(&self, slot: Slot) -> bool {
+ self.completed_certificates.keys().any(|cert_id| {
+ matches!(cert_id, Certificate::Finalize(s) | Certificate::FinalizeFast(s, _) if *s == slot)
+ })
+ }
+
+ /// Check if the specific block `(block_id)` in slot `s` is notarized
+ pub fn is_notarized(&self, slot: Slot, block_id: Hash) -> bool {
+ self.completed_certificates
+ .contains_key(&Certificate::Notarize(slot, block_id))
+ }
+
+ /// Checks if the any block in slot `slot` has received a `NotarizeFallback` certificate, if so return
+ /// the size of the certificate
+ #[cfg(test)]
+ pub fn slot_has_notarized_fallback(&self, slot: Slot) -> bool {
+ self.completed_certificates
+ .iter()
+ .any(|(cert_id, _)| matches!(cert_id, Certificate::NotarizeFallback(s,_) if *s == slot))
+ }
+
+ /// Checks if `slot` has a `Skip` certificate
+ pub fn skip_certified(&self, slot: Slot) -> bool {
+ self.completed_certificates
+ .contains_key(&Certificate::Skip(slot))
+ }
+
+ #[cfg(test)]
+ fn make_start_leader_decision(
+ &self,
+ my_leader_slot: Slot,
+ parent_slot: Slot,
+ first_alpenglow_slot: Slot,
+ ) -> bool {
+ // TODO: for GCE tests we WFSM on 1 so slot 1 is exempt
+ let needs_notarization_certificate = parent_slot >= first_alpenglow_slot && parent_slot > 1;
+
+ if needs_notarization_certificate
+ && !self.slot_has_notarized_fallback(parent_slot)
+ && !self.is_finalized(parent_slot)
+ {
+ error!("Missing notarization certificate {parent_slot}");
+ return false;
+ }
+
+ let needs_skip_certificate =
+ // handles cases where we are entering the alpenglow epoch, where the first
+ // slot in the epoch will pass my_leader_slot == parent_slot
+ my_leader_slot != first_alpenglow_slot &&
+ my_leader_slot != parent_slot.saturating_add(1);
+
+ if needs_skip_certificate {
+ let begin_skip_slot = first_alpenglow_slot.max(parent_slot.saturating_add(1));
+ for slot in begin_skip_slot..my_leader_slot {
+ if !self.skip_certified(slot) {
+ error!(
+ "Missing skip certificate for {slot}, required for skip certificate from \
+ {begin_skip_slot} to build {my_leader_slot}"
+ );
+ return false;
+ }
+ }
+ }
+
+ true
+ }
+
+ /// Cleanup any old slots from the certificate pool
+ pub fn prune_old_state(&mut self, root_slot: Slot) {
+ // `completed_certificates`` now only contains entries >= `slot`
+ self.completed_certificates
+ .retain(|cert_id, _| match cert_id {
+ Certificate::Finalize(s)
+ | Certificate::FinalizeFast(s, _)
+ | Certificate::Notarize(s, _)
+ | Certificate::NotarizeFallback(s, _)
+ | Certificate::Skip(s) => s >= &root_slot,
+ });
+ self.vote_pools = self.vote_pools.split_off(&(root_slot, VoteType::Finalize));
+ self.slot_stake_counters_map = self.slot_stake_counters_map.split_off(&root_slot);
+ self.parent_ready_tracker.set_root(root_slot);
+ }
+
+ /// Updates the pubkey used for logging purposes only.
+ /// This avoids the need to recreate the entire certificate pool since it's
+ /// not distinguished by the pubkey.
+ pub fn update_pubkey(&mut self, new_pubkey: Pubkey) {
+ self.my_pubkey = new_pubkey;
+ self.parent_ready_tracker.update_pubkey(new_pubkey);
+ }
+
+ pub fn maybe_report(&mut self) {
+ self.stats.maybe_report();
+ }
+
+ pub fn get_certs_for_standstill(&self) -> Vec> {
+ let (highest_finalized_with_notarize_slot, has_fast_finalize) =
+ self.highest_finalized_with_notarize.unwrap_or((0, false));
+ self.completed_certificates
+ .iter()
+ .filter_map(|(cert_id, cert)| {
+ let cert_to_send = match (
+ cert_id.slot().cmp(&highest_finalized_with_notarize_slot),
+ cert_id.certificate_type(),
+ has_fast_finalize,
+ ) {
+ (Ordering::Greater, _, _)
+ | (
+ Ordering::Equal,
+ CertificateType::Finalize | CertificateType::Notarize,
+ false,
+ )
+ | (Ordering::Equal, CertificateType::FinalizeFast, true) => Some(cert.clone()),
+ (Ordering::Equal, CertificateType::FinalizeFast, false) => {
+ panic!("Should not happen while certificate pool is single threaded")
+ }
+ _ => None,
+ };
+ if cert_to_send.is_some() {
+ trace!("{}: Refreshing certificate {:?}", self.my_pubkey, cert_id);
+ }
+ cert_to_send
+ })
+ .collect()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use {
+ super::*,
+ solana_bls_signatures::{
+ keypair::Keypair as BLSKeypair, Pubkey as BLSPubkey, Signature as BLSSignature,
+ VerifiableSignature,
+ },
+ solana_clock::Slot,
+ solana_hash::Hash,
+ solana_runtime::{
+ bank::{Bank, NewBankOptions},
+ bank_forks::BankForks,
+ genesis_utils::{
+ create_genesis_config_with_alpenglow_vote_accounts, ValidatorVoteKeypairs,
+ },
+ },
+ solana_signer::Signer,
+ solana_votor_messages::consensus_message::{
+ CertificateType, VoteMessage, BLS_KEYPAIR_DERIVE_SEED,
+ },
+ std::sync::{Arc, RwLock},
+ test_case::test_case,
+ };
+
+ fn dummy_transaction(
+ keypairs: &[ValidatorVoteKeypairs],
+ vote: &Vote,
+ rank: usize,
+ ) -> ConsensusMessage {
+ let bls_keypair =
+ BLSKeypair::derive_from_signer(&keypairs[rank].vote_keypair, BLS_KEYPAIR_DERIVE_SEED)
+ .unwrap();
+ let signature: BLSSignature = bls_keypair
+ .sign(bincode::serialize(vote).unwrap().as_slice())
+ .into();
+ ConsensusMessage::new_vote(*vote, signature, rank as u16)
+ }
+
+ fn create_bank(slot: Slot, parent: Arc, pubkey: &Pubkey) -> Bank {
+ Bank::new_from_parent_with_options(parent, pubkey, slot, NewBankOptions::default())
+ }
+
+ fn create_bank_forks(validator_keypairs: &[ValidatorVoteKeypairs]) -> Arc> {
+ let genesis = create_genesis_config_with_alpenglow_vote_accounts(
+ 1_000_000_000,
+ validator_keypairs,
+ vec![100; validator_keypairs.len()],
+ );
+ let bank0 = Bank::new_for_tests(&genesis.genesis_config);
+ BankForks::new_rw_arc(bank0)
+ }
+
+ fn create_initial_state() -> (
+ Vec,
+ ConsensusPool,
+ Arc>,
+ ) {
+ // Create 10 node validatorvotekeypairs vec
+ let validator_keypairs = (0..10)
+ .map(|_| ValidatorVoteKeypairs::new_rand())
+ .collect::>();
+ let bank_forks = create_bank_forks(&validator_keypairs);
+ let root_bank = bank_forks.read().unwrap().root_bank();
+ (
+ validator_keypairs,
+ ConsensusPool::new_from_root_bank(Pubkey::new_unique(), &root_bank),
+ bank_forks,
+ )
+ }
+
+ fn add_certificate(
+ pool: &mut ConsensusPool,
+ bank: &Bank,
+ validator_keypairs: &[ValidatorVoteKeypairs],
+ vote: Vote,
+ ) {
+ for rank in 0..6 {
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(validator_keypairs, &vote, rank),
+ &mut vec![]
+ )
+ .is_ok());
+ }
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(validator_keypairs, &vote, 6),
+ &mut vec![]
+ )
+ .is_ok());
+ match vote {
+ Vote::Notarize(vote) => assert_eq!(pool.highest_notarized_slot(), vote.slot()),
+ Vote::NotarizeFallback(vote) => assert_eq!(pool.highest_notarized_slot(), vote.slot()),
+ Vote::Skip(vote) => assert_eq!(pool.highest_skip_slot(), vote.slot()),
+ Vote::SkipFallback(vote) => assert_eq!(pool.highest_skip_slot(), vote.slot()),
+ Vote::Finalize(vote) => assert_eq!(pool.highest_finalized_slot(), vote.slot()),
+ }
+ }
+
+ fn add_skip_vote_range(
+ pool: &mut ConsensusPool,
+ root_bank: &Bank,
+ start: Slot,
+ end: Slot,
+ keypairs: &[ValidatorVoteKeypairs],
+ rank: usize,
+ ) {
+ for slot in start..=end {
+ let vote = Vote::new_skip_vote(slot);
+ assert!(pool
+ .add_message(
+ root_bank.epoch_schedule(),
+ root_bank.epoch_stakes_map(),
+ root_bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(keypairs, &vote, rank),
+ &mut vec![]
+ )
+ .is_ok());
+ }
+ }
+
+ #[test]
+ fn test_make_decision_leader_does_not_start_if_notarization_missing() {
+ let (_, pool, _) = create_initial_state();
+
+ // No notarization set, pool is default
+ let parent_slot = 2;
+ let my_leader_slot = 3;
+ let first_alpenglow_slot = 0;
+ let decision =
+ pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot);
+ assert!(
+ !decision,
+ "Leader should not be allowed to start without notarization"
+ );
+ }
+
+ #[test]
+ fn test_make_decision_first_alpenglow_slot_edge_case_1() {
+ let (_, pool, _) = create_initial_state();
+
+ // If parent_slot == 0, you don't need a notarization certificate
+ // Because leader_slot == parent_slot + 1, you don't need a skip certificate
+ let parent_slot = 0;
+ let my_leader_slot = 1;
+ let first_alpenglow_slot = 0;
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_first_alpenglow_slot_edge_case_2() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // If parent_slot < first_alpenglow_slot, and parent_slot > 0
+ // no notarization certificate is required, but a skip
+ // certificate will be
+ let parent_slot = 1;
+ let my_leader_slot = 3;
+ let first_alpenglow_slot = 2;
+
+ assert!(!pool.make_start_leader_decision(
+ my_leader_slot,
+ parent_slot,
+ first_alpenglow_slot,
+ ));
+
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_vote(first_alpenglow_slot),
+ );
+
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_first_alpenglow_slot_edge_case_3() {
+ let (_, pool, _) = create_initial_state();
+ // If parent_slot == first_alpenglow_slot, and
+ // first_alpenglow_slot > 0, you need a notarization certificate
+ let parent_slot = 2;
+ let my_leader_slot = 3;
+ let first_alpenglow_slot = 2;
+ assert!(!pool.make_start_leader_decision(
+ my_leader_slot,
+ parent_slot,
+ first_alpenglow_slot,
+ ));
+ }
+
+ #[test]
+ fn test_make_decision_first_alpenglow_slot_edge_case_4() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // If parent_slot < first_alpenglow_slot, and parent_slot == 0,
+ // no notarization certificate is required, but a skip certificate will
+ // be
+ let parent_slot = 0;
+ let my_leader_slot = 2;
+ let first_alpenglow_slot = 1;
+
+ assert!(!pool.make_start_leader_decision(
+ my_leader_slot,
+ parent_slot,
+ first_alpenglow_slot,
+ ));
+
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_vote(first_alpenglow_slot),
+ );
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_first_alpenglow_slot_edge_case_5() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // Valid skip certificate for 1-9 exists
+ for slot in 1..=9 {
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_vote(slot),
+ );
+ }
+
+ // Parent slot is equal to 0, so no notarization certificate required
+ let my_leader_slot = 10;
+ let parent_slot = 0;
+ let first_alpenglow_slot = 0;
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_first_alpenglow_slot_edge_case_6() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // Valid skip certificate for 1-9 exists
+ for slot in 1..=9 {
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_vote(slot),
+ );
+ }
+ // Parent slot is less than first_alpenglow_slot, so no notarization certificate required
+ let my_leader_slot = 10;
+ let parent_slot = 4;
+ let first_alpenglow_slot = 5;
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_leader_does_not_start_if_skip_certificate_missing() {
+ let (validator_keypairs, mut pool, _) = create_initial_state();
+
+ let bank_forks = create_bank_forks(&validator_keypairs);
+ let my_pubkey = validator_keypairs[0].vote_keypair.pubkey();
+
+ // Create bank 5
+ let bank = create_bank(5, bank_forks.read().unwrap().get(0).unwrap(), &my_pubkey);
+ bank.freeze();
+ bank_forks.write().unwrap().insert(bank);
+
+ // Notarize slot 5
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_notarization_vote(5, Hash::default()),
+ );
+ assert_eq!(pool.highest_notarized_slot(), 5);
+
+ // No skip certificate for 6-10
+ let my_leader_slot = 10;
+ let parent_slot = 5;
+ let first_alpenglow_slot = 0;
+ let decision =
+ pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot);
+ assert!(
+ !decision,
+ "Leader should not be allowed to start if a skip certificate is missing"
+ );
+ }
+
+ #[test]
+ fn test_make_decision_leader_starts_when_no_skip_required() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // Notarize slot 5
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_notarization_vote(5, Hash::default()),
+ );
+ assert_eq!(pool.highest_notarized_slot(), 5);
+
+ // Leader slot is just +1 from notarized slot (no skip needed)
+ let my_leader_slot = 6;
+ let parent_slot = 5;
+ let first_alpenglow_slot = 0;
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_leader_starts_if_notarized_and_skips_valid() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // Notarize slot 5
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_notarization_vote(5, Hash::default()),
+ );
+ assert_eq!(pool.highest_notarized_slot(), 5);
+
+ // Valid skip certificate for 6-9 exists
+ for slot in 6..=9 {
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_vote(slot),
+ );
+ }
+
+ let my_leader_slot = 10;
+ let parent_slot = 5;
+ let first_alpenglow_slot = 0;
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test]
+ fn test_make_decision_leader_starts_if_skip_range_superset() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // Notarize slot 5
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_notarization_vote(5, Hash::default()),
+ );
+ assert_eq!(pool.highest_notarized_slot(), 5);
+
+ // Valid skip certificate for 4-9 exists
+ // Should start leader block even if the beginning of the range is from
+ // before your last notarized slot
+ for slot in 4..=9 {
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_fallback_vote(slot),
+ );
+ }
+
+ let my_leader_slot = 10;
+ let parent_slot = 5;
+ let first_alpenglow_slot = 0;
+ assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot));
+ }
+
+ #[test_case(Vote::new_finalization_vote(5), vec![CertificateType::Finalize])]
+ #[test_case(Vote::new_notarization_vote(6, Hash::new_unique()), vec![CertificateType::Notarize, CertificateType::NotarizeFallback])]
+ #[test_case(Vote::new_notarization_fallback_vote(7, Hash::new_unique()), vec![CertificateType::NotarizeFallback])]
+ #[test_case(Vote::new_skip_vote(8), vec![CertificateType::Skip])]
+ #[test_case(Vote::new_skip_fallback_vote(9), vec![CertificateType::Skip])]
+ fn test_add_vote_and_create_new_certificate_with_types(
+ vote: Vote,
+ expected_certificate_types: Vec,
+ ) {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ let my_validator_ix = 5;
+ let highest_slot_fn = match &vote {
+ Vote::Finalize(_) => |pool: &ConsensusPool| pool.highest_finalized_slot(),
+ Vote::Notarize(_) => |pool: &ConsensusPool| pool.highest_notarized_slot(),
+ Vote::NotarizeFallback(_) => |pool: &ConsensusPool| pool.highest_notarized_slot(),
+ Vote::Skip(_) => |pool: &ConsensusPool| pool.highest_skip_slot(),
+ Vote::SkipFallback(_) => |pool: &ConsensusPool| pool.highest_skip_slot(),
+ };
+ let bank = bank_forks.read().unwrap().root_bank();
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, my_validator_ix),
+ &mut vec![]
+ )
+ .is_ok());
+ let slot = vote.slot();
+ assert!(highest_slot_fn(&pool) < slot);
+ // Same key voting again shouldn't make a certificate
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, my_validator_ix),
+ &mut vec![]
+ )
+ .is_ok());
+ assert!(highest_slot_fn(&pool) < slot);
+ for rank in 0..4 {
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut vec![]
+ )
+ .is_ok());
+ }
+ assert!(highest_slot_fn(&pool) < slot);
+ let new_validator_ix = 6;
+ let (new_finalized_slot, certs_to_send) = pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, new_validator_ix),
+ &mut vec![],
+ )
+ .unwrap();
+ if vote.is_finalize() {
+ assert_eq!(new_finalized_slot, Some(slot));
+ } else {
+ assert!(new_finalized_slot.is_none());
+ }
+ // Assert certs_to_send contains the expected certificate types
+ for cert_type in expected_certificate_types {
+ assert!(certs_to_send.iter().any(|cert| {
+ cert.certificate.certificate_type() == cert_type && cert.certificate.slot() == slot
+ }));
+ }
+ assert_eq!(highest_slot_fn(&pool), slot);
+ // Now add the same certificate again, this should silently exit.
+ for cert in certs_to_send {
+ let (new_finalized_slot, certs_to_send) = pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate((*cert).clone()),
+ &mut vec![],
+ )
+ .unwrap();
+ assert!(new_finalized_slot.is_none());
+ assert_eq!(certs_to_send, []);
+ }
+ }
+
+ #[test_case(CertificateType::Finalize, Vote::new_finalization_vote(5))]
+ #[test_case(
+ CertificateType::FinalizeFast,
+ Vote::new_notarization_vote(6, Hash::new_unique())
+ )]
+ #[test_case(
+ CertificateType::Notarize,
+ Vote::new_notarization_vote(6, Hash::new_unique())
+ )]
+ #[test_case(
+ CertificateType::NotarizeFallback,
+ Vote::new_notarization_fallback_vote(7, Hash::new_unique())
+ )]
+ #[test_case(CertificateType::Skip, Vote::new_skip_vote(8))]
+ fn test_add_certificate_with_types(certificate_type: CertificateType, vote: Vote) {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ let certificate = Certificate::new(certificate_type, vote.slot(), vote.block_id().copied());
+
+ let certificate_message = CertificateMessage {
+ certificate,
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ let bank = bank_forks.read().unwrap().root_bank();
+ let message = ConsensusMessage::Certificate(certificate_message.clone());
+ // Add the certificate to the pool
+ let (new_finalized_slot, certs_to_send) = pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &message,
+ &mut vec![],
+ )
+ .unwrap();
+ // Because this is the first certificate of this type, it should be sent out.
+ if certificate_type == CertificateType::Finalize
+ || certificate_type == CertificateType::FinalizeFast
+ {
+ assert_eq!(new_finalized_slot, Some(certificate.slot()));
+ } else {
+ assert!(new_finalized_slot.is_none());
+ }
+ assert_eq!(certs_to_send.len(), 1);
+ assert_eq!(*certs_to_send[0], certificate_message);
+
+ // Adding the cert again will not trigger another send
+ let (new_finalized_slot, certs_to_send) = pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &message,
+ &mut vec![],
+ )
+ .unwrap();
+ assert!(new_finalized_slot.is_none());
+ assert_eq!(certs_to_send, []);
+
+ // Now add the vote from everyone else, this will not trigger a certificate send
+ for rank in 0..validator_keypairs.len() {
+ let (_, certs_to_send) = pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut vec![],
+ )
+ .unwrap();
+ assert!(!certs_to_send
+ .iter()
+ .any(|cert| { cert.certificate.certificate_type() == certificate_type }));
+ }
+ }
+
+ #[test]
+ fn test_add_vote_zero_stake() {
+ let (_, mut pool, bank_forks) = create_initial_state();
+ let bank = bank_forks.read().unwrap().root_bank();
+ assert_eq!(
+ pool.add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Vote(VoteMessage {
+ vote: Vote::new_skip_vote(5),
+ rank: 100,
+ signature: BLSSignature::default(),
+ }),
+ &mut vec![]
+ ),
+ Err(AddVoteError::InvalidRank(100))
+ );
+ }
+
+ fn assert_single_certificate_range(
+ pool: &ConsensusPool,
+ exp_range_start: Slot,
+ exp_range_end: Slot,
+ ) {
+ for i in exp_range_start..=exp_range_end {
+ assert!(pool.skip_certified(i));
+ }
+ }
+
+ #[test]
+ fn test_consecutive_slots() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ add_certificate(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ Vote::new_skip_vote(15),
+ );
+ assert_eq!(pool.highest_skip_slot(), 15);
+
+ let bank = bank_forks.read().unwrap().root_bank();
+ for i in 0..validator_keypairs.len() {
+ let slot = (i as u64).saturating_add(16);
+ let vote = Vote::new_skip_vote(slot);
+ // These should not extend the skip range
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, i),
+ &mut vec![]
+ )
+ .is_ok());
+ }
+
+ assert_single_certificate_range(&pool, 15, 15);
+ }
+
+ #[test]
+ fn test_multi_skip_cert() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // We have 10 validators, 40% voted for (5, 15)
+ for rank in 0..4 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 5,
+ 15,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ // 30% voted for (5, 8)
+ for rank in 4..7 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 5,
+ 8,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ // The rest voted for (11, 15)
+ for rank in 7..10 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 11,
+ 15,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ // Test slots from 5 to 15, [5, 8] and [11, 15] should be certified, the others aren't
+ for slot in 5..9 {
+ assert!(pool.skip_certified(slot));
+ }
+ for slot in 9..11 {
+ assert!(!pool.skip_certified(slot));
+ }
+ for slot in 11..=15 {
+ assert!(pool.skip_certified(slot));
+ }
+ }
+
+ #[test]
+ fn test_add_multiple_votes() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+
+ // 10 validators, half vote for (5, 15), the other (20, 30)
+ for rank in 0..5 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 5,
+ 15,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ for rank in 5..10 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 20,
+ 30,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ assert_eq!(pool.highest_skip_slot(), 0);
+
+ // Now the first half vote for (5, 30)
+ for rank in 0..5 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 5,
+ 30,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ assert_single_certificate_range(&pool, 20, 30);
+ }
+
+ #[test]
+ fn test_add_multiple_disjoint_votes() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ // 50% of the validators vote for (1, 10)
+ for rank in 0..5 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 1,
+ 10,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ let bank = bank_forks.read().unwrap().root_bank();
+ // 10% vote for skip 2
+ let vote = Vote::new_skip_vote(2);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 6),
+ &mut vec![]
+ )
+ .is_ok());
+ assert_eq!(pool.highest_skip_slot(), 2);
+
+ assert_single_certificate_range(&pool, 2, 2);
+ // 10% vote for skip 4
+ let vote = Vote::new_skip_vote(4);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 7),
+ &mut vec![]
+ )
+ .is_ok());
+ assert_eq!(pool.highest_skip_slot(), 4);
+
+ assert_single_certificate_range(&pool, 2, 2);
+ assert_single_certificate_range(&pool, 4, 4);
+ // 10% vote for skip 3
+ let vote = Vote::new_skip_vote(3);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 8),
+ &mut vec![]
+ )
+ .is_ok());
+ assert_eq!(pool.highest_skip_slot(), 4);
+ assert_single_certificate_range(&pool, 2, 4);
+ assert!(pool.skip_certified(3));
+ // Let the last 10% vote for (3, 10) now
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 3,
+ 10,
+ &validator_keypairs,
+ 8,
+ );
+ assert_eq!(pool.highest_skip_slot(), 10);
+ assert_single_certificate_range(&pool, 2, 10);
+ assert!(pool.skip_certified(7));
+ }
+
+ #[test]
+ fn test_update_existing_singleton_vote() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ // 50% voted on (1, 6)
+ for rank in 0..5 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 1,
+ 6,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ let bank = bank_forks.read().unwrap().root_bank();
+ // Range expansion on a singleton vote should be ok
+ let vote = Vote::new_skip_vote(1);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 6),
+ &mut vec![]
+ )
+ .is_ok());
+ assert_eq!(pool.highest_skip_slot(), 1);
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 1,
+ 6,
+ &validator_keypairs,
+ 6,
+ );
+ assert_eq!(pool.highest_skip_slot(), 6);
+ assert_single_certificate_range(&pool, 1, 6);
+ }
+
+ #[test]
+ fn test_update_existing_vote() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ let bank = bank_forks.read().unwrap().root_bank();
+ // 50% voted for (10, 25)
+ for rank in 0..5 {
+ add_skip_vote_range(&mut pool, &bank, 10, 25, &validator_keypairs, rank);
+ }
+
+ add_skip_vote_range(&mut pool, &bank, 10, 20, &validator_keypairs, 6);
+ assert_eq!(pool.highest_skip_slot(), 20);
+ assert_single_certificate_range(&pool, 10, 20);
+
+ // AlreadyExists, silently fail
+ let vote = Vote::new_skip_vote(20);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 6),
+ &mut vec![]
+ )
+ .is_ok());
+ }
+
+ #[test]
+ fn test_threshold_not_reached() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ // half voted (5, 15) and the other half voted (20, 30)
+ for rank in 0..5 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 5,
+ 15,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ for rank in 5..10 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 20,
+ 30,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ for slot in 5..31 {
+ assert!(!pool.skip_certified(slot));
+ }
+ }
+
+ #[test]
+ fn test_update_and_skip_range_certify() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ // half voted (5, 15) and the other half voted (10, 30)
+ for rank in 0..5 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 5,
+ 15,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ for rank in 5..10 {
+ add_skip_vote_range(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ 10,
+ 30,
+ &validator_keypairs,
+ rank,
+ );
+ }
+ for slot in 5..10 {
+ assert!(!pool.skip_certified(slot));
+ }
+ for slot in 16..31 {
+ assert!(!pool.skip_certified(slot));
+ }
+ assert_single_certificate_range(&pool, 10, 15);
+ }
+
+ #[test]
+ fn test_safe_to_notar() {
+ solana_logger::setup();
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ let bank = bank_forks.read().unwrap().root_bank();
+ let (my_vote_key, _, _) =
+ get_key_and_stakes(bank.epoch_schedule(), bank.epoch_stakes_map(), 0, 0).unwrap();
+
+ // Create bank 2
+ let slot = 2;
+ let block_id = Hash::new_unique();
+
+ // Add a skip from myself.
+ let vote = Vote::new_skip_vote(2);
+ let mut new_events = vec![];
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &my_vote_key,
+ &dummy_transaction(&validator_keypairs, &vote, 0),
+ &mut new_events
+ )
+ .is_ok());
+ assert!(new_events.is_empty());
+
+ // 40% notarized, should succeed
+ for rank in 1..5 {
+ let vote = Vote::new_notarization_vote(2, block_id);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut new_events
+ )
+ .is_ok());
+ }
+ assert_eq!(new_events.len(), 1);
+ if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[0] {
+ assert_eq!(block_id, event_block_id);
+ assert_eq!(slot, event_slot);
+ } else {
+ panic!("Expected SafeToNotar event");
+ }
+ new_events.clear();
+
+ // Create bank 3
+ let slot = 3;
+ let block_id = Hash::new_unique();
+
+ // Add 20% notarize, but no vote from myself, should fail
+ for rank in 1..3 {
+ let vote = Vote::new_notarization_vote(3, block_id);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut new_events
+ )
+ .is_ok());
+ }
+ assert!(new_events.is_empty());
+
+ // Add a notarize from myself for some other block, but still not enough notar or skip, should fail.
+ let vote = Vote::new_notarization_vote(3, Hash::new_unique());
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &my_vote_key,
+ &dummy_transaction(&validator_keypairs, &vote, 0),
+ &mut new_events
+ )
+ .is_ok());
+ assert!(new_events.is_empty());
+
+ // Now add 40% skip, should succeed
+ // Funny thing is in this case we will also get SafeToSkip(3)
+ for rank in 3..7 {
+ let vote = Vote::new_skip_vote(3);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut new_events
+ )
+ .is_ok());
+ }
+ assert_eq!(new_events.len(), 2);
+ if let VotorEvent::SafeToSkip(event_slot) = new_events[0] {
+ assert_eq!(slot, event_slot);
+ } else {
+ panic!("Expected SafeToSkip event");
+ }
+ if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[1] {
+ assert_eq!(block_id, event_block_id);
+ assert_eq!(slot, event_slot);
+ } else {
+ panic!("Expected SafeToNotar event");
+ }
+ new_events.clear();
+
+ // Add 20% notarization for another block, we should notify on new block_id
+ // but not on the same block_id because we already sent the event
+ let duplicate_block_id = Hash::new_unique();
+ for rank in 7..9 {
+ let vote = Vote::new_notarization_vote(3, duplicate_block_id);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut new_events
+ )
+ .is_ok());
+ }
+
+ assert_eq!(new_events.len(), 1);
+ if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[0] {
+ assert_eq!(duplicate_block_id, event_block_id);
+ assert_eq!(slot, event_slot);
+ } else {
+ panic!("Expected SafeToNotar event");
+ }
+ }
+
+ #[test]
+ fn test_safe_to_skip() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ let bank = bank_forks.read().unwrap().root_bank();
+ let (my_vote_key, _, _) =
+ get_key_and_stakes(bank.epoch_schedule(), bank.epoch_stakes_map(), 0, 0).unwrap();
+ let slot = 2;
+ let mut new_events = vec![];
+
+ // Add a notarize from myself.
+ let block_id = Hash::new_unique();
+ let vote = Vote::new_notarization_vote(2, block_id);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &my_vote_key,
+ &dummy_transaction(&validator_keypairs, &vote, 0),
+ &mut new_events
+ )
+ .is_ok());
+ // Should still fail because there are no other votes.
+ assert!(new_events.is_empty());
+ // Add 50% skip, should succeed
+ for rank in 1..6 {
+ let vote = Vote::new_skip_vote(2);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, rank),
+ &mut new_events
+ )
+ .is_ok());
+ }
+ assert_eq!(new_events.len(), 1);
+ if let VotorEvent::SafeToSkip(event_slot) = new_events[0] {
+ assert_eq!(slot, event_slot);
+ } else {
+ panic!("Expected SafeToSkip event");
+ }
+ new_events.clear();
+ // Add 10% more notarize, will not send new SafeToSkip because the event was already sent
+ let vote = Vote::new_notarization_vote(2, block_id);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 6),
+ &mut new_events
+ )
+ .is_ok());
+ assert!(new_events.is_empty());
+ }
+
+ fn create_new_vote(vote_type: VoteType, slot: Slot) -> Vote {
+ match vote_type {
+ VoteType::Notarize => Vote::new_notarization_vote(slot, Hash::default()),
+ VoteType::NotarizeFallback => {
+ Vote::new_notarization_fallback_vote(slot, Hash::default())
+ }
+ VoteType::Skip => Vote::new_skip_vote(slot),
+ VoteType::SkipFallback => Vote::new_skip_fallback_vote(slot),
+ VoteType::Finalize => Vote::new_finalization_vote(slot),
+ }
+ }
+
+ fn test_reject_conflicting_vote(
+ pool: &mut ConsensusPool,
+ bank: &Bank,
+ validator_keypairs: &[ValidatorVoteKeypairs],
+ vote_type_1: VoteType,
+ vote_type_2: VoteType,
+ slot: Slot,
+ ) {
+ let vote_1 = create_new_vote(vote_type_1, slot);
+ let vote_2 = create_new_vote(vote_type_2, slot);
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(validator_keypairs, &vote_1, 0),
+ &mut vec![]
+ )
+ .is_ok());
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(validator_keypairs, &vote_2, 0),
+ &mut vec![]
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn test_reject_conflicting_votes_with_type() {
+ let (validator_keypairs, mut pool, bank_forks) = create_initial_state();
+ let mut slot = 2;
+ for vote_type_1 in [
+ VoteType::Finalize,
+ VoteType::Notarize,
+ VoteType::NotarizeFallback,
+ VoteType::Skip,
+ VoteType::SkipFallback,
+ ] {
+ let conflicting_vote_types = conflicting_types(vote_type_1);
+ for vote_type_2 in conflicting_vote_types {
+ test_reject_conflicting_vote(
+ &mut pool,
+ &bank_forks.read().unwrap().root_bank(),
+ &validator_keypairs,
+ vote_type_1,
+ *vote_type_2,
+ slot,
+ );
+ }
+ slot = slot.saturating_add(4);
+ }
+ }
+
+ #[test]
+ fn test_handle_new_root() {
+ let validator_keypairs = (0..10)
+ .map(|_| ValidatorVoteKeypairs::new_rand())
+ .collect::>();
+ let bank_forks = create_bank_forks(&validator_keypairs);
+ let mut pool = ConsensusPool::new_from_root_bank(
+ Pubkey::new_unique(),
+ &bank_forks.read().unwrap().root_bank(),
+ );
+
+ let root_bank = bank_forks.read().unwrap().root_bank();
+ let new_bank = Arc::new(create_bank(2, root_bank, &Pubkey::new_unique()));
+ pool.prune_old_state(new_bank.slot());
+ let new_bank = Arc::new(create_bank(3, new_bank, &Pubkey::new_unique()));
+ pool.prune_old_state(new_bank.slot());
+ // Send a vote on slot 1, it should be rejected
+ let vote = Vote::new_skip_vote(1);
+ assert!(pool
+ .add_message(
+ new_bank.epoch_schedule(),
+ new_bank.epoch_stakes_map(),
+ new_bank.slot(),
+ &Pubkey::new_unique(),
+ &dummy_transaction(&validator_keypairs, &vote, 0),
+ &mut vec![]
+ )
+ .is_err());
+
+ // Send a cert on slot 2, it should be rejected
+ let certificate = Certificate::new(CertificateType::Notarize, 2, Some(Hash::new_unique()));
+
+ let cert = ConsensusMessage::Certificate(CertificateMessage {
+ certificate,
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ });
+ assert!(pool
+ .add_message(
+ new_bank.epoch_schedule(),
+ new_bank.epoch_stakes_map(),
+ new_bank.slot(),
+ &Pubkey::new_unique(),
+ &cert,
+ &mut vec![]
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn test_get_certs_for_standstill() {
+ let (_, mut pool, bank_forks) = create_initial_state();
+
+ // Should return empty vector if no certificates
+ assert!(pool.get_certs_for_standstill().is_empty());
+
+ // Add notar-fallback cert on 3 and finalize cert on 4
+ let cert_3 = CertificateMessage {
+ certificate: Certificate::new(
+ CertificateType::NotarizeFallback,
+ 3,
+ Some(Hash::new_unique()),
+ ),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ let bank = bank_forks.read().unwrap().root_bank();
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_3.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ let cert_4 = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Finalize, 4, None),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_4.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ // Should return both certificates
+ let certs = pool.get_certs_for_standstill();
+ assert_eq!(certs.len(), 2);
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 3
+ && cert.certificate.certificate_type() == CertificateType::NotarizeFallback));
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 4
+ && cert.certificate.certificate_type() == CertificateType::Finalize));
+
+ // Add Notarize cert on 5
+ let cert_5 = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Notarize, 5, Some(Hash::new_unique())),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_5.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+
+ // Add Finalize cert on 5
+ let cert_5_finalize = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Finalize, 5, None),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_5_finalize.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+
+ // Add FinalizeFast cert on 5
+ let cert_5 = CertificateMessage {
+ certificate: Certificate::new(
+ CertificateType::FinalizeFast,
+ 5,
+ Some(Hash::new_unique()),
+ ),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_5.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ // Should return only FinalizeFast cert on 5
+ let certs = pool.get_certs_for_standstill();
+ assert_eq!(certs.len(), 1);
+ assert!(
+ certs[0].certificate.slot() == 5
+ && certs[0].certificate.certificate_type() == CertificateType::FinalizeFast
+ );
+
+ // Now add Notarize cert on 6
+ let cert_6 = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Notarize, 6, Some(Hash::new_unique())),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_6.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ // Should return certs on 5 and 6
+ let certs = pool.get_certs_for_standstill();
+ assert_eq!(certs.len(), 2);
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 5
+ && cert.certificate.certificate_type() == CertificateType::FinalizeFast));
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 6
+ && cert.certificate.certificate_type() == CertificateType::Notarize));
+
+ // Add another Finalize cert on 6
+ let cert_6_finalize = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Finalize, 6, None),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_6_finalize.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ // Add a NotarizeFallback cert on 6
+ let cert_6_notarize_fallback = CertificateMessage {
+ certificate: Certificate::new(
+ CertificateType::NotarizeFallback,
+ 6,
+ Some(Hash::new_unique()),
+ ),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_6_notarize_fallback.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ // This should not be returned because 6 is the current highest finalized slot
+ // only Notarize/Finalze/FinalizeFast should be returned
+ let certs = pool.get_certs_for_standstill();
+ assert_eq!(certs.len(), 2);
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 6
+ && cert.certificate.certificate_type() == CertificateType::Finalize));
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 6
+ && cert.certificate.certificate_type() == CertificateType::Notarize));
+
+ // Add another skip on 7
+ let cert_7 = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Skip, 7, None),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_7.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ // Should return certs on 6 and 7
+ let certs = pool.get_certs_for_standstill();
+ assert_eq!(certs.len(), 3);
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 6
+ && cert.certificate.certificate_type() == CertificateType::Finalize));
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 6
+ && cert.certificate.certificate_type() == CertificateType::Notarize));
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 7
+ && cert.certificate.certificate_type() == CertificateType::Skip));
+ }
+
+ #[test]
+ fn test_new_parent_ready_with_certificates() {
+ solana_logger::setup();
+ let (_, mut pool, bank_forks) = create_initial_state();
+ let bank = bank_forks.read().unwrap().root_bank();
+ let mut events = vec![];
+
+ // Add a notarization cert on slot 1 to 3
+ let hash = Hash::new_unique();
+ for slot in 1..=3 {
+ let cert = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Notarize, slot, Some(hash)),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert),
+ &mut events,
+ )
+ .is_ok());
+ }
+ // events should now contain ParentReady for slot 4
+ error!("Events: {events:?}");
+ assert!(events
+ .iter()
+ .any(|event| matches!(event, VotorEvent::ParentReady {
+ slot: 4,
+ parent_block: (3, h)
+ } if h == &hash)));
+ events.clear();
+
+ // Also works if we add FinalizeFast for slot 4 to 7
+ for slot in 4..=7 {
+ let cert = CertificateMessage {
+ certificate: Certificate::new(CertificateType::FinalizeFast, slot, Some(hash)),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert),
+ &mut events,
+ )
+ .is_ok());
+ }
+ // events should now contain ParentReady for slot 8
+ error!("Events: {events:?}");
+ assert!(events
+ .iter()
+ .any(|event| matches!(event, VotorEvent::ParentReady {
+ slot: 8,
+ parent_block: (7, h)
+ } if h == &hash)));
+ events.clear();
+
+ // NotarizeFallback on slot 8 to 10 and FinalizeFast on slot 11
+ for slot in 8..=10 {
+ let cert = CertificateMessage {
+ certificate: Certificate::new(CertificateType::NotarizeFallback, slot, Some(hash)),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert),
+ &mut events,
+ )
+ .is_ok());
+ }
+ let cert = CertificateMessage {
+ certificate: Certificate::new(CertificateType::FinalizeFast, 11, Some(hash)),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert),
+ &mut events,
+ )
+ .is_ok());
+ // events should now contain ParentReady for slot 12
+ error!("Events: {events:?}");
+ assert!(events
+ .iter()
+ .any(|event| matches!(event, VotorEvent::ParentReady {
+ slot: 12,
+ parent_block: (11, h)
+ } if h == &hash)));
+ }
+
+ #[test]
+ fn test_vote_message_signature_verification() {
+ let (validator_keypairs, _, _) = create_initial_state();
+ let rank_to_test = 3;
+ let vote = Vote::new_notarization_vote(42, Hash::new_unique());
+
+ let consensus_message = dummy_transaction(&validator_keypairs, &vote, rank_to_test);
+ let ConsensusMessage::Vote(vote_message) = consensus_message else {
+ panic!("Expected Vote message")
+ };
+
+ let validator_vote_keypair = &validator_keypairs[rank_to_test].vote_keypair;
+ let bls_keypair =
+ BLSKeypair::derive_from_signer(validator_vote_keypair, BLS_KEYPAIR_DERIVE_SEED)
+ .unwrap();
+ let bls_pubkey: BLSPubkey = bls_keypair.public;
+
+ let signed_message = bincode::serialize(&vote).unwrap();
+
+ assert!(
+ vote_message
+ .signature
+ .verify(&bls_pubkey, &signed_message)
+ .is_ok(),
+ "BLS signature verification failed for VoteMessage"
+ );
+ }
+}
diff --git a/votor/src/lib.rs b/votor/src/lib.rs
index f796bba1faf879..b5a44e907c3099 100644
--- a/votor/src/lib.rs
+++ b/votor/src/lib.rs
@@ -1,5 +1,8 @@
#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
+#[cfg(feature = "agave-unstable-api")]
+pub mod commitment;
+
#[cfg(feature = "agave-unstable-api")]
pub mod common;
From d45b62469b156d02e5d4181b3b207aadd7485ce2 Mon Sep 17 00:00:00 2001
From: Wen <113942165+wen-coding@users.noreply.github.com>
Date: Sun, 21 Sep 2025 14:43:51 -0700
Subject: [PATCH 2/6] Remove accidentally added pub keyword.
---
runtime/src/genesis_utils.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs
index 2d71c2c0bfb9f0..b63e3703186bec 100644
--- a/runtime/src/genesis_utils.rs
+++ b/runtime/src/genesis_utils.rs
@@ -279,7 +279,7 @@ pub fn activate_all_features(genesis_config: &mut GenesisConfig) {
do_activate_all_features::(genesis_config);
}
-pub fn do_activate_all_features(genesis_config: &mut GenesisConfig) {
+fn do_activate_all_features(genesis_config: &mut GenesisConfig) {
// Activate all features at genesis in development mode
for feature_id in FeatureSet::default().inactive() {
if IS_ALPENGLOW || *feature_id != agave_feature_set::alpenglow::id() {
From 6bf65db4c01a45f9cd6009f64f9c42d3d84e5c16 Mon Sep 17 00:00:00 2001
From: Wen <113942165+wen-coding@users.noreply.github.com>
Date: Sun, 21 Sep 2025 17:41:52 -0700
Subject: [PATCH 3/6] Fix the keypair used.
---
runtime/src/genesis_utils.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs
index b63e3703186bec..05c096df18d78a 100644
--- a/runtime/src/genesis_utils.rs
+++ b/runtime/src/genesis_utils.rs
@@ -178,7 +178,7 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type(
let node_account = Account::new(VALIDATOR_LAMPORTS, 0, &system_program::id());
let vote_account = if is_alpenglow {
let bls_keypair = BLSKeypair::derive_from_signer(
- &voting_keypairs[0].borrow().vote_keypair,
+ &validator_voting_keypairs.borrow().vote_keypair,
BLS_KEYPAIR_DERIVE_SEED,
)
.unwrap();
From 3ad8f170e94b219deb46e9e0bf2191b6cbbc1984 Mon Sep 17 00:00:00 2001
From: Wen <113942165+wen-coding@users.noreply.github.com>
Date: Sun, 21 Sep 2025 20:43:39 -0700
Subject: [PATCH 4/6] Remove unused functions.
---
votor/src/consensus_pool.rs | 31 -------------------------------
1 file changed, 31 deletions(-)
diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs
index b0d777e2910545..2dded462b5d843 100644
--- a/votor/src/consensus_pool.rs
+++ b/votor/src/consensus_pool.rs
@@ -110,8 +110,6 @@ pub struct ConsensusPool {
/// - They have a potential parent block with a NotarizeFallback certificate
/// - All slots from the parent have a Skip certificate
pub parent_ready_tracker: ParentReadyTracker,
- /// Highest block that has a NotarizeFallback certificate, for use in producing our leader window
- highest_notarized_fallback: Option<(Slot, Hash)>,
/// Highest slot that has a Finalized variant certificate
highest_finalized_slot: Option,
/// Highest slot that has Finalize+Notarize or FinalizeFast, for use in standstill
@@ -134,7 +132,6 @@ impl ConsensusPool {
my_pubkey,
vote_pools: BTreeMap::new(),
completed_certificates: BTreeMap::new(),
- highest_notarized_fallback: None,
highest_finalized_slot: None,
highest_finalized_with_notarize: None,
parent_ready_tracker,
@@ -185,7 +182,6 @@ impl ConsensusPool {
/// of the related certificates are newly complete.
/// For each newly constructed certificate
/// - Insert it into `self.certificates`
- /// - Potentially update `self.highest_notarized_fallback`,
/// - Potentially update `self.highest_finalized_slot`,
/// - If we have a new highest finalized slot, return it
/// - update any newly created events
@@ -296,12 +292,6 @@ impl ConsensusPool {
Certificate::NotarizeFallback(slot, block_id) => {
self.parent_ready_tracker
.add_new_notar_fallback_or_stronger((slot, block_id), events);
- if self
- .highest_notarized_fallback
- .is_none_or(|(s, _)| s < slot)
- {
- self.highest_notarized_fallback = Some((slot, block_id));
- }
}
Certificate::Skip(slot) => self.parent_ready_tracker.add_new_skip(slot, events),
Certificate::Notarize(slot, block_id) => {
@@ -492,11 +482,6 @@ impl ConsensusPool {
Ok(vec![new_certificate])
}
- /// The highest notarized fallback slot, for use as the parent slot in leader window
- pub fn highest_notarized_fallback(&self) -> Option<(Slot, Hash)> {
- self.highest_notarized_fallback
- }
-
/// Get the notarized block in `slot`
pub fn get_notarized_block(&self, slot: Slot) -> Option {
self.completed_certificates
@@ -548,16 +533,6 @@ impl ConsensusPool {
.unwrap_or(0)
}
- pub fn highest_fast_finalized_block(&self) -> Option {
- self.completed_certificates
- .iter()
- .filter_map(|(cert_id, _)| match cert_id {
- Certificate::FinalizeFast(s, bid) => Some((*s, *bid)),
- _ => None,
- })
- .max()
- }
-
/// Checks if any block in the slot `s` is finalized
pub fn is_finalized(&self, slot: Slot) -> bool {
self.completed_certificates.keys().any(|cert_id| {
@@ -565,12 +540,6 @@ impl ConsensusPool {
})
}
- /// Check if the specific block `(block_id)` in slot `s` is notarized
- pub fn is_notarized(&self, slot: Slot, block_id: Hash) -> bool {
- self.completed_certificates
- .contains_key(&Certificate::Notarize(slot, block_id))
- }
-
/// Checks if the any block in slot `slot` has received a `NotarizeFallback` certificate, if so return
/// the size of the certificate
#[cfg(test)]
From 97a4195098c28734ec48c9740d7ee4c12f72023a Mon Sep 17 00:00:00 2001
From: Wen <113942165+wen-coding@users.noreply.github.com>
Date: Mon, 22 Sep 2025 16:37:54 -0700
Subject: [PATCH 5/6] Add stake_state::create_alpenglow_account which can is
only called in genesis.
---
programs/stake/src/stake_state.rs | 31 +++++++++++++++----
runtime/src/genesis_utils.rs | 49 ++++++++++++++++++++++---------
2 files changed, 60 insertions(+), 20 deletions(-)
diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs
index 15b236a90a1f04..3b820f9e844ffe 100644
--- a/programs/stake/src/stake_state.rs
+++ b/programs/stake/src/stake_state.rs
@@ -88,6 +88,25 @@ pub fn create_account(
rent,
lamports,
Epoch::MAX,
+ false,
+ )
+}
+
+pub fn create_alpenglow_account(
+ authorized: &Pubkey,
+ voter_pubkey: &Pubkey,
+ vote_account: &AccountSharedData,
+ rent: &Rent,
+ lamports: u64,
+) -> AccountSharedData {
+ do_create_account(
+ authorized,
+ voter_pubkey,
+ vote_account,
+ rent,
+ lamports,
+ Epoch::MAX,
+ true,
)
}
@@ -98,16 +117,16 @@ fn do_create_account(
rent: &Rent,
lamports: u64,
activation_epoch: Epoch,
+ is_alpenglow: bool,
) -> AccountSharedData {
let mut stake_account = AccountSharedData::new(lamports, StakeStateV2::size_of(), &id());
- let credits = if let Ok(vote_state_v3) = VoteStateV3::deserialize(vote_account.data()) {
- vote_state_v3.credits()
+ let credits = if is_alpenglow {
+ let vote_state_v4 = VoteStateV4::deserialize(vote_account.data(), voter_pubkey).unwrap();
+ vote_state_v4.epoch_credits.last().map_or(0, |(_, c, _)| *c)
} else {
- match VoteStateV4::deserialize(vote_account.data(), voter_pubkey) {
- Ok(vote_state_v4) => vote_state_v4.epoch_credits.last().map_or(0, |(_, c, _)| *c),
- Err(e) => panic!("Invalid vote account state data: {e}"),
- }
+ let vote_state = VoteStateV3::deserialize(vote_account.data()).expect("vote_state");
+ vote_state.credits()
};
let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len());
diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs
index 05c096df18d78a..2c00a5ee392137 100644
--- a/runtime/src/genesis_utils.rs
+++ b/runtime/src/genesis_utils.rs
@@ -194,13 +194,23 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type(
} else {
vote_state::create_account(&vote_pubkey, &node_pubkey, 0, *stake)
};
- let stake_account = Account::from(stake_state::create_account(
- &stake_pubkey,
- &vote_pubkey,
- &vote_account,
- &genesis_config_info.genesis_config.rent,
- *stake,
- ));
+ let stake_account = if is_alpenglow {
+ Account::from(stake_state::create_alpenglow_account(
+ &stake_pubkey,
+ &vote_pubkey,
+ &vote_account,
+ &genesis_config_info.genesis_config.rent,
+ *stake,
+ ))
+ } else {
+ Account::from(stake_state::create_account(
+ &stake_pubkey,
+ &vote_pubkey,
+ &vote_account,
+ &genesis_config_info.genesis_config.rent,
+ *stake,
+ ))
+ };
let vote_account = Account::from(vote_account);
@@ -339,6 +349,7 @@ pub fn create_genesis_config_with_leader_ex_no_features(
cluster_type: ClusterType,
mut initial_accounts: Vec<(Pubkey, AccountSharedData)>,
) -> GenesisConfig {
+ let is_alpenglow = validator_bls_pubkey.is_some();
let validator_vote_account = if let Some(bls_pubkey_compressed) = validator_bls_pubkey {
vote_state::create_v4_account_with_authorized(
validator_pubkey,
@@ -357,13 +368,23 @@ pub fn create_genesis_config_with_leader_ex_no_features(
)
};
- let validator_stake_account = stake_state::create_account(
- validator_stake_account_pubkey,
- validator_vote_account_pubkey,
- &validator_vote_account,
- &rent,
- validator_stake_lamports,
- );
+ let validator_stake_account = if is_alpenglow {
+ stake_state::create_alpenglow_account(
+ validator_stake_account_pubkey,
+ validator_vote_account_pubkey,
+ &validator_vote_account,
+ &rent,
+ validator_stake_lamports,
+ )
+ } else {
+ stake_state::create_account(
+ validator_stake_account_pubkey,
+ validator_vote_account_pubkey,
+ &validator_vote_account,
+ &rent,
+ validator_stake_lamports,
+ )
+ };
initial_accounts.push((
*mint_pubkey,
From 89e0954a90848aa4d6980926e4a24975dfd68be7 Mon Sep 17 00:00:00 2001
From: Wen <113942165+wen-coding@users.noreply.github.com>
Date: Mon, 22 Sep 2025 17:08:02 -0700
Subject: [PATCH 6/6] Upstream changes in Alpenglow #460.
---
votor/src/consensus_pool.rs | 102 +++++++++++++++++-
.../consensus_pool/parent_ready_tracker.rs | 5 +
2 files changed, 106 insertions(+), 1 deletion(-)
diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs
index 2dded462b5d843..51e89f10aaf733 100644
--- a/votor/src/consensus_pool.rs
+++ b/votor/src/consensus_pool.rs
@@ -555,6 +555,11 @@ impl ConsensusPool {
.contains_key(&Certificate::Skip(slot))
}
+ #[cfg(test)]
+ pub(crate) fn my_pubkey(&self) -> Pubkey {
+ self.my_pubkey
+ }
+
#[cfg(test)]
fn make_start_leader_decision(
&self,
@@ -1850,10 +1855,54 @@ mod tests {
);
let root_bank = bank_forks.read().unwrap().root_bank();
+ // Add a skip cert on slot 1 and finalize cert on slot 2
+ let cert_1 = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Skip, 1, None),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ root_bank.epoch_schedule(),
+ root_bank.epoch_stakes_map(),
+ root_bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_1.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ let cert_2 = CertificateMessage {
+ certificate: Certificate::new(
+ CertificateType::FinalizeFast,
+ 2,
+ Some(Hash::new_unique()),
+ ),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ root_bank.epoch_schedule(),
+ root_bank.epoch_stakes_map(),
+ root_bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_2.clone()),
+ &mut vec![]
+ )
+ .is_ok());
+ assert!(pool.skip_certified(1));
+ assert!(pool.is_finalized(2));
+
let new_bank = Arc::new(create_bank(2, root_bank, &Pubkey::new_unique()));
pool.prune_old_state(new_bank.slot());
+ // Check that cert for 1 is gone, but cert for 2 is still there
+ assert!(!pool.skip_certified(1));
+ assert!(pool.is_finalized(2));
let new_bank = Arc::new(create_bank(3, new_bank, &Pubkey::new_unique()));
pool.prune_old_state(new_bank.slot());
+ // Now both certs should be gone
+ assert!(!pool.skip_certified(1));
+ assert!(!pool.is_finalized(2));
// Send a vote on slot 1, it should be rejected
let vote = Vote::new_skip_vote(1);
assert!(pool
@@ -2094,11 +2143,50 @@ mod tests {
&& cert.certificate.certificate_type() == CertificateType::Notarize));
assert!(certs.iter().any(|cert| cert.certificate.slot() == 7
&& cert.certificate.certificate_type() == CertificateType::Skip));
+
+ // Add Finalize then Notarize cert on 8
+ let cert_8_finalize = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Finalize, 8, None),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_8_finalize),
+ &mut vec![]
+ )
+ .is_ok());
+ let cert_8_notarize = CertificateMessage {
+ certificate: Certificate::new(CertificateType::Notarize, 8, Some(Hash::new_unique())),
+ signature: BLSSignature::default(),
+ bitmap: Vec::new(),
+ };
+ assert!(pool
+ .add_message(
+ bank.epoch_schedule(),
+ bank.epoch_stakes_map(),
+ bank.slot(),
+ &Pubkey::new_unique(),
+ &ConsensusMessage::Certificate(cert_8_notarize),
+ &mut vec![]
+ )
+ .is_ok());
+
+ // Should only return certs on 8 now
+ let certs = pool.get_certs_for_standstill();
+ assert_eq!(certs.len(), 2);
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 8
+ && cert.certificate.certificate_type() == CertificateType::Finalize));
+ assert!(certs.iter().any(|cert| cert.certificate.slot() == 8
+ && cert.certificate.certificate_type() == CertificateType::Notarize));
}
#[test]
fn test_new_parent_ready_with_certificates() {
- solana_logger::setup();
let (_, mut pool, bank_forks) = create_initial_state();
let bank = bank_forks.read().unwrap().root_bank();
let mut events = vec![];
@@ -2230,4 +2318,16 @@ mod tests {
"BLS signature verification failed for VoteMessage"
);
}
+
+ #[test]
+ fn test_update_pubkey() {
+ let new_pubkey = Pubkey::new_unique();
+ let (_, mut pool, _) = create_initial_state();
+ let old_pubkey = pool.my_pubkey();
+ assert_eq!(pool.parent_ready_tracker.my_pubkey(), old_pubkey);
+ assert_ne!(old_pubkey, new_pubkey);
+ pool.update_pubkey(new_pubkey);
+ assert_eq!(pool.my_pubkey(), new_pubkey);
+ assert_eq!(pool.parent_ready_tracker.my_pubkey(), new_pubkey);
+ }
}
diff --git a/votor/src/consensus_pool/parent_ready_tracker.rs b/votor/src/consensus_pool/parent_ready_tracker.rs
index 36709f4be44386..7e60dfd6c68eb8 100644
--- a/votor/src/consensus_pool/parent_ready_tracker.rs
+++ b/votor/src/consensus_pool/parent_ready_tracker.rs
@@ -237,6 +237,11 @@ impl ParentReadyTracker {
pub fn update_pubkey(&mut self, new_pubkey: Pubkey) {
self.my_pubkey = new_pubkey;
}
+
+ #[cfg(test)]
+ pub(crate) fn my_pubkey(&self) -> Pubkey {
+ self.my_pubkey
+ }
}
#[cfg(test)]