Преглед на файлове

(Alpenglow) Allow creating v4 account in genesis, upstream Consensus Pool. (#8122)

* Allow creating v4 account in genesis, upstream Consensus Pool.

* Remove accidentally added pub keyword.

* Fix the keypair used.

* Remove unused functions.

* Add stake_state::create_alpenglow_account which can is only called in genesis.

* Upstream changes in Alpenglow #460.
Wen преди 1 месец
родител
ревизия
20852db292

+ 1 - 0
Cargo.lock

@@ -10404,6 +10404,7 @@ dependencies = [
  "solana-vote",
  "solana-vote-interface",
  "solana-vote-program",
+ "solana-votor-messages",
  "spl-generic-token",
  "static_assertions",
  "strum",

+ 1 - 0
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

+ 1 - 0
program-test/src/lib.rs

@@ -806,6 +806,7 @@ impl ProgramTest {
             &bootstrap_validator_pubkey,
             &voting_keypair.pubkey(),
             &Pubkey::new_unique(),
+            None,
             bootstrap_validator_stake_lamports,
             42,
             fee_rate_governor,

+ 1 - 0
programs/sbf/Cargo.lock

@@ -8181,6 +8181,7 @@ dependencies = [
  "solana-vote",
  "solana-vote-interface",
  "solana-vote-program",
+ "solana-votor-messages",
  "spl-generic-token",
  "static_assertions",
  "strum",

+ 1 - 0
programs/sbf/tests/programs.rs

@@ -1661,6 +1661,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

+ 31 - 10
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<Meta> {
     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,
     }
 }
 
@@ -93,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,
     )
 }
 
@@ -103,10 +117,17 @@ 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 vote_state = VoteStateV3::deserialize(vote_account.data()).expect("vote_state");
+    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 {
+        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());
 
@@ -120,7 +141,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(),

+ 1 - 0
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"] }

+ 114 - 21
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<ValidatorVoteKeypairs>],
+    stakes: Vec<u64>,
+) -> 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<ValidatorVoteKeypairs>],
     stakes: Vec<u64>,
     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,14 +176,41 @@ 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 stake_account = Account::from(stake_state::create_account(
-            &stake_pubkey,
-            &vote_pubkey,
-            &vote_account,
-            &genesis_config_info.genesis_config.rent,
-            *stake,
-        ));
+        let vote_account = if is_alpenglow {
+            let bls_keypair = BLSKeypair::derive_from_signer(
+                &validator_voting_keypairs.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 = 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);
 
@@ -204,6 +264,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
@@ -266,6 +327,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 +341,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,20 +349,42 @@ 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 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,
+            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,
-        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,
@@ -342,6 +433,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 +447,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,

+ 1 - 0
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(),

+ 7 - 0
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,
+}

+ 2329 - 1
votor/src/consensus_pool.rs

@@ -1,5 +1,2333 @@
+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<AlpenglowCommitmentError> for AddVoteError {
+    fn from(_: AlpenglowCommitmentError) -> Self {
+        AddVoteError::ChannelDisconnected("CommitmentSender".to_string())
+    }
+}
+
+fn get_key_and_stakes(
+    epoch_schedule: &EpochSchedule,
+    epoch_stakes_map: &HashMap<Epoch, VersionedEpochStakes>,
+    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<PoolId, VotePoolType>,
+    /// Completed certificates
+    completed_certificates: BTreeMap<Certificate, Arc<CertificateMessage>>,
+    /// 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 slot that has a Finalized variant certificate
+    highest_finalized_slot: Option<Slot>,
+    /// 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<Slot, SlotStakeCounters>,
+}
+
+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_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<Hash>,
+        transaction: &VoteMessage,
+        validator_vote_key: &Pubkey,
+        validator_stake: Stake,
+    ) -> Option<Stake> {
+        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_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<Hash>,
+        events: &mut Vec<VotorEvent>,
+        total_stake: Stake,
+    ) -> Result<Vec<Arc<CertificateMessage>>, 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::<Stake>();
+            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<Hash>,
+    ) -> Option<VoteType> {
+        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<CertificateMessage>,
+        events: &mut Vec<VotorEvent>,
+    ) {
+        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);
+            }
+            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<Epoch, VersionedEpochStakes>,
+        root_slot: Slot,
+        my_vote_pubkey: &Pubkey,
+        message: &ConsensusMessage,
+        events: &mut Vec<VotorEvent>,
+    ) -> Result<(Option<Slot>, Vec<Arc<CertificateMessage>>), 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<Epoch, VersionedEpochStakes>,
+        root_slot: Slot,
+        my_vote_pubkey: &Pubkey,
+        vote_message: &VoteMessage,
+        events: &mut Vec<VotorEvent>,
+    ) -> Result<Vec<Arc<CertificateMessage>>, 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<VotorEvent>,
+    ) -> Result<Vec<Arc<CertificateMessage>>, 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])
+    }
+
+    /// Get the notarized block in `slot`
+    pub fn get_notarized_block(&self, slot: Slot) -> Option<Block> {
+        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)
+    }
+
+    /// 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)
+        })
+    }
+
+    /// 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)]
+    pub(crate) fn my_pubkey(&self) -> Pubkey {
+        self.my_pubkey
+    }
+
+    #[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<Arc<CertificateMessage>> {
+        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<Bank>, pubkey: &Pubkey) -> Bank {
+        Bank::new_from_parent_with_options(parent, pubkey, slot, NewBankOptions::default())
+    }
+
+    fn create_bank_forks(validator_keypairs: &[ValidatorVoteKeypairs]) -> Arc<RwLock<BankForks>> {
+        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<ValidatorVoteKeypairs>,
+        ConsensusPool,
+        Arc<RwLock<BankForks>>,
+    ) {
+        // Create 10 node validatorvotekeypairs vec
+        let validator_keypairs = (0..10)
+            .map(|_| ValidatorVoteKeypairs::new_rand())
+            .collect::<Vec<_>>();
+        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<CertificateType>,
+    ) {
+        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::<Vec<_>>();
+        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();
+        // 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
+            .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));
+
+        // 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() {
+        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"
+        );
+    }
+
+    #[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);
+    }
+}

+ 5 - 0
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)]

+ 3 - 0
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;