diff --git a/Cargo.lock b/Cargo.lock index a9a5ae57c1d373..6e880e7117912b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11865,6 +11865,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/feature-set/src/lib.rs b/feature-set/src/lib.rs index 1019c4249131bf..7912e7fa4ae6de 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -158,6 +158,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()), } } } @@ -1132,6 +1133,10 @@ pub mod provide_instruction_data_offset_in_vm_r2 { solana_pubkey::declare_id!("5xXZc66h4UdB6Yq7FzdBxBiRAFMMScMLwHxk2QZDaNZL"); } +pub mod vote_state_v4 { + solana_pubkey::declare_id!("Gx4XFcrVMt4HUvPzTpTSVkdDVgcDSjKhDN1RqRS6KDuZ"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2045,6 +2050,7 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n provide_instruction_data_offset_in_vm_r2::id(), "SIMD-0321: Provide instruction data offset in VM r2", ), + (vote_state_v4::id(), "SIMD-0185: Vote State v4"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/programs/vote/Cargo.toml b/programs/vote/Cargo.toml index cbacb867fc1087..4e1cd4fee4a1ed 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 e852ebdedcb741..93edeb082b95f7 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -19,6 +19,7 @@ fn process_authorize_with_seed_instruction( invoke_context: &InvokeContext, instruction_context: &InstructionContext, vote_account: &mut BorrowedInstructionAccount, + target_version: vote_state::VoteStateTargetVersion, new_authority: &Pubkey, authorization_type: VoteAuthorize, current_authority_derived_key_owner: &Pubkey, @@ -41,6 +42,7 @@ fn process_authorize_with_seed_instruction( }; vote_state::authorize( vote_account, + target_version, new_authority, authorization_type, &expected_authority_keys, @@ -64,6 +66,13 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| return Err(InstructionError::InvalidAccountOwner); } + // Determine the target vote state version to use for all operations. + let target_version = if invoke_context.get_feature_set().vote_state_v4 { + vote_state::VoteStateTargetVersion::V4 + } else { + vote_state::VoteStateTargetVersion::V3 + }; + let signers = instruction_context.get_signers()?; match limited_deserialize(data, solana_packet::PACKET_DATA_SIZE as u64)? { VoteInstruction::InitializeAccount(vote_init) => { @@ -74,12 +83,19 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| } let clock = get_sysvar_with_account_check::clock(invoke_context, &instruction_context, 2)?; - vote_state::initialize_account(&mut me, &vote_init, &signers, &clock) + vote_state::initialize_account(&mut me, target_version, &vote_init, &signers, &clock) } VoteInstruction::Authorize(voter_pubkey, vote_authorize) => { let clock = get_sysvar_with_account_check::clock(invoke_context, &instruction_context, 1)?; - vote_state::authorize(&mut me, &voter_pubkey, vote_authorize, &signers, &clock) + vote_state::authorize( + &mut me, + target_version, + &voter_pubkey, + vote_authorize, + &signers, + &clock, + ) } VoteInstruction::AuthorizeWithSeed(args) => { instruction_context.check_number_of_instruction_accounts(3)?; @@ -87,6 +103,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| invoke_context, &instruction_context, &mut me, + target_version, &args.new_authority, args.authorization_type, &args.current_authority_derived_key_owner, @@ -103,6 +120,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| invoke_context, &instruction_context, &mut me, + target_version, new_authority, args.authorization_type, &args.current_authority_derived_key_owner, @@ -112,13 +130,14 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| VoteInstruction::UpdateValidatorIdentity => { instruction_context.check_number_of_instruction_accounts(2)?; let node_pubkey = instruction_context.get_key_of_instruction_account(1)?; - vote_state::update_validator_identity(&mut me, node_pubkey, &signers) + vote_state::update_validator_identity(&mut me, target_version, node_pubkey, &signers) } VoteInstruction::UpdateCommission(commission) => { let sysvar_cache = invoke_context.get_sysvar_cache(); vote_state::update_commission( &mut me, + target_version, commission, &signers, sysvar_cache.get_epoch_schedule()?.as_ref(), @@ -136,7 +155,14 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| )?; let clock = get_sysvar_with_account_check::clock(invoke_context, &instruction_context, 2)?; - vote_state::process_vote_with_account(&mut me, &slot_hashes, &clock, &vote, &signers) + vote_state::process_vote_with_account( + &mut me, + target_version, + &slot_hashes, + &clock, + &vote, + &signers, + ) } VoteInstruction::UpdateVoteState(vote_state_update) | VoteInstruction::UpdateVoteStateSwitch(vote_state_update, _) => { @@ -148,6 +174,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| let clock = sysvar_cache.get_clock()?; vote_state::process_vote_state_update( &mut me, + target_version, slot_hashes.slot_hashes(), &clock, vote_state_update, @@ -164,6 +191,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| let clock = sysvar_cache.get_clock()?; vote_state::process_vote_state_update( &mut me, + target_version, slot_hashes.slot_hashes(), &clock, vote_state_update, @@ -177,6 +205,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| let clock = sysvar_cache.get_clock()?; vote_state::process_tower_sync( &mut me, + target_version, slot_hashes.slot_hashes(), &clock, tower_sync, @@ -192,6 +221,7 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| vote_state::withdraw( &instruction_context, 0, + target_version, lamports, 1, &signers, @@ -207,7 +237,14 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| } let clock = get_sysvar_with_account_check::clock(invoke_context, &instruction_context, 1)?; - vote_state::authorize(&mut me, voter_pubkey, vote_authorize, &signers, &clock) + vote_state::authorize( + &mut me, + target_version, + voter_pubkey, + vote_authorize, + &signers, + &clock, + ) } } }); @@ -225,26 +262,29 @@ mod tests { vote_switch, withdraw, CreateVoteAccountConfig, VoteInstruction, }, vote_state::{ - self, Lockout, TowerSync, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, - VoteAuthorizeWithSeedArgs, VoteInit, VoteStateUpdate, VoteStateV3, - VoteStateVersions, + self, create_v4_account_with_authorized, Lockout, TowerSync, Vote, VoteAuthorize, + VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, VoteInit, + VoteStateHandle, VoteStateUpdate, VoteStateV3, VoteStateV4, VoteStateVersions, }, }, bincode::serialize, solana_account::{ self as account, state_traits::StateMut, Account, AccountSharedData, ReadableAccount, + WritableAccount, }, solana_clock::Clock, 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}, + test_case::test_case, }; struct VoteAccountTestFixtureWithAuthorities { @@ -263,12 +303,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, @@ -278,10 +319,15 @@ mod tests { Entrypoint::vm, |_invoke_context| {}, |_invoke_context| {}, + &SVMFeatureSet { + vote_state_v4: vote_state_v4_enabled, + ..SVMFeatureSet::all_enabled() + }, ) } fn process_instruction_as_one_arg( + vote_state_v4_enabled: bool, instruction: &Instruction, expected_result: Result<(), InstructionError>, ) -> Vec { @@ -324,6 +370,7 @@ mod tests { }) .collect(); process_instruction( + vote_state_v4_enabled, &instruction.data, transaction_accounts, instruction.accounts.clone(), @@ -343,22 +390,44 @@ 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(); + + if vote_state_v4_enabled { + let balance = rent.minimum_balance(VoteStateV4::size_of()); + ( + vote_pubkey, + create_v4_account_with_authorized( + &node_pubkey, + &vote_pubkey, + &vote_pubkey, + None, + 0, + balance, + ), + ) + } else { + let balance = rent.minimum_balance(VoteStateV3::size_of()); + ( + vote_pubkey, + vote_state::create_account(&vote_pubkey, &node_pubkey, 0, balance), + ) + } } - 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, @@ -371,17 +440,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(); @@ -398,14 +483,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, @@ -417,20 +516,21 @@ 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, vote_account) = create_test_account(vote_state_v4_enabled); + let space = vote_account.data().len(); + let lamports = vote_account.lamports(); - let mut vote_state = vote_state::from(&vote_account).unwrap(); - vote_state.authorized_withdrawer = vote_pubkey; - vote_state.epoch_credits = Vec::new(); + let authorized_withdrawer = vote_pubkey; + let mut epoch_credits = Vec::new(); 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, @@ -438,11 +538,30 @@ 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_state::to(&versioned, &mut vote_account_with_epoch_credits); + let mut vote_account_with_epoch_credits = AccountSharedData::new(lamports, space, &id()); + + if vote_state_v4_enabled { + let mut vote_state = + VoteStateV4::deserialize(vote_account.data(), &vote_pubkey).unwrap(); + vote_state.authorized_withdrawer = authorized_withdrawer; + vote_state.epoch_credits = epoch_credits; + let versioned = VoteStateVersions::new_v4(vote_state); + VoteStateV4::serialize( + &versioned, + vote_account_with_epoch_credits.data_as_mut_slice(), + ) + .unwrap(); + } else { + let mut vote_state = VoteStateV3::deserialize(vote_account.data()).unwrap(); + vote_state.authorized_withdrawer = authorized_withdrawer; + vote_state.epoch_credits = epoch_credits; + let versioned = VoteStateVersions::new_v3(vote_state); + VoteStateV3::serialize( + &versioned, + vote_account_with_epoch_credits.data_as_mut_slice(), + ) + .unwrap(); + }; (vote_pubkey, vote_account_with_epoch_credits) } @@ -474,9 +593,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( + vote_state_v4_enabled, &[], Vec::new(), Vec::new(), @@ -484,8 +605,9 @@ 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, VoteStateV3::size_of(), &id()); let node_pubkey = solana_pubkey::new_rand(); @@ -522,6 +644,7 @@ mod tests { // init should pass let accounts = process_instruction( + vote_state_v4_enabled, &instruction_data, vec![ (vote_pubkey, vote_account.clone()), @@ -535,6 +658,7 @@ mod tests { // reinit should fail process_instruction( + vote_state_v4_enabled, &instruction_data, vec![ (vote_pubkey, accounts[0].clone()), @@ -548,11 +672,12 @@ mod tests { // init should fail, account is too big process_instruction( + vote_state_v4_enabled, &instruction_data, vec![ ( vote_pubkey, - AccountSharedData::new(100, 2 * VoteStateV3::size_of(), &id()), + AccountSharedData::new(100, VoteStateV3::size_of().saturating_mul(2), &id()), ), (sysvar::rent::id(), create_default_rent_account()), (sysvar::clock::id(), create_default_clock_account()), @@ -565,6 +690,7 @@ mod tests { // init should fail, node_pubkey didn't sign the transaction instruction_accounts[3].is_signer = false; process_instruction( + vote_state_v4_enabled, &instruction_data, vec![ (vote_pubkey, vote_account), @@ -577,10 +703,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(); + 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![ @@ -609,42 +736,61 @@ mod tests { // should fail, node_pubkey didn't sign the transaction instruction_accounts[1].is_signer = false; let accounts = process_instruction( + 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); + if vote_state_v4_enabled { + let vote_state = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_ne!(vote_state.node_pubkey, node_pubkey); + } else { + let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); + 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( + 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); + if vote_state_v4_enabled { + let vote_state = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_ne!(vote_state.node_pubkey, node_pubkey); + } else { + let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); + assert_ne!(vote_state.node_pubkey, node_pubkey); + }; // should pass let accounts = process_instruction( + 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); + if vote_state_v4_enabled { + let vote_state = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_eq!(vote_state.node_pubkey, node_pubkey); + } else { + let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); + assert_eq!(vote_state.node_pubkey, 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(); + 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), @@ -674,39 +820,61 @@ mod tests { // should pass let accounts = process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::UpdateCommission(u8::MAX)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); - let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); - assert_eq!(vote_state.commission, u8::MAX); + if vote_state_v4_enabled { + let vote_state = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_eq!( + vote_state.inflation_rewards_commission_bps, + (u8::MAX as u16).saturating_mul(100), + ); + } else { + let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); + assert_eq!(vote_state.commission, u8::MAX); + } // should pass let accounts = process_instruction( + 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); + if vote_state_v4_enabled { + let vote_state = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_eq!(vote_state.inflation_rewards_commission_bps, 42u16 * 100); + } else { + let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); + 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( + 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); + if vote_state_v4_enabled { + let vote_state = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_eq!(vote_state.inflation_rewards_commission_bps, 0); + } else { + let vote_state = VoteStateV3::deserialize(accounts[0].data()).unwrap(); + assert_eq!(vote_state.commission, 0); + } } - #[test] - fn test_vote_signature() { - let (vote_pubkey, vote_account) = create_test_account(); + #[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); @@ -746,6 +914,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -755,6 +924,7 @@ mod tests { // should pass let accounts = process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -765,14 +935,26 @@ 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() - ))] - ); - assert_eq!(vote_state.credits(), 0); + if vote_state_v4_enabled { + let vote_state = + VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + assert_eq!( + vote_state.votes, + vec![vote_state::LandedVote::from(Lockout::new( + *vote.slots.last().unwrap() + ))] + ); + assert_eq!(vote_state.credits(), 0); + } else { + 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() + ))] + ); + assert_eq!(vote_state.credits(), 0); + }; } // should fail, wrong hash @@ -784,6 +966,7 @@ mod tests { )])), ); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -796,6 +979,7 @@ mod tests { account::create_account_shared_data_for_test(&SlotHashes::new(&[(0, vote.hash)])), ); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -808,6 +992,7 @@ mod tests { account::create_account_shared_data_for_test(&SlotHashes::new(&[])), ); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -819,6 +1004,7 @@ mod tests { let vote_account = AccountSharedData::new(100, VoteStateV3::size_of(), &id()); transaction_accounts[0] = (vote_pubkey, vote_account); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -827,9 +1013,10 @@ mod tests { } } - #[test] - fn test_authorize_voter() { - let (vote_pubkey, vote_account) = create_test_account(); + #[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, @@ -863,6 +1050,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -872,6 +1060,7 @@ mod tests { // should pass let accounts = process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -881,6 +1070,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( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -904,6 +1094,7 @@ mod tests { let clock_account = account::create_account_shared_data_for_test(&clock); transaction_accounts[1] = (sysvar::clock::id(), clock_account); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -934,6 +1125,7 @@ mod tests { for (instruction_data, is_tower_sync) in instruction_datas { process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -946,6 +1138,7 @@ mod tests { // should pass, signed by authorized voter process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), authorized_instruction_accounts.clone(), @@ -958,9 +1151,10 @@ mod tests { } } - #[test] - fn test_authorize_withdrawer() { - let (vote_pubkey, vote_account) = create_test_account(); + #[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, @@ -988,6 +1182,7 @@ mod tests { // should fail, unsigned instruction_accounts[0].is_signer = false; process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -997,6 +1192,7 @@ mod tests { // should pass let accounts = process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1012,6 +1208,7 @@ mod tests { }); transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1027,6 +1224,7 @@ mod tests { )) .unwrap(); process_instruction( + vote_state_v4_enabled, &instruction_data, transaction_accounts.clone(), instruction_accounts.clone(), @@ -1034,9 +1232,10 @@ mod tests { ); } - #[test] - fn test_vote_withdraw() { - let (vote_pubkey, vote_account) = create_test_account(); + #[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![ @@ -1060,6 +1259,7 @@ mod tests { // should pass, withdraw using authorized_withdrawer to authorized_withdrawer's account let accounts = process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Authorize( authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, @@ -1077,6 +1277,7 @@ mod tests { }; transaction_accounts[0] = (vote_pubkey, accounts[0].clone()); let accounts = process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1084,13 +1285,21 @@ mod tests { ); assert_eq!(accounts[0].lamports(), 0); assert_eq!(accounts[3].lamports(), lamports); - let post_state: VoteStateVersions = accounts[0].state().unwrap(); // State has been deinitialized since balance is zero - assert!(post_state.is_uninitialized()); + if vote_state_v4_enabled { + // v4 is always "initialized" per SIMD-0185. + let v4 = VoteStateV4::deserialize(accounts[0].data(), &vote_pubkey).unwrap(); + // After deinitialize, it contains default state. + assert_eq!(v4, VoteStateV4::default()); + } else { + let post_state: VoteStateVersions = accounts[0].state().unwrap(); + assert!(post_state.is_uninitialized()); + } // should fail, unsigned transaction_accounts[0] = (vote_pubkey, vote_account); process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1100,6 +1309,7 @@ mod tests { // should pass process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1108,7 +1318,8 @@ mod tests { // should fail, insufficient funds process_instruction( - &serialize(&VoteInstruction::Withdraw(lamports + 1)).unwrap(), + vote_state_v4_enabled, + &serialize(&VoteInstruction::Withdraw(lamports.saturating_add(1))).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::InsufficientFunds), @@ -1117,22 +1328,27 @@ mod tests { // should pass, partial withdraw let withdraw_lamports = 42; let accounts = process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(withdraw_lamports)).unwrap(), transaction_accounts, instruction_accounts, Ok(()), ); - assert_eq!(accounts[0].lamports(), lamports - withdraw_lamports); + assert_eq!( + accounts[0].lamports(), + lamports.saturating_sub(withdraw_lamports) + ); 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(&[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(&[2, 1, 3]); + create_test_account_with_epoch_credits(vote_state_v4_enabled, &[2, 1, 3]); let clock = Clock { epoch: 3, ..Clock::default() @@ -1169,7 +1385,11 @@ mod tests { // non rent exempt withdraw, with 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_1; process_instruction( - &serialize(&VoteInstruction::Withdraw(lamports - minimum_balance + 1)).unwrap(), + vote_state_v4_enabled, + &serialize(&VoteInstruction::Withdraw( + lamports.saturating_sub(minimum_balance).saturating_add(1), + )) + .unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::InsufficientFunds), @@ -1178,7 +1398,11 @@ mod tests { // non rent exempt withdraw, without 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_2; process_instruction( - &serialize(&VoteInstruction::Withdraw(lamports - minimum_balance + 1)).unwrap(), + vote_state_v4_enabled, + &serialize(&VoteInstruction::Withdraw( + lamports.saturating_sub(minimum_balance).saturating_add(1), + )) + .unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Err(InstructionError::InsufficientFunds), @@ -1187,6 +1411,7 @@ mod tests { // full withdraw, with 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_1; process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), @@ -1196,6 +1421,7 @@ mod tests { // full withdraw, without 0 credit epoch instruction_accounts[0].pubkey = vote_pubkey_2; process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::Withdraw(lamports)).unwrap(), transaction_accounts, instruction_accounts, @@ -1204,6 +1430,7 @@ mod tests { } fn perform_authorize_with_seed_test( + vote_state_v4_enabled: bool, authorization_type: VoteAuthorize, vote_pubkey: Pubkey, vote_account: AccountSharedData, @@ -1244,6 +1471,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, @@ -1261,6 +1489,7 @@ mod tests { // Can't change authority if seed doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type, @@ -1277,6 +1506,7 @@ mod tests { // Can't change authority if owner doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type, @@ -1293,6 +1523,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, @@ -1309,6 +1540,7 @@ mod tests { } fn perform_authorize_checked_with_seed_test( + vote_state_v4_enabled: bool, authorization_type: VoteAuthorize, vote_pubkey: Pubkey, vote_account: AccountSharedData, @@ -1355,6 +1587,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, @@ -1372,6 +1605,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, @@ -1388,6 +1622,7 @@ mod tests { // Can't change authority if seed doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1403,6 +1638,7 @@ mod tests { // Can't change authority if owner doesn't match. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type, @@ -1418,6 +1654,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, @@ -1432,8 +1669,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, @@ -1441,9 +1679,10 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1454,8 +1693,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, @@ -1463,9 +1703,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1476,8 +1717,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, @@ -1485,7 +1727,7 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); let clock = Clock { epoch: 1, @@ -1517,6 +1759,7 @@ mod tests { ]; // Despite having Voter authority, you may not change the Withdrawer authority. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeWithSeed( VoteAuthorizeWithSeedArgs { authorization_type: VoteAuthorize::Withdrawer, @@ -1532,8 +1775,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, @@ -1541,9 +1785,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); perform_authorize_with_seed_test( + vote_state_v4_enabled, VoteAuthorize::Withdrawer, vote_pubkey, vote_account, @@ -1554,8 +1799,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, @@ -1563,9 +1809,10 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1576,8 +1823,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, @@ -1585,9 +1833,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_voter_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( + vote_state_v4_enabled, VoteAuthorize::Voter, vote_pubkey, vote_account, @@ -1598,8 +1847,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, @@ -1607,7 +1857,7 @@ mod tests { voter_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); let clock = Clock { epoch: 1, @@ -1645,6 +1895,7 @@ mod tests { ]; // Despite having Voter authority, you may not change the Withdrawer authority. process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( VoteAuthorizeCheckedWithSeedArgs { authorization_type: VoteAuthorize::Withdrawer, @@ -1659,8 +1910,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, @@ -1668,9 +1920,10 @@ mod tests { withdrawer_seed, vote_account, .. - } = create_test_account_with_authorized_from_seed(); + } = create_test_account_with_authorized_from_seed(vote_state_v4_enabled); let new_withdrawer_pubkey = Pubkey::new_unique(); perform_authorize_checked_with_seed_test( + vote_state_v4_enabled, VoteAuthorize::Withdrawer, vote_pubkey, vote_account, @@ -1681,9 +1934,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( + vote_state_v4_enabled, &vote( &invalid_vote_state_pubkey(), &Pubkey::new_unique(), @@ -1692,6 +1947,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &update_vote_state( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1700,6 +1956,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &compact_update_vote_state( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1708,6 +1965,7 @@ mod tests { Err(InstructionError::InvalidAccountOwner), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &tower_sync( &invalid_vote_state_pubkey(), &Pubkey::default(), @@ -1717,8 +1975,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( @@ -1751,6 +2010,7 @@ mod tests { // should fail, since VoteState1_14_11 isn't supported anymore process_instruction( + vote_state_v4_enabled, &instructions[1].data, transaction_accounts, instructions[1].accounts.clone(), @@ -1758,8 +2018,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( @@ -1791,6 +2052,7 @@ mod tests { ]; process_instruction( + vote_state_v4_enabled, &instructions[1].data, transaction_accounts, instructions[1].accounts.clone(), @@ -1798,8 +2060,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(), @@ -1810,8 +2073,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( + vote_state_v4_enabled, + &instructions[1], + Err(InstructionError::InvalidAccountData), + ); + process_instruction_as_one_arg( + vote_state_v4_enabled, &vote( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1820,6 +2088,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &vote_switch( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1829,6 +2098,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &authorize( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1838,6 +2108,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &update_vote_state( &Pubkey::default(), &Pubkey::default(), @@ -1847,6 +2118,7 @@ mod tests { ); process_instruction_as_one_arg( + vote_state_v4_enabled, &update_vote_state_switch( &Pubkey::default(), &Pubkey::default(), @@ -1856,6 +2128,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &compact_update_vote_state( &Pubkey::default(), &Pubkey::default(), @@ -1864,6 +2137,7 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &compact_update_vote_state_switch( &Pubkey::default(), &Pubkey::default(), @@ -1873,10 +2147,12 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &tower_sync(&Pubkey::default(), &Pubkey::default(), TowerSync::default()), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &tower_sync_switch( &Pubkey::default(), &Pubkey::default(), @@ -1887,6 +2163,7 @@ mod tests { ); process_instruction_as_one_arg( + vote_state_v4_enabled, &update_validator_identity( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1895,11 +2172,13 @@ mod tests { Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &update_commission(&Pubkey::new_unique(), &Pubkey::new_unique(), 0), Err(InstructionError::InvalidAccountData), ); process_instruction_as_one_arg( + vote_state_v4_enabled, &withdraw( &Pubkey::new_unique(), &Pubkey::new_unique(), @@ -1910,8 +2189,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(); @@ -1924,7 +2204,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( + vote_state_v4_enabled, + &instruction, + Err(InstructionError::MissingAccount), + ); let mut instruction = authorize_checked( &vote_pubkey, @@ -1933,7 +2217,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( + vote_state_v4_enabled, + &instruction, + Err(InstructionError::MissingAccount), + ); // Test with non-signing new_authorized_pubkey let mut instruction = authorize_checked( @@ -1944,6 +2232,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); process_instruction_as_one_arg( + vote_state_v4_enabled, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1956,6 +2245,7 @@ mod tests { ); instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); process_instruction_as_one_arg( + vote_state_v4_enabled, &instruction, Err(InstructionError::MissingRequiredSignature), ); @@ -1965,6 +2255,7 @@ mod tests { let vote_account = create_test_account_with_provided_authorized( &default_authorized_pubkey, &default_authorized_pubkey, + vote_state_v4_enabled, ); let clock_address = sysvar::clock::id(); let clock_account = account::create_account_shared_data_for_test(&Clock::default()); @@ -1999,12 +2290,14 @@ mod tests { }, ]; process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(), transaction_accounts.clone(), instruction_accounts.clone(), Ok(()), ); process_instruction( + vote_state_v4_enabled, &serialize(&VoteInstruction::AuthorizeChecked( VoteAuthorize::Withdrawer, )) diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index 79ab557c6368a0..eeaefbca00a3c8 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -231,6 +231,20 @@ pub trait VoteStateHandle { self.epoch_credits().last().unwrap().1 } } + + #[cfg(test)] + fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> { + if position < self.votes().len() { + let pos = self + .votes() + .len() + .checked_sub(position) + .and_then(|pos| pos.checked_sub(1))?; + self.votes().get(pos).map(|vote| &vote.lockout) + } else { + None + } + } } impl VoteStateHandle for VoteStateV3 { @@ -582,13 +596,11 @@ impl VoteStateHandle for VoteStateV4 { } /// Default block revenue commission rate in basis points (100%) per SIMD-0185. -#[cfg(test)] // Test-only for now, until later commits. const DEFAULT_BLOCK_REVENUE_COMMISSION_BPS: u16 = 10_000; /// Create a new VoteStateV4 from `VoteInit` with proper SIMD-0185 defaults. /// Note this is a temporary substitute for `VoteStateV4::new`. #[allow(clippy::arithmetic_side_effects)] -#[cfg(test)] // Test-only for now, until later commits. pub(crate) fn create_new_vote_state_v4( vote_pubkey: &Pubkey, vote_init: &VoteInit, @@ -626,14 +638,17 @@ pub(crate) fn create_new_vote_state_v4_for_tests( } /// The target version to convert all deserialized vote state into. +#[derive(Clone, Copy, Debug, PartialEq)] pub enum VoteStateTargetVersion { V3, + V4, // New vote state versions will be added here... } #[derive(Clone, Debug, PartialEq)] enum TargetVoteState { V3(VoteStateV3), + V4(VoteStateV4), // New vote state versions will be added here... } @@ -651,24 +666,28 @@ impl VoteStateHandle for VoteStateHandler { fn is_uninitialized(&self) -> bool { match &self.target_state { TargetVoteState::V3(v3) => v3.is_uninitialized(), + TargetVoteState::V4(v4) => v4.is_uninitialized(), } } fn authorized_withdrawer(&self) -> &Pubkey { match &self.target_state { TargetVoteState::V3(v3) => v3.authorized_withdrawer(), + TargetVoteState::V4(v4) => v4.authorized_withdrawer(), } } fn set_authorized_withdrawer(&mut self, authorized_withdrawer: Pubkey) { match &mut self.target_state { TargetVoteState::V3(v3) => v3.set_authorized_withdrawer(authorized_withdrawer), + TargetVoteState::V4(v4) => v4.set_authorized_withdrawer(authorized_withdrawer), } } fn authorized_voters(&self) -> &AuthorizedVoters { match &self.target_state { TargetVoteState::V3(v3) => v3.authorized_voters(), + TargetVoteState::V4(v4) => v4.authorized_voters(), } } @@ -686,6 +705,9 @@ impl VoteStateHandle for VoteStateHandler { TargetVoteState::V3(v3) => { v3.set_new_authorized_voter(authorized_pubkey, current_epoch, target_epoch, verify) } + TargetVoteState::V4(v4) => { + v4.set_new_authorized_voter(authorized_pubkey, current_epoch, target_epoch, verify) + } } } @@ -695,108 +717,126 @@ impl VoteStateHandle for VoteStateHandler { ) -> Result { match &mut self.target_state { TargetVoteState::V3(v3) => v3.get_and_update_authorized_voter(current_epoch), + TargetVoteState::V4(v4) => v4.get_and_update_authorized_voter(current_epoch), } } fn commission(&self) -> u8 { match &self.target_state { TargetVoteState::V3(v3) => v3.commission(), + TargetVoteState::V4(v4) => v4.commission(), } } fn set_commission(&mut self, commission: u8) { match &mut self.target_state { TargetVoteState::V3(v3) => v3.set_commission(commission), + TargetVoteState::V4(v4) => v4.set_commission(commission), } } fn node_pubkey(&self) -> &Pubkey { match &self.target_state { TargetVoteState::V3(v3) => v3.node_pubkey(), + TargetVoteState::V4(v4) => v4.node_pubkey(), } } fn set_node_pubkey(&mut self, node_pubkey: Pubkey) { match &mut self.target_state { TargetVoteState::V3(v3) => v3.set_node_pubkey(node_pubkey), + TargetVoteState::V4(v4) => v4.set_node_pubkey(node_pubkey), } } fn votes(&self) -> &VecDeque { match &self.target_state { TargetVoteState::V3(v3) => v3.votes(), + TargetVoteState::V4(v4) => v4.votes(), } } fn votes_mut(&mut self) -> &mut VecDeque { match &mut self.target_state { TargetVoteState::V3(v3) => v3.votes_mut(), + TargetVoteState::V4(v4) => v4.votes_mut(), } } fn set_votes(&mut self, votes: VecDeque) { match &mut self.target_state { TargetVoteState::V3(v3) => v3.set_votes(votes), + TargetVoteState::V4(v4) => v4.set_votes(votes), } } fn contains_slot(&self, candidate_slot: Slot) -> bool { match &self.target_state { TargetVoteState::V3(v3) => v3.contains_slot(candidate_slot), + TargetVoteState::V4(v4) => v4.contains_slot(candidate_slot), } } fn last_lockout(&self) -> Option<&Lockout> { match &self.target_state { TargetVoteState::V3(v3) => v3.last_lockout(), + TargetVoteState::V4(v4) => v4.last_lockout(), } } fn last_voted_slot(&self) -> Option { match &self.target_state { TargetVoteState::V3(v3) => v3.last_voted_slot(), + TargetVoteState::V4(v4) => v4.last_voted_slot(), } } fn root_slot(&self) -> Option { match &self.target_state { TargetVoteState::V3(v3) => v3.root_slot(), + TargetVoteState::V4(v4) => v4.root_slot(), } } fn set_root_slot(&mut self, root_slot: Option) { match &mut self.target_state { TargetVoteState::V3(v3) => v3.set_root_slot(root_slot), + TargetVoteState::V4(v4) => v4.set_root_slot(root_slot), } } fn current_epoch(&self) -> Epoch { match &self.target_state { TargetVoteState::V3(v3) => v3.current_epoch(), + TargetVoteState::V4(v4) => v4.current_epoch(), } } fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { match &self.target_state { TargetVoteState::V3(v3) => v3.epoch_credits(), + TargetVoteState::V4(v4) => v4.epoch_credits(), } } fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { match &mut self.target_state { TargetVoteState::V3(v3) => v3.epoch_credits_mut(), + TargetVoteState::V4(v4) => v4.epoch_credits_mut(), } } fn last_timestamp(&self) -> &BlockTimestamp { match &self.target_state { TargetVoteState::V3(v3) => v3.last_timestamp(), + TargetVoteState::V4(v4) => v4.last_timestamp(), } } fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { match &mut self.target_state { TargetVoteState::V3(v3) => v3.set_last_timestamp(timestamp), + TargetVoteState::V4(v4) => v4.set_last_timestamp(timestamp), } } @@ -806,6 +846,7 @@ impl VoteStateHandle for VoteStateHandler { ) -> Result<(), InstructionError> { match self.target_state { TargetVoteState::V3(v3) => v3.set_vote_account_state(vote_account), + TargetVoteState::V4(v4) => v4.set_vote_account_state(vote_account), } } } @@ -822,6 +863,17 @@ impl VoteStateHandler { let vote_state = VoteStateV3::deserialize(vote_account.get_data())?; TargetVoteState::V3(vote_state) } + VoteStateTargetVersion::V4 => { + // Once `VoteStateV4` is active, `VoteState0_23_5` will be + // considered uninitialized. Fail here early if we detect an + // enum variant discriminator of 0. + let data = vote_account.get_data(); + if data.first().is_some_and(|b| *b == 0) { + return Err(InstructionError::UninitializedAccount); + } + let vote_state = VoteStateV4::deserialize(data, vote_account.get_key())?; + TargetVoteState::V4(vote_state) + } }; Ok(Self { target_state }) } @@ -836,6 +888,10 @@ impl VoteStateHandler { VoteStateTargetVersion::V3 => { VoteStateV3::new(vote_init, clock).set_vote_account_state(vote_account) } + VoteStateTargetVersion::V4 => { + let vote_state = create_new_vote_state_v4(vote_account.get_key(), vote_init, clock); + vote_state.set_vote_account_state(vote_account) + } } } @@ -847,6 +903,9 @@ impl VoteStateHandler { VoteStateTargetVersion::V3 => { VoteStateV3::default().set_vote_account_state(vote_account) } + VoteStateTargetVersion::V4 => { + VoteStateV4::default().set_vote_account_state(vote_account) + } } } @@ -857,6 +916,7 @@ impl VoteStateHandler { let length = vote_account.get_data().len(); let expected = match target_version { VoteStateTargetVersion::V3 => VoteStateV3::size_of(), + VoteStateTargetVersion::V4 => VoteStateV4::size_of(), }; if length != expected { Err(InstructionError::InvalidAccountData) @@ -873,33 +933,20 @@ impl VoteStateHandler { } #[cfg(test)] - pub fn default_v3() -> Self { - Self::new_v3(VoteStateV3::default()) + pub fn new_v4(vote_state: VoteStateV4) -> Self { + Self { + target_state: TargetVoteState::V4(vote_state), + } } #[cfg(test)] - pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { - match &self.target_state { - TargetVoteState::V3(v3) => &v3.epoch_credits, - } + pub fn default_v3() -> Self { + Self::new_v3(VoteStateV3::default()) } #[cfg(test)] - pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> { - match &self.target_state { - TargetVoteState::V3(v3) => { - if position < v3.votes.len() { - let pos = v3 - .votes - .len() - .checked_sub(position) - .and_then(|pos| pos.checked_sub(1))?; - v3.votes.get(pos).map(|vote| &vote.lockout) - } else { - None - } - } - } + pub fn default_v4() -> Self { + Self::new_v4(VoteStateV4::default()) } } @@ -1542,6 +1589,68 @@ mod tests { } } + fn init_vote_account_state_v4_and_assert( + vote_pubkey: Pubkey, + vote_account: AccountSharedData, + vote_init: &VoteInit, + clock: &Clock, + rent: Rent, + expected_result: Result<(), InstructionError>, + ) { + let transaction_context = mock_transaction_context(vote_pubkey, vote_account, rent); + let instruction_context = transaction_context.get_next_instruction_context().unwrap(); + let mut vote_account = instruction_context + .try_borrow_instruction_account(0) + .unwrap(); + + // Initialize. + let result = VoteStateHandler::init_vote_account_state( + &mut vote_account, + vote_init, + clock, + VoteStateTargetVersion::V4, + ); + assert_eq!(result, expected_result); + + if result.is_ok() { + let vote_state_versions = vote_account.get_state::().unwrap(); + assert!(matches!(vote_state_versions, VoteStateVersions::V4(_))); + assert!(!vote_state_versions.is_uninitialized()); + + // Verify fields. + if let VoteStateVersions::V4(v4) = vote_state_versions { + assert_eq!(v4.node_pubkey, vote_init.node_pubkey); + assert_eq!( + v4.authorized_voters.get_authorized_voter(0), + Some(vote_init.authorized_voter) + ); + assert_eq!(v4.authorized_withdrawer, vote_init.authorized_withdrawer); + assert_eq!( + v4.inflation_rewards_commission_bps, + (vote_init.commission as u16) * 100 + ); + + // SIMD-0185 fields + assert_eq!(v4.inflation_rewards_collector, vote_pubkey); + assert_eq!(v4.block_revenue_collector, vote_init.node_pubkey); + assert_eq!( + v4.block_revenue_commission_bps, + DEFAULT_BLOCK_REVENUE_COMMISSION_BPS + ); + + // Fields that should be default + assert_eq!(v4.pending_delegator_rewards, 0); + assert_eq!(v4.bls_pubkey_compressed, None); + assert!(v4.votes.is_empty()); + assert_eq!(v4.root_slot, None); + assert!(v4.epoch_credits.is_empty()); + assert_eq!(v4.last_timestamp, BlockTimestamp::default()); + } else { + panic!("should be v4"); + } + } + } + fn deinit_vote_account_state_v3_and_assert( vote_pubkey: Pubkey, vote_account: AccountSharedData, @@ -1578,6 +1687,32 @@ mod tests { } } + fn deinit_vote_account_state_v4_and_assert( + vote_pubkey: Pubkey, + vote_account: AccountSharedData, + rent: Rent, + expected_result: Result<(), InstructionError>, + ) { + let transaction_context = mock_transaction_context(vote_pubkey, vote_account, rent.clone()); + let instruction_context = transaction_context.get_next_instruction_context().unwrap(); + let mut vote_account = instruction_context + .try_borrow_instruction_account(0) + .unwrap(); + + // Deinitialize. + let result = VoteStateHandler::deinitialize_vote_account_state( + &mut vote_account, + VoteStateTargetVersion::V4, + ); + assert_eq!(result, expected_result); + + if result.is_ok() { + let vote_state_versions = vote_account.get_state::().unwrap(); + assert!(matches!(vote_state_versions, VoteStateVersions::V4(_))); + // v4 is always initialized + } + } + #[test] fn test_init_vote_account_state_v3() { let vote_pubkey = Pubkey::new_unique(); @@ -1636,6 +1771,64 @@ mod tests { ); } + #[test] + fn test_init_vote_account_state_v4() { + let vote_pubkey = Pubkey::new_unique(); + let vote_init = VoteInit { + node_pubkey: Pubkey::new_unique(), + authorized_voter: Pubkey::new_unique(), + authorized_withdrawer: Pubkey::new_unique(), + commission: 5, + }; + let clock = Clock::default(); + let rent = Rent::default(); + + // First create a vote account that's too small for v4. + let v1_14_11_size = VoteState1_14_11::size_of(); + let lamports = rent.minimum_balance(v1_14_11_size); + let vote_account = AccountSharedData::new(lamports, v1_14_11_size, &id()); + + // Initialize - should fail. + init_vote_account_state_v4_and_assert( + vote_pubkey, + vote_account, + &vote_init, + &clock, + rent.clone(), + Err(InstructionError::AccountNotRentExempt), + ); + + // Create a vote account that's too small for v4, but has enough + // lamports for resize. + let v4_size = VoteStateV4::size_of(); + let lamports = rent.minimum_balance(v4_size); + let vote_account = AccountSharedData::new(lamports, v1_14_11_size, &id()); + + // Initialize - should resize and create v4. + init_vote_account_state_v4_and_assert( + vote_pubkey, + vote_account, + &vote_init, + &clock, + rent.clone(), + Ok(()), + ); + + // Now create a vote account that's large enough for v4. + let lamports = rent.minimum_balance(v4_size); + let vote_account = AccountSharedData::new(lamports, v4_size, &id()); + + // Initialize - should create v4. + init_vote_account_state_v4_and_assert( + vote_pubkey, + vote_account, + &vote_init, + &clock, + rent, + Ok(()), + ); + } + #[test] fn test_deinitialize_vote_account_state_v3() { let vote_pubkey = Pubkey::new_unique(); @@ -1680,4 +1873,39 @@ mod tests { ExpectedVoteStateVersion::V3, ); } + + #[test] + fn test_deinitialize_vote_account_state_v4() { + let vote_pubkey = Pubkey::new_unique(); + let rent = Rent::default(); + + // First create a vote account that's too small for v4. + let v1_14_11_size = VoteState1_14_11::size_of(); + let lamports = rent.minimum_balance(v1_14_11_size); + let vote_account = AccountSharedData::new(lamports, v1_14_11_size, &id()); + + // Deinitialize - should fail. + deinit_vote_account_state_v4_and_assert( + vote_pubkey, + vote_account, + rent.clone(), + Err(InstructionError::AccountNotRentExempt), + ); + + // Create a vote account that's too small for v4, but has enough + // lamports for resize. + let v4_size = VoteStateV4::size_of(); + let lamports = rent.minimum_balance(v4_size); + let vote_account = AccountSharedData::new(lamports, v1_14_11_size, &id()); + + // Deinitialize - should resize and create v4. + deinit_vote_account_state_v4_and_assert(vote_pubkey, vote_account, rent.clone(), Ok(())); + + // Now create a vote account that's large enough for v4. + let lamports = rent.minimum_balance(v4_size); + let vote_account = AccountSharedData::new(lamports, v4_size, &id()); + + // Deinitialize - should create v4. + deinit_vote_account_state_v4_and_assert(vote_pubkey, vote_account, rent, Ok(())); + } } diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 702ae9cbdd1c33..aff127ea56b5d6 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -3,9 +3,8 @@ mod handler; -pub use solana_vote_interface::state::{vote_state_versions::*, *}; use { - handler::{VoteStateHandle, VoteStateHandler, VoteStateTargetVersion}, + handler::VoteStateHandler, log::*, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_clock::{Clock, Epoch, Slot}, @@ -22,6 +21,10 @@ use { collections::{HashSet, VecDeque}, }, }; +pub use { + handler::{VoteStateHandle, VoteStateTargetVersion}, + solana_vote_interface::state::{vote_state_versions::*, *}, +}; // utility function, used by Stakes, tests pub fn from(account: &T) -> Option { @@ -672,13 +675,13 @@ pub fn process_slot_vote_unchecked(vote_state: &mut T, slot: /// key pub fn authorize( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, authorized: &Pubkey, vote_authorize: VoteAuthorize, signers: &HashSet, clock: &Clock, ) -> Result<(), InstructionError> { - let mut vote_state = - VoteStateHandler::deserialize_and_convert(vote_account, VoteStateTargetVersion::V3)?; + let mut vote_state = VoteStateHandler::deserialize_and_convert(vote_account, target_version)?; match vote_authorize { VoteAuthorize::Voter => { @@ -715,11 +718,11 @@ pub fn authorize( /// Update the node_pubkey, requires signature of the authorized voter pub fn update_validator_identity( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, node_pubkey: &Pubkey, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = - VoteStateHandler::deserialize_and_convert(vote_account, VoteStateTargetVersion::V3)?; + let mut vote_state = VoteStateHandler::deserialize_and_convert(vote_account, target_version)?; // current authorized withdrawer must say "yay" verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?; @@ -735,13 +738,13 @@ pub fn update_validator_identity( /// Update the vote account's commission pub fn update_commission( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, commission: u8, signers: &HashSet, epoch_schedule: &EpochSchedule, clock: &Clock, ) -> Result<(), InstructionError> { - let vote_state_result = - VoteStateHandler::deserialize_and_convert(vote_account, VoteStateTargetVersion::V3); + let vote_state_result = VoteStateHandler::deserialize_and_convert(vote_account, target_version); let enforce_commission_update_rule = if let Ok(decoded_vote_state) = &vote_state_result { commission > decoded_vote_state.commission() } else { @@ -762,11 +765,6 @@ pub fn update_commission( vote_state.set_vote_account_state(vote_account) } -/// Given a proposed new commission, returns true if this would be a commission increase, false otherwise -pub fn is_commission_increase(vote_state: &VoteStateV3, commission: u8) -> bool { - commission > vote_state.commission -} - /// Given the current slot and epoch schedule, determine if a commission change /// is allowed pub fn is_commission_update_allowed(slot: Slot, epoch_schedule: &EpochSchedule) -> bool { @@ -798,6 +796,7 @@ fn verify_authorized_signer( pub fn withdraw( instruction_context: &InstructionContext, vote_account_index: IndexOfAccount, + target_version: VoteStateTargetVersion, lamports: u64, to_account_index: IndexOfAccount, signers: &HashSet, @@ -806,8 +805,7 @@ pub fn withdraw( ) -> Result<(), InstructionError> { let mut vote_account = instruction_context.try_borrow_instruction_account(vote_account_index)?; - let vote_state = - VoteStateHandler::deserialize_and_convert(&vote_account, VoteStateTargetVersion::V3)?; + let vote_state = VoteStateHandler::deserialize_and_convert(&vote_account, target_version)?; verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?; @@ -833,10 +831,7 @@ pub fn withdraw( return Err(VoteError::ActiveVoteAccountClose.into()); } else { // Deinitialize upon zero-balance - VoteStateHandler::deinitialize_vote_account_state( - &mut vote_account, - VoteStateTargetVersion::V3, - )?; + VoteStateHandler::deinitialize_vote_account_state(&mut vote_account, target_version)?; } } else { let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.get_data().len()); @@ -857,11 +852,12 @@ pub fn withdraw( /// that the transaction must be signed by the staker's keys pub fn initialize_account( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, vote_init: &VoteInit, signers: &HashSet, clock: &Clock, ) -> Result<(), InstructionError> { - VoteStateHandler::check_vote_account_length(vote_account, VoteStateTargetVersion::V3)?; + VoteStateHandler::check_vote_account_length(vote_account, target_version)?; let versioned = vote_account.get_state::()?; if !versioned.is_uninitialized() { @@ -871,21 +867,16 @@ pub fn initialize_account( // node must agree to accept this vote account verify_authorized_signer(&vote_init.node_pubkey, signers)?; - VoteStateHandler::init_vote_account_state( - vote_account, - vote_init, - clock, - VoteStateTargetVersion::V3, - ) + VoteStateHandler::init_vote_account_state(vote_account, vote_init, clock, target_version) } fn verify_and_get_vote_state_handler( vote_account: &BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, clock: &Clock, signers: &HashSet, ) -> Result { - let mut vote_state = - VoteStateHandler::deserialize_and_convert(vote_account, VoteStateTargetVersion::V3)?; + let mut vote_state = VoteStateHandler::deserialize_and_convert(vote_account, target_version)?; if vote_state.is_uninitialized() { return Err(InstructionError::UninitializedAccount); @@ -899,12 +890,14 @@ fn verify_and_get_vote_state_handler( pub fn process_vote_with_account( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, slot_hashes: &[SlotHash], clock: &Clock, vote: &Vote, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = verify_and_get_vote_state_handler(vote_account, clock, signers)?; + let mut vote_state = + verify_and_get_vote_state_handler(vote_account, target_version, clock, signers)?; process_vote(&mut vote_state, vote, slot_hashes, clock.epoch, clock.slot)?; if let Some(timestamp) = vote.timestamp { @@ -919,12 +912,14 @@ pub fn process_vote_with_account( pub fn process_vote_state_update( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, slot_hashes: &[SlotHash], clock: &Clock, vote_state_update: VoteStateUpdate, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = verify_and_get_vote_state_handler(vote_account, clock, signers)?; + let mut vote_state = + verify_and_get_vote_state_handler(vote_account, target_version, clock, signers)?; do_process_vote_state_update( &mut vote_state, slot_hashes, @@ -965,12 +960,14 @@ pub fn do_process_vote_state_update( pub fn process_tower_sync( vote_account: &mut BorrowedInstructionAccount, + target_version: VoteStateTargetVersion, slot_hashes: &[SlotHash], clock: &Clock, tower_sync: TowerSync, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = verify_and_get_vote_state_handler(vote_account, clock, signers)?; + let mut vote_state = + verify_and_get_vote_state_handler(vote_account, target_version, clock, signers)?; do_process_tower_sync( &mut vote_state, slot_hashes, @@ -1078,73 +1075,78 @@ pub fn create_account( create_account_with_authorized(node_pubkey, vote_pubkey, vote_pubkey, commission, lamports) } +#[allow(clippy::arithmetic_side_effects)] #[cfg(test)] mod tests { use { super::*, - crate::vote_state, + crate::vote_state::handler::create_new_vote_state_v4, assert_matches::assert_matches, solana_account::AccountSharedData, solana_clock::DEFAULT_SLOTS_PER_EPOCH, + solana_sdk_ids::native_loader, solana_sha256_hasher::hash, solana_transaction_context::{InstructionAccount, TransactionContext}, solana_vote_interface::authorized_voters::AuthorizedVoters, - std::cell::RefCell, - test_case::test_case, + test_case::{test_case, test_matrix}, }; const MAX_RECENT_VOTES: usize = 16; - fn vote_state_new_for_test(auth_pubkey: &Pubkey) -> VoteStateHandler { - VoteStateHandler::new_v3(VoteStateV3::new( - &VoteInit { - node_pubkey: solana_pubkey::new_rand(), - authorized_voter: *auth_pubkey, - authorized_withdrawer: *auth_pubkey, - commission: 0, - }, - &Clock::default(), - )) - } - - fn create_test_account() -> (Pubkey, RefCell) { - let rent = Rent::default(); - let balance = rent.minimum_balance(VoteStateV3::size_of()); - let vote_pubkey = solana_pubkey::new_rand(); - ( - vote_pubkey, - RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_pubkey::new_rand(), - 0, - balance, - )), - ) + fn vote_state_new_for_test( + target_version: VoteStateTargetVersion, + vote_pubkey: &Pubkey, + auth_pubkey: &Pubkey, + ) -> VoteStateHandler { + match target_version { + VoteStateTargetVersion::V3 => { + let vote_state_v3 = VoteStateV3::new( + &VoteInit { + node_pubkey: solana_pubkey::new_rand(), + authorized_voter: *auth_pubkey, + authorized_withdrawer: *auth_pubkey, + commission: 0, + }, + &Clock::default(), + ); + VoteStateHandler::new_v3(vote_state_v3) + } + VoteStateTargetVersion::V4 => { + let vote_state_v4 = create_new_vote_state_v4( + vote_pubkey, + &VoteInit { + node_pubkey: solana_pubkey::new_rand(), + authorized_voter: *auth_pubkey, + authorized_withdrawer: *auth_pubkey, + commission: 0, + }, + &Clock::default(), + ); + VoteStateHandler::new_v4(vote_state_v4) + } + } } - fn get_credits(epoch_credits: &[(Epoch, u64, u64)]) -> u64 { - if epoch_credits.is_empty() { - 0 - } else { - epoch_credits.last().unwrap().1 + fn vote_state_new_from_defaults_for_test( + target_version: VoteStateTargetVersion, + ) -> VoteStateHandler { + match target_version { + VoteStateTargetVersion::V3 => { + let vote_state_v3 = VoteStateV3::new(&VoteInit::default(), &Clock::default()); + VoteStateHandler::new_v3(vote_state_v3) + } + VoteStateTargetVersion::V4 => { + let vote_state_v4 = create_new_vote_state_v4( + &solana_pubkey::new_rand(), + &VoteInit::default(), + &Clock::default(), + ); + VoteStateHandler::new_v4(vote_state_v4) + } } } - #[test] - fn test_vote_state_upgrade_from_1_14_11() { - // Create an initial vote account that is sized for the 1_14_11 version of vote state, and has only the - // required lamports for rent exempt minimum at that size - let node_pubkey = solana_pubkey::new_rand(); - let withdrawer_pubkey = solana_pubkey::new_rand(); - let mut vote_state = VoteStateV3::new( - &VoteInit { - node_pubkey, - authorized_voter: withdrawer_pubkey, - authorized_withdrawer: withdrawer_pubkey, - commission: 10, - }, - &Clock::default(), - ); + fn setup_vote_state_for_test_upgrade_from_1_14_11(vote_state: &mut T) { // Simulate prior epochs completed with credits and each setting a new authorized voter vote_state.increment_credits(0, 100); assert_eq!( @@ -1169,24 +1171,17 @@ mod tests { ] .into_iter() .for_each(|v| vote_state.process_next_vote_slot(v, 4, 0)); + } - let version1_14_11_serialized = bincode::serialize(&VoteStateVersions::V1_14_11(Box::new( - VoteState1_14_11::from(vote_state.clone()), - ))) - .unwrap(); - let version1_14_11_serialized_len = version1_14_11_serialized.len(); - let rent = Rent::default(); - let lamports = rent.minimum_balance(version1_14_11_serialized_len); - let mut vote_account = - AccountSharedData::new(lamports, version1_14_11_serialized_len, &id()); - vote_account.set_data_from_slice(&version1_14_11_serialized); - - // Create a fake TransactionContext with a fake InstructionContext with a single account which is the - // vote account that was just created - let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id()); + fn mock_transaction_context( + vote_pubkey: Pubkey, + vote_account: AccountSharedData, + rent: Rent, + ) -> TransactionContext { + let program_account = AccountSharedData::new(0, 0, &native_loader::id()); let mut transaction_context = TransactionContext::new( - vec![(id(), processor_account), (node_pubkey, vote_account)], - rent.clone(), + vec![(id(), program_account), (vote_pubkey, vote_account)], + rent, 0, 0, ); @@ -1197,6 +1192,40 @@ mod tests { &[], ) .unwrap(); + transaction_context + } + + #[test] + fn test_vote_state_upgrade_from_1_14_11_to_v3() { + // Create an initial vote account that is sized for the 1_14_11 version of vote state, and has only the + // required lamports for rent exempt minimum at that size + let node_pubkey = solana_pubkey::new_rand(); + let vote_pubkey = solana_pubkey::new_rand(); + let withdrawer_pubkey = solana_pubkey::new_rand(); + let mut vote_state = VoteStateV3::new( + &VoteInit { + node_pubkey, + authorized_voter: withdrawer_pubkey, + authorized_withdrawer: withdrawer_pubkey, + commission: 10, + }, + &Clock::default(), + ); + + setup_vote_state_for_test_upgrade_from_1_14_11(&mut vote_state); + + let version1_14_11_serialized = bincode::serialize(&VoteStateVersions::V1_14_11(Box::new( + VoteState1_14_11::from(vote_state.clone()), + ))) + .unwrap(); + let version1_14_11_serialized_len = version1_14_11_serialized.len(); + let rent = Rent::default(); + let lamports = rent.minimum_balance(version1_14_11_serialized_len); + let mut vote_account = + AccountSharedData::new(lamports, version1_14_11_serialized_len, &id()); + vote_account.set_data_from_slice(&version1_14_11_serialized); + + let transaction_context = mock_transaction_context(vote_pubkey, vote_account, rent.clone()); let instruction_context = transaction_context.get_next_instruction_context().unwrap(); // Get the BorrowedAccount from the InstructionContext which is what is used to manipulate and inspect account @@ -1275,12 +1304,141 @@ mod tests { } #[test] - fn test_vote_lockout() { - let (_vote_pubkey, vote_account) = create_test_account(); + fn test_vote_state_upgrade_from_1_14_11_to_v4() { + let node_pubkey = solana_pubkey::new_rand(); + let vote_pubkey = solana_pubkey::new_rand(); + let withdrawer_pubkey = solana_pubkey::new_rand(); + + let mut vote_state = VoteStateV4 { + node_pubkey, + authorized_withdrawer: withdrawer_pubkey, + authorized_voters: AuthorizedVoters::new(Clock::default().epoch, withdrawer_pubkey), + inflation_rewards_commission_bps: 1000, // 10% in basis points + inflation_rewards_collector: Pubkey::default(), + block_revenue_collector: Pubkey::default(), + block_revenue_commission_bps: 0, + pending_delegator_rewards: 0, + bls_pubkey_compressed: None, + votes: VecDeque::new(), + root_slot: None, + epoch_credits: Vec::new(), + last_timestamp: BlockTimestamp::default(), + }; + + setup_vote_state_for_test_upgrade_from_1_14_11(&mut vote_state); + + // Convert V4 to V1_14_11 for serialization (lossy). + let vote_state_1_14_11 = VoteState1_14_11 { + node_pubkey: vote_state.node_pubkey, + authorized_withdrawer: vote_state.authorized_withdrawer, + commission: (vote_state.inflation_rewards_commission_bps / 100) as u8, + votes: vote_state + .votes + .clone() + .into_iter() + .map(|landed_vote| landed_vote.into()) + .collect(), + root_slot: vote_state.root_slot, + authorized_voters: vote_state.authorized_voters.clone(), + epoch_credits: vote_state.epoch_credits.clone(), + last_timestamp: vote_state.last_timestamp.clone(), + ..Default::default() + }; + + let version1_14_11_serialized = + bincode::serialize(&VoteStateVersions::V1_14_11(Box::new(vote_state_1_14_11))).unwrap(); + let version1_14_11_serialized_len = version1_14_11_serialized.len(); + let rent = Rent::default(); + let lamports = rent.minimum_balance(version1_14_11_serialized_len); + let mut vote_account = + AccountSharedData::new(lamports, version1_14_11_serialized_len, &id()); + vote_account.set_data_from_slice(&version1_14_11_serialized); + + let transaction_context = mock_transaction_context(vote_pubkey, vote_account, rent.clone()); + let instruction_context = transaction_context.get_next_instruction_context().unwrap(); + let mut borrowed_account = instruction_context + .try_borrow_instruction_account(0) + .unwrap(); + + // Ensure that the vote state started out as a 1_14_11. + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_)); + + // Convert the vote state to V4 as would occur during vote processing. + let converted_vote_state = + VoteStateV4::deserialize(borrowed_account.get_data(), &vote_pubkey).unwrap(); + + // Some v4 fields get synthesized values when deserializing from V1_14_11: + // * inflation_rewards_collector is set to the vote_pubkey + // * block_revenue_collector is set to the node_pubkey + // * block_revenue_commission_bps is set to 10_000 (100%) + assert_eq!( + converted_vote_state.inflation_rewards_collector, + vote_pubkey + ); + assert_eq!(converted_vote_state.block_revenue_collector, node_pubkey); + assert_eq!(converted_vote_state.block_revenue_commission_bps, 10_000); + assert_eq!(converted_vote_state.pending_delegator_rewards, 0); + + // The remaining fields are preserved. + assert_eq!(converted_vote_state.node_pubkey, vote_state.node_pubkey); + assert_eq!( + converted_vote_state.authorized_withdrawer, + vote_state.authorized_withdrawer + ); + assert_eq!( + converted_vote_state.authorized_voters, + vote_state.authorized_voters + ); + assert_eq!(converted_vote_state.votes, vote_state.votes); + assert_eq!(converted_vote_state.root_slot, vote_state.root_slot); + assert_eq!(converted_vote_state.epoch_credits, vote_state.epoch_credits); + assert_eq!( + converted_vote_state.last_timestamp, + vote_state.last_timestamp + ); + assert_eq!( + converted_vote_state.inflation_rewards_commission_bps, + vote_state.inflation_rewards_commission_bps + ); + + let vote_state = converted_vote_state; + + // Now attempt to re-set the vote account state. + // The program will return an error, since a target version v4 means an + // account too small for v4 size returns an error. + assert_eq!( + VoteStateHandler::new_v4(vote_state.clone()) + .set_vote_account_state(&mut borrowed_account), + Err(InstructionError::AccountNotRentExempt) + ); + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_)); - let vote_state_v3 = VoteStateV3::deserialize(vote_account.borrow().data()).unwrap(); - let mut vote_state = VoteStateHandler::new_v3(vote_state_v3); + // Now give it sufficient lamports for V4 space. + assert_eq!( + borrowed_account.set_lamports(rent.minimum_balance(VoteStateV4::size_of())), + Ok(()) + ); + // Now it should succeed. + assert_eq!( + VoteStateHandler::new_v4(vote_state.clone()) + .set_vote_account_state(&mut borrowed_account), + Ok(()) + ); + let vote_state_version = borrowed_account.get_state::().unwrap(); + assert_matches!(vote_state_version, VoteStateVersions::V4(_)); + + // Verify the vote state data is preserved. + let final_vote_state = + VoteStateV4::deserialize(borrowed_account.get_data(), &vote_pubkey).unwrap(); + assert_eq!(final_vote_state, vote_state); + } + + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_vote_lockout(mut vote_state: VoteStateHandler) { for i in 0..(MAX_LOCKOUT_HISTORY + 1) { process_slot_vote_unchecked(&mut vote_state, (INITIAL_LOCKOUT * i) as u64); } @@ -1310,24 +1468,46 @@ mod tests { assert_eq!(vote_state.votes().len(), 2); } - #[test] - fn test_update_commission() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_update_commission(target_version: VoteStateTargetVersion) { let node_pubkey = Pubkey::new_unique(); let withdrawer_pubkey = Pubkey::new_unique(); + let vote_pubkey = Pubkey::new_unique(); let clock = Clock::default(); - let vote_state = VoteStateV3::new( - &VoteInit { - node_pubkey, - authorized_voter: withdrawer_pubkey, - authorized_withdrawer: withdrawer_pubkey, - commission: 10, - }, - &clock, - ); - let serialized = - bincode::serialize(&VoteStateVersions::V3(Box::new(vote_state.clone()))).unwrap(); - let serialized_len = serialized.len(); + let (vote_state_versions, serialized_len) = match target_version { + VoteStateTargetVersion::V3 => { + let vote_state = VoteStateV3::new( + &VoteInit { + node_pubkey, + authorized_voter: withdrawer_pubkey, + authorized_withdrawer: withdrawer_pubkey, + commission: 10, + }, + &clock, + ); + ( + VoteStateVersions::V3(Box::new(vote_state)), + VoteStateV3::size_of(), + ) + } + VoteStateTargetVersion::V4 => { + let vote_state = VoteStateV4 { + node_pubkey, + authorized_withdrawer: withdrawer_pubkey, + authorized_voters: AuthorizedVoters::new(clock.epoch, withdrawer_pubkey), + inflation_rewards_commission_bps: 10 * 100, // commission is in basis points for V4 + ..VoteStateV4::default() + }; + ( + VoteStateVersions::V4(Box::new(vote_state)), + VoteStateV4::size_of(), + ) + } + }; + + let serialized = bincode::serialize(&vote_state_versions).unwrap(); let rent = Rent::default(); let lamports = rent.minimum_balance(serialized_len); let mut vote_account = AccountSharedData::new(lamports, serialized_len, &id()); @@ -1371,16 +1551,28 @@ mod tests { let signers: HashSet = vec![withdrawer_pubkey].into_iter().collect(); + // Helper to get current commission based on vote state version + let get_commission = |account: &BorrowedInstructionAccount| -> u8 { + match target_version { + VoteStateTargetVersion::V3 => { + VoteStateV3::deserialize(account.get_data()) + .unwrap() + .commission + } + VoteStateTargetVersion::V4 => { + let vote_state = + VoteStateV4::deserialize(account.get_data(), &vote_pubkey).unwrap(); + (vote_state.inflation_rewards_commission_bps / 100) as u8 + } + } + }; + // Increase commission in first half of epoch -- allowed - assert_eq!( - VoteStateV3::deserialize(borrowed_account.get_data()) - .unwrap() - .commission, - 10 - ); + assert_eq!(get_commission(&borrowed_account), 10); assert_matches!( update_commission( &mut borrowed_account, + target_version, 11, &signers, &epoch_schedule, @@ -1388,17 +1580,13 @@ mod tests { ), Ok(()) ); - assert_eq!( - VoteStateV3::deserialize(borrowed_account.get_data()) - .unwrap() - .commission, - 11 - ); + assert_eq!(get_commission(&borrowed_account), 11); // Increase commission in second half of epoch -- disallowed assert_matches!( update_commission( &mut borrowed_account, + target_version, 12, &signers, &epoch_schedule, @@ -1406,17 +1594,13 @@ mod tests { ), Err(_) ); - assert_eq!( - VoteStateV3::deserialize(borrowed_account.get_data()) - .unwrap() - .commission, - 11 - ); + assert_eq!(get_commission(&borrowed_account), 11); // Decrease commission in first half of epoch -- allowed assert_matches!( update_commission( &mut borrowed_account, + target_version, 10, &signers, &epoch_schedule, @@ -1424,23 +1608,12 @@ mod tests { ), Ok(()) ); - assert_eq!( - VoteStateV3::deserialize(borrowed_account.get_data()) - .unwrap() - .commission, - 10 - ); - - assert_eq!( - VoteStateV3::deserialize(borrowed_account.get_data()) - .unwrap() - .commission, - 10 - ); + assert_eq!(get_commission(&borrowed_account), 10); assert_matches!( update_commission( &mut borrowed_account, + target_version, 9, &signers, &epoch_schedule, @@ -1448,18 +1621,15 @@ mod tests { ), Ok(()) ); - assert_eq!( - VoteStateV3::deserialize(borrowed_account.get_data()) - .unwrap() - .commission, - 9 - ); + assert_eq!(get_commission(&borrowed_account), 9); } - #[test] - fn test_vote_double_lockout_after_expiration() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_vote_double_lockout_after_expiration(target_version: VoteStateTargetVersion) { + let vote_pubkey = solana_pubkey::new_rand(); let voter_pubkey = solana_pubkey::new_rand(); - let mut vote_state = vote_state_new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(target_version, &vote_pubkey, &voter_pubkey); for i in 0..3 { process_slot_vote_unchecked(&mut vote_state, i as u64); @@ -1484,10 +1654,12 @@ mod tests { check_lockouts(&vote_state); } - #[test] - fn test_expire_multiple_votes() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_expire_multiple_votes(target_version: VoteStateTargetVersion) { + let vote_pubkey = solana_pubkey::new_rand(); let voter_pubkey = solana_pubkey::new_rand(); - let mut vote_state = vote_state_new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(target_version, &vote_pubkey, &voter_pubkey); for i in 0..3 { process_slot_vote_unchecked(&mut vote_state, i as u64); @@ -1516,29 +1688,33 @@ mod tests { assert_eq!(vote_state.votes()[2].confirmation_count(), 1); } - #[test] - fn test_vote_credits() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_vote_credits(target_version: VoteStateTargetVersion) { + let vote_pubkey = solana_pubkey::new_rand(); let voter_pubkey = solana_pubkey::new_rand(); - let mut vote_state = vote_state_new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(target_version, &vote_pubkey, &voter_pubkey); for i in 0..MAX_LOCKOUT_HISTORY { process_slot_vote_unchecked(&mut vote_state, i as u64); } - assert_eq!(get_credits(vote_state.epoch_credits()), 0); + assert_eq!(vote_state.credits(), 0); process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 1); - assert_eq!(get_credits(vote_state.epoch_credits()), 1); + assert_eq!(vote_state.credits(), 1); process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 2); - assert_eq!(get_credits(vote_state.epoch_credits()), 2); + assert_eq!(vote_state.credits(), 2); process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 3); - assert_eq!(get_credits(vote_state.epoch_credits()), 3); + assert_eq!(vote_state.credits(), 3); } - #[test] - fn test_duplicate_vote() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_duplicate_vote(target_version: VoteStateTargetVersion) { + let vote_pubkey = solana_pubkey::new_rand(); let voter_pubkey = solana_pubkey::new_rand(); - let mut vote_state = vote_state_new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(target_version, &vote_pubkey, &voter_pubkey); process_slot_vote_unchecked(&mut vote_state, 0); process_slot_vote_unchecked(&mut vote_state, 1); process_slot_vote_unchecked(&mut vote_state, 0); @@ -1547,10 +1723,12 @@ mod tests { assert!(vote_state.nth_recent_lockout(2).is_none()); } - #[test] - fn test_nth_recent_lockout() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_nth_recent_lockout(target_version: VoteStateTargetVersion) { + let vote_pubkey = solana_pubkey::new_rand(); let voter_pubkey = solana_pubkey::new_rand(); - let mut vote_state = vote_state_new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(target_version, &vote_pubkey, &voter_pubkey); for i in 0..MAX_LOCKOUT_HISTORY { process_slot_vote_unchecked(&mut vote_state, i as u64); } @@ -1586,12 +1764,14 @@ mod tests { } /// check that two accounts with different data can be brought to the same state with one vote submission - #[test] - fn test_process_missed_votes() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_process_missed_votes(target_version: VoteStateTargetVersion) { + let vote_pubkey = solana_pubkey::new_rand(); let account_a = solana_pubkey::new_rand(); - let mut vote_state_a = vote_state_new_for_test(&account_a); + let mut vote_state_a = vote_state_new_for_test(target_version, &vote_pubkey, &account_a); let account_b = solana_pubkey::new_rand(); - let mut vote_state_b = vote_state_new_for_test(&account_b); + let mut vote_state_b = vote_state_new_for_test(target_version, &vote_pubkey, &account_b); // process some votes on account a (0..5).for_each(|i| process_slot_vote_unchecked(&mut vote_state_a, i as u64)); @@ -1613,10 +1793,9 @@ mod tests { assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b)); } - #[test] - fn test_process_vote_skips_old_vote() { - let mut vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_process_vote_skips_old_vote(mut vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(0, vote.hash)]; assert_eq!( @@ -1631,10 +1810,9 @@ mod tests { assert_eq!(recent, recent_votes(&vote_state)); } - #[test] - fn test_check_slots_are_valid_vote_empty_slot_hashes() { - let vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_vote_empty_slot_hashes(vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); assert_eq!( check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &[]), @@ -1642,10 +1820,9 @@ mod tests { ); } - #[test] - fn test_check_slots_are_valid_new_vote() { - let vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_new_vote(vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( @@ -1654,10 +1831,9 @@ mod tests { ); } - #[test] - fn test_check_slots_are_valid_bad_hash() { - let vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_bad_hash(vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), hash(vote.hash.as_ref()))]; assert_eq!( @@ -1666,10 +1842,9 @@ mod tests { ); } - #[test] - fn test_check_slots_are_valid_bad_slot() { - let vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_bad_slot(vote_state: VoteStateHandler) { let vote = Vote::new(vec![1], Hash::default()); let slot_hashes: Vec<_> = vec![(0, vote.hash)]; assert_eq!( @@ -1678,10 +1853,9 @@ mod tests { ); } - #[test] - fn test_check_slots_are_valid_duplicate_vote() { - let mut vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_duplicate_vote(mut vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( @@ -1694,10 +1868,9 @@ mod tests { ); } - #[test] - fn test_check_slots_are_valid_next_vote() { - let mut vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_next_vote(mut vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( @@ -1713,10 +1886,9 @@ mod tests { ); } - #[test] - fn test_check_slots_are_valid_next_vote_only() { - let mut vote_state = VoteStateHandler::default_v3(); - + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_check_slots_are_valid_next_vote_only(mut vote_state: VoteStateHandler) { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( @@ -1731,10 +1903,10 @@ mod tests { Ok(()) ); } - #[test] - fn test_process_vote_empty_slots() { - let mut vote_state = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_process_vote_empty_slots(mut vote_state: VoteStateHandler) { let vote = Vote::new(vec![], Hash::default()); assert_eq!( process_vote(&mut vote_state, &vote, &[], 0, 0), @@ -1760,12 +1932,9 @@ mod tests { } // Test vote credit updates after "one credit per slot" feature is enabled - #[test] - fn test_vote_state_update_increment_credits() { - // Create a new VoteStateV3 handler - let mut vote_state = - VoteStateHandler::new_v3(VoteStateV3::new(&VoteInit::default(), &Clock::default())); - + #[test_case(vote_state_new_from_defaults_for_test(VoteStateTargetVersion::V3); "V3")] + #[test_case(vote_state_new_from_defaults_for_test(VoteStateTargetVersion::V4); "V4")] + fn test_vote_state_update_increment_credits(mut vote_state: VoteStateHandler) { // Test data: a sequence of groups of votes to simulate having been cast, after each group a vote // state update is compared to "normal" vote processing to ensure that credits are earned equally let test_vote_groups: Vec> = vec![ @@ -1840,8 +2009,14 @@ mod tests { } // Test vote credit updates after "timely vote credits" feature is enabled - #[test] - fn test_timely_credits() { + #[test_matrix( + (VoteStateTargetVersion::V3, VoteStateTargetVersion::V4), + (VoteStateTargetVersion::V3, VoteStateTargetVersion::V4) + )] + fn test_timely_credits( + target_version_1: VoteStateTargetVersion, + target_version_2: VoteStateTargetVersion, + ) { // Each of the following (Vec, Slot, u32) tuples gives a set of slots to cast votes on, a slot in which // the vote was cast, and the number of credits that should have been earned by the vote account after this // and all prior votes were cast. @@ -2010,11 +2185,9 @@ mod tests { // credits earned is correct for both regular votes and vote state updates for i in 0..test_vote_groups.len() { // Create a new VoteStateV3 for vote transaction - let mut vote_state_1 = - VoteStateHandler::new_v3(VoteStateV3::new(&VoteInit::default(), &Clock::default())); + let mut vote_state_1 = vote_state_new_from_defaults_for_test(target_version_1); // Create a new VoteStateV3 for vote state update transaction - let mut vote_state_2 = - VoteStateHandler::new_v3(VoteStateV3::new(&VoteInit::default(), &Clock::default())); + let mut vote_state_2 = vote_state_new_from_defaults_for_test(target_version_2); test_vote_groups.iter().take(i + 1).for_each(|vote_group| { let vote = Vote { slots: vote_group.0.clone(), //vote_group.0 is the set of slots to cast votes on @@ -2049,19 +2222,14 @@ mod tests { // Ensure that the credits earned is correct for both vote states let vote_group = &test_vote_groups[i]; - assert_eq!( - get_credits(vote_state_1.epoch_credits()), - vote_group.2 as u64 - ); // vote_group.2 is the expected number of credits - assert_eq!( - get_credits(vote_state_2.epoch_credits()), - vote_group.2 as u64 - ); // vote_group.2 is the expected number of credits + assert_eq!(vote_state_1.credits(), vote_group.2 as u64); // vote_group.2 is the expected number of credits + assert_eq!(vote_state_2.credits(), vote_group.2 as u64); // vote_group.2 is the expected number of credits } } - #[test] - fn test_retroactive_voting_timely_credits() { + #[test_case(vote_state_new_from_defaults_for_test(VoteStateTargetVersion::V3); "V3")] + #[test_case(vote_state_new_from_defaults_for_test(VoteStateTargetVersion::V4); "V4")] + fn test_retroactive_voting_timely_credits(mut vote_state: VoteStateHandler) { // Each of the following (Vec<(Slot, int)>, Slot, Option, u32) tuples gives the following data: // Vec<(Slot, int)> -- the set of slots and confirmation_counts that is the proposed vote state // Slot -- the slot in which the proposed vote state landed @@ -2142,10 +2310,6 @@ mod tests { ), ]; - // Initial vote state - let mut vote_state = - VoteStateHandler::new_v3(VoteStateV3::new(&VoteInit::default(), &Clock::default())); - // Process the vote state updates in sequence and ensure that the credits earned after each is processed is // correct test_vote_state_updates @@ -2172,16 +2336,13 @@ mod tests { ); // Ensure that the credits earned is correct - assert_eq!( - get_credits(vote_state.epoch_credits()), - proposed_vote_state.3 as u64 - ); + assert_eq!(vote_state.credits(), proposed_vote_state.3 as u64); }); } - #[test] - fn test_process_new_vote_too_many_votes() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_process_new_vote_too_many_votes(mut vote_state1: VoteStateHandler) { let bad_votes: VecDeque = (0..=MAX_LOCKOUT_HISTORY) .map(|slot| { Lockout::new_with_confirmation_count( @@ -2204,9 +2365,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_root_rollback() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_process_new_vote_state_root_rollback(mut vote_state1: VoteStateHandler) { for i in 0..MAX_LOCKOUT_HISTORY + 2 { process_slot_vote_unchecked(&mut vote_state1, i as Slot); } @@ -2248,9 +2409,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_zero_confirmations() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_process_new_vote_state_zero_confirmations(mut vote_state1: VoteStateHandler) { let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ @@ -2288,9 +2449,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_confirmations_too_large() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3())] + #[test_case(VoteStateHandler::default_v4())] + fn test_process_new_vote_state_confirmations_too_large(mut vote_state1: VoteStateHandler) { let current_epoch = vote_state1.current_epoch(); let good_votes: VecDeque = vec![Lockout::new_with_confirmation_count( @@ -2328,9 +2489,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_slot_smaller_than_root() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_slot_smaller_than_root(mut vote_state1: VoteStateHandler) { let current_epoch = vote_state1.current_epoch(); let root_slot = 5; @@ -2369,9 +2530,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_slots_not_ordered() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_slots_not_ordered(mut vote_state1: VoteStateHandler) { let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ @@ -2409,9 +2570,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_confirmations_not_ordered() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_confirmations_not_ordered(mut vote_state1: VoteStateHandler) { let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ @@ -2449,9 +2610,11 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_new_vote_state_lockout_mismatch() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_new_vote_state_lockout_mismatch( + mut vote_state1: VoteStateHandler, + ) { let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ @@ -2474,9 +2637,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_confirmation_rollback() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_confirmation_rollback(mut vote_state1: VoteStateHandler) { let current_epoch = vote_state1.current_epoch(); let votes: VecDeque = vec![ Lockout::new_with_confirmation_count(0, 4), @@ -2509,9 +2672,9 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_root_progress() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_root_progress(mut vote_state1: VoteStateHandler) { for i in 0..MAX_LOCKOUT_HISTORY { process_slot_vote_unchecked(&mut vote_state1, i as u64); } @@ -2543,8 +2706,12 @@ mod tests { } } - #[test] - fn test_process_new_vote_state_same_slot_but_not_common_ancestor() { + #[test_case(VoteStateHandler::default_v3(), VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4(), VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_same_slot_but_not_common_ancestor( + mut vote_state1: VoteStateHandler, + mut vote_state2: VoteStateHandler, + ) { // It might be possible that during the switch from old vote instructions // to new vote instructions, new_state contains votes for slots LESS // than the current state, for instance: @@ -2563,7 +2730,6 @@ mod tests { // will immediately pop off 2. // Construct on-chain vote state - let mut vote_state1 = VoteStateHandler::default_v3(); process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5]); assert_eq!( vote_state1 @@ -2574,8 +2740,6 @@ mod tests { vec![1, 5] ); - // Construct local tower state - let mut vote_state2 = VoteStateHandler::default_v3(); process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]); assert_eq!( vote_state2 @@ -2600,10 +2764,10 @@ mod tests { assert_eq!(vote_state1, vote_state2); } - #[test] - fn test_process_new_vote_state_lockout_violation() { + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_lockout_violation(mut vote_state1: VoteStateHandler) { // Construct on-chain vote state - let mut vote_state1 = VoteStateHandler::default_v3(); process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 4, 5]); assert_eq!( vote_state1 @@ -2641,10 +2805,10 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_lockout_violation2() { + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_lockout_violation2(mut vote_state1: VoteStateHandler) { // Construct on-chain vote state - let mut vote_state1 = VoteStateHandler::default_v3(); process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5, 6, 7]); assert_eq!( vote_state1 @@ -2683,10 +2847,10 @@ mod tests { ); } - #[test] - fn test_process_new_vote_state_expired_ancestor_not_removed() { + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_state_expired_ancestor_not_removed(mut vote_state1: VoteStateHandler) { // Construct on-chain vote state - let mut vote_state1 = VoteStateHandler::default_v3(); process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 3, 9]); assert_eq!( vote_state1 @@ -2728,9 +2892,11 @@ mod tests { assert_eq!(vote_state1, vote_state2,); } - #[test] - fn test_process_new_vote_current_state_contains_bigger_slots() { - let mut vote_state1 = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_process_new_vote_current_state_contains_bigger_slots( + mut vote_state1: VoteStateHandler, + ) { process_slot_votes_unchecked(&mut vote_state1, &[6, 7, 8]); assert_eq!( vote_state1 @@ -2783,9 +2949,9 @@ mod tests { assert_eq!(*vote_state1.votes(), good_votes); } - #[test] - fn test_filter_old_votes() { - let mut vote_state = VoteStateHandler::default_v3(); + #[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")] + #[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")] + fn test_filter_old_votes(mut vote_state: VoteStateHandler) { let old_vote_slot = 1; let vote = Vote::new(vec![old_vote_slot], Hash::default()); @@ -2826,8 +2992,15 @@ mod tests { .collect() } - fn build_vote_state(vote_slots: Vec, slot_hashes: &[(Slot, Hash)]) -> VoteStateHandler { - let mut vote_state = VoteStateHandler::default_v3(); + fn build_vote_state( + target_version: VoteStateTargetVersion, + vote_slots: Vec, + slot_hashes: &[(Slot, Hash)], + ) -> VoteStateHandler { + let mut vote_state = match target_version { + VoteStateTargetVersion::V3 => VoteStateHandler::default_v3(), + VoteStateTargetVersion::V4 => VoteStateHandler::default_v4(), + }; if !vote_slots.is_empty() { let vote_hash = slot_hashes @@ -2843,10 +3016,11 @@ mod tests { vote_state } - #[test] - fn test_check_and_filter_proposed_vote_state_empty() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_empty(target_version: VoteStateTargetVersion) { let empty_slot_hashes = build_slot_hashes(vec![]); - let empty_vote_state = build_vote_state(vec![], &empty_slot_hashes); + let empty_vote_state = build_vote_state(target_version, vec![], &empty_slot_hashes); // Test with empty TowerSync, should return EmptySlots error let mut tower_sync = TowerSync::from(vec![]); @@ -2875,11 +3049,12 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_too_old() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_too_old(target_version: VoteStateTargetVersion) { let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]); let latest_vote = 4; - let vote_state = build_vote_state(vec![1, 2, 3, latest_vote], &slot_hashes); + let vote_state = build_vote_state(target_version, vec![1, 2, 3, latest_vote], &slot_hashes); // Test with a vote for a slot less than the latest vote in the vote_state, // should return error `VoteTooOld` @@ -2914,6 +3089,7 @@ mod tests { } fn run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version: VoteStateTargetVersion, earliest_slot_in_history: Slot, current_vote_state_slots: Vec, current_vote_state_root: Option, @@ -2941,7 +3117,8 @@ mod tests { .collect::>(), ); - let mut vote_state = build_vote_state(current_vote_state_slots, &slot_hashes); + let mut vote_state = + build_vote_state(target_version, current_vote_state_slots, &slot_hashes); vote_state.set_root_slot(current_vote_state_root); slot_hashes.retain(|slot| slot.0 >= earliest_slot_in_history); @@ -2984,8 +3161,11 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_older_than_history_root() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version: VoteStateTargetVersion, + ) { // Test when `proposed_root` is in `current_vote_state_slots` but it's not the latest // slot let earliest_slot_in_history = 5; @@ -2996,6 +3176,7 @@ mod tests { let expected_root = Some(4); let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)]; run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version, earliest_slot_in_history, current_vote_state_slots, current_vote_state_root, @@ -3015,6 +3196,7 @@ mod tests { let expected_root = Some(4); let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)]; run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version, earliest_slot_in_history, current_vote_state_slots, current_vote_state_root, @@ -3037,6 +3219,7 @@ mod tests { Lockout::new_with_confirmation_count(5, 1), ]; run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version, earliest_slot_in_history, current_vote_state_slots, current_vote_state_root, @@ -3058,6 +3241,7 @@ mod tests { Lockout::new_with_confirmation_count(5, 1), ]; run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version, earliest_slot_in_history, current_vote_state_slots, current_vote_state_root, @@ -3081,6 +3265,7 @@ mod tests { Lockout::new_with_confirmation_count(5, 1), ]; run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version, earliest_slot_in_history, current_vote_state_slots, current_vote_state_root, @@ -3099,6 +3284,7 @@ mod tests { let expected_root = None; let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)]; run_test_check_and_filter_proposed_vote_state_older_than_history_root( + target_version, earliest_slot_in_history, current_vote_state_slots, current_vote_state_root, @@ -3109,10 +3295,13 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_slots_not_ordered() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_slots_not_ordered( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]); - let vote_state = build_vote_state(vec![1], &slot_hashes); + let vote_state = build_vote_state(target_version, vec![1], &slot_hashes); // Test with a `TowerSync` where the slots are out of order let vote_slot = 3; @@ -3149,10 +3338,13 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_older_than_history_slots_filtered() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_older_than_history_slots_filtered( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]); - let mut vote_state = build_vote_state(vec![1, 2, 3, 4], &slot_hashes); + let mut vote_state = build_vote_state(target_version, vec![1, 2, 3, 4], &slot_hashes); // Test with a `TowerSync` where there: // 1) Exists a slot less than `earliest_slot_in_history` @@ -3197,10 +3389,13 @@ mod tests { assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok()); } - #[test] - fn test_check_and_filter_proposed_vote_state_older_than_history_slots_not_filtered() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_older_than_history_slots_not_filtered( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![4]); - let mut vote_state = build_vote_state(vec![4], &slot_hashes); + let mut vote_state = build_vote_state(target_version, vec![4], &slot_hashes); // Test with a `TowerSync` where there: // 1) Exists a slot less than `earliest_slot_in_history` @@ -3242,11 +3437,13 @@ mod tests { assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok()); } - #[test] + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] fn test_check_and_filter_proposed_vote_state_older_than_history_slots_filtered_and_not_filtered( + target_version: VoteStateTargetVersion, ) { let slot_hashes = build_slot_hashes(vec![6]); - let mut vote_state = build_vote_state(vec![6], &slot_hashes); + let mut vote_state = build_vote_state(target_version, vec![6], &slot_hashes); // Test with a `TowerSync` where there exists both a slot: // 1) Less than `earliest_slot_in_history` @@ -3301,10 +3498,13 @@ mod tests { assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok()); } - #[test] - fn test_check_and_filter_proposed_vote_state_slot_not_on_fork() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_slot_not_on_fork( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]); - let vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes); + let vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes); // Test with a `TowerSync` where there: // 1) Exists a slot not in the slot hashes history @@ -3356,10 +3556,13 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_root_on_different_fork() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_root_on_different_fork( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]); - let vote_state = build_vote_state(vec![6], &slot_hashes); + let vote_state = build_vote_state(target_version, vec![6], &slot_hashes); // Test with a `TowerSync` where: // 1) The root is not present in slot hashes history @@ -3392,10 +3595,13 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_slot_newer_than_slot_history() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_slot_newer_than_slot_history( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8, 10]); - let vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes); + let vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes); // Test with a `TowerSync` where there: // 1) The last slot in the update is a slot not in the slot hashes history @@ -3418,10 +3624,13 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_slot_all_slot_hashes_in_update_ok() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_slot_all_slot_hashes_in_update_ok( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]); - let mut vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes); + let mut vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes); // Test with a `TowerSync` where every slot in the history is // in the update @@ -3463,10 +3672,13 @@ mod tests { assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok()); } - #[test] - fn test_check_and_filter_proposed_vote_state_slot_some_slot_hashes_in_update_ok() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_slot_some_slot_hashes_in_update_ok( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8, 10]); - let mut vote_state = build_vote_state(vec![6], &slot_hashes); + let mut vote_state = build_vote_state(target_version, vec![6], &slot_hashes); // Test with a `TowerSync` where only some slots in the history are // in the update, and others slots in the history are missing. @@ -3512,10 +3724,13 @@ mod tests { ); } - #[test] - fn test_check_and_filter_proposed_vote_state_slot_hash_mismatch() { + #[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")] + #[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")] + fn test_check_and_filter_proposed_vote_state_slot_hash_mismatch( + target_version: VoteStateTargetVersion, + ) { let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]); - let vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes); + let vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes); // Test with a `TowerSync` where the hash is mismatched diff --git a/svm-feature-set/src/lib.rs b/svm-feature-set/src/lib.rs index a1c148c56d5e30..a575ddd7e0c297 100644 --- a/svm-feature-set/src/lib.rs +++ b/svm-feature-set/src/lib.rs @@ -37,6 +37,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 { @@ -79,6 +80,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, } } }