diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index 87acb53098..42c9ab8c37 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -74,6 +74,7 @@ pub struct ErrorCounters { pub invalid_account_for_fee: usize, pub invalid_account_index: usize, pub invalid_program_for_execution: usize, + pub not_allowed_during_cluster_maintenance: usize, } #[derive(Default, Debug, PartialEq, Clone)] diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 3b834d8f2b..e0977fa25d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -45,6 +45,7 @@ use solana_sdk::{ incinerator, inflation::Inflation, native_loader, nonce, + program_utils::limited_deserialize, pubkey::Pubkey, signature::{Keypair, Signature}, slot_hashes::SlotHashes, @@ -55,7 +56,7 @@ use solana_sdk::{ transaction::{Result, Transaction, TransactionError}, }; use solana_stake_program::stake_state::{self, Delegation}; -use solana_vote_program::vote_state::VoteState; +use solana_vote_program::{vote_instruction::VoteInstruction, vote_state::VoteState}; use std::{ cell::RefCell, collections::{HashMap, HashSet}, @@ -73,7 +74,7 @@ pub const SECONDS_PER_YEAR: f64 = 365.25 * 24.0 * 60.0 * 60.0; pub const MAX_LEADER_SCHEDULE_STAKES: Epoch = 5; type BankStatusCache = StatusCache>; -#[frozen_abi(digest = "9chBcbXVJ4fK7uGgydQzam5aHipaAKFw6V4LDFpjbE4w")] +#[frozen_abi(digest = "BHtoJzwGJ1seQ2gZmtPSLLgdvq3gRZMj5mpUJsX4wGHT")] pub type BankSlotDelta = SlotDelta>; type TransactionAccountRefCells = Vec>>; type TransactionLoaderRefCells = Vec)>>; @@ -1404,6 +1405,43 @@ impl Bank { }) .collect() } + fn filter_by_vote_transactions( + &self, + txs: &[Transaction], + iteration_order: Option<&[usize]>, + lock_results: Vec, + error_counters: &mut ErrorCounters, + ) -> Vec { + OrderedIterator::new(txs, iteration_order) + .zip(lock_results.into_iter()) + .map(|(tx, lock_res)| { + if lock_res.0.is_ok() { + if tx.message.instructions.len() == 1 { + let instruction = &tx.message.instructions[0]; + let program_pubkey = + tx.message.account_keys[instruction.program_id_index as usize]; + if program_pubkey == solana_vote_program::id() { + if let Ok(vote_instruction) = + limited_deserialize::(&instruction.data) + { + match vote_instruction { + VoteInstruction::Vote(_) + | VoteInstruction::VoteSwitch(_, _) => { + return lock_res; + } + _ => {} + } + } + } + } + + error_counters.not_allowed_during_cluster_maintenance += 1; + return (Err(TransactionError::ClusterMaintenance), lock_res.1); + } + lock_res + }) + .collect() + } pub fn check_hash_age(&self, hash: &Hash, max_age: usize) -> Option { self.blockhash_queue @@ -1424,6 +1462,18 @@ impl Bank { }) } + // Determine if the bank is currently in an upgrade epoch, where only votes are permitted + fn upgrade_epoch(&self) -> bool { + match self.operating_mode() { + #[cfg(test)] + OperatingMode::Development => self.epoch == 0xdead, // Value assumed by `test_upgrade_epoch()` + #[cfg(not(test))] + OperatingMode::Development => false, + OperatingMode::Preview => false, + OperatingMode::Stable => self.epoch == Epoch::max_value(), + } + } + pub fn check_transactions( &self, txs: &[Transaction], @@ -1439,7 +1489,19 @@ impl Bank { max_age, &mut error_counters, ); - self.check_signatures(txs, iteration_order, age_results, &mut error_counters) + let sigcheck_results = + self.check_signatures(txs, iteration_order, age_results, &mut error_counters); + if self.upgrade_epoch() { + // Reject all non-vote transactions + self.filter_by_vote_transactions( + txs, + iteration_order, + sigcheck_results, + &mut error_counters, + ) + } else { + sigcheck_results + } } pub fn collect_balances(&self, batch: &[Transaction]) -> TransactionBalances { @@ -1522,6 +1584,12 @@ impl Bank { error_counters.duplicate_signature ); } + if 0 != error_counters.not_allowed_during_cluster_maintenance { + inc_new_counter_error!( + "bank-process_transactions-error-cluster-maintenance", + error_counters.not_allowed_during_cluster_maintenance + ); + } } /// Converts Accounts into RefCell, this involves moving @@ -7728,4 +7796,96 @@ mod tests { consumed_budgets.sort(); assert_eq!(consumed_budgets, vec![0, 1, 8]); } + + #[test] + fn test_upgrade_epoch() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(500, &Pubkey::new_rand(), 0); + genesis_config.fee_rate_governor = FeeRateGovernor::new(1, 0); + let bank = Arc::new(Bank::new(&genesis_config)); + + // Jump to the test-only upgrade epoch -- see `Bank::upgrade_epoch()` + let bank = Bank::new_from_parent( + &bank, + &Pubkey::default(), + genesis_config + .epoch_schedule + .get_first_slot_in_epoch(0xdead), + ); + + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 500); + + // Normal transfers are not allowed + assert_eq!( + bank.transfer(2, &mint_keypair, &mint_keypair.pubkey()), + Err(TransactionError::ClusterMaintenance) + ); + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 500); // no transaction fee charged + + let vote_pubkey = Pubkey::new_rand(); + let authorized_voter = Keypair::new(); + + // VoteInstruction::Vote is allowed. The transaction fails with a vote program instruction + // error because the vote account is not actually setup + let tx = Transaction::new_signed_with_payer( + &[vote_instruction::vote( + &vote_pubkey, + &authorized_voter.pubkey(), + Vote::new(vec![1], Hash::default()), + )], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &authorized_voter], + bank.last_blockhash(), + ); + assert_eq!( + bank.process_transaction(&tx), + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidAccountData + )) + ); + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 498); // transaction fee charged + + // VoteInstruction::VoteSwitch is allowed. The transaction fails with a vote program + // instruction error because the vote account is not actually setup + let tx = Transaction::new_signed_with_payer( + &[vote_instruction::vote_switch( + &vote_pubkey, + &authorized_voter.pubkey(), + Vote::new(vec![1], Hash::default()), + Hash::default(), + )], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &authorized_voter], + bank.last_blockhash(), + ); + assert_eq!( + bank.process_transaction(&tx), + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidAccountData + )) + ); + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 496); // transaction fee charged + + // Other vote program instructions, like VoteInstruction::UpdateCommission are not allowed + let tx = Transaction::new_signed_with_payer( + &[vote_instruction::update_commission( + &vote_pubkey, + &authorized_voter.pubkey(), + 123, + )], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &authorized_voter], + bank.last_blockhash(), + ); + assert_eq!( + bank.process_transaction(&tx), + Err(TransactionError::ClusterMaintenance) + ); + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 496); // no transaction fee charged + } } diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index 6105c3d7f5..eb7e5829e7 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -83,6 +83,9 @@ pub enum TransactionError { /// not be unlocked. #[error("Transaction failed to sanitize accounts offsets correctly")] SanitizeFailure, + + #[error("Transactions are currently disabled due to cluster maintenance")] + ClusterMaintenance, } pub type Result = result::Result;