diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 9dd45a0f7..1de8e2f14 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -42,7 +42,9 @@ use { }, solana_vote_program::{ authorized_voters::AuthorizedVoters, - vote_state::{BlockTimestamp, Lockout, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, + vote_state::{ + BlockTimestamp, LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, + }, }, std::{ collections::{BTreeMap, HashMap}, @@ -1643,6 +1645,15 @@ impl From<&Lockout> for CliLockout { } } +impl From<&LandedVote> for CliLockout { + fn from(vote: &LandedVote) -> Self { + Self { + slot: vote.slot(), + confirmation_count: vote.confirmation_count(), + } + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliBlockTime { diff --git a/core/src/consensus.rs b/core/src/consensus.rs index afbd9814c..440cc6e34 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -3,6 +3,7 @@ use { heaviest_subtree_fork_choice::HeaviestSubtreeForkChoice, latest_validator_votes_for_frozen_banks::LatestValidatorVotesForFrozenBanks, progress_map::{LockoutIntervals, ProgressMap}, + tower1_14_11::Tower1_14_11, tower1_7_14::Tower1_7_14, tower_storage::{SavedTower, SavedTowerVersions, TowerStorage}, }, @@ -24,8 +25,9 @@ use { solana_vote_program::{ vote_instruction, vote_state::{ - process_slot_vote_unchecked, process_vote_unchecked, BlockTimestamp, Lockout, Vote, - VoteState, VoteStateUpdate, VoteTransaction, MAX_LOCKOUT_HISTORY, + process_slot_vote_unchecked, process_vote_unchecked, BlockTimestamp, LandedVote, + Lockout, Vote, VoteState, VoteState1_14_11, VoteStateUpdate, VoteStateVersions, + VoteTransaction, MAX_LOCKOUT_HISTORY, }, }, std::{ @@ -139,6 +141,7 @@ pub(crate) struct ComputedBankState { #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub enum TowerVersions { V1_17_14(Tower1_7_14), + V1_14_11(Tower1_14_11), Current(Tower), } @@ -156,7 +159,8 @@ impl TowerVersions { node_pubkey: tower.node_pubkey, threshold_depth: tower.threshold_depth, threshold_size: tower.threshold_size, - vote_state: tower.vote_state, + vote_state: VoteStateVersions::V1_14_11(Box::new(tower.vote_state)) + .convert_to_current(), last_vote: box_last_vote, last_vote_tx_blockhash: tower.last_vote_tx_blockhash, last_timestamp: tower.last_timestamp, @@ -164,12 +168,24 @@ impl TowerVersions { last_switch_threshold_check: tower.last_switch_threshold_check, } } + TowerVersions::V1_14_11(tower) => Tower { + node_pubkey: tower.node_pubkey, + threshold_depth: tower.threshold_depth, + threshold_size: tower.threshold_size, + vote_state: VoteStateVersions::V1_14_11(Box::new(tower.vote_state)) + .convert_to_current(), + last_vote: tower.last_vote, + last_vote_tx_blockhash: tower.last_vote_tx_blockhash, + last_timestamp: tower.last_timestamp, + stray_restored_slot: tower.stray_restored_slot, + last_switch_threshold_check: tower.last_switch_threshold_check, + }, TowerVersions::Current(tower) => tower, } } } -#[frozen_abi(digest = "HQoLKAJEQTuVy8nMSkVWbrH3M5xKksxdMEZHGLWbnX6w")] +#[frozen_abi(digest = "iZi6s9BvytU3HbRsibrAD71jwMLvrqHdCjVk6qKcVvd")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower { pub node_pubkey: Pubkey, @@ -302,24 +318,30 @@ impl Tower { }; for vote in &vote_state.votes { lockout_intervals - .entry(vote.last_locked_out_slot()) + .entry(vote.lockout.last_locked_out_slot()) .or_insert_with(Vec::new) .push((vote.slot(), key)); } if key == *vote_account_pubkey { - my_latest_landed_vote = vote_state.nth_recent_vote(0).map(|v| v.slot()); + my_latest_landed_vote = vote_state.nth_recent_lockout(0).map(|l| l.slot()); debug!("vote state {:?}", vote_state); debug!( "observed slot {}", - vote_state.nth_recent_vote(0).map(|v| v.slot()).unwrap_or(0) as i64 + vote_state + .nth_recent_lockout(0) + .map(|l| l.slot()) + .unwrap_or(0) as i64 ); debug!("observed root {}", vote_state.root_slot.unwrap_or(0) as i64); datapoint_info!( "tower-observed", ( "slot", - vote_state.nth_recent_vote(0).map(|v| v.slot()).unwrap_or(0), + vote_state + .nth_recent_lockout(0) + .map(|l| l.slot()) + .unwrap_or(0), i64 ), ("root", vote_state.root_slot.unwrap_or(0), i64) @@ -340,7 +362,7 @@ impl Tower { process_slot_vote_unchecked(&mut vote_state, bank_slot); for vote in &vote_state.votes { - bank_weight += vote.lockout() as u128 * voted_stake as u128; + bank_weight += vote.lockout.lockout() as u128 * voted_stake as u128; vote_slots.insert(vote.slot()); } @@ -369,10 +391,10 @@ impl Tower { // this vote stack is the simulated vote, so this fetch should be sufficient // to find the last unsimulated vote. assert_eq!( - vote_state.nth_recent_vote(0).map(|l| l.slot()), + vote_state.nth_recent_lockout(0).map(|l| l.slot()), Some(bank_slot) ); - if let Some(vote) = vote_state.nth_recent_vote(1) { + if let Some(vote) = vote_state.nth_recent_lockout(1) { // Update all the parents of this last vote with the stake of this vote account Self::update_ancestor_voted_stakes( &mut voted_stakes, @@ -480,7 +502,11 @@ impl Tower { let vote = Vote::new(vec![vote_slot], vote_hash); process_vote_unchecked(&mut self.vote_state, vote); VoteTransaction::from(VoteStateUpdate::new( - self.vote_state.votes.clone(), + self.vote_state + .votes + .iter() + .map(|vote| vote.lockout) + .collect(), self.vote_state.root_slot, vote_hash, )) @@ -518,7 +544,8 @@ impl Tower { /// Used for tests pub fn increase_lockout(&mut self, confirmation_count_increase: u32) { for vote in self.vote_state.votes.iter_mut() { - vote.increase_confirmation_count(confirmation_count_increase); + vote.lockout + .increase_confirmation_count(confirmation_count_increase); } } @@ -986,24 +1013,24 @@ impl Tower { ) -> bool { let mut vote_state = self.vote_state.clone(); process_slot_vote_unchecked(&mut vote_state, slot); - let vote = vote_state.nth_recent_vote(self.threshold_depth); - if let Some(vote) = vote { - if let Some(fork_stake) = voted_stakes.get(&vote.slot()) { - let lockout = *fork_stake as f64 / total_stake as f64; + let lockout = vote_state.nth_recent_lockout(self.threshold_depth); + if let Some(lockout) = lockout { + if let Some(fork_stake) = voted_stakes.get(&lockout.slot()) { + let lockout_stake = *fork_stake as f64 / total_stake as f64; trace!( "fork_stake slot: {}, vote slot: {}, lockout: {} fork_stake: {} total_stake: {}", - slot, vote.slot(), lockout, fork_stake, total_stake + slot, lockout.slot(), lockout_stake, fork_stake, total_stake ); - if vote.confirmation_count() as usize > self.threshold_depth { + if lockout.confirmation_count() as usize > self.threshold_depth { for old_vote in &self.vote_state.votes { - if old_vote.slot() == vote.slot() - && old_vote.confirmation_count() == vote.confirmation_count() + if old_vote.slot() == lockout.slot() + && old_vote.confirmation_count() == lockout.confirmation_count() { return true; } } } - lockout > self.threshold_size + lockout_stake > self.threshold_size } else { false } @@ -1281,7 +1308,7 @@ impl Tower { } } - fn initialize_lockouts bool>(&mut self, should_retain: F) { + fn initialize_lockouts bool>(&mut self, should_retain: F) { self.vote_state.votes.retain(should_retain); } @@ -1329,6 +1356,25 @@ pub enum TowerError { HardFork(Slot), } +// Tower1_14_11 is the persisted data format for the Tower, decoupling it from VoteState::Current +// From Tower1_14_11 to Tower is not implemented because it is not an expected conversion +#[allow(clippy::from_over_into)] +impl Into for Tower { + fn into(self) -> Tower1_14_11 { + Tower1_14_11 { + node_pubkey: self.node_pubkey, + threshold_depth: self.threshold_depth, + threshold_size: self.threshold_size, + vote_state: VoteState1_14_11::from(self.vote_state.clone()), + last_vote: self.last_vote.clone(), + last_vote_tx_blockhash: self.last_vote_tx_blockhash, + last_timestamp: self.last_timestamp, + stray_restored_slot: self.stray_restored_slot, + last_switch_threshold_check: self.last_switch_threshold_check, + } + } +} + impl TowerError { pub fn is_file_missing(&self) -> bool { if let TowerError::IoError(io_err) = &self { @@ -2167,7 +2213,7 @@ pub mod test { .vote_state .votes .iter() - .map(|v| v.lockout() as u128) + .map(|v| v.lockout.lockout() as u128) .sum::() + root_weight; let expected_bank_weight = 2 * vote_account_expected_weight; @@ -3161,8 +3207,14 @@ pub mod test { #[test] fn test_adjust_lockouts_after_replay_time_warped() { let mut tower = Tower::new_for_tests(10, 0.9); - tower.vote_state.votes.push_back(Lockout::new(1)); - tower.vote_state.votes.push_back(Lockout::new(0)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(1))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(0))); let vote = Vote::new(vec![0], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3179,8 +3231,14 @@ pub mod test { #[test] fn test_adjust_lockouts_after_replay_diverged_ancestor() { let mut tower = Tower::new_for_tests(10, 0.9); - tower.vote_state.votes.push_back(Lockout::new(1)); - tower.vote_state.votes.push_back(Lockout::new(2)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(1))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(2))); let vote = Vote::new(vec![2], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3203,9 +3261,15 @@ pub mod test { tower .vote_state .votes - .push_back(Lockout::new(MAX_ENTRIES - 1)); - tower.vote_state.votes.push_back(Lockout::new(0)); - tower.vote_state.votes.push_back(Lockout::new(1)); + .push_back(LandedVote::from(Lockout::new(MAX_ENTRIES - 1))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(0))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(1))); let vote = Vote::new(vec![1], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3223,8 +3287,14 @@ pub mod test { #[should_panic(expected = "slot_in_tower(2) < checked_slot(1)")] fn test_adjust_lockouts_after_replay_reversed_votes() { let mut tower = Tower::new_for_tests(10, 0.9); - tower.vote_state.votes.push_back(Lockout::new(2)); - tower.vote_state.votes.push_back(Lockout::new(1)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(2))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(1))); let vote = Vote::new(vec![1], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3241,9 +3311,18 @@ pub mod test { #[should_panic(expected = "slot_in_tower(3) < checked_slot(3)")] fn test_adjust_lockouts_after_replay_repeated_non_root_votes() { let mut tower = Tower::new_for_tests(10, 0.9); - tower.vote_state.votes.push_back(Lockout::new(2)); - tower.vote_state.votes.push_back(Lockout::new(3)); - tower.vote_state.votes.push_back(Lockout::new(3)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(2))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(3))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(3))); let vote = Vote::new(vec![3], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3260,9 +3339,18 @@ pub mod test { fn test_adjust_lockouts_after_replay_vote_on_root() { let mut tower = Tower::new_for_tests(10, 0.9); tower.vote_state.root_slot = Some(42); - tower.vote_state.votes.push_back(Lockout::new(42)); - tower.vote_state.votes.push_back(Lockout::new(43)); - tower.vote_state.votes.push_back(Lockout::new(44)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(42))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(43))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(44))); let vote = Vote::new(vec![44], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3276,7 +3364,10 @@ pub mod test { #[test] fn test_adjust_lockouts_after_replay_vote_on_genesis() { let mut tower = Tower::new_for_tests(10, 0.9); - tower.vote_state.votes.push_back(Lockout::new(0)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(0))); let vote = Vote::new(vec![0], Hash::default()); tower.last_vote = VoteTransaction::from(vote); @@ -3289,8 +3380,14 @@ pub mod test { #[test] fn test_adjust_lockouts_after_replay_future_tower() { let mut tower = Tower::new_for_tests(10, 0.9); - tower.vote_state.votes.push_back(Lockout::new(13)); - tower.vote_state.votes.push_back(Lockout::new(14)); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(13))); + tower + .vote_state + .votes + .push_back(LandedVote::from(Lockout::new(14))); let vote = Vote::new(vec![14], Hash::default()); tower.last_vote = VoteTransaction::from(vote); tower.initialize_root(12); diff --git a/core/src/lib.rs b/core/src/lib.rs index aa105de25..4891bce93 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -72,6 +72,7 @@ pub mod snapshot_packager_service; pub mod staked_nodes_updater_service; pub mod stats_reporter_service; pub mod system_monitor_service; +mod tower1_14_11; mod tower1_7_14; pub mod tower_storage; pub mod tpu; diff --git a/core/src/tower1_14_11.rs b/core/src/tower1_14_11.rs new file mode 100644 index 000000000..3d13efbd5 --- /dev/null +++ b/core/src/tower1_14_11.rs @@ -0,0 +1,34 @@ +use { + crate::consensus::SwitchForkDecision, + solana_sdk::{clock::Slot, hash::Hash, pubkey::Pubkey}, + solana_vote_program::vote_state::{ + vote_state_1_14_11::VoteState1_14_11, BlockTimestamp, VoteTransaction, + }, +}; + +#[frozen_abi(digest = "F83xHQA1wxoFDy25MTKXXmFXTc9Jbp6SXRXEPcehtKbQ")] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] +pub struct Tower1_14_11 { + pub(crate) node_pubkey: Pubkey, + pub(crate) threshold_depth: usize, + pub(crate) threshold_size: f64, + pub(crate) vote_state: VoteState1_14_11, + pub(crate) last_vote: VoteTransaction, + #[serde(skip)] + // The blockhash used in the last vote transaction, may or may not equal the + // blockhash of the voted block itself, depending if the vote slot was refreshed. + // For instance, a vote for slot 5, may be refreshed/resubmitted for inclusion in + // block 10, in which case `last_vote_tx_blockhash` equals the blockhash of 10, not 5. + pub(crate) last_vote_tx_blockhash: Hash, + pub(crate) last_timestamp: BlockTimestamp, + #[serde(skip)] + // Restored last voted slot which cannot be found in SlotHistory at replayed root + // (This is a special field for slashing-free validator restart with edge cases). + // This could be emptied after some time; but left intact indefinitely for easier + // implementation + // Further, stray slot can be stale or not. `Stale` here means whether given + // bank_forks (=~ ledger) lacks the slot or not. + pub(crate) stray_restored_slot: Option, + #[serde(skip)] + pub(crate) last_switch_threshold_check: Option<(Slot, SwitchForkDecision)>, +} diff --git a/core/src/tower1_7_14.rs b/core/src/tower1_7_14.rs index 7fe7881e0..823b678f2 100644 --- a/core/src/tower1_7_14.rs +++ b/core/src/tower1_7_14.rs @@ -6,16 +6,16 @@ use { pubkey::Pubkey, signature::{Signature, Signer}, }, - solana_vote_program::vote_state::{BlockTimestamp, Vote, VoteState}, + solana_vote_program::vote_state::{vote_state_1_14_11::VoteState1_14_11, BlockTimestamp, Vote}, }; -#[frozen_abi(digest = "8EBpwHf9gys2irNgyRCEe6A5KSh4RK875Fa46yA2NSoN")] +#[frozen_abi(digest = "9Kc3Cpak93xdL8bCnEwMWA8ZLGCBNfqh9PLo1o5RiPyT")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower1_7_14 { pub(crate) node_pubkey: Pubkey, pub(crate) threshold_depth: usize, pub(crate) threshold_size: f64, - pub(crate) vote_state: VoteState, + pub(crate) vote_state: VoteState1_14_11, pub(crate) last_vote: Vote, #[serde(skip)] // The blockhash used in the last vote transaction, may or may not equal the diff --git a/core/src/tower_storage.rs b/core/src/tower_storage.rs index 5841a1a48..353f11137 100644 --- a/core/src/tower_storage.rs +++ b/core/src/tower_storage.rs @@ -1,6 +1,7 @@ use { crate::{ consensus::{Result, Tower, TowerError, TowerVersions}, + tower1_14_11::Tower1_14_11, tower1_7_14::SavedTower1_7_14, }, solana_sdk::{ @@ -36,7 +37,7 @@ impl SavedTowerVersions { if !t.signature.verify(node_pubkey.as_ref(), &t.data) { return Err(TowerError::InvalidSignature); } - bincode::deserialize(&t.data).map(TowerVersions::Current) + bincode::deserialize(&t.data).map(TowerVersions::V1_14_11) } }; tv.map_err(|e| e.into()).and_then(|tv: TowerVersions| { @@ -94,7 +95,10 @@ impl SavedTower { ))); } - let data = bincode::serialize(tower)?; + // SavedTower always stores its data in 1_14_11 format + let tower: Tower1_14_11 = tower.clone().into(); + + let data = bincode::serialize(&tower)?; let signature = keypair.sign_message(&data); Ok(Self { signature, @@ -376,7 +380,8 @@ pub mod test { }, solana_sdk::{hash::Hash, signature::Keypair}, solana_vote_program::vote_state::{ - BlockTimestamp, Lockout, Vote, VoteState, VoteTransaction, MAX_LOCKOUT_HISTORY, + BlockTimestamp, LandedVote, Vote, VoteState, VoteState1_14_11, VoteTransaction, + MAX_LOCKOUT_HISTORY, }, tempfile::TempDir, }; @@ -389,7 +394,7 @@ pub mod test { let mut vote_state = VoteState::default(); vote_state .votes - .resize(MAX_LOCKOUT_HISTORY, Lockout::default()); + .resize(MAX_LOCKOUT_HISTORY, LandedVote::default()); vote_state.root_slot = Some(1); let vote = Vote::new(vec![1, 2, 3, 4], Hash::default()); @@ -399,7 +404,7 @@ pub mod test { node_pubkey, threshold_depth: 10, threshold_size: 0.9, - vote_state, + vote_state: VoteState1_14_11::from(vote_state), last_vote: vote.clone(), last_timestamp: BlockTimestamp::default(), last_vote_tx_blockhash: Hash::default(), diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index 90a05dc58..96f133e48 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -76,7 +76,13 @@ pub fn process_instruction(invoke_context: &mut InvokeContext) -> Result<(), Ins } let clock = get_sysvar_with_account_check::clock(invoke_context, instruction_context, 2)?; - vote_state::initialize_account(&mut me, &vote_init, &signers, &clock) + vote_state::initialize_account( + &mut me, + &vote_init, + &signers, + &clock, + &invoke_context.feature_set, + ) } VoteInstruction::Authorize(voter_pubkey, vote_authorize) => { let clock = @@ -127,7 +133,12 @@ pub fn process_instruction(invoke_context: &mut InvokeContext) -> Result<(), Ins let node_pubkey = transaction_context.get_key_of_account_at_index( instruction_context.get_index_of_instruction_account_in_transaction(1)?, )?; - vote_state::update_validator_identity(&mut me, node_pubkey, &signers) + vote_state::update_validator_identity( + &mut me, + node_pubkey, + &signers, + &invoke_context.feature_set, + ) } VoteInstruction::UpdateCommission(commission) => { if invoke_context.feature_set.is_active( @@ -140,7 +151,12 @@ pub fn process_instruction(invoke_context: &mut InvokeContext) -> Result<(), Ins return Err(VoteError::CommissionUpdateTooLate.into()); } } - vote_state::update_commission(&mut me, commission, &signers) + vote_state::update_commission( + &mut me, + commission, + &signers, + &invoke_context.feature_set, + ) } VoteInstruction::Vote(vote) | VoteInstruction::VoteSwitch(vote, _) => { let slot_hashes = @@ -225,6 +241,7 @@ pub fn process_instruction(invoke_context: &mut InvokeContext) -> Result<(), Ins &signers, &rent_sysvar, clock_if_feature_active.as_deref(), + &invoke_context.feature_set, ) } VoteInstruction::AuthorizeChecked(vote_authorize) => { @@ -793,7 +810,9 @@ mod tests { .convert_to_current(); assert_eq!( vote_state.votes, - vec![Lockout::new(*vote.slots.last().unwrap())] + vec![vote_state::LandedVote::from(Lockout::new( + *vote.slots.last().unwrap() + ))] ); assert_eq!(vote_state.credits(), 0); diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index ee8d6994d..f8a62263b 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -141,6 +141,37 @@ pub fn to(versioned: &VoteStateVersions, account: &mut T) -> VoteState::serialize(versioned, account.data_as_mut_slice()).ok() } +// Updates the vote account state with a new VoteState instance. This is required temporarily during the +// upgrade of vote account state from V1_14_11 to Current. +fn set_vote_account_state( + vote_account: &mut BorrowedAccount, + vote_state: VoteState, + feature_set: &FeatureSet, +) -> Result<(), InstructionError> { + // Only if vote_state_add_vote_latency feature is enabled should the new version of vote state be stored + if feature_set.is_active(&feature_set::vote_state_add_vote_latency::id()) { + // If the account is not large enough to store the vote state, then attempt a realloc to make it large enough. + // The realloc can only proceed if the vote account has balance sufficient for rent exemption at the new size. + if (vote_account.get_data().len() < VoteState::size_of()) + && (!vote_account.is_rent_exempt_at_data_length(VoteState::size_of()) + || vote_account.set_data_length(VoteState::size_of()).is_err()) + { + // Account cannot be resized to the size of a vote state as it will not be rent exempt, or failed to be + // resized for other reasons. So store the V1_14_11 version. + return vote_account.set_state(&VoteStateVersions::V1_14_11(Box::new( + VoteState1_14_11::from(vote_state), + ))); + } + // Vote account is large enough to store the newest version of vote state + vote_account.set_state(&VoteStateVersions::new_current(vote_state)) + // Else when the vote_state_add_vote_latency feature is not enabled, then the V1_14_11 version is stored + } else { + vote_account.set_state(&VoteStateVersions::V1_14_11(Box::new( + VoteState1_14_11::from(vote_state), + ))) + } +} + fn check_update_vote_state_slots_are_valid( vote_state: &VoteState, vote_state_update: &mut VoteStateUpdate, @@ -191,18 +222,18 @@ fn check_update_vote_state_slots_are_valid( if is_root_fix_enabled { let mut prev_slot = Slot::MAX; let current_root = vote_state_update.root; - for lockout in vote_state.votes.iter().rev() { + for vote in vote_state.votes.iter().rev() { let is_slot_bigger_than_root = current_root - .map(|current_root| lockout.slot() > current_root) + .map(|current_root| vote.slot() > current_root) .unwrap_or(true); // Ensure we're iterating from biggest to smallest vote in the // current vote state - assert!(lockout.slot() < prev_slot && is_slot_bigger_than_root); - if lockout.slot() <= new_proposed_root { - vote_state_update.root = Some(lockout.slot()); + assert!(vote.slot() < prev_slot && is_slot_bigger_than_root); + if vote.slot() <= new_proposed_root { + vote_state_update.root = Some(vote.slot()); break; } - prev_slot = lockout.slot(); + prev_slot = vote.slot(); } } } @@ -630,7 +661,7 @@ pub fn process_new_vote_state( // lockouts are corrects. match current_vote.slot().cmp(&new_vote.slot()) { Ordering::Less => { - if current_vote.last_locked_out_slot() >= new_vote.slot() { + if current_vote.lockout.last_locked_out_slot() >= new_vote.slot() { return Err(VoteError::LockoutConflict); } current_vote_state_index = current_vote_state_index @@ -681,7 +712,10 @@ pub fn process_new_vote_state( vote_state.process_timestamp(last_slot, timestamp)?; } vote_state.root_slot = new_root; - vote_state.votes = new_state; + vote_state.votes = new_state + .into_iter() + .map(|lockout| lockout.into()) + .collect(); Ok(()) } @@ -796,7 +830,7 @@ pub fn authorize( } } - vote_account.set_state(&VoteStateVersions::new_current(vote_state)) + set_vote_account_state(vote_account, vote_state, feature_set) } /// Update the node_pubkey, requires signature of the authorized voter @@ -804,6 +838,7 @@ pub fn update_validator_identity( vote_account: &mut BorrowedAccount, node_pubkey: &Pubkey, signers: &HashSet, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { let mut vote_state: VoteState = vote_account .get_state::()? @@ -817,7 +852,7 @@ pub fn update_validator_identity( vote_state.node_pubkey = *node_pubkey; - vote_account.set_state(&VoteStateVersions::new_current(vote_state)) + set_vote_account_state(vote_account, vote_state, feature_set) } /// Update the vote account's commission @@ -825,6 +860,7 @@ pub fn update_commission( vote_account: &mut BorrowedAccount, commission: u8, signers: &HashSet, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { let mut vote_state: VoteState = vote_account .get_state::()? @@ -835,7 +871,7 @@ pub fn update_commission( vote_state.commission = commission; - vote_account.set_state(&VoteStateVersions::new_current(vote_state)) + set_vote_account_state(vote_account, vote_state, feature_set) } /// Given the current slot and epoch schedule, determine if a commission change @@ -875,6 +911,7 @@ pub fn withdraw( signers: &HashSet, rent_sysvar: &Rent, clock: Option<&Clock>, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { let mut vote_account = instruction_context .try_borrow_instruction_account(transaction_context, vote_account_index)?; @@ -907,7 +944,7 @@ pub fn withdraw( } else { // Deinitialize upon zero-balance datapoint_debug!("vote-account-close", ("allow", 1, i64)); - vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?; + set_vote_account_state(&mut vote_account, VoteState::default(), feature_set)?; } } else { let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.get_data().len()); @@ -932,6 +969,7 @@ pub fn initialize_account( vote_init: &VoteInit, signers: &HashSet, clock: &Clock, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { if vote_account.get_data().len() != VoteState::size_of() { return Err(InstructionError::InvalidAccountData); @@ -945,9 +983,7 @@ pub fn initialize_account( // node must agree to accept this vote account verify_authorized_signer(&vote_init.node_pubkey, signers)?; - vote_account.set_state(&VoteStateVersions::new_current(VoteState::new( - vote_init, clock, - ))) + set_vote_account_state(vote_account, VoteState::new(vote_init, clock), feature_set) } fn verify_and_get_vote_state( @@ -992,7 +1028,7 @@ pub fn process_vote_with_account( .ok_or(VoteError::EmptySlots) .and_then(|slot| vote_state.process_timestamp(*slot, timestamp))?; } - vote_account.set_state(&VoteStateVersions::new_current(vote_state)) + set_vote_account_state(vote_account, vote_state, feature_set) } pub fn process_vote_state_update( @@ -1011,7 +1047,7 @@ pub fn process_vote_state_update( vote_state_update, Some(feature_set), )?; - vote_account.set_state(&VoteStateVersions::new_current(vote_state)) + set_vote_account_state(vote_account, vote_state, feature_set) } pub fn do_process_vote_state_update( @@ -1037,6 +1073,12 @@ pub fn do_process_vote_state_update( ) } +// This function is used: +// a. In many tests. +// b. In the genesis tool that initializes a cluster to create the bootstrap validator. +// c. In the ledger tool when creating bootstrap vote accounts. +// In all cases, initializing with the 1_14_11 version of VoteState is safest, as this version will in-place upgrade +// the first time it is altered by a vote transaction. pub fn create_account_with_authorized( node_pubkey: &Pubkey, authorized_voter: &Pubkey, @@ -1056,8 +1098,8 @@ pub fn create_account_with_authorized( &Clock::default(), ); - let versioned = VoteStateVersions::new_current(vote_state); - VoteState::serialize(&versioned, vote_account.data_as_mut_slice()).unwrap(); + let version1_14_11 = VoteStateVersions::V1_14_11(Box::new(VoteState1_14_11::from(vote_state))); + VoteState::serialize(&version1_14_11, vote_account.data_as_mut_slice()).unwrap(); vote_account } @@ -1079,7 +1121,7 @@ mod tests { crate::vote_state, solana_sdk::{ account::AccountSharedData, account_utils::StateMut, clock::DEFAULT_SLOTS_PER_EPOCH, - hash::hash, + hash::hash, transaction_context::InstructionAccount, }, std::cell::RefCell, test_case::test_case, @@ -1114,6 +1156,138 @@ mod tests { ) } + #[test] + fn test_vote_state_upgrade_from_1_14_11() { + let mut feature_set = FeatureSet::default(); + + // Create an initial vote account that is sized for the 1_14_11 version of vote state, and has only the + // required lamports for rent exempt minimum at that size + let node_pubkey = solana_sdk::pubkey::new_rand(); + let withdrawer_pubkey = solana_sdk::pubkey::new_rand(); + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey, + authorized_voter: withdrawer_pubkey, + authorized_withdrawer: withdrawer_pubkey, + commission: 10, + }, + &Clock::default(), + ); + // Pretend that prior epochs completed with credits + vote_state.increment_credits(1, 100); + vote_state.increment_credits(2, 200); + vote_state.increment_credits(3, 300); + // Pretend that votes happened + vec![ + 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, + 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, + 134, 135, + ] + .into_iter() + .for_each(|v| vote_state.process_next_vote_slot(v, 4)); + + let version1_14_11_serialized = bincode::serialize(&VoteStateVersions::V1_14_11(Box::new( + VoteState1_14_11::from(vote_state.clone()), + ))) + .unwrap(); + + let version1_14_11_serialized_len = version1_14_11_serialized.len(); + let rent = Rent::default(); + let lamports = rent.minimum_balance(version1_14_11_serialized_len); + let mut vote_account = + AccountSharedData::new(lamports, version1_14_11_serialized_len, &id()); + vote_account.set_data(version1_14_11_serialized); + + // Create a fake TransactionContext with a fake InstructionContext with a single account which is the + // vote account that was just created + let transaction_context = + TransactionContext::new(vec![(node_pubkey, vote_account)], None, 0, 0); + let mut instruction_context = InstructionContext::default(); + instruction_context.configure( + &[0], + &[InstructionAccount { + index_in_transaction: 0, + index_in_caller: 0, + index_in_callee: 0, + is_signer: false, + is_writable: true, + }], + &[], + ); + + // Get the BorrowedAccount from the InstructionContext which is what is used to manipulate and inspect account + // state + let mut borrowed_account = instruction_context + .try_borrow_instruction_account(&transaction_context, 0) + .unwrap(); + + // Ensure that the vote state started out at 1_14_11 + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert!(matches!(vote_state_version, VoteStateVersions::V1_14_11(_))); + + // Convert the vote state to current as would occur during vote instructions + let converted_vote_state = vote_state_version.convert_to_current(); + + // Check to make sure that the vote_state is unchanged + assert!(vote_state == converted_vote_state); + + let vote_state = converted_vote_state; + + // Now re-set the vote account state; because the feature is not enabled, the old 1_14_11 format should be + // written out + assert_eq!( + set_vote_account_state(&mut borrowed_account, vote_state.clone(), &feature_set), + Ok(()) + ); + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert!(matches!(vote_state_version, VoteStateVersions::V1_14_11(_))); + + // Convert the vote state to current as would occur during vote instructions + let converted_vote_state = vote_state_version.convert_to_current(); + + // Check to make sure that the vote_state is unchanged + assert_eq!(vote_state, converted_vote_state); + + let vote_state = converted_vote_state; + + // Test that when the feature is enabled, if the vote account does not have sufficient lamports to realloc, + // the old vote state is written out + feature_set.activate(&feature_set::vote_state_add_vote_latency::id(), 1); + assert_eq!( + set_vote_account_state(&mut borrowed_account, vote_state.clone(), &feature_set), + Ok(()) + ); + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert!(matches!(vote_state_version, VoteStateVersions::V1_14_11(_))); + + // Convert the vote state to current as would occur during vote instructions + let converted_vote_state = vote_state_version.convert_to_current(); + + // Check to make sure that the vote_state is unchanged + assert_eq!(vote_state, converted_vote_state); + + let vote_state = converted_vote_state; + + // Test that when the feature is enabled, if the vote account does have sufficient lamports, the + // new vote state is written out + assert_eq!( + borrowed_account.set_lamports(rent.minimum_balance(VoteState::size_of())), + Ok(()) + ); + assert_eq!( + set_vote_account_state(&mut borrowed_account, vote_state.clone(), &feature_set), + Ok(()) + ); + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert!(matches!(vote_state_version, VoteStateVersions::Current(_))); + + // Convert the vote state to current as would occur during vote instructions + let converted_vote_state = vote_state_version.convert_to_current(); + + // Check to make sure that the vote_state is unchanged + assert_eq!(vote_state, converted_vote_state); + } + #[test] fn test_vote_lockout() { let (_vote_pubkey, vote_account) = create_test_account(); @@ -1141,7 +1315,12 @@ mod tests { assert_eq!(Some(top_vote), vote_state.root_slot); // Expire everything except the first vote - let slot = vote_state.votes.front().unwrap().last_locked_out_slot(); + let slot = vote_state + .votes + .front() + .unwrap() + .lockout + .last_locked_out_slot(); process_slot_vote_unchecked(&mut vote_state, slot); // First vote and new vote are both stored for a total of 2 votes assert_eq!(vote_state.votes.len(), 2); @@ -1187,7 +1366,7 @@ mod tests { assert_eq!(vote_state.votes[0].confirmation_count(), 3); // Expire the second and third votes - let expire_slot = vote_state.votes[1].slot() + vote_state.votes[1].lockout() + 1; + let expire_slot = vote_state.votes[1].slot() + vote_state.votes[1].lockout.lockout() + 1; process_slot_vote_unchecked(&mut vote_state, expire_slot); assert_eq!(vote_state.votes.len(), 2); @@ -1232,13 +1411,13 @@ mod tests { process_slot_vote_unchecked(&mut vote_state, 0); process_slot_vote_unchecked(&mut vote_state, 1); process_slot_vote_unchecked(&mut vote_state, 0); - assert_eq!(vote_state.nth_recent_vote(0).unwrap().slot(), 1); - assert_eq!(vote_state.nth_recent_vote(1).unwrap().slot(), 0); - assert!(vote_state.nth_recent_vote(2).is_none()); + assert_eq!(vote_state.nth_recent_lockout(0).unwrap().slot(), 1); + assert_eq!(vote_state.nth_recent_lockout(1).unwrap().slot(), 0); + assert!(vote_state.nth_recent_lockout(2).is_none()); } #[test] - fn test_nth_recent_vote() { + fn test_nth_recent_lockout() { let voter_pubkey = solana_sdk::pubkey::new_rand(); let mut vote_state = vote_state_new_for_test(&voter_pubkey); for i in 0..MAX_LOCKOUT_HISTORY { @@ -1246,11 +1425,11 @@ mod tests { } for i in 0..(MAX_LOCKOUT_HISTORY - 1) { assert_eq!( - vote_state.nth_recent_vote(i).unwrap().slot() as usize, + vote_state.nth_recent_lockout(i).unwrap().slot() as usize, MAX_LOCKOUT_HISTORY - i - 1, ); } - assert!(vote_state.nth_recent_vote(MAX_LOCKOUT_HISTORY).is_none()); + assert!(vote_state.nth_recent_lockout(MAX_LOCKOUT_HISTORY).is_none()); } fn check_lockouts(vote_state: &VoteState) { @@ -1260,7 +1439,10 @@ mod tests { .len() .checked_sub(i) .expect("`i` is less than `vote_state.votes.len()`"); - assert_eq!(vote.lockout(), INITIAL_LOCKOUT.pow(num_votes as u32) as u64); + assert_eq!( + vote.lockout.lockout(), + INITIAL_LOCKOUT.pow(num_votes as u32) as u64 + ); } } @@ -1475,6 +1657,24 @@ mod tests { ); } + pub fn process_new_vote_state_from_votes( + vote_state: &mut VoteState, + new_state: VecDeque, + new_root: Option, + timestamp: Option, + epoch: Epoch, + feature_set: Option<&FeatureSet>, + ) -> Result<(), VoteError> { + process_new_vote_state( + vote_state, + new_state.into_iter().map(|vote| vote.lockout).collect(), + new_root, + timestamp, + epoch, + feature_set, + ) + } + // Test vote credit updates after "one credit per slot" feature is enabled #[test] fn test_vote_state_update_increment_credits() { @@ -1537,7 +1737,7 @@ mod tests { // Now use the resulting new vote state to perform a vote state update on vote_state assert_eq!( - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state, vote_state_after_vote.votes, vote_state_after_vote.root_slot, @@ -1593,7 +1793,7 @@ mod tests { let current_epoch = vote_state2.current_epoch(); assert_eq!( - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), lesser_root, @@ -1607,7 +1807,7 @@ mod tests { // Trying to set root to None should error let none_root = None; assert_eq!( - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), none_root, @@ -1848,7 +2048,7 @@ mod tests { process_slot_vote_unchecked(&mut vote_state2, new_vote as Slot); assert_ne!(vote_state1.root_slot, vote_state2.root_slot); - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, @@ -1906,7 +2106,7 @@ mod tests { ); // See that on-chain vote state can update properly - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, @@ -1948,7 +2148,7 @@ mod tests { // See that on-chain vote state can update properly assert_eq!( - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, @@ -1990,7 +2190,7 @@ mod tests { // Both vote states contain `5`, but `5` is not part of the common prefix // of both vote states. However, the violation should still be detected. assert_eq!( - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, @@ -2024,7 +2224,7 @@ mod tests { // Slot 1 has been expired by 10, but is kept alive by its descendant // 9 which has not been expired yet. assert_eq!(vote_state2.votes[0].slot(), 1); - assert_eq!(vote_state2.votes[0].last_locked_out_slot(), 9); + assert_eq!(vote_state2.votes[0].lockout.last_locked_out_slot(), 9); assert_eq!( vote_state2 .votes @@ -2035,7 +2235,7 @@ mod tests { ); // Should be able to update vote_state1 - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, @@ -2076,15 +2276,15 @@ mod tests { Err(VoteError::LockoutConflict) ); - let good_votes: VecDeque = vec![ - Lockout::new_with_confirmation_count(2, 5), - Lockout::new_with_confirmation_count(15, 1), + let good_votes: VecDeque = vec![ + Lockout::new_with_confirmation_count(2, 5).into(), + Lockout::new_with_confirmation_count(15, 1).into(), ] .into_iter() .collect(); let current_epoch = vote_state1.current_epoch(); - process_new_vote_state( + process_new_vote_state_from_votes( &mut vote_state1, good_votes.clone(), root, @@ -2126,7 +2326,11 @@ mod tests { let vote = Vote::new(vec![old_vote_slot, vote_slot], vote_slot_hash); process_vote(&mut vote_state, &vote, &slot_hashes, 0, Some(&feature_set)).unwrap(); assert_eq!( - vote_state.votes.into_iter().collect::>(), + vote_state + .votes + .into_iter() + .map(|vote| vote.lockout) + .collect::>(), vec![Lockout::new_with_confirmation_count(vote_slot, 1)] ); } @@ -2293,7 +2497,11 @@ mod tests { .is_ok()); assert_eq!(vote_state.root_slot, expected_root); assert_eq!( - vote_state.votes.into_iter().collect::>(), + vote_state + .votes + .into_iter() + .map(|vote| vote.lockout) + .collect::>(), expected_vote_state, ); } diff --git a/sdk/program/src/vote/state/mod.rs b/sdk/program/src/vote/state/mod.rs index a63b41212..4610e092d 100644 --- a/sdk/program/src/vote/state/mod.rs +++ b/sdk/program/src/vote/state/mod.rs @@ -20,6 +20,8 @@ use { }; mod vote_state_0_23_5; +pub mod vote_state_1_14_11; +pub use vote_state_1_14_11::*; pub mod vote_state_versions; pub use vote_state_versions::*; @@ -105,6 +107,40 @@ impl Lockout { } } +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] +pub struct LandedVote { + // Latency is the difference in slot number between the slot that was voted on (lockout.slot) and the slot in + // which the vote that added this Lockout landed. For votes which were cast before versions of the validator + // software which recorded vote latencies, latency is recorded as 0. + pub latency: u8, + pub lockout: Lockout, +} + +impl LandedVote { + pub fn slot(&self) -> Slot { + self.lockout.slot + } + + pub fn confirmation_count(&self) -> u32 { + self.lockout.confirmation_count + } +} + +impl From for Lockout { + fn from(landed_vote: LandedVote) -> Self { + landed_vote.lockout + } +} + +impl From for LandedVote { + fn from(lockout: Lockout) -> Self { + Self { + latency: 0, + lockout, + } + } +} + #[frozen_abi(digest = "GwJfVFsATSj7nvKwtUkHYzqPRaPY6SLxPGXApuCya3x5")] #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] pub struct VoteStateUpdate { @@ -238,7 +274,7 @@ impl CircBuf { } } -#[frozen_abi(digest = "4oxo6mBc8zrZFA89RgKsNyMqqM52iVrCphsWfaHjaAAY")] +#[frozen_abi(digest = "EeenjJaSrm9hRM39gK6raRNtzG61hnk7GciUCJJRDUSQ")] #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] pub struct VoteState { /// the node that votes in this account @@ -250,7 +286,7 @@ pub struct VoteState { /// payout should be given to this VoteAccount pub commission: u8, - pub votes: VecDeque, + pub votes: VecDeque, // This usually the last Lockout which was popped from self.votes. // However, it can be arbitrary slot, when being used inside Tower @@ -302,7 +338,7 @@ impl VoteState { /// Upper limit on the size of the Vote State /// when votes.len() is MAX_LOCKOUT_HISTORY. pub const fn size_of() -> usize { - 3731 // see test_vote_state_size_of. + 3762 // see test_vote_state_size_of. } pub fn deserialize(_input: &[u8]) -> Result { @@ -362,7 +398,7 @@ impl VoteState { /// Returns if the vote state contains a slot `candidate_slot` pub fn contains_slot(&self, candidate_slot: Slot) -> bool { self.votes - .binary_search_by(|lockout| lockout.slot().cmp(&candidate_slot)) + .binary_search_by(|vote| vote.slot().cmp(&candidate_slot)) .is_ok() } @@ -374,7 +410,7 @@ impl VoteState { } VoteState { - votes: VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]), + votes: VecDeque::from(vec![LandedVote::default(); MAX_LOCKOUT_HISTORY]), root_slot: Some(std::u64::MAX), epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY], authorized_voters, @@ -391,7 +427,7 @@ impl VoteState { return; } - let vote = Lockout::new(next_vote_slot); + let lockout = Lockout::new(next_vote_slot); self.pop_expired_votes(next_vote_slot); @@ -402,7 +438,7 @@ impl VoteState { self.increment_credits(epoch, 1); } - self.votes.push_back(vote); + self.votes.push_back(lockout.into()); self.double_lockouts(); } @@ -435,21 +471,21 @@ impl VoteState { self.epoch_credits.last().unwrap().1.saturating_add(credits); } - pub fn nth_recent_vote(&self, position: usize) -> Option<&Lockout> { + pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> { if position < self.votes.len() { let pos = self .votes .len() .checked_sub(position) .and_then(|pos| pos.checked_sub(1))?; - self.votes.get(pos) + self.votes.get(pos).map(|vote| &vote.lockout) } else { None } } pub fn last_lockout(&self) -> Option<&Lockout> { - self.votes.back() + self.votes.back().map(|vote| &vote.lockout) } pub fn last_voted_slot(&self) -> Option { @@ -579,8 +615,11 @@ impl VoteState { for (i, v) in self.votes.iter_mut().enumerate() { // Don't increase the lockout for this vote until we get more confirmations // than the max number of confirmations this vote has seen - if stack_depth > i.checked_add(v.confirmation_count() as usize).expect("`confirmation_count` and tower_size should be bounded by `MAX_LOCKOUT_HISTORY`") { - v.increase_confirmation_count(1); + if stack_depth > + i.checked_add(v.confirmation_count() as usize) + .expect("`confirmation_count` and tower_size should be bounded by `MAX_LOCKOUT_HISTORY`") + { + v.lockout.increase_confirmation_count(1); } } } @@ -719,7 +758,7 @@ mod tests { let mut vote_state = VoteState::default(); vote_state .votes - .resize(MAX_LOCKOUT_HISTORY, Lockout::default()); + .resize(MAX_LOCKOUT_HISTORY, LandedVote::default()); vote_state.root_slot = Some(1); let versioned = VoteStateVersions::new_current(vote_state); assert!(VoteState::serialize(&versioned, &mut buffer[0..4]).is_err()); diff --git a/sdk/program/src/vote/state/vote_state_1_14_11.rs b/sdk/program/src/vote/state/vote_state_1_14_11.rs new file mode 100644 index 000000000..2eeadb69c --- /dev/null +++ b/sdk/program/src/vote/state/vote_state_1_14_11.rs @@ -0,0 +1,55 @@ +use super::*; + +#[frozen_abi(digest = "CZTgLymuevXjAx6tM8X8T5J3MCx9AkEsFSmu4FJrEpkG")] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] +pub struct VoteState1_14_11 { + /// the node that votes in this account + pub node_pubkey: Pubkey, + + /// the signer for withdrawals + pub authorized_withdrawer: Pubkey, + /// percentage (0-100) that represents what part of a rewards + /// payout should be given to this VoteAccount + pub commission: u8, + + pub votes: VecDeque, + + // This usually the last Lockout which was popped from self.votes. + // However, it can be arbitrary slot, when being used inside Tower + pub root_slot: Option, + + /// the signer for vote transactions + pub authorized_voters: AuthorizedVoters, + + /// history of prior authorized voters and the epochs for which + /// they were set, the bottom end of the range is inclusive, + /// the top of the range is exclusive + pub prior_voters: CircBuf<(Pubkey, Epoch, Epoch)>, + + /// history of how many credits earned by the end of each epoch + /// each tuple is (Epoch, credits, prev_credits) + pub epoch_credits: Vec<(Epoch, u64, u64)>, + + /// most recent timestamp submitted with a vote + pub last_timestamp: BlockTimestamp, +} + +impl From for VoteState1_14_11 { + fn from(vote_state: VoteState) -> Self { + Self { + node_pubkey: vote_state.node_pubkey, + authorized_withdrawer: vote_state.authorized_withdrawer, + commission: vote_state.commission, + votes: vote_state + .votes + .into_iter() + .map(|landed_vote| landed_vote.into()) + .collect(), + root_slot: vote_state.root_slot, + authorized_voters: vote_state.authorized_voters, + prior_voters: vote_state.prior_voters, + epoch_credits: vote_state.epoch_credits, + last_timestamp: vote_state.last_timestamp, + } + } +} diff --git a/sdk/program/src/vote/state/vote_state_versions.rs b/sdk/program/src/vote/state/vote_state_versions.rs index 50bfed052..3d1211bde 100644 --- a/sdk/program/src/vote/state/vote_state_versions.rs +++ b/sdk/program/src/vote/state/vote_state_versions.rs @@ -1,8 +1,9 @@ -use super::{vote_state_0_23_5::VoteState0_23_5, *}; +use super::{vote_state_0_23_5::VoteState0_23_5, vote_state_1_14_11::VoteState1_14_11, *}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub enum VoteStateVersions { V0_23_5(Box), + V1_14_11(Box), Current(Box), } @@ -27,7 +28,7 @@ impl VoteStateVersions { /// payout should be given to this VoteAccount commission: state.commission, - votes: state.votes.clone(), + votes: Self::landed_votes_from_lockouts(state.votes), root_slot: state.root_slot, @@ -47,16 +48,41 @@ impl VoteStateVersions { last_timestamp: state.last_timestamp.clone(), } } + + VoteStateVersions::V1_14_11(state) => VoteState { + node_pubkey: state.node_pubkey, + authorized_withdrawer: state.authorized_withdrawer, + commission: state.commission, + + votes: Self::landed_votes_from_lockouts(state.votes), + + root_slot: state.root_slot, + + authorized_voters: state.authorized_voters.clone(), + + prior_voters: CircBuf::default(), + + epoch_credits: state.epoch_credits, + + last_timestamp: state.last_timestamp, + }, + VoteStateVersions::Current(state) => *state, } } + fn landed_votes_from_lockouts(lockouts: VecDeque) -> VecDeque { + lockouts.into_iter().map(|lockout| lockout.into()).collect() + } + pub fn is_uninitialized(&self) -> bool { match self { VoteStateVersions::V0_23_5(vote_state) => { vote_state.authorized_voter == Pubkey::default() } + VoteStateVersions::V1_14_11(vote_state) => vote_state.authorized_voters.is_empty(), + VoteStateVersions::Current(vote_state) => vote_state.authorized_voters.is_empty(), } } diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 484fea3c5..6adb00b6c 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -630,6 +630,10 @@ pub mod round_up_heap_size { solana_sdk::declare_id!("CE2et8pqgyQMP2mQRg3CgvX8nJBKUArMu3wfiQiQKY1y"); } +pub mod vote_state_add_vote_latency { + solana_sdk::declare_id!("7axKe5BTYBDD87ftzWbk5DfzWMGyRvqmWTduuo22Yaqy"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -782,6 +786,7 @@ lazy_static! { (add_set_tx_loaded_accounts_data_size_instruction::id(), "add compute budget instruction for setting account data size per transaction #30366"), (switch_to_new_elf_parser::id(), "switch to new ELF parser #30497"), (round_up_heap_size::id(), "round up heap size when calculating heap cost #30679"), + (vote_state_add_vote_latency::id(), "replace Lockout with LandedVote (including vote latency) in vote state"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/transaction_context.rs b/sdk/src/transaction_context.rs index bf91b2a74..023607374 100644 --- a/sdk/src/transaction_context.rs +++ b/sdk/src/transaction_context.rs @@ -874,6 +874,16 @@ impl<'a> BorrowedAccount<'a> { Ok(()) } + // Returns whether or the lamports currently in the account is sufficient for rent exemption should the + // data be resized to the given size + #[cfg(not(target_os = "solana"))] + pub fn is_rent_exempt_at_data_length(&self, data_length: usize) -> bool { + self.transaction_context + .rent + .unwrap_or_default() + .is_exempt(self.get_lamports(), data_length) + } + /// Returns whether this account is executable (transaction wide) #[inline] pub fn is_executable(&self) -> bool {