From 23c5d703958b59276ec55a9d8d3cb9df1dab3cd5 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 25 Sep 2025 14:49:22 +0000 Subject: [PATCH 1/3] vote-program: refactor tests to use `vote_state_v4_enabled` switch --- Cargo.lock | 1 + programs/vote/Cargo.toml | 1 + programs/vote/src/vote_processor.rs | 407 ++++++++++++++++++++++------ programs/vote/src/vote_state/mod.rs | 9 +- 4 files changed, 323 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8c88a9a7df4e4..d9333ab4b9365b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11868,6 +11868,7 @@ dependencies = [ "solana-sha256-hasher", "solana-signer", "solana-slot-hashes", + "solana-svm-feature-set", "solana-transaction", "solana-transaction-context", "solana-vote-interface", diff --git a/programs/vote/Cargo.toml b/programs/vote/Cargo.toml index 099cf9ab119f8c..36e9d1c074c32f 100644 --- a/programs/vote/Cargo.toml +++ b/programs/vote/Cargo.toml @@ -73,6 +73,7 @@ solana-pubkey = { workspace = true, features = ["rand"] } solana-rent = { workspace = true } solana-sdk-ids = { workspace = true } solana-sha256-hasher = { workspace = true } +solana-svm-feature-set = { workspace = true } test-case = { workspace = true } [[bench]] diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index f7fe50a7d7fadd..40599fd4262e01 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -1,7 +1,7 @@ //! Vote program processor use { - crate::vote_state, + crate::vote_state::{self, handler::VoteStateTargetVersion}, log::*, solana_bincode::limited_deserialize, solana_instruction::error::InstructionError, @@ -19,7 +19,7 @@ fn process_authorize_with_seed_instruction( invoke_context: &InvokeContext, instruction_context: &InstructionContext, vote_account: &mut BorrowedInstructionAccount, - target_version: vote_state::VoteStateTargetVersion, + target_version: VoteStateTargetVersion, new_authority: &Pubkey, authorization_type: VoteAuthorize, current_authority_derived_key_owner: &Pubkey, @@ -258,8 +258,10 @@ mod tests { vote_switch, withdraw, CreateVoteAccountConfig, VoteInstruction, }, vote_state::{ - self, Lockout, TowerSync, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, - VoteAuthorizeWithSeedArgs, VoteInit, VoteStateUpdate, VoteStateV3, + self, + handler::{self, VoteStateHandle, VoteStateHandler}, + Lockout, TowerSync, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, + VoteAuthorizeWithSeedArgs, VoteInit, VoteStateUpdate, VoteStateV3, VoteStateV4, VoteStateVersions, }, }, @@ -271,15 +273,40 @@ mod tests { solana_epoch_schedule::EpochSchedule, solana_hash::Hash, solana_instruction::{AccountMeta, Instruction}, - solana_program_runtime::invoke_context::mock_process_instruction, + solana_program_runtime::invoke_context::mock_process_instruction_with_feature_set, solana_pubkey::Pubkey, solana_rent::Rent, solana_sdk_ids::sysvar, solana_slot_hashes::SlotHashes, + solana_svm_feature_set::SVMFeatureSet, solana_vote_interface::instruction::{tower_sync, tower_sync_switch}, std::{collections::HashSet, str::FromStr}, }; + // TODO: Remove when v4 feature gate is added. + const TEMP_VOTE_STATE_V4_ENABLED: bool = false; + + // They're the same, but just for posterity. + fn vote_state_size_of(vote_state_v4_enabled: bool) -> usize { + if vote_state_v4_enabled { + VoteStateV4::size_of() + } else { + VoteStateV3::size_of() + } + } + + fn deserialize_vote_state_for_test( + vote_state_v4_enabled: bool, + account_data: &[u8], + vote_pubkey: &Pubkey, + ) -> VoteStateHandler { + if vote_state_v4_enabled { + VoteStateHandler::new_v4(VoteStateV4::deserialize(account_data, vote_pubkey).unwrap()) + } else { + VoteStateHandler::new_v3(VoteStateV3::deserialize(account_data).unwrap()) + } + } + struct VoteAccountTestFixtureWithAuthorities { vote_account: AccountSharedData, vote_pubkey: Pubkey, @@ -296,12 +323,13 @@ mod tests { } fn process_instruction( + _vote_state_v4_enabled: bool, instruction_data: &[u8], transaction_accounts: Vec<(Pubkey, AccountSharedData)>, instruction_accounts: Vec, expected_result: Result<(), InstructionError>, ) -> Vec { - mock_process_instruction( + mock_process_instruction_with_feature_set( &id(), None, instruction_data, @@ -311,10 +339,12 @@ mod tests { Entrypoint::vm, |_invoke_context| {}, |_invoke_context| {}, + &SVMFeatureSet::all_enabled(), // <-- TODO: Add v4 feature flag ) } fn process_instruction_as_one_arg( + vote_state_v4_enabled: bool, instruction: &Instruction, expected_result: Result<(), InstructionError>, ) -> Vec { @@ -357,6 +387,7 @@ mod tests { }) .collect(); process_instruction( + vote_state_v4_enabled, &instruction.data, transaction_accounts, instruction.accounts.clone(), @@ -376,22 +407,46 @@ mod tests { account::create_account_shared_data_for_test(&Clock::default()) } - fn create_test_account() -> (Pubkey, AccountSharedData) { + fn create_test_account(vote_state_v4_enabled: bool) -> (Pubkey, AccountSharedData) { let rent = Rent::default(); - let balance = rent.minimum_balance(VoteStateV3::size_of()); let vote_pubkey = solana_pubkey::new_rand(); - ( - vote_pubkey, - vote_state::create_account(&vote_pubkey, &solana_pubkey::new_rand(), 0, balance), - ) + let node_pubkey = solana_pubkey::new_rand(); + + let account = if vote_state_v4_enabled { + let balance = rent.minimum_balance(VoteStateV4::size_of()); + vote_state::create_v4_account_with_authorized( + &node_pubkey, + &vote_pubkey, + &vote_pubkey, + None, + 0, + balance, + ) + } else { + let balance = rent.minimum_balance(vote_state_size_of(vote_state_v4_enabled)); + vote_state::create_account_with_authorized( + &node_pubkey, + &vote_pubkey, + &vote_pubkey, + 0, + balance, + ) + }; + + (vote_pubkey, account) } - fn create_test_account_with_authorized() -> (Pubkey, Pubkey, Pubkey, AccountSharedData) { + fn create_test_account_with_authorized( + vote_state_v4_enabled: bool, + ) -> (Pubkey, Pubkey, Pubkey, AccountSharedData) { let vote_pubkey = solana_pubkey::new_rand(); let authorized_voter = solana_pubkey::new_rand(); let authorized_withdrawer = solana_pubkey::new_rand(); - let account = - create_test_account_with_provided_authorized(&authorized_voter, &authorized_withdrawer); + let account = create_test_account_with_provided_authorized( + &authorized_voter, + &authorized_withdrawer, + vote_state_v4_enabled, + ); ( vote_pubkey, @@ -404,17 +459,33 @@ mod tests { fn create_test_account_with_provided_authorized( authorized_voter: &Pubkey, authorized_withdrawer: &Pubkey, + vote_state_v4_enabled: bool, ) -> AccountSharedData { - vote_state::create_account_with_authorized( - &solana_pubkey::new_rand(), - authorized_voter, - authorized_withdrawer, - 0, - 100, - ) + let node_pubkey = solana_pubkey::new_rand(); + + if vote_state_v4_enabled { + vote_state::create_v4_account_with_authorized( + &node_pubkey, + authorized_voter, + authorized_withdrawer, + None, + 0, + 100, + ) + } else { + vote_state::create_account_with_authorized( + &node_pubkey, + authorized_voter, + authorized_withdrawer, + 0, + 100, + ) + } } - fn create_test_account_with_authorized_from_seed() -> VoteAccountTestFixtureWithAuthorities { + fn create_test_account_with_authorized_from_seed( + vote_state_v4_enabled: bool, + ) -> VoteAccountTestFixtureWithAuthorities { let vote_pubkey = Pubkey::new_unique(); let voter_base_key = Pubkey::new_unique(); let voter_owner = Pubkey::new_unique(); @@ -431,14 +502,28 @@ mod tests { ) .unwrap(); - VoteAccountTestFixtureWithAuthorities { - vote_account: vote_state::create_account_with_authorized( - &Pubkey::new_unique(), + let node_pubkey = Pubkey::new_unique(); + let vote_account = if vote_state_v4_enabled { + vote_state::create_v4_account_with_authorized( + &node_pubkey, &authorized_voter, &authorized_withdrawer, + None, 0, 100, - ), + ) + } else { + vote_state::create_account_with_authorized( + &node_pubkey, + &authorized_voter, + &authorized_withdrawer, + 0, + 100, + ) + }; + + VoteAccountTestFixtureWithAuthorities { + vote_account, vote_pubkey, voter_base_key, voter_owner, @@ -450,23 +535,39 @@ mod tests { } fn create_test_account_with_epoch_credits( + vote_state_v4_enabled: bool, credits_to_append: &[u64], ) -> (Pubkey, AccountSharedData) { - let (vote_pubkey, vote_account) = create_test_account(); - let vote_account_space = vote_account.data().len(); + let vote_pubkey = solana_pubkey::new_rand(); + let node_pubkey = solana_pubkey::new_rand(); - // TODO: Update this test to use `VoteStateV4` after vote program - // migration is complete. Currently using `VoteStateV3` because this - // test invokes the vote program which hasn't been migrated to v4 yet. - let mut vote_state = VoteStateV3::deserialize(vote_account.data()).unwrap(); - vote_state.authorized_withdrawer = vote_pubkey; - vote_state.epoch_credits = Vec::new(); + let vote_init = VoteInit { + node_pubkey, + authorized_voter: vote_pubkey, + authorized_withdrawer: vote_pubkey, + commission: 0, + }; + let clock = Clock::default(); + + let space = vote_state_size_of(vote_state_v4_enabled); + let lamports = Rent::default().minimum_balance(space); + + let mut vote_state = if vote_state_v4_enabled { + let v4 = handler::create_new_vote_state_v4(&vote_pubkey, &vote_init, &clock); + VoteStateHandler::new_v4(v4) + } else { + let v3 = VoteStateV3::new(&vote_init, &clock); + VoteStateHandler::new_v3(v3) + }; + + let epoch_credits = vote_state.epoch_credits_mut(); + epoch_credits.clear(); let mut current_epoch_credits: u64 = 0; let mut previous_epoch_credits = 0; for (epoch, credits) in credits_to_append.iter().enumerate() { current_epoch_credits = current_epoch_credits.saturating_add(*credits); - vote_state.epoch_credits.push(( + epoch_credits.push(( u64::try_from(epoch).unwrap(), current_epoch_credits, previous_epoch_credits, @@ -474,15 +575,10 @@ mod tests { previous_epoch_credits = current_epoch_credits; } - let lamports = vote_account.lamports(); - let mut vote_account_with_epoch_credits = - AccountSharedData::new(lamports, vote_account_space, &id()); - let versioned = VoteStateVersions::new_v3(vote_state); - vote_account_with_epoch_credits - .set_state(&versioned) - .unwrap(); - - (vote_pubkey, vote_account_with_epoch_credits) + let mut account = AccountSharedData::new(lamports, space, &id()); + account.set_data_from_slice(&vote_state.serialize()); + + (vote_pubkey, account) } /// Returns Vec of serialized VoteInstruction and flag indicating if it is a tower sync @@ -515,6 +611,7 @@ mod tests { #[test] fn test_vote_process_instruction_decode_bail() { process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &[], Vec::new(), Vec::new(), @@ -525,7 +622,8 @@ mod tests { #[test] fn test_initialize_vote_account() { let vote_pubkey = solana_pubkey::new_rand(); - let vote_account = AccountSharedData::new(100, VoteStateV3::size_of(), &id()); + let vote_account = + AccountSharedData::new(100, vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED), &id()); let node_pubkey = solana_pubkey::new_rand(); let node_account = AccountSharedData::default(); let instruction_data = serialize(&VoteInstruction::InitializeAccount(VoteInit { @@ -560,6 +658,7 @@ mod tests { // init should pass let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, vec![ (vote_pubkey, vote_account.clone()), @@ -573,6 +672,7 @@ mod tests { // reinit should fail process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, vec![ (vote_pubkey, accounts[0].clone()), @@ -586,11 +686,16 @@ mod tests { // init should fail, account is too big process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, vec![ ( vote_pubkey, - AccountSharedData::new(100, 2 * VoteStateV3::size_of(), &id()), + AccountSharedData::new( + 100, + 2 * vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED), + &id(), + ), ), (sysvar::rent::id(), create_default_rent_account()), (sysvar::clock::id(), create_default_clock_account()), @@ -603,6 +708,7 @@ mod tests { // init should fail, node_pubkey didn't sign the transaction instruction_accounts[3].is_signer = false; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, vec![ (vote_pubkey, vote_account), @@ -618,7 +724,7 @@ mod tests { #[test] fn test_vote_update_validator_identity() { let (vote_pubkey, _authorized_voter, authorized_withdrawer, vote_account) = - create_test_account_with_authorized(); + create_test_account_with_authorized(TEMP_VOTE_STATE_V4_ENABLED); let node_pubkey = solana_pubkey::new_rand(); let instruction_data = serialize(&VoteInstruction::UpdateValidatorIdentity).unwrap(); let transaction_accounts = vec![ @@ -647,36 +753,51 @@ mod tests { // should fail, node_pubkey didn't sign the transaction instruction_accounts[1].is_signer = false; let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::MissingRequiredSignature), ); instruction_accounts[1].is_signer = true; - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_ne!(vote_state.node_pubkey, node_pubkey); + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, + ); + assert_ne!(*vote_state.node_pubkey(), node_pubkey); // should fail, authorized_withdrawer didn't sign the transaction instruction_accounts[2].is_signer = false; let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::MissingRequiredSignature), ); instruction_accounts[2].is_signer = true; - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_ne!(vote_state.node_pubkey, node_pubkey); + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, + ); + assert_ne!(*vote_state.node_pubkey(), node_pubkey); // should pass let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts, instruction_accounts, Ok(()), ); - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_eq!(vote_state.node_pubkey, node_pubkey); + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, + ); + assert_eq!(*vote_state.node_pubkey(), node_pubkey); // TODO: When the feature gate for vote state v4 is added, check the // block revenue collector here for the v4 target. } @@ -684,7 +805,7 @@ mod tests { #[test] fn test_vote_update_commission() { let (vote_pubkey, _authorized_voter, authorized_withdrawer, vote_account) = - create_test_account_with_authorized(); + create_test_account_with_authorized(TEMP_VOTE_STATE_V4_ENABLED); let instruction_data = serialize(&VoteInstruction::UpdateCommission(42)).unwrap(); let transaction_accounts = vec![ (vote_pubkey, vote_account), @@ -714,39 +835,54 @@ mod tests { // should pass let accounts = process_instruction( - &serialize(&VoteInstruction::UpdateCommission(u8::MAX)).unwrap(), + TEMP_VOTE_STATE_V4_ENABLED, + &serialize(&VoteInstruction::UpdateCommission(200)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_eq!(vote_state.commission, u8::MAX); + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, + ); + assert_eq!(vote_state.commission(), 200); // should pass let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_eq!(vote_state.commission, 42); + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, + ); + assert_eq!(vote_state.commission(), 42); // should fail, authorized_withdrawer didn't sign the transaction instruction_accounts[1].is_signer = false; let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts, instruction_accounts, Err(InstructionError::MissingRequiredSignature), ); - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_eq!(vote_state.commission, 0); + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, + ); + assert_eq!(vote_state.commission(), 0); } #[test] fn test_vote_signature() { - let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); let (vote, instruction_datas) = create_serialized_votes(); let slot_hashes = SlotHashes::new(&[(*vote.slots.last().unwrap(), vote.hash)]); let slot_hashes_account = account::create_account_shared_data_for_test(&slot_hashes); @@ -786,6 +922,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -795,6 +932,7 @@ mod tests { // should pass let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -805,13 +943,14 @@ mod tests { }, ); if is_tower_sync { - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_eq!( - vote_state.votes, - vec![vote_state::LandedVote::from(Lockout::new( - *vote.slots.last().unwrap() - ))] + let vote_state = deserialize_vote_state_for_test( + TEMP_VOTE_STATE_V4_ENABLED, + accounts[0].data(), + &vote_pubkey, ); + let expected_lockout = Lockout::new(*vote.slots.last().unwrap()); + assert_eq!(vote_state.votes().len(), 1); + assert_eq!(vote_state.votes()[0].lockout, expected_lockout); assert_eq!(vote_state.credits(), 0); } @@ -824,6 +963,7 @@ mod tests { )])), ); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -836,6 +976,7 @@ mod tests { account::create_account_shared_data_for_test(&SlotHashes::new(&[(0, vote.hash)])), ); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -848,6 +989,7 @@ mod tests { account::create_account_shared_data_for_test(&SlotHashes::new(&[])), ); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -856,20 +998,33 @@ mod tests { transaction_accounts[1] = (sysvar::slot_hashes::id(), slot_hashes_account.clone()); // should fail, uninitialized - let vote_account = AccountSharedData::new(100, VoteStateV3::size_of(), &id()); + let vote_account = + AccountSharedData::new(100, vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED), &id()); transaction_accounts[0] = (vote_pubkey, vote_account); + let expected_error = if TEMP_VOTE_STATE_V4_ENABLED { + // When the target version is v4, the entire account is zeroed + // on deinit. It will then fail deserialization with + // `InvalidAccountData`, since the variant is 0. + error(InstructionError::InvalidAccountData) + } else { + // When the target version is v3, the variant (3) is retained on + // deinit, so it will successfully deserialize, but then fail + // the uninitialized check. + error(InstructionError::UninitializedAccount) + }; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), - error(InstructionError::UninitializedAccount), + expected_error, ); } } #[test] fn test_authorize_voter() { - let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); let authorized_voter_pubkey = solana_pubkey::new_rand(); let clock = Clock { epoch: 1, @@ -903,6 +1058,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -912,6 +1068,7 @@ mod tests { // should pass let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -921,6 +1078,7 @@ mod tests { // should fail, already set an authorized voter earlier for leader_schedule_epoch == 2 transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -944,6 +1102,7 @@ mod tests { let clock_account = account::create_account_shared_data_for_test(&clock); transaction_accounts[1] = (sysvar::clock::id(), clock_account); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -974,6 +1133,7 @@ mod tests { for (instruction_data, is_tower_sync) in instruction_datas { process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -986,6 +1146,7 @@ mod tests { // should pass, signed by authorized voter process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), authorized_instruction_accounts.clone(), @@ -1000,7 +1161,7 @@ mod tests { #[test] fn test_authorize_withdrawer() { - let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); let authorized_withdrawer_pubkey = solana_pubkey::new_rand(); let instruction_data = serialize(&VoteInstruction::Authorize( authorized_withdrawer_pubkey, @@ -1028,6 +1189,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1037,6 +1199,7 @@ mod tests { // should pass let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1052,6 +1215,7 @@ mod tests { }); transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1067,6 +1231,7 @@ mod tests { )) .unwrap(); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1076,7 +1241,7 @@ mod tests { #[test] fn test_vote_withdraw() { - let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); let lamports = vote_account.lamports(); let authorized_withdrawer_pubkey = solana_pubkey::new_rand(); let mut transaction_accounts = vec![ @@ -1100,6 +1265,7 @@ mod tests { // should pass, withdraw using authorized_withdrawer to authorized_withdrawer's account let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Authorize( authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, @@ -1117,6 +1283,7 @@ mod tests { }; transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1131,6 +1298,7 @@ mod tests { // should fail, unsigned transaction_accounts[0] = (vote_pubkey, vote_account); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1140,6 +1308,7 @@ mod tests { // should pass process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1148,6 +1317,7 @@ mod tests { // should fail, insufficient funds process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1157,6 +1327,7 @@ mod tests { // should pass, partial withdraw let withdraw_lamports = 42; let accounts = process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(withdraw_lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -1170,9 +1341,9 @@ mod tests { fn test_vote_state_withdraw() { let authorized_withdrawer_pubkey = solana_pubkey::new_rand(); let (vote_pubkey_1, vote_account_with_epoch_credits_1) = - create_test_account_with_epoch_credits(&[2, 1]); + create_test_account_with_epoch_credits(TEMP_VOTE_STATE_V4_ENABLED, &[2, 1]); let (vote_pubkey_2, vote_account_with_epoch_credits_2) = - create_test_account_with_epoch_credits(&[2, 1, 3]); + create_test_account_with_epoch_credits(TEMP_VOTE_STATE_V4_ENABLED, &[2, 1, 3]); let clock = Clock { epoch: 3, ..Clock::default() @@ -1209,6 +1380,7 @@ mod tests { // non rent exempt withdraw, with 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_1; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports - minimum_balance + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1218,6 +1390,7 @@ mod tests { // non rent exempt withdraw, without 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_2; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports - minimum_balance + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1227,6 +1400,7 @@ mod tests { // full withdraw, with 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_1; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1236,6 +1410,7 @@ mod tests { // full withdraw, without 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_2; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -1244,6 +1419,7 @@ mod tests { } fn perform_authorize_with_seed_test( + vote_state_v4_enabled: bool, authorization_type: VoteAuthorize, vote_pubkey: Pubkey, vote_account: AccountSharedData, @@ -1284,6 +1460,7 @@ mod tests { // Can't change authority unless base key signs. instruction_accounts[2].is_signer = false; process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type, @@ -1301,6 +1478,7 @@ mod tests { // Can't change authority if seed doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type, @@ -1317,6 +1495,7 @@ mod tests { // Can't change authority if owner doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type, @@ -1333,6 +1512,7 @@ mod tests { // Can change authority when base key signs for related derived key. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type, @@ -1349,6 +1529,7 @@ mod tests { } fn perform_authorize_checked_with_seed_test( + vote_state_v4_enabled: bool, authorization_type: VoteAuthorize, vote_pubkey: Pubkey, vote_account: AccountSharedData, @@ -1395,6 +1576,7 @@ mod tests { // Can't change authority unless base key signs. instruction_accounts[2].is_signer = false; process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1412,6 +1594,7 @@ mod tests { // Can't change authority unless new authority signs. instruction_accounts[3].is_signer = false; process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1428,6 +1611,7 @@ mod tests { // Can't change authority if seed doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1443,6 +1627,7 @@ mod tests { // Can't change authority if owner doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1458,6 +1643,7 @@ mod tests { // Can change authority when base key signs for related derived key and new authority signs. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1481,9 +1667,10 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( + TEMP_VOTE_STATE_V4_ENABLED, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1503,9 +1690,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( + TEMP_VOTE_STATE_V4_ENABLED, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1525,7 +1713,7 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_withdrawer_pubkey = Pubkey::new_unique(); let clock = Clock { epoch: 1, @@ -1557,6 +1745,7 @@ mod tests { ]; // Despite having Voter authority, you may not change the Withdrawer authority. process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type: VoteAuthorize::Withdrawer, @@ -1581,9 +1770,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_withdrawer_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( + TEMP_VOTE_STATE_V4_ENABLED, VoteAuthorize::Withdrawer, vote_pubkey, vote_account, @@ -1603,9 +1793,10 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( + TEMP_VOTE_STATE_V4_ENABLED, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1625,9 +1816,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( + TEMP_VOTE_STATE_V4_ENABLED, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1647,7 +1839,7 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_withdrawer_pubkey = Pubkey::new_unique(); let clock = Clock { epoch: 1, @@ -1685,6 +1877,7 @@ mod tests { ]; // Despite having Voter authority, you may not change the Withdrawer authority. process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type: VoteAuthorize::Withdrawer, @@ -1708,9 +1901,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); let new_withdrawer_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( + TEMP_VOTE_STATE_V4_ENABLED, VoteAuthorize::Withdrawer, vote_pubkey, vote_account, @@ -1724,6 +1918,7 @@ mod tests { #[test] fn test_spoofed_vote() { process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &vote( &invalid_vote_state_pubkey(), &Pubkey::new_unique(), @@ -1732,6 +1927,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &update_vote_state( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1740,6 +1936,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &compact_update_vote_state( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1748,6 +1945,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &tower_sync( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1791,6 +1989,7 @@ mod tests { // should fail, since VoteState1_14_11 isn't supported anymore process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instructions[1].data, transaction_accounts, instructions[1].accounts.clone(), @@ -1813,14 +2012,14 @@ mod tests { }, 101, CreateVoteAccountConfig { - space: vote_state::VoteStateV3::size_of() as u64, + space: vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED) as u64, ..CreateVoteAccountConfig::default() }, ); // grab the `space` value from SystemInstruction::CreateAccount by directly indexing, for // expediency let space = usize::from_le_bytes(instructions[0].data[12..20].try_into().unwrap()); - assert_eq!(space, vote_state::VoteStateV3::size_of()); + assert_eq!(space, vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED)); let empty_vote_account = AccountSharedData::new(101, space, &id()); let transaction_accounts = vec![ @@ -1831,6 +2030,7 @@ mod tests { ]; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &instructions[1].data, transaction_accounts, instructions[1].accounts.clone(), @@ -1850,8 +2050,13 @@ mod tests { ); // this case fails regardless of CreateVoteAccountConfig::space, because // process_instruction_as_one_arg passes a default (empty) account - process_instruction_as_one_arg(&instructions[1], Err(InstructionError::InvalidAccountData)); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, + &instructions[1], + Err(InstructionError::InvalidAccountData), + ); + process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &vote( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1860,6 +2065,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &vote_switch( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1869,6 +2075,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &authorize( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1878,6 +2085,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &update_vote_state( &Pubkey::default(), &Pubkey::default(), @@ -1887,6 +2095,7 @@ mod tests { ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &update_vote_state_switch( &Pubkey::default(), &Pubkey::default(), @@ -1896,6 +2105,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &compact_update_vote_state( &Pubkey::default(), &Pubkey::default(), @@ -1904,6 +2114,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &compact_update_vote_state_switch( &Pubkey::default(), &Pubkey::default(), @@ -1913,10 +2124,12 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &tower_sync(&Pubkey::default(), &Pubkey::default(), TowerSync::default()), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &tower_sync_switch( &Pubkey::default(), &Pubkey::default(), @@ -1927,6 +2140,7 @@ mod tests { ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &update_validator_identity( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1935,11 +2149,13 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &update_commission(&Pubkey::new_unique(), &Pubkey::new_unique(), 0), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &withdraw( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1964,7 +2180,11 @@ mod tests { VoteAuthorize::Voter, ); instruction.accounts = instruction.accounts[0..2].to_vec(); - process_instruction_as_one_arg(&instruction, Err(InstructionError::MissingAccount)); + process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, + &instruction, + Err(InstructionError::MissingAccount), + ); let mut instruction = authorize_checked( &vote_pubkey, @@ -1973,7 +2193,11 @@ mod tests { VoteAuthorize::Withdrawer, ); instruction.accounts = instruction.accounts[0..2].to_vec(); - process_instruction_as_one_arg(&instruction, Err(InstructionError::MissingAccount)); + process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, + &instruction, + Err(InstructionError::MissingAccount), + ); // Test with non-signing new_authorized_pubkey let mut instruction = authorize_checked( @@ -1984,6 +2208,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1996,6 +2221,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); process_instruction_as_one_arg( + TEMP_VOTE_STATE_V4_ENABLED, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -2005,6 +2231,7 @@ mod tests { let vote_account = create_test_account_with_provided_authorized( &default_authorized_pubkey, &default_authorized_pubkey, + TEMP_VOTE_STATE_V4_ENABLED, ); let clock_address = sysvar::clock::id(); let clock_account = account::create_account_shared_data_for_test(&Clock::default()); @@ -2039,12 +2266,14 @@ mod tests { }, ]; process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); process_instruction( + TEMP_VOTE_STATE_V4_ENABLED, &serialize(&VoteInstruction::AuthorizeChecked( VoteAuthorize::Withdrawer, )) diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 1273755d3b3dde..ad328007b733a1 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -4,14 +4,11 @@ #[cfg(feature = "dev-context-only-utils")] pub mod handler; #[cfg(not(feature = "dev-context-only-utils"))] -mod handler; +pub(crate) mod handler; -pub use { - handler::VoteStateTargetVersion, - solana_vote_interface::state::{vote_state_versions::*, *}, -}; +pub use solana_vote_interface::state::{vote_state_versions::*, *}; use { - handler::{VoteStateHandle, VoteStateHandler}, + handler::{VoteStateHandle, VoteStateHandler, VoteStateTargetVersion}, log::*, solana_account::{AccountSharedData, WritableAccount}, solana_clock::{Clock, Epoch, Slot}, From 83b169ca3d1d7016db1b7d8d2de0bd026ec97411 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 25 Sep 2025 14:52:42 +0000 Subject: [PATCH 2/3] vote-program: add vote state v4 feature gate --- feature-set/src/lib.rs | 9 +- programs/vote/src/vote_processor.rs | 344 +++++++++++++++------------- programs/vote/src/vote_state/mod.rs | 3 - svm-feature-set/src/lib.rs | 2 + 4 files changed, 191 insertions(+), 167 deletions(-) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index b525e208bfd8b8..35bc7d833dab93 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -159,6 +159,7 @@ impl FeatureSet { raise_cpi_nesting_limit_to_8: self.is_active(&raise_cpi_nesting_limit_to_8::id()), provide_instruction_data_offset_in_vm_r2: self .is_active(&provide_instruction_data_offset_in_vm_r2::id()), + vote_state_v4: self.is_active(&vote_state_v4::id()), } } } @@ -1145,6 +1146,10 @@ pub mod discard_unexpected_data_complete_shreds { solana_pubkey::declare_id!("8MhfKhoZEoiySpVe248bDkisyEcBA7JQLyUS94xoTSqN"); } +pub mod vote_state_v4 { + solana_pubkey::declare_id!("Gx4XFcrVMt4HUvPzTpTSVkdDVgcDSjKhDN1RqRS6KDuZ"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2070,7 +2075,9 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n discard_unexpected_data_complete_shreds::id(), "SIMD-0337: Markers for Alpenglow Fast Leader Handover, DATA_COMPLETE_SHRED placement \ rules", - ), /*************** ADD NEW FEATURES HERE ***************/ + ), + (vote_state_v4::id(), "SIMD-0185: Vote State v4"), + /*************** ADD NEW FEATURES HERE ***************/ ] .iter() .cloned() diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index 40599fd4262e01..2a0be3e4d8e88a 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -67,7 +67,11 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| } // Determine the target vote state version to use for all operations. - let target_version = vote_state::TEMP_HARDCODED_TARGET_VERSION; + let target_version = if invoke_context.get_feature_set().vote_state_v4 { + VoteStateTargetVersion::V4 + } else { + VoteStateTargetVersion::V3 + }; let signers = instruction_context.get_signers()?; match limited_deserialize(data, solana_packet::PACKET_DATA_SIZE as u64)? { @@ -245,6 +249,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| } }); +#[allow(clippy::arithmetic_side_effects)] #[cfg(test)] mod tests { use { @@ -281,11 +286,9 @@ mod tests { solana_svm_feature_set::SVMFeatureSet, solana_vote_interface::instruction::{tower_sync, tower_sync_switch}, std::{collections::HashSet, str::FromStr}, + test_case::test_case, }; - // TODO: Remove when v4 feature gate is added. - const TEMP_VOTE_STATE_V4_ENABLED: bool = false; - // They're the same, but just for posterity. fn vote_state_size_of(vote_state_v4_enabled: bool) -> usize { if vote_state_v4_enabled { @@ -323,7 +326,7 @@ mod tests { } fn process_instruction( - _vote_state_v4_enabled: bool, + vote_state_v4_enabled: bool, instruction_data: &[u8], transaction_accounts: Vec<(Pubkey, AccountSharedData)>, instruction_accounts: Vec, @@ -339,7 +342,10 @@ mod tests { Entrypoint::vm, |_invoke_context| {}, |_invoke_context| {}, - &SVMFeatureSet::all_enabled(), // <-- TODO: Add v4 feature flag + &SVMFeatureSet { + vote_state_v4: vote_state_v4_enabled, + ..SVMFeatureSet::all_enabled() + }, ) } @@ -608,10 +614,11 @@ mod tests { ) } - #[test] - fn test_vote_process_instruction_decode_bail() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_process_instruction_decode_bail(vote_state_v4_enabled: bool) { process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &[], Vec::new(), Vec::new(), @@ -619,11 +626,12 @@ mod tests { ); } - #[test] - fn test_initialize_vote_account() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_initialize_vote_account(vote_state_v4_enabled: bool) { let vote_pubkey = solana_pubkey::new_rand(); let vote_account = - AccountSharedData::new(100, vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED), &id()); + AccountSharedData::new(100, vote_state_size_of(vote_state_v4_enabled), &id()); let node_pubkey = solana_pubkey::new_rand(); let node_account = AccountSharedData::default(); let instruction_data = serialize(&VoteInstruction::InitializeAccount(VoteInit { @@ -658,7 +666,7 @@ mod tests { // init should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, vec![ (vote_pubkey, vote_account.clone()), @@ -672,7 +680,7 @@ mod tests { // reinit should fail process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, vec![ (vote_pubkey, accounts[0].clone()), @@ -686,14 +694,14 @@ mod tests { // init should fail, account is too big process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, vec![ ( vote_pubkey, AccountSharedData::new( 100, - 2 * vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED), + 2 * vote_state_size_of(vote_state_v4_enabled), &id(), ), ), @@ -708,7 +716,7 @@ mod tests { // init should fail, node_pubkey didn't sign the transaction instruction_accounts[3].is_signer = false; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, vec![ (vote_pubkey, vote_account), @@ -721,10 +729,11 @@ mod tests { ); } - #[test] - fn test_vote_update_validator_identity() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_update_validator_identity(vote_state_v4_enabled: bool) { let (vote_pubkey, _authorized_voter, authorized_withdrawer, vote_account) = - create_test_account_with_authorized(TEMP_VOTE_STATE_V4_ENABLED); + create_test_account_with_authorized(vote_state_v4_enabled); let node_pubkey = solana_pubkey::new_rand(); let instruction_data = serialize(&VoteInstruction::UpdateValidatorIdentity).unwrap(); let transaction_accounts = vec![ @@ -753,7 +762,7 @@ mod tests { // should fail, node_pubkey didn't sign the transaction instruction_accounts[1].is_signer = false; let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -761,7 +770,7 @@ mod tests { ); instruction_accounts[1].is_signer = true; let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); @@ -770,7 +779,7 @@ mod tests { // should fail, authorized_withdrawer didn't sign the transaction instruction_accounts[2].is_signer = false; let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -778,7 +787,7 @@ mod tests { ); instruction_accounts[2].is_signer = true; let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); @@ -786,26 +795,28 @@ mod tests { // should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts, instruction_accounts, Ok(()), ); let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); assert_eq!(*vote_state.node_pubkey(), node_pubkey); - // TODO: When the feature gate for vote state v4 is added, check the - // block revenue collector here for the v4 target. + if vote_state_v4_enabled { + assert_eq!(vote_state.as_ref_v4().block_revenue_collector, node_pubkey,); + } } - #[test] - fn test_vote_update_commission() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_update_commission(vote_state_v4_enabled: bool) { let (vote_pubkey, _authorized_voter, authorized_withdrawer, vote_account) = - create_test_account_with_authorized(TEMP_VOTE_STATE_V4_ENABLED); + create_test_account_with_authorized(vote_state_v4_enabled); let instruction_data = serialize(&VoteInstruction::UpdateCommission(42)).unwrap(); let transaction_accounts = vec![ (vote_pubkey, vote_account), @@ -835,14 +846,14 @@ mod tests { // should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::UpdateCommission(200)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); @@ -850,14 +861,14 @@ mod tests { // should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); @@ -866,23 +877,24 @@ mod tests { // should fail, authorized_withdrawer didn't sign the transaction instruction_accounts[1].is_signer = false; let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts, instruction_accounts, Err(InstructionError::MissingRequiredSignature), ); let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); assert_eq!(vote_state.commission(), 0); } - #[test] - fn test_vote_signature() { - let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_signature(vote_state_v4_enabled: bool) { + let (vote_pubkey, vote_account) = create_test_account(vote_state_v4_enabled); let (vote, instruction_datas) = create_serialized_votes(); let slot_hashes = SlotHashes::new(&[(*vote.slots.last().unwrap(), vote.hash)]); let slot_hashes_account = account::create_account_shared_data_for_test(&slot_hashes); @@ -922,7 +934,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -932,7 +944,7 @@ mod tests { // should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -944,7 +956,7 @@ mod tests { ); if is_tower_sync { let vote_state = deserialize_vote_state_for_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, accounts[0].data(), &vote_pubkey, ); @@ -963,7 +975,7 @@ mod tests { )])), ); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -976,7 +988,7 @@ mod tests { account::create_account_shared_data_for_test(&SlotHashes::new(&[(0, vote.hash)])), ); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -989,7 +1001,7 @@ mod tests { account::create_account_shared_data_for_test(&SlotHashes::new(&[])), ); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -999,32 +1011,22 @@ mod tests { // should fail, uninitialized let vote_account = - AccountSharedData::new(100, vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED), &id()); + AccountSharedData::new(100, vote_state_size_of(vote_state_v4_enabled), &id()); transaction_accounts[0] = (vote_pubkey, vote_account); - let expected_error = if TEMP_VOTE_STATE_V4_ENABLED { - // When the target version is v4, the entire account is zeroed - // on deinit. It will then fail deserialization with - // `InvalidAccountData`, since the variant is 0. - error(InstructionError::InvalidAccountData) - } else { - // When the target version is v3, the variant (3) is retained on - // deinit, so it will successfully deserialize, but then fail - // the uninitialized check. - error(InstructionError::UninitializedAccount) - }; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), - expected_error, + error(InstructionError::UninitializedAccount), ); } } - #[test] - fn test_authorize_voter() { - let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_authorize_voter(vote_state_v4_enabled: bool) { + let (vote_pubkey, vote_account) = create_test_account(vote_state_v4_enabled); let authorized_voter_pubkey = solana_pubkey::new_rand(); let clock = Clock { epoch: 1, @@ -1058,7 +1060,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1068,7 +1070,7 @@ mod tests { // should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1078,7 +1080,7 @@ mod tests { // should fail, already set an authorized voter earlier for leader_schedule_epoch == 2 transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1102,7 +1104,7 @@ mod tests { let clock_account = account::create_account_shared_data_for_test(&clock); transaction_accounts[1] = (sysvar::clock::id(), clock_account); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1133,7 +1135,7 @@ mod tests { for (instruction_data, is_tower_sync) in instruction_datas { process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1146,7 +1148,7 @@ mod tests { // should pass, signed by authorized voter process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), authorized_instruction_accounts.clone(), @@ -1159,9 +1161,10 @@ mod tests { } } - #[test] - fn test_authorize_withdrawer() { - let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_authorize_withdrawer(vote_state_v4_enabled: bool) { + let (vote_pubkey, vote_account) = create_test_account(vote_state_v4_enabled); let authorized_withdrawer_pubkey = solana_pubkey::new_rand(); let instruction_data = serialize(&VoteInstruction::Authorize( authorized_withdrawer_pubkey, @@ -1189,7 +1192,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1199,7 +1202,7 @@ mod tests { // should pass let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1215,7 +1218,7 @@ mod tests { }); transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1231,7 +1234,7 @@ mod tests { )) .unwrap(); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1239,9 +1242,10 @@ mod tests { ); } - #[test] - fn test_vote_withdraw() { - let (vote_pubkey, vote_account) = create_test_account(TEMP_VOTE_STATE_V4_ENABLED); + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_withdraw(vote_state_v4_enabled: bool) { + let (vote_pubkey, vote_account) = create_test_account(vote_state_v4_enabled); let lamports = vote_account.lamports(); let authorized_withdrawer_pubkey = solana_pubkey::new_rand(); let mut transaction_accounts = vec![ @@ -1265,7 +1269,7 @@ mod tests { // should pass, withdraw using authorized_withdrawer to authorized_withdrawer's account let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Authorize( authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, @@ -1283,7 +1287,7 @@ mod tests { }; transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1298,7 +1302,7 @@ mod tests { // should fail, unsigned transaction_accounts[0] = (vote_pubkey, vote_account); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1308,7 +1312,7 @@ mod tests { // should pass process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1317,7 +1321,7 @@ mod tests { // should fail, insufficient funds process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1327,7 +1331,7 @@ mod tests { // should pass, partial withdraw let withdraw_lamports = 42; let accounts = process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(withdraw_lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -1337,13 +1341,14 @@ mod tests { assert_eq!(accounts[3].lamports(), withdraw_lamports); } - #[test] - fn test_vote_state_withdraw() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_state_withdraw(vote_state_v4_enabled: bool) { let authorized_withdrawer_pubkey = solana_pubkey::new_rand(); let (vote_pubkey_1, vote_account_with_epoch_credits_1) = - create_test_account_with_epoch_credits(TEMP_VOTE_STATE_V4_ENABLED, &[2, 1]); + create_test_account_with_epoch_credits(vote_state_v4_enabled, &[2, 1]); let (vote_pubkey_2, vote_account_with_epoch_credits_2) = - create_test_account_with_epoch_credits(TEMP_VOTE_STATE_V4_ENABLED, &[2, 1, 3]); + create_test_account_with_epoch_credits(vote_state_v4_enabled, &[2, 1, 3]); let clock = Clock { epoch: 3, ..Clock::default() @@ -1380,7 +1385,7 @@ mod tests { // non rent exempt withdraw, with 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_1; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports - minimum_balance + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1390,7 +1395,7 @@ mod tests { // non rent exempt withdraw, without 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_2; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports - minimum_balance + 1)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1400,7 +1405,7 @@ mod tests { // full withdraw, with 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_1; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1410,7 +1415,7 @@ mod tests { // full withdraw, without 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_2; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -1658,8 +1663,9 @@ mod tests { ); } - #[test] - fn test_voter_base_key_can_authorize_new_voter() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_voter_base_key_can_authorize_new_voter(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, voter_base_key, @@ -1667,10 +1673,10 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1681,8 +1687,9 @@ mod tests { ); } - #[test] - fn test_withdrawer_base_key_can_authorize_new_voter() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_withdrawer_base_key_can_authorize_new_voter(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, withdrawer_base_key, @@ -1690,10 +1697,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1704,8 +1711,9 @@ mod tests { ); } - #[test] - fn test_voter_base_key_can_not_authorize_new_withdrawer() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_voter_base_key_can_not_authorize_new_withdrawer(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, voter_base_key, @@ -1713,7 +1721,7 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); let clock = Clock { epoch: 1, @@ -1745,7 +1753,7 @@ mod tests { ]; // Despite having Voter authority, you may not change the Withdrawer authority. process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type: VoteAuthorize::Withdrawer, @@ -1761,8 +1769,9 @@ mod tests { ); } - #[test] - fn test_withdrawer_base_key_can_authorize_new_withdrawer() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_withdrawer_base_key_can_authorize_new_withdrawer(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, withdrawer_base_key, @@ -1770,10 +1779,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, VoteAuthorize::Withdrawer, vote_pubkey, vote_account, @@ -1784,8 +1793,9 @@ mod tests { ); } - #[test] - fn test_voter_base_key_can_authorize_new_voter_checked() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_voter_base_key_can_authorize_new_voter_checked(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, voter_base_key, @@ -1793,10 +1803,10 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1807,8 +1817,9 @@ mod tests { ); } - #[test] - fn test_withdrawer_base_key_can_authorize_new_voter_checked() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_withdrawer_base_key_can_authorize_new_voter_checked(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, withdrawer_base_key, @@ -1816,10 +1827,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1830,8 +1841,9 @@ mod tests { ); } - #[test] - fn test_voter_base_key_can_not_authorize_new_withdrawer_checked() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_voter_base_key_can_not_authorize_new_withdrawer_checked(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, voter_base_key, @@ -1839,7 +1851,7 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); let clock = Clock { epoch: 1, @@ -1877,7 +1889,7 @@ mod tests { ]; // Despite having Voter authority, you may not change the Withdrawer authority. process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type: VoteAuthorize::Withdrawer, @@ -1892,8 +1904,9 @@ mod tests { ); } - #[test] - fn test_withdrawer_base_key_can_authorize_new_withdrawer_checked() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_withdrawer_base_key_can_authorize_new_withdrawer_checked(vote_state_v4_enabled: bool) { let VoteAccountTestFixtureWithAuthorities { vote_pubkey, withdrawer_base_key, @@ -1901,10 +1914,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(TEMP_VOTE_STATE_V4_ENABLED); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, VoteAuthorize::Withdrawer, vote_pubkey, vote_account, @@ -1915,10 +1928,11 @@ mod tests { ); } - #[test] - fn test_spoofed_vote() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_spoofed_vote(vote_state_v4_enabled: bool) { process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &vote( &invalid_vote_state_pubkey(), &Pubkey::new_unique(), @@ -1927,7 +1941,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &update_vote_state( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1936,7 +1950,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &compact_update_vote_state( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1945,7 +1959,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &tower_sync( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1955,8 +1969,9 @@ mod tests { ); } - #[test] - fn test_create_account_vote_state_1_14_11() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_create_account_vote_state_1_14_11(vote_state_v4_enabled: bool) { let node_pubkey = Pubkey::new_unique(); let vote_pubkey = Pubkey::new_unique(); let instructions = create_account_with_config( @@ -1989,7 +2004,7 @@ mod tests { // should fail, since VoteState1_14_11 isn't supported anymore process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instructions[1].data, transaction_accounts, instructions[1].accounts.clone(), @@ -1997,8 +2012,9 @@ mod tests { ); } - #[test] - fn test_create_account_vote_state_current() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_create_account_vote_state_current(vote_state_v4_enabled: bool) { let node_pubkey = Pubkey::new_unique(); let vote_pubkey = Pubkey::new_unique(); let instructions = create_account_with_config( @@ -2012,14 +2028,14 @@ mod tests { }, 101, CreateVoteAccountConfig { - space: vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED) as u64, + space: vote_state_size_of(vote_state_v4_enabled) as u64, ..CreateVoteAccountConfig::default() }, ); // grab the `space` value from SystemInstruction::CreateAccount by directly indexing, for // expediency let space = usize::from_le_bytes(instructions[0].data[12..20].try_into().unwrap()); - assert_eq!(space, vote_state_size_of(TEMP_VOTE_STATE_V4_ENABLED)); + assert_eq!(space, vote_state_size_of(vote_state_v4_enabled)); let empty_vote_account = AccountSharedData::new(101, space, &id()); let transaction_accounts = vec![ @@ -2030,7 +2046,7 @@ mod tests { ]; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instructions[1].data, transaction_accounts, instructions[1].accounts.clone(), @@ -2038,8 +2054,9 @@ mod tests { ); } - #[test] - fn test_vote_process_instruction() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_process_instruction(vote_state_v4_enabled: bool) { solana_logger::setup(); let instructions = create_account_with_config( &Pubkey::new_unique(), @@ -2051,12 +2068,12 @@ mod tests { // this case fails regardless of CreateVoteAccountConfig::space, because // process_instruction_as_one_arg passes a default (empty) account process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instructions[1], Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &vote( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -2065,7 +2082,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &vote_switch( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -2075,7 +2092,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &authorize( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -2085,7 +2102,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &update_vote_state( &Pubkey::default(), &Pubkey::default(), @@ -2095,7 +2112,7 @@ mod tests { ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &update_vote_state_switch( &Pubkey::default(), &Pubkey::default(), @@ -2105,7 +2122,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &compact_update_vote_state( &Pubkey::default(), &Pubkey::default(), @@ -2114,7 +2131,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &compact_update_vote_state_switch( &Pubkey::default(), &Pubkey::default(), @@ -2124,12 +2141,12 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &tower_sync(&Pubkey::default(), &Pubkey::default(), TowerSync::default()), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &tower_sync_switch( &Pubkey::default(), &Pubkey::default(), @@ -2140,7 +2157,7 @@ mod tests { ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &update_validator_identity( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -2149,13 +2166,13 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &update_commission(&Pubkey::new_unique(), &Pubkey::new_unique(), 0), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &withdraw( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -2166,8 +2183,9 @@ mod tests { ); } - #[test] - fn test_vote_authorize_checked() { + #[test_case(false ; "VoteStateV3")] + #[test_case(true ; "VoteStateV4")] + fn test_vote_authorize_checked(vote_state_v4_enabled: bool) { let vote_pubkey = Pubkey::new_unique(); let authorized_pubkey = Pubkey::new_unique(); let new_authorized_pubkey = Pubkey::new_unique(); @@ -2181,7 +2199,7 @@ mod tests { ); instruction.accounts = instruction.accounts[0..2].to_vec(); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction, Err(InstructionError::MissingAccount), ); @@ -2194,7 +2212,7 @@ mod tests { ); instruction.accounts = instruction.accounts[0..2].to_vec(); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction, Err(InstructionError::MissingAccount), ); @@ -2208,7 +2226,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -2221,7 +2239,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); process_instruction_as_one_arg( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -2231,7 +2249,7 @@ mod tests { let vote_account = create_test_account_with_provided_authorized( &default_authorized_pubkey, &default_authorized_pubkey, - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, ); let clock_address = sysvar::clock::id(); let clock_account = account::create_account_shared_data_for_test(&Clock::default()); @@ -2266,14 +2284,14 @@ mod tests { }, ]; process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); process_instruction( - TEMP_VOTE_STATE_V4_ENABLED, + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeChecked( VoteAuthorize::Withdrawer, )) diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index ad328007b733a1..6a04c7d865f8ab 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -26,9 +26,6 @@ use { }, }; -// TODO: Change me once the program has full v4 feature gate support. -pub(crate) const TEMP_HARDCODED_TARGET_VERSION: VoteStateTargetVersion = VoteStateTargetVersion::V3; - // Switch that preserves old behavior before vote state v4 feature gate. // This should be cleaned up when vote state v4 is activated. enum PreserveBehaviorInHandlerHelper { diff --git a/svm-feature-set/src/lib.rs b/svm-feature-set/src/lib.rs index e66cd3bdc43843..b53054ae71c43d 100644 --- a/svm-feature-set/src/lib.rs +++ b/svm-feature-set/src/lib.rs @@ -38,6 +38,7 @@ pub struct SVMFeatureSet { pub reenable_zk_elgamal_proof_program: bool, pub raise_cpi_nesting_limit_to_8: bool, pub provide_instruction_data_offset_in_vm_r2: bool, + pub vote_state_v4: bool, } impl SVMFeatureSet { @@ -81,6 +82,7 @@ impl SVMFeatureSet { reenable_zk_elgamal_proof_program: true, raise_cpi_nesting_limit_to_8: true, provide_instruction_data_offset_in_vm_r2: true, + vote_state_v4: true, } } } From 95304dc580614429a8b3ca1dcfa716f5e4d8e951 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Fri, 17 Oct 2025 02:16:10 +0000 Subject: [PATCH 3/3] runtime: add stake program vote state v4 upgrade --- feature-set/src/lib.rs | 4 ++++ runtime/src/bank.rs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 35bc7d833dab93..f3037df9337564 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1148,6 +1148,10 @@ pub mod discard_unexpected_data_complete_shreds { pub mod vote_state_v4 { solana_pubkey::declare_id!("Gx4XFcrVMt4HUvPzTpTSVkdDVgcDSjKhDN1RqRS6KDuZ"); + + pub mod stake_program_buffer { + solana_pubkey::declare_id!("BM11F4hqrpinQs28sEZfzQ2fYddivYs4NEAHF6QMjkJF"); + } } pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index bb81c27f97df36..785297e6d80e2b 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -5274,6 +5274,16 @@ impl Bank { if new_feature_activations.contains(&feature_set::raise_account_cu_limit::id()) { self.apply_simd_0306_cost_tracker_changes(); } + + if new_feature_activations.contains(&feature_set::vote_state_v4::id()) { + if let Err(e) = self.upgrade_core_bpf_program( + &solana_sdk_ids::stake::id(), + &feature_set::vote_state_v4::stake_program_buffer::id(), + "upgrade_stake_program_for_vote_state_v4", + ) { + error!("Failed to upgrade Core BPF Stake program: {e}"); + } + } } fn apply_new_builtin_program_feature_transitions(