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