diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 167477117f8a20..1d5e227b7b3c4f 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -433,7 +433,7 @@ pub fn process_show_vote_account( build_balance_message(vote_account.lamports, use_lamports_unit, true) ); println!("Validator Identity: {}", vote_state.node_pubkey); - println!("Authorized Voter: {}", vote_state.authorized_voter); + println!("Authorized Voter: {:?}", vote_state.authorized_voters()); println!( "Authorized Withdrawer: {}", vote_state.authorized_withdrawer diff --git a/core/src/commitment.rs b/core/src/commitment.rs index 80a49f2118233d..a7df5e328f23c2 100644 --- a/core/src/commitment.rs +++ b/core/src/commitment.rs @@ -233,7 +233,7 @@ mod tests { use crate::genesis_utils::{create_genesis_config, GenesisConfigInfo}; use solana_sdk::pubkey::Pubkey; use solana_stake_program::stake_state; - use solana_vote_program::vote_state; + use solana_vote_program::vote_state::{self, VoteStateVersions}; #[test] fn test_block_commitment() { @@ -446,13 +446,15 @@ mod tests { let mut vote_state1 = VoteState::from(&vote_account1).unwrap(); vote_state1.process_slot_vote_unchecked(3); vote_state1.process_slot_vote_unchecked(5); - vote_state1.to(&mut vote_account1).unwrap(); + let versioned = VoteStateVersions::Current(Box::new(vote_state1)); + VoteState::to(&versioned, &mut vote_account1).unwrap(); bank.store_account(&pk1, &vote_account1); let mut vote_state2 = VoteState::from(&vote_account2).unwrap(); vote_state2.process_slot_vote_unchecked(9); vote_state2.process_slot_vote_unchecked(10); - vote_state2.to(&mut vote_account2).unwrap(); + let versioned = VoteStateVersions::Current(Box::new(vote_state2)); + VoteState::to(&versioned, &mut vote_account2).unwrap(); bank.store_account(&pk2, &vote_account2); let commitment = AggregateCommitmentService::aggregate_commitment(&ancestors, &bank); diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 344af0f337db7a..fd853d19ff4f14 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -484,7 +484,10 @@ pub mod test { signature::{Keypair, Signer}, transaction::Transaction, }; - use solana_vote_program::{vote_instruction, vote_state::Vote}; + use solana_vote_program::{ + vote_instruction, + vote_state::{Vote, VoteStateVersions}, + }; use std::collections::{HashMap, VecDeque}; use std::sync::RwLock; use std::{thread::sleep, time::Duration}; @@ -707,9 +710,11 @@ pub mod test { for slot in *votes { vote_state.process_slot_vote_unchecked(*slot); } - vote_state - .serialize(&mut account.data) - .expect("serialize state"); + VoteState::serialize( + &VoteStateVersions::Current(Box::new(vote_state)), + &mut account.data, + ) + .expect("serialize state"); stakes.push((Pubkey::new_rand(), (*lamports, account))); } stakes diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index d5ffe8108e09be..e1f25fe9ceeb42 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1087,7 +1087,7 @@ pub(crate) mod tests { transaction::TransactionError, }; use solana_stake_program::stake_state; - use solana_vote_program::vote_state::{self, Vote, VoteState}; + use solana_vote_program::vote_state::{self, Vote, VoteState, VoteStateVersions}; use std::{ fs::remove_dir_all, iter, @@ -1122,7 +1122,8 @@ pub(crate) mod tests { let mut vote_account = bank.get_account(&pubkey).unwrap(); let mut vote_state = VoteState::from(&vote_account).unwrap(); vote_state.process_slot_vote_unchecked(slot); - vote_state.to(&mut vote_account).unwrap(); + let versioned = VoteStateVersions::Current(Box::new(vote_state)); + VoteState::to(&versioned, &mut vote_account).unwrap(); bank.store_account(&pubkey, &vote_account); } @@ -1706,7 +1707,8 @@ pub(crate) mod tests { let mut leader_vote_account = bank.get_account(&pubkey).unwrap(); let mut vote_state = VoteState::from(&leader_vote_account).unwrap(); vote_state.process_slot_vote_unchecked(bank.slot()); - vote_state.to(&mut leader_vote_account).unwrap(); + let versioned = VoteStateVersions::Current(Box::new(vote_state)); + VoteState::to(&versioned, &mut leader_vote_account).unwrap(); bank.store_account(&pubkey, &leader_vote_account); } diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 4aebff057973fb..b11151d630031d 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -14,12 +14,18 @@ use solana_ledger::{ shred_version::compute_shred_version, snapshot_utils, }; +use solana_runtime::bank::Bank; use solana_sdk::{ clock::Slot, genesis_config::GenesisConfig, native_token::lamports_to_sol, program_utils::limited_deserialize, pubkey::Pubkey, }; -use solana_vote_program::vote_state::VoteState; +use solana_vote_program::{ + self, + vote_state::{VoteState, VoteStateVersions}, +}; + use std::{ + boxed::Box, collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, fs::{self, File}, @@ -524,6 +530,21 @@ fn hardforks_of(matches: &ArgMatches<'_>, name: &str) -> Option> { } } +fn load_bank_from_snapshot(arg_matches: &ArgMatches, ledger_path: &PathBuf) -> Bank { + let snapshot_config = SnapshotConfig { + snapshot_interval_slots: 0, // Value doesn't matter + snapshot_package_output_path: ledger_path.clone(), + snapshot_path: ledger_path.clone().join("snapshot"), + }; + let account_paths = if let Some(account_paths) = arg_matches.value_of("account_paths") { + account_paths.split(',').map(PathBuf::from).collect() + } else { + vec![ledger_path.join("accounts")] + }; + + bank_forks_utils::load_bank_from_archive(account_paths, &snapshot_config) +} + fn load_bank_forks( arg_matches: &ArgMatches, ledger_path: &PathBuf, @@ -692,6 +713,27 @@ fn main() { .takes_value(true) .help("Output directory for the snapshot"), ) + ).subcommand( + SubCommand::with_name("verify-snapshot") + .about("Convert accounts in snapshot") + .arg(&no_snapshot_arg) + .arg(&account_paths_arg) + .arg(&halt_at_slot_arg) + .arg(&hard_forks_arg) + ).subcommand( + SubCommand::with_name("convert-accounts") + .about("Convert accounts in snapshot") + .arg(&no_snapshot_arg) + .arg(&account_paths_arg) + .arg(&halt_at_slot_arg) + .arg(&hard_forks_arg) + .arg( + Arg::with_name("output_directory") + .index(1) + .value_name("DIR") + .takes_value(true) + .help("Output directory for the snapshot"), + ) ).subcommand( SubCommand::with_name("print-accounts") .about("Print account contents after processing in the ledger") @@ -942,6 +984,115 @@ fn main() { } } } + ("verify-snapshot", Some(arg_matches)) => { + load_bank_from_snapshot(arg_matches, &ledger_path); + } + ("convert-accounts", Some(arg_matches)) => { + let output_directory = value_t_or_exit!(arg_matches, "output_directory", String); + let genesis_config = open_genesis_config(&ledger_path); + let mut root_bank = load_bank_from_snapshot(arg_matches, &ledger_path); + // Convert root_bank.EpochStakes + for (_, stakes) in root_bank.epoch_stakes.iter_mut() { + for (pubkey, (_, vote_account)) in stakes.vote_accounts.iter_mut() { + VoteStateVersions::convert_from_raw(vote_account, &pubkey); + } + } + + for (pubkey, (_, vote_account)) in + root_bank.stakes.write().unwrap().vote_accounts.iter_mut() + { + VoteStateVersions::convert_from_raw(vote_account, &pubkey); + } + + let index: Vec<_> = { + let index = root_bank + .rc + .accounts + .accounts_db + .accounts_index + .read() + .unwrap(); + + // Write out the new accounts + let total: usize = index + .account_maps + .values() + .map(|slot_list| slot_list.read().unwrap().len()) + .sum(); + + println!("Converting {} accounts", total); + index + .account_maps + .iter() + .map(|(pubkey, slot_list)| (*pubkey, slot_list.read().unwrap().clone())) + .collect() + }; + + for (pubkey, slot_list) in index.iter() { + for (slot, _) in slot_list.iter() { + let ancestors = vec![(*slot, 1)].into_iter().collect(); + let res = root_bank.rc.accounts.load_slow(&ancestors, pubkey); + + if let Some((mut account, _)) = res { + if account.owner == solana_vote_program::id() { + VoteStateVersions::convert_from_raw(&mut account, pubkey); + root_bank.rc.accounts.store_slow(*slot, pubkey, &account); + (*slot, pubkey, &account); + } + } + } + } + + // Update the bank hash + println!("Updating bank hash"); + root_bank + .rc + .accounts + .accounts_db + .recompute_bank_hash(root_bank.slot()) + .expect("Failed to recompute bank hash"); + + root_bank.recompute_hash(); + + let temp_dir = tempfile::TempDir::new().unwrap_or_else(|err| { + eprintln!("Unable to create temporary directory: {}", err); + exit(1); + }); + + let storages: Vec<_> = root_bank.get_snapshot_storages(); + snapshot_utils::add_snapshot(&temp_dir, &root_bank, &storages) + .and_then(|slot_snapshot_paths| { + snapshot_utils::package_snapshot( + &root_bank, + &slot_snapshot_paths, + snapshot_utils::get_snapshot_archive_path(output_directory), + &temp_dir, + &root_bank.src.roots(), + storages, + ) + }) + .and_then(|package| { + snapshot_utils::archive_snapshot_package(&package).map(|ok| { + println!( + "Successfully created snapshot for slot {}: {:?}", + root_bank.slot(), + package.tar_output_file + ); + println!( + "Shred version: {}", + compute_shred_version( + &genesis_config.hash(), + Some(&root_bank.hard_forks().read().unwrap()) + ) + ); + ok + }) + }) + .unwrap_or_else(|err| { + eprintln!("Unable to create snapshot archive: {}", err); + exit(1); + }); + } ("print-accounts", Some(arg_matches)) => { let dev_halt_at_slot = value_t!(arg_matches, "halt_at_slot", Slot).ok(); let process_options = ProcessOptions { diff --git a/ledger/src/bank_forks.rs b/ledger/src/bank_forks.rs index b60dd3eae5abef..a155c9bcab5267 100644 --- a/ledger/src/bank_forks.rs +++ b/ledger/src/bank_forks.rs @@ -122,6 +122,10 @@ impl BankForks { self.banks.get(&bank_slot) } + pub fn remove(&mut self, bank_slot: Slot) -> Arc { + self.banks.remove(&bank_slot).unwrap() + } + pub fn new_from_banks(initial_forks: &[Arc], rooted_path: Vec) -> Self { let mut banks = HashMap::new(); let working_bank = initial_forks[0].clone(); diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs index bf7482652c7c27..b07a8dcb6f3358 100644 --- a/ledger/src/bank_forks_utils.rs +++ b/ledger/src/bank_forks_utils.rs @@ -9,6 +9,7 @@ use crate::{ snapshot_utils, }; use log::*; +use solana_runtime::bank::Bank; use solana_sdk::{clock::Slot, genesis_config::GenesisConfig, hash::Hash}; use std::{fs, path::PathBuf, result, sync::Arc}; @@ -36,6 +37,15 @@ fn to_loadresult( }) } +pub fn load_bank_from_archive( + account_paths: Vec, + snapshot_config: &SnapshotConfig, +) -> Bank { + let tar = + snapshot_utils::get_snapshot_archive_path(&snapshot_config.snapshot_package_output_path); + snapshot_utils::bank_from_archive(&account_paths, &snapshot_config.snapshot_path, &tar).unwrap() +} + pub fn load( genesis_config: &GenesisConfig, blockstore: &Blockstore, diff --git a/ledger/src/snapshot_utils.rs b/ledger/src/snapshot_utils.rs index f4705a5f09fa7a..1e2fbbd8a1dced 100644 --- a/ledger/src/snapshot_utils.rs +++ b/ledger/src/snapshot_utils.rs @@ -556,15 +556,7 @@ where &root_paths.snapshot_file_path, MAX_SNAPSHOT_DATA_FILE_SIZE, |stream| { - let mut bank: Bank = match snapshot_version { - env!("CARGO_PKG_VERSION") => deserialize_from_snapshot(stream.by_ref())?, - _ => { - return Err(get_io_error(&format!( - "unsupported snapshot version: {}", - snapshot_version - ))); - } - }; + let mut bank: Bank = deserialize_from_snapshot(stream.by_ref())?; info!("Rebuilding accounts..."); bank.set_bank_rc( bank::BankRc::new(account_paths.to_vec(), 0, bank.slot()), diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 53304c2bc98662..714a92f3c77075 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -14,7 +14,7 @@ use solana_sdk::{ rent::Rent, stake_history::{StakeHistory, StakeHistoryEntry}, }; -use solana_vote_program::vote_state::VoteState; +use solana_vote_program::vote_state::{VoteState, VoteStateVersions}; use std::collections::HashSet; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] @@ -595,7 +595,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { let stake = Stake::new( self.lamports()?.saturating_sub(meta.rent_exempt_reserve), // can't stake the rent ;) vote_account.unsigned_key(), - &vote_account.state()?, + &State::::state(vote_account)?.convert_to_current(), clock.epoch, config, ); @@ -605,7 +605,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { meta.authorized.check(signers, StakeAuthorize::Staker)?; stake.redelegate( vote_account.unsigned_key(), - &vote_account.state()?, + &State::::state(vote_account)?.convert_to_current(), clock, stake_history, config, @@ -778,7 +778,8 @@ pub fn redeem_rewards( stake_history: Option<&StakeHistory>, ) -> Result<(u64, u64), InstructionError> { if let StakeState::Stake(meta, mut stake) = stake_account.state()? { - let vote_state = vote_account.state()?; + let vote_state: VoteState = + StateMut::::state(vote_account)?.convert_to_current(); if let Some((voters_reward, stakers_reward)) = stake.redeem_rewards(point_value, &vote_state, stake_history) diff --git a/programs/vote/src/authorized_voters.rs b/programs/vote/src/authorized_voters.rs new file mode 100644 index 00000000000000..9643148024788f --- /dev/null +++ b/programs/vote/src/authorized_voters.rs @@ -0,0 +1,102 @@ +use log::*; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; +use std::collections::BTreeMap; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AuthorizedVoters { + authorized_voters: BTreeMap, +} + +impl AuthorizedVoters { + pub fn new(epoch: u64, pubkey: Pubkey) -> Self { + let mut authorized_voters = BTreeMap::new(); + authorized_voters.insert(epoch, pubkey); + Self { authorized_voters } + } + + pub fn get_authorized_voter(&self, epoch: u64) -> Option { + self.get_or_calculate_authorized_voter_for_epoch(epoch) + .map(|(pubkey, _)| pubkey) + } + + pub fn get_and_cache_authorized_voter_for_epoch(&mut self, epoch: u64) -> Option { + let res = self.get_or_calculate_authorized_voter_for_epoch(epoch); + + res.map(|(pubkey, existed)| { + if !existed { + self.authorized_voters.insert(epoch, pubkey); + } + pubkey + }) + } + + pub fn insert(&mut self, epoch: u64, authorized_voter: Pubkey) { + self.authorized_voters.insert(epoch, authorized_voter); + } + + pub fn purge_authorized_voters(&mut self, current_epoch: u64) -> bool { + // Iterate through the keys in order, filtering out the ones + // less than the current epoch + let expired_keys: Vec<_> = self + .authorized_voters + .range(0..current_epoch) + .map(|(authorized_epoch, _)| *authorized_epoch) + .collect(); + + for key in expired_keys { + self.authorized_voters.remove(&key); + } + + // Have to uphold this invariant b/c this is + // 1) The check for whether the vote state is initialized + // 2) How future authorized voters for uninitialized epochs are set + // by this function + assert!(!self.authorized_voters.is_empty()); + true + } + + pub fn is_empty(&self) -> bool { + self.authorized_voters.is_empty() + } + + pub fn first(&self) -> Option<(&u64, &Pubkey)> { + self.authorized_voters.iter().next() + } + + pub fn last(&self) -> Option<(&u64, &Pubkey)> { + self.authorized_voters.iter().next_back() + } + + pub fn len(&self) -> usize { + self.authorized_voters.len() + } + + pub fn contains(&self, epoch: u64) -> bool { + self.authorized_voters.get(&epoch).is_some() + } + + // Returns the authorized voter at the given epoch if the epoch is >= the + // current epoch, and a bool indicating whether the entry for this epoch + // exists in the self.authorized_voter map + fn get_or_calculate_authorized_voter_for_epoch(&self, epoch: u64) -> Option<(Pubkey, bool)> { + let res = self.authorized_voters.get(&epoch); + if res.is_none() { + // If no authorized voter has been set yet for this epoch, + // this must mean the authorized voter remains unchanged + // from the latest epoch before this one + let res = self.authorized_voters.range(0..epoch).next_back(); + + if res.is_none() { + warn!( + "Tried to query for the authorized voter of an epoch earlier + than the current epoch. Earlier epochs have been purged" + ); + } + + res.map(|(_, pubkey)| (*pubkey, false)) + } else { + res.map(|pubkey| (*pubkey, true)) + } + } +} diff --git a/programs/vote/src/lib.rs b/programs/vote/src/lib.rs index ec298ceb69d278..5365d3b26a8cba 100644 --- a/programs/vote/src/lib.rs +++ b/programs/vote/src/lib.rs @@ -1,3 +1,4 @@ +pub mod authorized_voters; pub mod vote_instruction; pub mod vote_state; diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index fe18ba2648a199..0b799ed36d7bf3 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -17,6 +17,7 @@ use solana_sdk::{ system_instruction, sysvar::{self, clock::Clock, slot_hashes::SlotHashes, Sysvar}, }; +use std::collections::HashSet; use thiserror::Error; /// Reasons the stake might have had an error @@ -138,8 +139,11 @@ pub fn update_node( authorized_voter_pubkey: &Pubkey, node_pubkey: &Pubkey, ) -> Instruction { - let account_metas = - vec![AccountMeta::new(*vote_pubkey, false)].with_signer(authorized_voter_pubkey); + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ] + .with_signer(authorized_voter_pubkey); Instruction::new( id(), @@ -184,7 +188,7 @@ pub fn process_instruction( trace!("process_instruction: {:?}", data); trace!("keyed_accounts: {:?}", keyed_accounts); - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let keyed_accounts = &mut keyed_accounts.iter(); let me = &mut next_keyed_account(keyed_accounts)?; @@ -205,9 +209,12 @@ pub fn process_instruction( &signers, &Clock::from_keyed_account(next_keyed_account(keyed_accounts)?)?, ), - VoteInstruction::UpdateNode(node_pubkey) => { - vote_state::update_node(me, &node_pubkey, &signers) - } + VoteInstruction::UpdateNode(node_pubkey) => vote_state::update_node( + me, + &node_pubkey, + &signers, + &Clock::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + ), VoteInstruction::Vote(vote) => { datapoint_debug!("vote-native", ("count", 1, i64)); vote_state::process_vote( diff --git a/programs/vote/src/vote_state.rs b/programs/vote/src/vote_state/mod.rs similarity index 70% rename from programs/vote/src/vote_state.rs rename to programs/vote/src/vote_state/mod.rs index 323dfc05a563c4..a6879b2254607c 100644 --- a/programs/vote/src/vote_state.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -1,6 +1,6 @@ -#![allow(clippy::implicit_hasher)] //! Vote state, vote program //! Receive and processes votes from validators +use crate::authorized_voters::AuthorizedVoters; use crate::{id, vote_instruction::VoteError}; use bincode::{deserialize, serialize_into, serialized_size, ErrorKind}; use log::*; @@ -16,8 +16,13 @@ use solana_sdk::{ slot_hashes::SlotHash, sysvar::clock::Clock, }; +use std::boxed::Box; use std::collections::{HashSet, VecDeque}; +pub mod vote_state_0_23_5; +pub mod vote_state_versions; +pub use vote_state_versions::*; + // Maximum number of votes to keep around, tightly coupled with epoch_schedule::MIN_SLOTS_PER_EPOCH pub const MAX_LOCKOUT_HISTORY: usize = 31; pub const INITIAL_LOCKOUT: usize = 2; @@ -104,9 +109,10 @@ const MAX_ITEMS: usize = 32; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct CircBuf { - pub buf: [I; MAX_ITEMS], + buf: [I; MAX_ITEMS], /// next pointer - pub idx: usize, + idx: usize, + is_empty: bool, } impl Default for CircBuf { @@ -114,6 +120,7 @@ impl Default for CircBuf { Self { buf: [I::default(); MAX_ITEMS], idx: MAX_ITEMS - 1, + is_empty: true, } } } @@ -125,6 +132,19 @@ impl CircBuf { self.idx %= MAX_ITEMS; self.buf[self.idx] = item; + self.is_empty = false; + } + + pub fn buf(&self) -> &[I; MAX_ITEMS] { + &self.buf + } + + pub fn last(&self) -> Option<&I> { + if !self.is_empty { + Some(&self.buf[self.idx]) + } else { + None + } } } @@ -133,15 +153,6 @@ pub struct VoteState { /// the node that votes in this account pub node_pubkey: Pubkey, - /// the signer for vote transactions - pub authorized_voter: Pubkey, - /// when the authorized voter was set/initialized - pub authorized_voter_epoch: Epoch, - - /// history of prior authorized voters and the epoch ranges for which - /// they were set - pub prior_voters: CircBuf<(Pubkey, Epoch, Epoch, Slot)>, - /// the signer for withdrawals pub authorized_withdrawer: Pubkey, /// percentage (0-100) that represents what part of a rewards @@ -151,6 +162,14 @@ pub struct VoteState { pub votes: VecDeque, pub root_slot: Option, + /// the signer for vote transactions + 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 + 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) epoch_credits: Vec<(Epoch, u64, u64)>, @@ -163,14 +182,25 @@ impl VoteState { pub fn new(vote_init: &VoteInit, clock: &Clock) -> Self { Self { node_pubkey: vote_init.node_pubkey, - authorized_voter: vote_init.authorized_voter, - authorized_voter_epoch: clock.epoch, + authorized_voters: AuthorizedVoters::new(clock.epoch, vote_init.authorized_voter), authorized_withdrawer: vote_init.authorized_withdrawer, commission: vote_init.commission, ..VoteState::default() } } + pub fn get_authorized_voter(&self, epoch: u64) -> Option { + self.authorized_voters.get_authorized_voter(epoch) + } + + pub fn authorized_voters(&self) -> &AuthorizedVoters { + &self.authorized_voters + } + + pub fn prior_voters(&mut self) -> &CircBuf<(Pubkey, Epoch, Epoch)> { + &self.prior_voters + } + pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { rent.minimum_balance(VoteState::size_of()) } @@ -182,7 +212,7 @@ impl VoteState { vote_state.votes = VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]); vote_state.root_slot = Some(std::u64::MAX); vote_state.epoch_credits = vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY]; - serialized_size(&vote_state).unwrap() as usize + serialized_size(&VoteStateVersions::Current(Box::new(vote_state))).unwrap() as usize } // utility function, used by Stakes, tests @@ -191,22 +221,26 @@ impl VoteState { } // utility function, used by Stakes, tests - pub fn to(&self, account: &mut Account) -> Option<()> { - Self::serialize(self, &mut account.data).ok() + pub fn to(versioned: &VoteStateVersions, account: &mut Account) -> Option<()> { + Self::serialize(versioned, &mut account.data).ok() } pub fn deserialize(input: &[u8]) -> Result { - deserialize(input).map_err(|_| InstructionError::InvalidAccountData) + deserialize::(&input) + .map(|versioned| versioned.convert_to_current()) + .map_err(|_| InstructionError::InvalidAccountData) } - pub fn serialize(&self, output: &mut [u8]) -> Result<(), InstructionError> { - serialize_into(output, self).map_err(|err| match *err { + pub fn serialize( + versioned: &VoteStateVersions, + output: &mut [u8], + ) -> Result<(), InstructionError> { + serialize_into(output, versioned).map_err(|err| match *err { ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, _ => InstructionError::GenericError, }) } - // utility function, used by Stakes, tests pub fn credits_from(account: &Account) -> Option { Self::from(account).map(|state| state.credits()) } @@ -272,6 +306,7 @@ impl VoteState { } Ok(()) } + pub fn process_vote( &mut self, vote: &Vote, @@ -386,6 +421,80 @@ impl VoteState { &self.epoch_credits } + fn set_new_authorized_voter( + &mut self, + authorized_pubkey: &Pubkey, + current_epoch: u64, + target_epoch: u64, + verify: F, + ) -> Result<(), InstructionError> + where + F: Fn(Pubkey) -> Result<(), InstructionError>, + { + let epoch_authorized_voter = self.get_and_update_authorized_voter(current_epoch).expect( + "the clock epoch is monotonically increasing, so authorized voter must be known", + ); + + verify(epoch_authorized_voter)?; + + // The offset in slots `n` on which the target_epoch + // (default value `DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET`) is + // calculated is the number of slots available from the + // first slot `S` of an epoch in which to set a new voter for + // the epoch at `S` + `n` + if self.authorized_voters.contains(target_epoch) { + return Err(VoteError::TooSoonToReauthorize.into()); + } + + // Get the latest authorized_voter + let (latest_epoch, latest_authorized_pubkey) = self.authorized_voters.last().expect( + "Earlier call to `get_and_update_authorized_voter()` guarantees + at least the voter for `epoch` exists in the map", + ); + + // If we're not setting the same pubkey as authorized pubkey again, + // then update the list of prior voters to mark the expiration + // of the old authorized pubkey + if latest_authorized_pubkey != authorized_pubkey { + // Update the epoch ranges of authorized pubkeys that will be expired + let epoch_of_last_authorized_switch = + self.prior_voters.last().map(|range| range.2).unwrap_or(0); + + // target_epoch must: + // 1) Be monotonically increasing due to the clock always + // moving forward + // 2) not be equal to latest epoch otherwise this + // function would have returned TooSoonToReauthorize error + // above + assert!(target_epoch > *latest_epoch); + + // Commit the new state + self.prior_voters.append(( + *latest_authorized_pubkey, + epoch_of_last_authorized_switch, + target_epoch, + )); + } + + self.authorized_voters + .insert(target_epoch, *authorized_pubkey); + + Ok(()) + } + + fn get_and_update_authorized_voter(&mut self, current_epoch: u64) -> Option { + let pubkey = self + .authorized_voters + .get_and_cache_authorized_voter_for_epoch(current_epoch) + .expect( + "Internal functions should + only call this will monotonically increasing current_epoch", + ); + self.authorized_voters + .purge_authorized_voters(current_epoch); + Some(pubkey) + } + fn pop_expired_votes(&mut self, slot: Slot) { loop { if self.votes.back().map_or(false, |v| v.is_expired(slot)) { @@ -427,32 +536,25 @@ impl VoteState { /// Authorize the given pubkey to withdraw or sign votes. This may be called multiple times, /// but will implicitly withdraw authorization from the previously authorized /// key -pub fn authorize( +pub fn authorize( vote_account: &KeyedAccount, authorized: &Pubkey, vote_authorize: VoteAuthorize, - signers: &HashSet, + signers: &HashSet, clock: &Clock, ) -> Result<(), InstructionError> { - let mut vote_state: VoteState = vote_account.state()?; + let mut vote_state: VoteState = + State::::state(vote_account)?.convert_to_current(); // current authorized signer must say "yay" match vote_authorize { VoteAuthorize::Voter => { - verify_authorized_signer(&vote_state.authorized_voter, signers)?; - // only one re-authorization supported per epoch - if vote_state.authorized_voter_epoch == clock.epoch { - return Err(VoteError::TooSoonToReauthorize.into()); - } - // remember prior - vote_state.prior_voters.append(( - vote_state.authorized_voter, - vote_state.authorized_voter_epoch, + vote_state.set_new_authorized_voter( + authorized, clock.epoch, - clock.slot, - )); - vote_state.authorized_voter = *authorized; - vote_state.authorized_voter_epoch = clock.epoch; + clock.leader_schedule_epoch + 1, + |epoch_authorized_voter| verify_authorized_signer(&epoch_authorized_voter, signers), + )?; } VoteAuthorize::Withdrawer => { verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?; @@ -460,28 +562,33 @@ pub fn authorize( } } - vote_account.set_state(&vote_state) + vote_account.set_state(&VoteStateVersions::Current(Box::new(vote_state))) } /// Update the node_pubkey, requires signature of the authorized voter -pub fn update_node( +pub fn update_node( vote_account: &KeyedAccount, node_pubkey: &Pubkey, - signers: &HashSet, + signers: &HashSet, + clock: &Clock, ) -> Result<(), InstructionError> { - let mut vote_state: VoteState = vote_account.state()?; + let mut vote_state: VoteState = + State::::state(vote_account)?.convert_to_current(); + let authorized_voter = vote_state + .get_and_update_authorized_voter(clock.epoch) + .expect("the clock epoch is monotonically increasing, so authorized voter must be known"); // current authorized voter must say "yay" - verify_authorized_signer(&vote_state.authorized_voter, signers)?; + verify_authorized_signer(&authorized_voter, signers)?; vote_state.node_pubkey = *node_pubkey; - vote_account.set_state(&vote_state) + vote_account.set_state(&VoteStateVersions::Current(Box::new(vote_state))) } -fn verify_authorized_signer( +fn verify_authorized_signer( authorized: &Pubkey, - signers: &HashSet, + signers: &HashSet, ) -> Result<(), InstructionError> { if signers.contains(authorized) { Ok(()) @@ -491,13 +598,14 @@ fn verify_authorized_signer( } /// Withdraw funds from the vote account -pub fn withdraw( +pub fn withdraw( vote_account: &KeyedAccount, lamports: u64, to_account: &KeyedAccount, - signers: &HashSet, + signers: &HashSet, ) -> Result<(), InstructionError> { - let vote_state: VoteState = vote_account.state()?; + let vote_state: VoteState = + State::::state(vote_account)?.convert_to_current(); verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?; @@ -517,28 +625,39 @@ pub fn initialize_account( vote_init: &VoteInit, clock: &Clock, ) -> Result<(), InstructionError> { - let vote_state: VoteState = vote_account.state()?; - - if vote_state.authorized_voter != Pubkey::default() { + if !vote_account + .try_account_ref() + .unwrap() + .data + .iter() + .all(|x| *x == 0) + { return Err(InstructionError::AccountAlreadyInitialized); } - vote_account.set_state(&VoteState::new(vote_init, clock)) + + vote_account.set_state(&VoteStateVersions::Current(Box::new(VoteState::new( + vote_init, clock, + )))) } -pub fn process_vote( +pub fn process_vote( vote_account: &KeyedAccount, slot_hashes: &[SlotHash], clock: &Clock, vote: &Vote, - signers: &HashSet, + signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state: VoteState = vote_account.state()?; + let mut vote_state: VoteState = + State::::state(vote_account)?.convert_to_current(); - if vote_state.authorized_voter == Pubkey::default() { + if vote_state.authorized_voters.is_empty() { return Err(InstructionError::UninitializedAccount); } - verify_authorized_signer(&vote_state.authorized_voter, signers)?; + let authorized_voter = vote_state + .get_and_update_authorized_voter(clock.epoch) + .expect("the clock epoch is monotonically increasinig, so authorized voter must be known"); + verify_authorized_signer(&authorized_voter, signers)?; vote_state.process_vote(vote, slot_hashes, clock.epoch)?; if let Some(timestamp) = vote.timestamp { @@ -548,7 +667,7 @@ pub fn process_vote( .ok_or_else(|| VoteError::EmptySlots) .and_then(|slot| vote_state.process_timestamp(*slot, timestamp))?; } - vote_account.set_state(&vote_state) + vote_account.set_state(&VoteStateVersions::Current(Box::new(vote_state))) } // utility function, used by Bank, tests @@ -560,7 +679,7 @@ pub fn create_account( ) -> Account { let mut vote_account = Account::new(lamports, VoteState::size_of(), &id()); - VoteState::new( + let vote_state = VoteState::new( &VoteInit { node_pubkey: *node_pubkey, authorized_voter: *vote_pubkey, @@ -568,9 +687,10 @@ pub fn create_account( commission, }, &Clock::default(), - ) - .to(&mut vote_account) - .unwrap(); + ); + + let versioned = VoteStateVersions::Current(Box::new(vote_state)); + VoteState::to(&versioned, &mut vote_account).unwrap(); vote_account } @@ -659,7 +779,7 @@ mod tests { epoch: Epoch, ) -> Result { let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); process_vote( &keyed_accounts[0], slot_hashes, @@ -670,7 +790,8 @@ mod tests { &vote.clone(), &signers, )?; - vote_account.borrow().state() + StateMut::::state(&*vote_account.borrow()) + .map(|versioned| versioned.convert_to_current()) } /// exercises all the keyed accounts stuff @@ -695,17 +816,27 @@ mod tests { vote_state .votes .resize(MAX_LOCKOUT_HISTORY, Lockout::default()); - assert!(vote_state.serialize(&mut buffer[0..4]).is_err()); - vote_state.serialize(&mut buffer).unwrap(); - assert_eq!(VoteState::deserialize(&buffer).unwrap(), vote_state); + let versioned = VoteStateVersions::Current(Box::new(vote_state)); + assert!(VoteState::serialize(&versioned, &mut buffer[0..4]).is_err()); + VoteState::serialize(&versioned, &mut buffer).unwrap(); + assert_eq!( + VoteStateVersions::Current(Box::new(VoteState::deserialize(&buffer).unwrap())), + versioned + ); } #[test] fn test_voter_registration() { let (vote_pubkey, vote_account) = create_test_account(); - let vote_state: VoteState = vote_account.borrow().state().unwrap(); - assert_eq!(vote_state.authorized_voter, vote_pubkey); + let vote_state: VoteState = StateMut::::state(&*vote_account.borrow()) + .unwrap() + .convert_to_current(); + assert_eq!(vote_state.authorized_voters.len(), 1); + assert_eq!( + *vote_state.authorized_voters.first().unwrap().1, + vote_pubkey + ); assert!(vote_state.votes.is_empty()); } @@ -762,17 +893,42 @@ mod tests { let node_pubkey = Pubkey::new_rand(); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; - let signers = get_signers(keyed_accounts); - let res = update_node(&keyed_accounts[0], &node_pubkey, &signers); + let signers: HashSet = get_signers(keyed_accounts); + let res = update_node( + &keyed_accounts[0], + &node_pubkey, + &signers, + &Clock::default(), + ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); - let vote_state: VoteState = vote_account.borrow().state().unwrap(); + let vote_state: VoteState = StateMut::::state(&*vote_account.borrow()) + .unwrap() + .convert_to_current(); assert!(vote_state.node_pubkey != node_pubkey); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); - let res = update_node(&keyed_accounts[0], &node_pubkey, &signers); + let signers: HashSet = get_signers(keyed_accounts); + let res = update_node( + &keyed_accounts[0], + &node_pubkey, + &signers, + &Clock::default(), + ); + assert_eq!(res, Ok(())); + let vote_state: VoteState = StateMut::::state(&*vote_account.borrow()) + .unwrap() + .convert_to_current(); + assert_eq!(vote_state.node_pubkey, node_pubkey); + + let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; + let signers: HashSet = get_signers(keyed_accounts); + let mut clock = Clock::default(); + clock.epoch += 10; + let res = update_node(&keyed_accounts[0], &node_pubkey, &signers, &clock); assert_eq!(res, Ok(())); - let vote_state: VoteState = vote_account.borrow().state().unwrap(); + let vote_state: VoteState = StateMut::::state(&*vote_account.borrow()) + .unwrap() + .convert_to_current(); assert_eq!(vote_state.node_pubkey, node_pubkey); } @@ -783,11 +939,15 @@ mod tests { // unsigned let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = process_vote( &keyed_accounts[0], &[(*vote.slots.last().unwrap(), vote.hash)], - &Clock::default(), + &Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }, &vote, &signers, ); @@ -795,11 +955,15 @@ mod tests { // signed let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = process_vote( &keyed_accounts[0], &[(*vote.slots.last().unwrap(), vote.hash)], - &Clock::default(), + &Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }, &vote, &signers, ); @@ -807,7 +971,7 @@ mod tests { // another voter, unsigned let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let authorized_voter_pubkey = Pubkey::new_rand(); let res = authorize( &keyed_accounts[0], @@ -816,22 +980,29 @@ mod tests { &signers, &Clock { epoch: 1, + leader_schedule_epoch: 2, ..Clock::default() }, ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = authorize( &keyed_accounts[0], &authorized_voter_pubkey, VoteAuthorize::Voter, &signers, - &Clock::default(), + &Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }, ); - assert_eq!(res, Err(VoteError::TooSoonToReauthorize.into())); + assert_eq!(res, Ok(())); + // Already set an authorized voter earlier for leader_schedule_epoch == 2 + let signers: HashSet = get_signers(keyed_accounts); let res = authorize( &keyed_accounts[0], &authorized_voter_pubkey, @@ -839,10 +1010,11 @@ mod tests { &signers, &Clock { epoch: 1, + leader_schedule_epoch: 2, ..Clock::default() }, ); - assert_eq!(res, Ok(())); + assert_eq!(res, Err(VoteError::TooSoonToReauthorize.into())); // verify authorized_voter_pubkey can authorize authorized_voter_pubkey ;) let authorized_voter_account = RefCell::new(Account::default()); @@ -850,27 +1022,37 @@ mod tests { KeyedAccount::new(&vote_pubkey, false, &vote_account), KeyedAccount::new(&authorized_voter_pubkey, true, &authorized_voter_account), ]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = authorize( &keyed_accounts[0], &authorized_voter_pubkey, VoteAuthorize::Voter, &signers, - &Clock::default(), + &Clock { + // The authorized voter was set when leader_schedule_epoch == 2, so will + // take effect when epoch == 3 + epoch: 3, + leader_schedule_epoch: 4, + ..Clock::default() + }, ); assert_eq!(res, Ok(())); // authorize another withdrawer // another voter let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let authorized_withdrawer_pubkey = Pubkey::new_rand(); let res = authorize( &keyed_accounts[0], &authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, &signers, - &Clock::default(), + &Clock { + epoch: 3, + leader_schedule_epoch: 4, + ..Clock::default() + }, ); assert_eq!(res, Ok(())); @@ -880,24 +1062,32 @@ mod tests { KeyedAccount::new(&vote_pubkey, false, &vote_account), KeyedAccount::new(&authorized_withdrawer_pubkey, true, &withdrawer_account), ]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = authorize( &keyed_accounts[0], &authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, &signers, - &Clock::default(), + &Clock { + epoch: 3, + leader_schedule_epoch: 4, + ..Clock::default() + }, ); assert_eq!(res, Ok(())); // not signed by authorized voter let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let vote = Vote::new(vec![2], Hash::default()); let res = process_vote( &keyed_accounts[0], &[(*vote.slots.last().unwrap(), vote.hash)], - &Clock::default(), + &Clock { + epoch: 3, + leader_schedule_epoch: 4, + ..Clock::default() + }, &vote, &signers, ); @@ -909,12 +1099,16 @@ mod tests { KeyedAccount::new(&vote_pubkey, false, &vote_account), KeyedAccount::new(&authorized_voter_pubkey, true, &authorized_voter_account), ]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let vote = Vote::new(vec![2], Hash::default()); let res = process_vote( &keyed_accounts[0], &[(*vote.slots.last().unwrap(), vote.hash)], - &Clock::default(), + &Clock { + epoch: 3, + leader_schedule_epoch: 4, + ..Clock::default() + }, &vote, &signers, ); @@ -938,7 +1132,10 @@ mod tests { fn test_vote_lockout() { let (_vote_pubkey, vote_account) = create_test_account(); - let mut vote_state: VoteState = vote_account.borrow().state().unwrap(); + let mut vote_state: VoteState = + StateMut::::state(&*vote_account.borrow()) + .unwrap() + .convert_to_current(); for i in 0..(MAX_LOCKOUT_HISTORY + 1) { vote_state.process_slot_vote_unchecked((INITIAL_LOCKOUT as usize * i) as u64); @@ -1248,7 +1445,7 @@ mod tests { // unsigned request let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = withdraw( &keyed_accounts[0], 0, @@ -1263,7 +1460,7 @@ mod tests { // insufficient funds let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = withdraw( &keyed_accounts[0], 101, @@ -1280,7 +1477,7 @@ mod tests { let to_account = RefCell::new(Account::default()); let lamports = vote_account.borrow().lamports; let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = withdraw( &keyed_accounts[0], lamports, @@ -1297,7 +1494,7 @@ mod tests { // authorize authorized_withdrawer let authorized_withdrawer_pubkey = Pubkey::new_rand(); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let res = authorize( &keyed_accounts[0], &authorized_withdrawer_pubkey, @@ -1313,7 +1510,7 @@ mod tests { KeyedAccount::new(&vote_pubkey, false, &vote_account), KeyedAccount::new(&authorized_withdrawer_pubkey, true, &withdrawer_account), ]; - let signers = get_signers(keyed_accounts); + let signers: HashSet = get_signers(keyed_accounts); let keyed_accounts = &mut keyed_accounts.iter(); let vote_keyed_account = next_keyed_account(keyed_accounts).unwrap(); let withdrawer_keyed_account = next_keyed_account(keyed_accounts).unwrap(); @@ -1425,4 +1622,204 @@ mod tests { vote_state.last_timestamp = BlockTimestamp::default(); assert_eq!(vote_state.process_timestamp(0, timestamp), Ok(())); } + + #[test] + fn test_get_and_update_authorized_voter() { + let original_voter = Pubkey::new_rand(); + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + // If no new authorized voter was set, the same authorized voter + // is locked into the next epoch + assert_eq!( + vote_state.get_and_update_authorized_voter(1).unwrap(), + original_voter + ); + + // Try to get the authorized voter for epoch 5, implies + // the authorized voter for epochs 1-4 were unchanged + assert_eq!( + vote_state.get_and_update_authorized_voter(5).unwrap(), + original_voter + ); + + // Authorized voter for expired epoch 0..5 should have been + // purged and no longer queryable + assert_eq!(vote_state.authorized_voters.len(), 1); + for i in 0..5 { + assert!(vote_state + .authorized_voters + .get_authorized_voter(i) + .is_none()); + } + + // Set an authorized voter change at slot 7 + let new_authorized_voter = Pubkey::new_rand(); + vote_state + .set_new_authorized_voter(&new_authorized_voter, 5, 7, |_| Ok(())) + .unwrap(); + + // Try to get the authorized voter for epoch 6, unchanged + assert_eq!( + vote_state.get_and_update_authorized_voter(6).unwrap(), + original_voter + ); + + // Try to get the authorized voter for epoch 7 and onwards, should + // be the new authorized voter + for i in 7..10 { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_authorized_voter + ); + } + assert_eq!(vote_state.authorized_voters.len(), 1); + } + + #[test] + fn test_set_new_authorized_voter() { + let original_voter = Pubkey::new_rand(); + let epoch_offset = 15; + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + assert!(vote_state.prior_voters.last().is_none()); + + let new_voter = Pubkey::new_rand(); + // Set a new authorized voter + vote_state + .set_new_authorized_voter(&new_voter, 0, 0 + epoch_offset, |_| Ok(())) + .unwrap(); + + assert_eq!(vote_state.prior_voters.idx, 0); + assert_eq!( + vote_state.prior_voters.last(), + Some(&(original_voter, 0, 0 + epoch_offset)) + ); + + // Trying to set authorized voter for same epoch again should fail + assert_eq!( + vote_state.set_new_authorized_voter(&new_voter, 0, epoch_offset, |_| Ok(())), + Err(VoteError::TooSoonToReauthorize.into()) + ); + + // Setting the same authorized voter again should succeed + vote_state + .set_new_authorized_voter(&new_voter, 2, 2 + epoch_offset, |_| Ok(())) + .unwrap(); + + // Set a third and fourth authorized voter + let new_voter2 = Pubkey::new_rand(); + vote_state + .set_new_authorized_voter(&new_voter2, 3, 3 + epoch_offset, |_| Ok(())) + .unwrap(); + assert_eq!(vote_state.prior_voters.idx, 1); + assert_eq!( + vote_state.prior_voters.last(), + Some(&(new_voter, epoch_offset, 3 + epoch_offset)) + ); + + let new_voter3 = Pubkey::new_rand(); + vote_state + .set_new_authorized_voter(&new_voter3, 6, 6 + epoch_offset, |_| Ok(())) + .unwrap(); + assert_eq!(vote_state.prior_voters.idx, 2); + assert_eq!( + vote_state.prior_voters.last(), + Some(&(new_voter2, 3 + epoch_offset, 6 + epoch_offset)) + ); + + // Check can set back to original voter + vote_state + .set_new_authorized_voter(&original_voter, 9, 9 + epoch_offset, |_| Ok(())) + .unwrap(); + + // Run with these voters for a while, check the ranges of authorized + // voters is correct + for i in 9..epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + original_voter + ); + } + for i in epoch_offset..3 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_voter + ); + } + for i in 3 + epoch_offset..6 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_voter2 + ); + } + for i in 6 + epoch_offset..9 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_voter3 + ); + } + for i in 9 + epoch_offset..=10 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + original_voter + ); + } + } + + #[test] + fn test_authorized_voter_is_locked_within_epoch() { + let original_voter = Pubkey::new_rand(); + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + // Test that it's not possible to set a new authorized + // voter within the same epoch, even if none has been + // explicitly set before + let new_voter = Pubkey::new_rand(); + assert_eq!( + vote_state.set_new_authorized_voter(&new_voter, 1, 1, |_| Ok(())), + Err(VoteError::TooSoonToReauthorize.into()) + ); + + assert_eq!(vote_state.get_authorized_voter(1), Some(original_voter)); + + // Set a new authorized voter for a future epoch + assert_eq!( + vote_state.set_new_authorized_voter(&new_voter, 1, 2, |_| Ok(())), + Ok(()) + ); + + // Test that it's not possible to set a new authorized + // voter within the same epoch, even if none has been + // explicitly set before + assert_eq!( + vote_state.set_new_authorized_voter(&original_voter, 3, 3, |_| Ok(())), + Err(VoteError::TooSoonToReauthorize.into()) + ); + + assert_eq!(vote_state.get_authorized_voter(3), Some(new_voter)); + } } diff --git a/programs/vote/src/vote_state/vote_state_0_23_5.rs b/programs/vote/src/vote_state/vote_state_0_23_5.rs new file mode 100644 index 00000000000000..89b99dc2f498db --- /dev/null +++ b/programs/vote/src/vote_state/vote_state_0_23_5.rs @@ -0,0 +1,60 @@ +use super::*; + +const MAX_ITEMS: usize = 32; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct VoteState0_23_5 { + /// the node that votes in this account + pub node_pubkey: Pubkey, + + /// the signer for vote transactions + pub authorized_voter: Pubkey, + /// when the authorized voter was set/initialized + pub authorized_voter_epoch: Epoch, + + /// history of prior authorized voters and the epoch ranges for which + /// they were set + pub prior_voters: CircBuf<(Pubkey, Epoch, Epoch, Slot)>, + + /// 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, + pub root_slot: Option, + + /// 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, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct CircBuf { + pub buf: [I; MAX_ITEMS], + /// next pointer + pub idx: usize, +} + +impl Default for CircBuf { + fn default() -> Self { + Self { + buf: [I::default(); MAX_ITEMS], + idx: MAX_ITEMS - 1, + } + } +} + +impl CircBuf { + pub fn append(&mut self, item: I) { + // remember prior delegate and when we switched, to support later slashing + self.idx += 1; + self.idx %= MAX_ITEMS; + + self.buf[self.idx] = item; + } +} diff --git a/programs/vote/src/vote_state/vote_state_versions.rs b/programs/vote/src/vote_state/vote_state_versions.rs new file mode 100644 index 00000000000000..57ea9ebfbf409c --- /dev/null +++ b/programs/vote/src/vote_state/vote_state_versions.rs @@ -0,0 +1,67 @@ +use super::*; +use crate::vote_state::vote_state_0_23_5::VoteState0_23_5; +use solana_sdk::account_utils::StateMut; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub enum VoteStateVersions { + V0_23_5(Box), + Current(Box), +} + +impl VoteStateVersions { + pub fn convert_to_current(self) -> VoteState { + match self { + VoteStateVersions::V0_23_5(state) => { + let authorized_voters = + AuthorizedVoters::new(state.authorized_voter_epoch, state.authorized_voter); + + VoteState { + node_pubkey: state.node_pubkey, + + /// the signer for withdrawals + authorized_withdrawer: state.authorized_withdrawer, + + /// percentage (0-100) that represents what part of a rewards + /// payout should be given to this VoteAccount + commission: state.commission, + + votes: state.votes.clone(), + + root_slot: state.root_slot, + + /// the signer for vote transactions + authorized_voters, + + /// 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 + prior_voters: CircBuf::default(), + + /// history of how many credits earned by the end of each epoch + /// each tuple is (Epoch, credits, prev_credits) + epoch_credits: state.epoch_credits.clone(), + + /// most recent timestamp submitted with a vote + last_timestamp: state.last_timestamp.clone(), + } + } + VoteStateVersions::Current(state) => *state, + } + } + + pub fn convert_from_raw(account: &mut Account, pubkey: &Pubkey) { + let vote_state: VoteState0_23_5 = account.state().unwrap_or_else(|e| { + panic!( + "Couldn't deserialize vote account {}, error: {:?}", + pubkey, e + ) + }); + + let current_vote_state = + VoteStateVersions::V0_23_5(Box::new(vote_state)).convert_to_current(); + VoteState::to( + &VoteStateVersions::Current(Box::new(current_vote_state)), + account, + ); + } +} diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 6430bfe2d191ea..b34f9fdd6a29e0 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -318,14 +318,21 @@ impl Accounts { ancestors: &HashMap, pubkey: &Pubkey, ) -> Option<(Account, Slot)> { - let (account, slot) = self - .accounts_db - .load_slow(ancestors, pubkey) - .unwrap_or((Account::default(), self.slot)); + let res = self.accounts_db.load_slow(ancestors, pubkey); + + if res.is_none() { + panic!("Could not find pubkey"); + } + + let (account, slot) = res.unwrap_or((Account::default(), self.slot)); if account.lamports > 0 { Some((account, slot)) } else { + println!( + "0 lamports account owner: {}, {}", + account.owner, account.lamports + ); None } } diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index 4d082c920ed6ad..d8bb76f928aa3e 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -1089,6 +1089,48 @@ impl AccountsDB { } } + pub fn recompute_bank_hash(&self, slot: Slot) -> Result { + use BankHashVerificatonError::*; + + let ancestors = vec![(slot, 1)].into_iter().collect(); + let (hashes, mismatch_found) = self.scan_accounts( + &ancestors, + |(collector, mismatch_found): &mut (Vec, bool), + option: Option<(&Pubkey, Account, Slot)>| { + if let Some((pubkey, account, slot)) = option { + let hash = Self::hash_account(slot, &account, pubkey); + if hash != account.hash { + *mismatch_found = true; + } + if *mismatch_found { + return; + } + let hash = BankHash::from_hash(&hash); + debug!("xoring..{} key: {}", hash, pubkey); + collector.push(hash); + } + }, + ); + if mismatch_found { + return Err(MismatchedAccountHash); + } + let mut calculated_hash = BankHash::default(); + for hash in hashes { + calculated_hash.xor(hash); + } + let mut bank_hashes = self.bank_hashes.write().unwrap(); + if let Some(found_hash_info) = bank_hashes.get_mut(&slot) { + println!( + "old bank hash for slot {}: {}, new hash {}", + slot, found_hash_info.hash, calculated_hash + ); + found_hash_info.hash = calculated_hash; + Ok(calculated_hash) + } else { + Err(MissingBankHash) + } + } + pub fn xor_in_hash_state(&self, slot_id: Slot, hash: BankHash, stats: &BankHashStats) { let mut bank_hashes = self.bank_hashes.write().unwrap(); let bank_hash = bank_hashes diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 35bb5655de90f5..6b55447a53f3b8 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -76,7 +76,7 @@ type TransactionLoaderRefCells = Vec)>>; #[derive(Default)] pub struct BankRc { /// where all the Accounts are stored - accounts: Arc, + pub accounts: Arc, /// Previous checkpoint of this bank parent: RwLock>>, @@ -317,14 +317,14 @@ pub struct Bank { inflation: Arc>, /// cache of vote_account and stake_account state for this fork - stakes: RwLock, + pub stakes: RwLock, /// cache of validator and archiver storage accounts for this fork storage_accounts: RwLock, /// staked nodes on epoch boundaries, saved off when a bank.slot() is at /// a leader schedule calculation boundary - epoch_stakes: HashMap, + pub epoch_stakes: HashMap, /// A boolean reflecting whether any entries were recorded into the PoH /// stream for the slot == self.slot @@ -766,6 +766,11 @@ impl Bank { } } + pub fn recompute_hash(&self) { + let mut hash = self.hash.write().unwrap(); + *hash = self.hash_internal_state(); + } + pub fn epoch_schedule(&self) -> &EpochSchedule { &self.epoch_schedule } @@ -2161,6 +2166,7 @@ mod tests { stake_instruction, stake_state::{self, Authorized, Delegation, Lockup, Stake}, }; + use solana_vote_program::vote_state::VoteStateVersions; use solana_vote_program::{ vote_instruction, vote_state::{self, Vote, VoteInit, VoteState, MAX_LOCKOUT_HISTORY}, @@ -3058,11 +3064,20 @@ mod tests { bank.store_account(&archiver_id, &archiver_account); // generate some rewards - let mut vote_state = VoteState::from(&vote_account).unwrap(); + let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); for i in 0..MAX_LOCKOUT_HISTORY + 42 { - vote_state.process_slot_vote_unchecked(i as u64); - vote_state.to(&mut vote_account).unwrap(); + vote_state + .as_mut() + .map(|v| v.process_slot_vote_unchecked(i as u64)); + let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + VoteState::to(&versioned, &mut vote_account).unwrap(); bank.store_account(&vote_id, &vote_account); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; } bank.store_account(&vote_id, &vote_account); diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index 4b31123d5d4899..785c14512c8b57 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; #[derive(Default, Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct Stakes { /// vote accounts - vote_accounts: HashMap, + pub vote_accounts: HashMap, /// stake_delegations stake_delegations: HashMap, @@ -195,7 +195,9 @@ pub mod tests { use super::*; use solana_sdk::{pubkey::Pubkey, rent::Rent}; use solana_stake_program::stake_state; - use solana_vote_program::vote_state::{self, VoteState, MAX_LOCKOUT_HISTORY}; + use solana_vote_program::vote_state::{ + self, VoteState, VoteStateVersions, MAX_LOCKOUT_HISTORY, + }; // set up some dummies for a staked node (( vote ) ( stake )) pub fn create_staked_node_accounts(stake: u64) -> ((Pubkey, Account), (Pubkey, Account)) { @@ -319,31 +321,55 @@ pub mod tests { assert_eq!(stakes.points(), 0); assert_eq!(stakes.claim_points(), 0); - let mut vote_state = VoteState::from(&vote_account).unwrap(); + let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); for i in 0..MAX_LOCKOUT_HISTORY + 42 { - vote_state.process_slot_vote_unchecked(i as u64); - vote_state.to(&mut vote_account).unwrap(); + vote_state + .as_mut() + .map(|v| v.process_slot_vote_unchecked(i as u64)); + let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + VoteState::to(&versioned, &mut vote_account).unwrap(); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; stakes.store(&vote_pubkey, &vote_account); - assert_eq!(stakes.points(), vote_state.credits() * stake); + assert_eq!( + stakes.points(), + vote_state.as_ref().unwrap().credits() * stake + ); } vote_account.lamports = 0; stakes.store(&vote_pubkey, &vote_account); - assert_eq!(stakes.points(), vote_state.credits() * stake); + assert_eq!( + stakes.points(), + vote_state.as_ref().unwrap().credits() * stake + ); - assert_eq!(stakes.claim_points(), vote_state.credits() * stake); + assert_eq!( + stakes.claim_points(), + vote_state.as_ref().unwrap().credits() * stake + ); assert_eq!(stakes.claim_points(), 0); assert_eq!(stakes.claim_points(), 0); // points come out of nowhere, but don't care here ;) vote_account.lamports = 1; stakes.store(&vote_pubkey, &vote_account); - assert_eq!(stakes.points(), vote_state.credits() * stake); + assert_eq!( + stakes.points(), + vote_state.as_ref().unwrap().credits() * stake + ); // test going backwards, should never go backwards let old_vote_state = vote_state; let vote_account = vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 1); stakes.store(&vote_pubkey, &vote_account); - assert_eq!(stakes.points(), old_vote_state.credits() * stake); + assert_eq!( + stakes.points(), + old_vote_state.as_ref().unwrap().credits() * stake + ); } #[test] diff --git a/runtime/tests/stake.rs b/runtime/tests/stake.rs index f8ddd5def93766..6388c08c7cdc03 100644 --- a/runtime/tests/stake.rs +++ b/runtime/tests/stake.rs @@ -18,7 +18,7 @@ use solana_stake_program::{ }; use solana_vote_program::{ vote_instruction, - vote_state::{Vote, VoteInit, VoteState}, + vote_state::{Vote, VoteInit, VoteState, VoteStateVersions}, }; use std::sync::Arc; @@ -254,7 +254,9 @@ fn test_stake_account_lifetime() { // Test that votes and credits are there let account = bank.get_account(&vote_pubkey).expect("account not found"); - let vote_state: VoteState = account.state().expect("couldn't unpack account data"); + let vote_state: VoteState = StateMut::::state(&account) + .expect("couldn't unpack account data") + .convert_to_current(); // 1 less vote, as the first vote should have cleared the lockout assert_eq!(vote_state.votes.len(), 31); diff --git a/validator/src/main.rs b/validator/src/main.rs index c1ebaf2aca8444..f7751c6ea42d4b 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -350,10 +350,32 @@ fn check_vote_account( let found_vote_account = solana_vote_program::vote_state::VoteState::from(&found_vote_account); if let Some(found_vote_account) = found_vote_account { - if found_vote_account.authorized_voter != *voting_pubkey { + if found_vote_account.authorized_voters().is_empty() { + return Err("Vote account not yet initialized".to_string()); + } + + let epoch_info = rpc_client + .get_epoch_info() + .map_err(|err| format!("Failed to get epoch info: {}", err.to_string()))?; + + let mut authorized_voter; + authorized_voter = found_vote_account.get_authorized_voter(epoch_info.epoch); + if authorized_voter.is_none() { + // Must have gotten a clock on the boundary + authorized_voter = found_vote_account.get_authorized_voter(epoch_info.epoch + 1); + } + + let authorized_voter = authorized_voter.expect( + "Could not get the authorized voter, which only happens if the + client gets an epoch earlier than the current epoch, + but the received epoch should not be off by more than + one epoch", + ); + + if authorized_voter != *voting_pubkey { return Err(format!( "account's authorized voter ({}) does not match to the given voting keypair ({}).", - found_vote_account.authorized_voter, voting_pubkey + authorized_voter, voting_pubkey )); } if found_vote_account.node_pubkey != *node_pubkey {