From 1a832e094dab62450b4eeda734e51e9471dde510 Mon Sep 17 00:00:00 2001 From: bji Date: Mon, 6 Jun 2022 16:37:03 -0700 Subject: [PATCH] =?UTF-8?q?Award=20one=20credit=20per=20dequeued=20vote=20?= =?UTF-8?q?when=20processing=20VoteStateUpdate=20in=E2=80=A6=20(#25743)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Award one credit per dequeued vote when processing VoteStateUpdate instruction, to match vote rewards of Vote instruction. * Update feature pubkey to one owned by cc (ashwin) Co-authored-by: Ashwin Sekar (cherry picked from commit cbb0f07d543f424e8c90ac99f23ecea1a66b9327) # Conflicts: # programs/stake/src/stake_instruction.rs # sdk/src/feature_set.rs --- program-test/src/lib.rs | 2 +- programs/stake/src/stake_instruction.rs | 3333 +++++++++++++++++++++++ programs/stake/src/stake_state.rs | 14 +- programs/vote/src/vote_processor.rs | 1 + programs/vote/src/vote_state/mod.rs | 230 +- sdk/src/feature_set.rs | 9 + 6 files changed, 3557 insertions(+), 32 deletions(-) diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index fdfaa2cc1319ca..c450a7352e06ce 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -1073,7 +1073,7 @@ impl ProgramTestContext { let epoch = bank.epoch(); for _ in 0..number_of_credits { - vote_state.increment_credits(epoch); + vote_state.increment_credits(epoch, 1); } let versioned = VoteStateVersions::new_current(vote_state); VoteState::to(&versioned, &mut vote_account).unwrap(); diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 3f2be50198b291..c9d00d4bbad942 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -3432,4 +3432,3337 @@ mod tests { Err(InstructionError::MissingRequiredSignature), ); } +<<<<<<< HEAD +======= + + /// Ensure that `initialize()` respects the minimum balance requirements + /// - Assert 1: accounts with a balance equal-to the rent exemption initialize OK + /// - Assert 2: accounts with a balance less-than the rent exemption do not initialize + fn do_test_initialize_minimum_balance(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let instruction_data = serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + Lockup::default(), + )) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::rent::id(), + is_signer: false, + is_writable: false, + }, + ]; + for (lamports, expected_result) in [ + (rent_exempt_reserve, Ok(())), + ( + rent_exempt_reserve - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + let stake_account = AccountSharedData::new(lamports, StakeState::size_of(), &id()); + process_instruction( + &feature_set, + &instruction_data, + vec![ + (stake_address, stake_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ], + instruction_accounts.clone(), + expected_result, + ); + } + } + + /// Ensure that `delegate()` respects the minimum delegation requirements + /// - Assert 1: delegating an amount equal-to the minimum succeeds + /// - Assert 2: delegating an amount less-than the minimum fails + /// Also test both asserts above over both StakeState::{Initialized and Stake}, since the logic + /// is slightly different for the variants. + /// + /// NOTE: Even though new stake accounts must have a minimum balance that is at least + /// the minimum delegation (plus rent exempt reserve), the old behavior allowed + /// withdrawing below the minimum delegation, then re-delegating successfully (see + /// `test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation()` for + /// more information.) + fn do_test_delegate_minimum_stake_delegation(feature_set: FeatureSet) { + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }; + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + for (stake_delegation, expected_result) in &[ + (minimum_delegation, Ok(())), + ( + minimum_delegation - 1, + Err(StakeError::InsufficientDelegation), + ), + ] { + for stake_state in &[ + StakeState::Initialized(meta), + just_stake(meta, *stake_delegation), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_delegation + rent_exempt_reserve, + stake_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + vec![ + (stake_address, stake_account), + (vote_address, vote_account.clone()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ], + instruction_accounts.clone(), + expected_result.clone().map_err(|e| e.into()), + ); + } + } + } + + /// Ensure that `split()` respects the minimum delegation requirements. This applies to + /// both the source and destination acounts. Thus, we have four permutations possible based on + /// if each account's post-split delegation is equal-to (EQ) or less-than (LT) the minimum: + /// + /// source | dest | result + /// --------+------+-------- + /// EQ | EQ | Ok + /// EQ | LT | Err + /// LT | EQ | Err + /// LT | LT | Err + fn do_test_split_minimum_stake_delegation(feature_set: FeatureSet) { + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let source_address = Pubkey::new_unique(); + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let dest_address = Pubkey::new_unique(); + let dest_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: source_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: dest_address, + is_signer: false, + is_writable: false, + }, + ]; + for (source_reserve, dest_reserve, expected_result) in [ + (rent_exempt_reserve, rent_exempt_reserve, Ok(())), + ( + rent_exempt_reserve, + rent_exempt_reserve - 1, + Err(InstructionError::InsufficientFunds), + ), + ( + rent_exempt_reserve - 1, + rent_exempt_reserve, + Err(InstructionError::InsufficientFunds), + ), + ( + rent_exempt_reserve - 1, + rent_exempt_reserve - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + // The source account's starting balance is equal to *both* the source and dest + // accounts' *final* balance + let mut source_starting_balance = source_reserve + dest_reserve; + for (delegation, source_stake_state) in &[ + (0, StakeState::Initialized(source_meta)), + ( + minimum_delegation, + just_stake( + source_meta, + minimum_delegation * 2 + source_starting_balance - rent_exempt_reserve, + ), + ), + ] { + source_starting_balance += delegation * 2; + let source_account = AccountSharedData::new_data_with_space( + source_starting_balance, + source_stake_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(dest_reserve + delegation)).unwrap(), + vec![ + (source_address, source_account), + (dest_address, dest_account.clone()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + } + } + } + + /// Ensure that splitting the full amount from an account respects the minimum delegation + /// requirements. This ensures that we are future-proofing/testing any raises to the minimum + /// delegation. + /// - Assert 1: splitting the full amount from an account that has at least the minimum + /// delegation is OK + /// - Assert 2: splitting the full amount from an account that has less than the minimum + /// delegation is not OK + fn do_test_split_full_amount_minimum_stake_delegation(feature_set: FeatureSet) { + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let source_address = Pubkey::new_unique(); + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let dest_address = Pubkey::new_unique(); + let dest_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: source_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: dest_address, + is_signer: false, + is_writable: false, + }, + ]; + for (reserve, expected_result) in [ + (rent_exempt_reserve, Ok(())), + ( + rent_exempt_reserve - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + for (stake_delegation, source_stake_state) in &[ + (0, StakeState::Initialized(source_meta)), + ( + minimum_delegation, + just_stake(source_meta, minimum_delegation), + ), + ] { + let source_account = AccountSharedData::new_data_with_space( + stake_delegation + reserve, + source_stake_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(source_account.lamports())).unwrap(), + vec![ + (source_address, source_account), + (dest_address, dest_account.clone()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + } + } + } + + /// Ensure that `split()` correctly handles prefunded destination accounts from + /// initialized stakes. When a destination account already has funds, ensure + /// the minimum split amount reduces accordingly. + fn do_test_initialized_split_destination_minimum_balance(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let source_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: source_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: destination_address, + is_signer: false, + is_writable: false, + }, + ]; + for (destination_starting_balance, split_amount, expected_result) in [ + // split amount must be non zero + ( + rent_exempt_reserve, + 0, + Err(InstructionError::InsufficientFunds), + ), + // any split amount is OK when destination account is already fully funded + (rent_exempt_reserve, 1, Ok(())), + // if destination is only short by 1 lamport, then split amount can be 1 lamport + (rent_exempt_reserve - 1, 1, Ok(())), + // destination short by 2 lamports, then 1 isn't enough (non-zero split amount) + ( + rent_exempt_reserve - 2, + 1, + Err(InstructionError::InsufficientFunds), + ), + // destination has smallest non-zero balance, so can split the minimum balance + // requirements minus what destination already has + (1, rent_exempt_reserve - 1, Ok(())), + // destination has smallest non-zero balance, but cannot split less than the minimum + // balance requirements minus what destination already has + ( + 1, + rent_exempt_reserve - 2, + Err(InstructionError::InsufficientFunds), + ), + // destination has zero lamports, so split must be at least rent exempt reserve + (0, rent_exempt_reserve, Ok(())), + // destination has zero lamports, but split amount is less than rent exempt reserve + ( + 0, + rent_exempt_reserve - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + // Set the source's starting balance to something large to ensure its post-split + // balance meets all the requirements + let source_balance = u64::MAX; + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let source_account = AccountSharedData::new_data_with_space( + source_balance, + &StakeState::Initialized(source_meta), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let destination_account = AccountSharedData::new_data_with_space( + destination_starting_balance, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + vec![ + (source_address, source_account), + (destination_address, destination_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + } + } + + /// Ensure that `split()` correctly handles prefunded destination accounts from staked stakes. + /// When a destination account already has funds, ensure the minimum split amount reduces + /// accordingly. + fn do_test_staked_split_destination_minimum_balance( + feature_set: FeatureSet, + expected_results: &[Result<(), InstructionError>], + ) { + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let source_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: source_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: destination_address, + is_signer: false, + is_writable: false, + }, + ]; + for (destination_starting_balance, split_amount, expected_result) in [ + // split amount must be non zero + ( + rent_exempt_reserve + minimum_delegation, + 0, + Err(InstructionError::InsufficientFunds), + ), + // destination is fully funded: + // - old behavior: any split amount is OK + // - new behavior: split amount must be at least the minimum delegation + ( + rent_exempt_reserve + minimum_delegation, + 1, + expected_results[0].clone(), + ), + // if destination is only short by 1 lamport, then... + // - old behavior: split amount can be 1 lamport + // - new behavior: split amount must be at least the minimum delegation + ( + rent_exempt_reserve + minimum_delegation - 1, + 1, + expected_results[1].clone(), + ), + // destination short by 2 lamports, so 1 isn't enough (non-zero split amount) + ( + rent_exempt_reserve + minimum_delegation - 2, + 1, + Err(InstructionError::InsufficientFunds), + ), + // destination is rent exempt, so split enough for minimum delegation + (rent_exempt_reserve, minimum_delegation, Ok(())), + // destination is rent exempt, but split amount less than minimum delegation + ( + rent_exempt_reserve, + minimum_delegation.saturating_sub(1), // when minimum is 0, this blows up! + Err(InstructionError::InsufficientFunds), + ), + // destination is not rent exempt, so split enough for rent and minimum delegation + (rent_exempt_reserve - 1, minimum_delegation + 1, Ok(())), + // destination is not rent exempt, but split amount only for minimum delegation + ( + rent_exempt_reserve - 1, + minimum_delegation, + Err(InstructionError::InsufficientFunds), + ), + // destination has smallest non-zero balance, so can split the minimum balance + // requirements minus what destination already has + (1, rent_exempt_reserve + minimum_delegation - 1, Ok(())), + // destination has smallest non-zero balance, but cannot split less than the minimum + // balance requirements minus what destination already has + ( + 1, + rent_exempt_reserve + minimum_delegation - 2, + Err(InstructionError::InsufficientFunds), + ), + // destination has zero lamports, so split must be at least rent exempt reserve plus + // minimum delegation + (0, rent_exempt_reserve + minimum_delegation, Ok(())), + // destination has zero lamports, but split amount is less than rent exempt reserve + // plus minimum delegation + ( + 0, + rent_exempt_reserve + minimum_delegation - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + // Set the source's starting balance to something large to ensure its post-split + // balance meets all the requirements + let source_balance = u64::MAX; + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let source_stake_delegation = source_balance - rent_exempt_reserve; + let source_account = AccountSharedData::new_data_with_space( + source_balance, + &just_stake(source_meta, source_stake_delegation), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let destination_account = AccountSharedData::new_data_with_space( + destination_starting_balance, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + vec![ + (source_address, source_account.clone()), + (destination_address, destination_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + // For the expected OK cases, when the source's StakeState is Stake, then the + // destination's StakeState *must* also end up as Stake as well. Additionally, + // check to ensure the destination's delegation amount is correct. If the + // destination is already rent exempt, then the destination's stake delegation + // *must* equal the split amount. Otherwise, the split amount must first be used to + // make the destination rent exempt, and then the leftover lamports are delegated. + if expected_result.is_ok() { + assert_matches!(accounts[0].state().unwrap(), StakeState::Stake(_, _)); + if let StakeState::Stake(_, destination_stake) = accounts[1].state().unwrap() { + let destination_initial_rent_deficit = + rent_exempt_reserve.saturating_sub(destination_starting_balance); + let expected_destination_stake_delegation = + split_amount - destination_initial_rent_deficit; + assert_eq!( + expected_destination_stake_delegation, + destination_stake.delegation.stake + ); + assert!(destination_stake.delegation.stake >= minimum_delegation,); + } else { + panic!("destination state must be StakeStake::Stake after successful split when source is also StakeState::Stake!"); + } + } + } + } + + /// Ensure that `withdraw()` respects the minimum delegation requirements + /// - Assert 1: withdrawing so remaining stake is equal-to the minimum is OK + /// - Assert 2: withdrawing so remaining stake is less-than the minimum is not OK + fn do_test_withdraw_minimum_stake_delegation(feature_set: FeatureSet) { + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }; + let recipient_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + let starting_stake_delegation = minimum_delegation; + for (ending_stake_delegation, expected_result) in [ + (minimum_delegation, Ok(())), + ( + minimum_delegation - 1, + Err(InstructionError::InsufficientFunds), + ), + ] { + for (stake_delegation, stake_state) in &[ + (0, StakeState::Initialized(meta)), + (minimum_delegation, just_stake(meta, minimum_delegation)), + ] { + let rewards_balance = 123; + let stake_account = AccountSharedData::new_data_with_space( + stake_delegation + rent_exempt_reserve + rewards_balance, + stake_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let withdraw_amount = + (starting_stake_delegation + rewards_balance) - ending_stake_delegation; + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(), + vec![ + (stake_address, stake_account), + ( + recipient_address, + AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()), + ), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&Rent::free()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + } + } + } + + /// The stake program's old behavior allowed delegations below the minimum stake delegation + /// (see also `test_delegate_minimum_stake_delegation()`). This was not the desired behavior, + /// and has been fixed in the new behavior. This test ensures the behavior is not changed + /// inadvertently. + /// + /// This test: + /// 1. Initialises a stake account (with sufficient balance for both rent and minimum delegation) + /// 2. Delegates the minimum amount + /// 3. Deactives the delegation + /// 4. Withdraws from the account such that the ending balance is *below* rent + minimum delegation + /// 5. Re-delegates, now with less than the minimum delegation, but it still succeeds + fn do_test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + feature_set: FeatureSet, + expected_result: Result<(), InstructionError>, + ) { + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new( + rent_exempt_reserve + minimum_delegation, + StakeState::size_of(), + &id(), + ); + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + let recipient_address = solana_sdk::pubkey::new_rand(); + let mut clock = Clock::default(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + ( + recipient_address, + AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()), + ), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + Lockup::default(), + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::rent::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + transaction_accounts[1] = (vote_address, accounts[1].clone()); + + clock.epoch += 1; + transaction_accounts[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + clock.epoch += 1; + transaction_accounts[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + let withdraw_amount = + accounts[0].lamports() - (rent_exempt_reserve + minimum_delegation - 1); + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts, + instruction_accounts, + expected_result, + ); + } + + fn do_test_split_source_uninitialized(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + ]; + + // splitting an uninitialized account where the destination is the same as the source + { + // splitting should work when... + // - when split amount is the full balance + // - when split amount is zero + // - when split amount is non-zero and less than the full balance + // + // and splitting should fail when the split amount is greater than the balance + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(0)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + } + + // this should work + instruction_accounts[1].pubkey = split_to_address; + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[0].lamports(), accounts[1].lamports()); + + // no signers should fail + instruction_accounts[0].is_signer = false; + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + fn do_test_split_split_not_uninitialized(feature_set: FeatureSet) { + let stake_lamports = 42; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&stake_address), stake_lamports), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + ]; + + for split_to_state in &[ + StakeState::Initialized(Meta::default()), + StakeState::Stake(Meta::default(), Stake::default()), + StakeState::RewardsPool, + ] { + let split_to_account = AccountSharedData::new_data_with_space( + 0, + split_to_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ], + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + } + } + + fn do_test_split_more_than_staked(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake( + Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }, + stake_lamports / 2 - 1, + ), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(StakeError::InsufficientStake.into()), + ); + } + + fn do_test_split_with_rent(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_address = solana_sdk::pubkey::new_rand(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + + // test splitting both an Initialized stake and a Staked stake + for (minimum_balance, state) in &[ + (rent_exempt_reserve, StakeState::Initialized(meta)), + ( + rent_exempt_reserve + minimum_delegation, + just_stake(meta, minimum_delegation * 2 + rent_exempt_reserve), + ), + ] { + let stake_lamports = minimum_balance * 2; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account.clone()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // not enough to make a non-zero stake account + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(minimum_balance - 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // doesn't leave enough for initial stake to be non-zero + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split( + stake_lamports - minimum_balance + 1, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // split account already has way enough lamports + transaction_accounts[1].1.set_lamports(*minimum_balance); + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports - minimum_balance)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // verify no stake leakage in the case of a stake + if let StakeState::Stake(meta, stake) = state { + assert_eq!( + accounts[1].state(), + Ok(StakeState::Stake( + *meta, + Stake { + delegation: Delegation { + stake: stake_lamports - minimum_balance, + ..stake.delegation + }, + ..*stake + } + )) + ); + assert_eq!(accounts[0].lamports(), *minimum_balance,); + assert_eq!(accounts[1].lamports(), stake_lamports,); + } + } + } + + fn do_test_split_to_account_with_rent_exempt_reserve(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in + // test_split, since that test uses a Meta with rent_exempt_reserve = 0 + let split_lamport_balances = vec![ + 0, + rent_exempt_reserve - 1, + rent_exempt_reserve, + rent_exempt_reserve + minimum_delegation - 1, + rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split more than available fails + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should work + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + initial_balance, + ); + + if let StakeState::Stake(meta, stake) = state { + let expected_stake = + stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance)); + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 + - (rent_exempt_reserve.saturating_sub(initial_balance)), + ..stake.delegation + }, + ..stake + } + )), + accounts[1].state(), + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + + rent_exempt_reserve + + initial_balance.saturating_sub(rent_exempt_reserve), + ); + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 - rent_exempt_reserve, + ..stake.delegation + }, + ..stake + } + )), + accounts[0].state(), + ); + } + } + } + + fn do_test_split_from_larger_sized_account(feature_set: FeatureSet) { + let rent = Rent::default(); + let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); + let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_larger_rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - source_larger_rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of() + 100, + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in + // test_split, since that test uses a Meta with rent_exempt_reserve = 0 + let split_lamport_balances = vec![ + 0, + split_rent_exempt_reserve - 1, + split_rent_exempt_reserve, + split_rent_exempt_reserve + minimum_delegation - 1, + split_rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split more than available fails + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + + // should work + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + initial_balance + ); + + if let StakeState::Stake(meta, stake) = state { + let expected_split_meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: split_rent_exempt_reserve, + ..Meta::default() + }; + let expected_stake = stake_lamports / 2 + - (split_rent_exempt_reserve.saturating_sub(initial_balance)); + + assert_eq!( + Ok(StakeState::Stake( + expected_split_meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..stake + } + )), + accounts[1].state() + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + + split_rent_exempt_reserve + + initial_balance.saturating_sub(split_rent_exempt_reserve) + ); + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 - source_larger_rent_exempt_reserve, + ..stake.delegation + }, + ..stake + } + )), + accounts[0].state() + ); + } + } + } + + fn do_test_split_from_smaller_sized_account(feature_set: FeatureSet) { + let rent = Rent::default(); + let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); + let stake_lamports = split_rent_exempt_reserve + 1; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_smaller_rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - source_smaller_rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + let split_amount = stake_lamports - (source_smaller_rent_exempt_reserve + 1); // Enough so that split stake is > 0 + let split_lamport_balances = vec![ + 0, + 1, + split_rent_exempt_reserve, + split_rent_exempt_reserve + 1, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + StakeState::size_of() + 100, + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // should always return error when splitting to larger account + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + + // Splitting 100% of source should not make a difference + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + } + } + + fn do_test_split_100_percent_of_source(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // test splitting both an Initialized stake and a Staked stake + for state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports - rent_exempt_reserve), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account.clone()), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split 100% over to dest + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + ); + + match state { + StakeState::Initialized(_) => { + assert_eq!(Ok(*state), accounts[1].state()); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + StakeState::Stake(meta, stake) => { + assert_eq!( + Ok(StakeState::Stake( + *meta, + Stake { + delegation: Delegation { + stake: stake_lamports - rent_exempt_reserve, + ..stake.delegation + }, + ..*stake + } + )), + accounts[1].state() + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + _ => unreachable!(), + } + } + } + + fn do_test_split_100_percent_of_source_to_account_with_lamports(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. Technically, the empty case is + // covered in test_split_100_percent_of_source, but included here as well for readability + let split_lamport_balances = vec![ + 0, + rent_exempt_reserve - 1, + rent_exempt_reserve, + rent_exempt_reserve + minimum_delegation - 1, + rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + + // split 100% over to dest + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + stake_lamports + initial_balance + ); + + if let StakeState::Stake(meta, stake) = state { + assert_eq!( + Ok(StakeState::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports - rent_exempt_reserve, + ..stake.delegation + }, + ..stake + } + )), + accounts[1].state() + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + } + } + + fn do_test_split_rent_exemptness(feature_set: FeatureSet) { + let rent = Rent::default(); + let source_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); + let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let stake_lamports = source_rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_rent_exempt_reserve, + ..Meta::default() + }; + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: false, + }, + ]; + + for state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports - source_rent_exempt_reserve), + ] { + // Test that splitting to a larger account fails + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of() + 10000, + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + + // Test that splitting from a larger account to a smaller one works. + // Split amount should not matter, assuming other fund criteria are met + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeState::size_of() + 100, + &id(), + ) + .unwrap(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeState::Uninitialized, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ( + sysvar::rent::id(), + account::create_account_shared_data_for_test(&rent), + ), + ]; + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[1].lamports(), stake_lamports); + + let expected_split_meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: split_rent_exempt_reserve, + ..Meta::default() + }; + match state { + StakeState::Initialized(_) => { + assert_eq!( + Ok(StakeState::Initialized(expected_split_meta)), + accounts[1].state() + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + StakeState::Stake(_meta, stake) => { + // Expected stake should reflect original stake amount so that extra lamports + // from the rent_exempt_reserve inequality do not magically activate + let expected_stake = stake_lamports - source_rent_exempt_reserve; + + assert_eq!( + Ok(StakeState::Stake( + expected_split_meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..*stake + } + )), + accounts[1].state() + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + source_rent_exempt_reserve, + ); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); + } + _ => unreachable!(), + } + } + } + + fn do_test_merge(feature_set: FeatureSet) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let meta = Meta::auto(&authorized_address); + let stake_lamports = 42; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[ + StakeState::Initialized(meta), + just_stake(meta, stake_lamports), + ] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + + // Authorized staker signature required... + instruction_accounts[4].is_signer = false; + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[4].is_signer = true; + + let accounts = process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // check lamports + assert_eq!(accounts[0].lamports(), stake_lamports * 2); + assert_eq!(accounts[1].lamports(), 0); + + // check state + match state { + StakeState::Initialized(meta) => { + assert_eq!(accounts[0].state(), Ok(StakeState::Initialized(*meta)),); + } + StakeState::Stake(meta, stake) => { + let expected_stake = stake.delegation.stake + + merge_from_state + .stake() + .map(|stake| stake.delegation.stake) + .unwrap_or_else(|| { + stake_lamports + - merge_from_state.meta().unwrap().rent_exempt_reserve + }); + assert_eq!( + accounts[0].state(), + Ok(StakeState::Stake( + *meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..*stake + } + )), + ); + } + _ => unreachable!(), + } + assert_eq!(accounts[1].state(), Ok(StakeState::Uninitialized)); + } + } + } + + fn do_test_merge_self_fails(feature_set: FeatureSet) { + let stake_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_amount = 4242424242; + let stake_lamports = rent_exempt_reserve + stake_amount; + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&authorized_address) + }; + let stake = Stake { + delegation: Delegation { + stake: stake_amount, + activation_epoch: 0, + ..Delegation::default() + }, + ..Stake::default() + }; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Stake(meta, stake), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (authorized_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidArgument), + ); + } + + fn do_test_merge_incorrect_authorized_staker(feature_set: FeatureSet) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let wrong_authorized_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeState::Initialized(Meta::auto(&authorized_address)), + just_stake(Meta::auto(&authorized_address), stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[ + StakeState::Initialized(Meta::auto(&wrong_authorized_address)), + just_stake(Meta::auto(&wrong_authorized_address), stake_lamports), + ] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + (wrong_authorized_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + + instruction_accounts[4].pubkey = wrong_authorized_address; + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[4].pubkey = authorized_address; + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(StakeError::MergeMismatch.into()), + ); + } + } + } + + fn do_test_merge_invalid_account_data(feature_set: FeatureSet) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeState::Uninitialized, + StakeState::RewardsPool, + StakeState::Initialized(Meta::auto(&authorized_address)), + just_stake(Meta::auto(&authorized_address), stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[StakeState::Uninitialized, StakeState::RewardsPool] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(InstructionError::InvalidAccountData), + ); + } + } + } + + fn do_test_merge_fake_stake_source(feature_set: FeatureSet) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&authorized_address), stake_lamports), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&authorized_address), stake_lamports), + StakeState::size_of(), + &solana_sdk::pubkey::new_rand(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&Clock::default()), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + process_instruction( + &feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::IncorrectProgramId), + ); + } + + fn do_test_merge_active_stake(feature_set: FeatureSet) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let base_lamports = 4242424242; + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); + let stake_amount = base_lamports; + let stake_lamports = rent_exempt_reserve + stake_amount; + let merge_from_amount = base_lamports; + let merge_from_lamports = rent_exempt_reserve + merge_from_amount; + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&authorized_address) + }; + let mut stake = Stake { + delegation: Delegation { + stake: stake_amount, + activation_epoch: 0, + ..Delegation::default() + }, + ..Stake::default() + }; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeState::Stake(meta, stake), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let merge_from_activation_epoch = 2; + let mut merge_from_stake = Stake { + delegation: Delegation { + stake: merge_from_amount, + activation_epoch: merge_from_activation_epoch, + ..stake.delegation + }, + ..stake + }; + let merge_from_account = AccountSharedData::new_data_with_space( + merge_from_lamports, + &StakeState::Stake(meta, merge_from_stake), + StakeState::size_of(), + &id(), + ) + .unwrap(); + let mut clock = Clock::default(); + let mut stake_history = StakeHistory::default(); + let mut effective = base_lamports; + let mut activating = stake_amount; + let mut deactivating = 0; + stake_history.add( + clock.epoch, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ), + ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&stake_history), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: sysvar::stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + fn try_merge( + feature_set: &FeatureSet, + transaction_accounts: Vec<(Pubkey, AccountSharedData)>, + mut instruction_accounts: Vec, + expected_result: Result<(), InstructionError>, + ) { + for iteration in 0..2 { + if iteration == 1 { + instruction_accounts.swap(0, 1); + } + let accounts = process_instruction( + feature_set, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + expected_result.clone(), + ); + if expected_result.is_ok() { + assert_eq!( + accounts[1 - iteration].state(), + Ok(StakeState::Uninitialized) + ); + } + } + } + + // stake activation epoch, source initialized succeeds + try_merge( + &feature_set, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // both activating fails + loop { + clock.epoch += 1; + if clock.epoch == merge_from_activation_epoch { + activating += merge_from_amount; + } + let delta = + activating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64); + effective += delta; + activating -= delta; + stake_history.add( + clock.epoch, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + transaction_accounts[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + transaction_accounts[4] = ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&stake_history), + ); + if stake_amount == stake.stake(clock.epoch, Some(&stake_history)) + && merge_from_amount == merge_from_stake.stake(clock.epoch, Some(&stake_history)) + { + break; + } + try_merge( + &feature_set, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::from(StakeError::MergeTransientStake)), + ); + } + + // Both fully activated works + try_merge( + &feature_set, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // deactivate setup for deactivation + let merge_from_deactivation_epoch = clock.epoch + 1; + let stake_deactivation_epoch = clock.epoch + 2; + + // active/deactivating and deactivating/inactive mismatches fail + loop { + clock.epoch += 1; + let delta = + deactivating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64); + effective -= delta; + deactivating -= delta; + if clock.epoch == stake_deactivation_epoch { + deactivating += stake_amount; + stake = Stake { + delegation: Delegation { + deactivation_epoch: stake_deactivation_epoch, + ..stake.delegation + }, + ..stake + }; + transaction_accounts[0] + .1 + .set_state(&StakeState::Stake(meta, stake)) + .unwrap(); + } + if clock.epoch == merge_from_deactivation_epoch { + deactivating += merge_from_amount; + merge_from_stake = Stake { + delegation: Delegation { + deactivation_epoch: merge_from_deactivation_epoch, + ..merge_from_stake.delegation + }, + ..merge_from_stake + }; + transaction_accounts[1] + .1 + .set_state(&StakeState::Stake(meta, merge_from_stake)) + .unwrap(); + } + stake_history.add( + clock.epoch, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + transaction_accounts[3] = ( + sysvar::clock::id(), + account::create_account_shared_data_for_test(&clock), + ); + transaction_accounts[4] = ( + sysvar::stake_history::id(), + account::create_account_shared_data_for_test(&stake_history), + ); + if 0 == stake.stake(clock.epoch, Some(&stake_history)) + && 0 == merge_from_stake.stake(clock.epoch, Some(&stake_history)) + { + break; + } + try_merge( + &feature_set, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::from(StakeError::MergeTransientStake)), + ); + } + + // Both fully deactivated works + try_merge( + &feature_set, + transaction_accounts, + instruction_accounts, + Ok(()), + ); + } + + fn do_test_stake_get_minimum_delegation(feature_set: FeatureSet) { + let stake_address = Pubkey::new_unique(); + let stake_account = create_default_stake_account(); + let instruction_data = serialize(&StakeInstruction::GetMinimumDelegation).unwrap(); + let transaction_accounts = vec![(stake_address, stake_account)]; + let instruction_accounts = vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: false, + }]; + + mock_process_instruction( + &id(), + Vec::new(), + &instruction_data, + transaction_accounts, + instruction_accounts, + None, + Some(Arc::new(feature_set)), + Ok(()), + |first_instruction_account, invoke_context| { + super::process_instruction(first_instruction_account, invoke_context)?; + let expected_minimum_delegation = + crate::get_minimum_delegation(&invoke_context.feature_set).to_le_bytes(); + let actual_minimum_delegation = + invoke_context.transaction_context.get_return_data().1; + assert_eq!(expected_minimum_delegation, actual_minimum_delegation); + Ok(()) + }, + ); + } + + // Ensure that the correct errors are returned when processing instructions + // + // The GetMinimumDelegation instruction does not take any accounts; so when it was added, + // `process_instruction()` needed to be updated to *not* need a stake account passed in, which + // changes the error *ordering* conditions. These changes shall only occur when the + // `add_get_minimum_delegation_instruction_to_stake_program` feature is enabled, and this test + // ensures it. + // + // For the following combinations of the feature enabled/disabled, if the instruction is + // valid/invalid, and if a stake account is passed in or not, assert the result: + // + // feature | instruction | account || result + // ---------+-------------+---------++-------- + // enabled | good | some || Ok + // enabled | bad | some || Err InvalidInstructionData + // enabled | good | none || Err NotEnoughAccountKeys + // enabled | bad | none || Err InvalidInstructionData + // disabled | good | some || Ok + // disabled | bad | some || Err InvalidInstructionData + // disabled | good | none || Err NotEnoughAccountKeys + // disabled | bad | none || Err NotEnoughAccountKeys + fn do_test_stake_process_instruction_error_ordering(feature_set: FeatureSet) { + let rent = Rent::default(); + let rent_address = sysvar::rent::id(); + let rent_account = account::create_account_shared_data_for_test(&rent); + + let good_stake_address = Pubkey::new_unique(); + let good_stake_account = AccountSharedData::new(u64::MAX, StakeState::size_of(), &id()); + let good_instruction = instruction::initialize( + &good_stake_address, + &Authorized::auto(&good_stake_address), + &Lockup::default(), + ); + let good_transaction_accounts = vec![ + (good_stake_address, good_stake_account), + (rent_address, rent_account), + ]; + let good_instruction_accounts = vec![ + AccountMeta { + pubkey: good_stake_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: rent_address, + is_signer: false, + is_writable: false, + }, + ]; + let good_accounts = (good_transaction_accounts, good_instruction_accounts); + + // The instruction data needs to deserialize to a bogus StakeInstruction. We likely never + // will have `usize::MAX`-number of instructions, so this should be a safe constant to + // always map to an invalid stake instruction. + let bad_instruction = Instruction::new_with_bincode(id(), &usize::MAX, Vec::default()); + let bad_transaction_accounts = Vec::default(); + let bad_instruction_accounts = Vec::default(); + let bad_accounts = (bad_transaction_accounts, bad_instruction_accounts); + + for ( + is_feature_enabled, + instruction, + (transaction_accounts, instruction_accounts), + expected_result, + ) in [ + (true, &good_instruction, &good_accounts, Ok(())), + ( + true, + &bad_instruction, + &good_accounts, + Err(InstructionError::InvalidInstructionData), + ), + ( + true, + &good_instruction, + &bad_accounts, + Err(InstructionError::NotEnoughAccountKeys), + ), + ( + true, + &bad_instruction, + &bad_accounts, + Err(InstructionError::InvalidInstructionData), + ), + (false, &good_instruction, &good_accounts, Ok(())), + ( + false, + &bad_instruction, + &good_accounts, + Err(InstructionError::InvalidInstructionData), + ), + ( + false, + &good_instruction, + &bad_accounts, + Err(InstructionError::NotEnoughAccountKeys), + ), + ( + false, + &bad_instruction, + &bad_accounts, + Err(InstructionError::NotEnoughAccountKeys), + ), + ] { + let mut feature_set = feature_set.clone(); + if !is_feature_enabled { + feature_set.deactivate( + &feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(), + ); + } + + mock_process_instruction( + &id(), + Vec::new(), + &instruction.data, + transaction_accounts.clone(), + instruction_accounts.clone(), + None, + Some(Arc::new(feature_set)), + expected_result, + super::process_instruction, + ); + } + } + + fn do_test_deactivate_delinquent(feature_set: FeatureSet) { + let feature_set = Arc::new(feature_set); + let mut sysvar_cache_override = SysvarCache::default(); + + let reference_vote_address = Pubkey::new_unique(); + let vote_address = Pubkey::new_unique(); + let stake_address = Pubkey::new_unique(); + + let initial_stake_state = StakeState::Stake( + Meta::default(), + new_stake( + 1, /* stake */ + &vote_address, + &VoteState::default(), + 1, /* activation_epoch */ + &stake_config::Config::default(), + ), + ); + + let stake_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &initial_stake_state, + StakeState::size_of(), + &id(), + ) + .unwrap(); + + let mut vote_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &VoteStateVersions::new_current(VoteState::default()), + VoteState::size_of(), + &solana_vote_program::id(), + ) + .unwrap(); + + let mut reference_vote_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &VoteStateVersions::new_current(VoteState::default()), + VoteState::size_of(), + &solana_vote_program::id(), + ) + .unwrap(); + + let current_epoch = 20; + + sysvar_cache_override.set_clock(Clock { + epoch: current_epoch, + ..Clock::default() + }); + + let process_instruction_deactivate_delinquent = + |stake_address: &Pubkey, + stake_account: &AccountSharedData, + vote_account: &AccountSharedData, + reference_vote_account: &AccountSharedData, + expected_result| { + process_instruction_with_overrides( + &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), + vec![ + (*stake_address, stake_account.clone()), + (vote_address, vote_account.clone()), + (reference_vote_address, reference_vote_account.clone()), + ], + vec![ + AccountMeta { + pubkey: *stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: reference_vote_address, + is_signer: false, + is_writable: false, + }, + ], + Some(&sysvar_cache_override), + Some(Arc::clone(&feature_set)), + expected_result, + ) + }; + + // `reference_vote_account` has not voted. Instruction will fail + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::InsufficientReferenceVotes.into()), + ); + + // `reference_vote_account` has not consistently voted for at least + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. + // Instruction will fail + let mut reference_vote_state = VoteState::default(); + for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 { + reference_vote_state.increment_credits(epoch as Epoch, 1); + } + reference_vote_account + .borrow_mut() + .serialize_data(&VoteStateVersions::new_current(reference_vote_state)) + .unwrap(); + + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::InsufficientReferenceVotes.into()), + ); + + // `reference_vote_account` has not consistently voted for the last + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. + // Instruction will fail + let mut reference_vote_state = VoteState::default(); + for epoch in 0..=current_epoch { + reference_vote_state.increment_credits(epoch, 1); + } + assert_eq!( + reference_vote_state.epoch_credits[current_epoch as usize - 2].0, + current_epoch - 2 + ); + reference_vote_state + .epoch_credits + .remove(current_epoch as usize - 2); + assert_eq!( + reference_vote_state.epoch_credits[current_epoch as usize - 2].0, + current_epoch - 1 + ); + reference_vote_account + .borrow_mut() + .serialize_data(&VoteStateVersions::new_current(reference_vote_state)) + .unwrap(); + + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::InsufficientReferenceVotes.into()), + ); + + // `reference_vote_account` has consistently voted and `vote_account` has never voted. + // Instruction will succeed + let mut reference_vote_state = VoteState::default(); + for epoch in 0..=current_epoch { + reference_vote_state.increment_credits(epoch, 1); + } + reference_vote_account + .borrow_mut() + .serialize_data(&VoteStateVersions::new_current(reference_vote_state)) + .unwrap(); + + let post_stake_account = &process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Ok(()), + )[0]; + + assert_eq!( + stake_from(post_stake_account) + .unwrap() + .delegation + .deactivation_epoch, + current_epoch + ); + + // `reference_vote_account` has consistently voted and `vote_account` has not voted for the + // last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. + // Instruction will succeed + + let mut vote_state = VoteState::default(); + for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 { + vote_state.increment_credits(epoch as Epoch, 1); + } + vote_account + .serialize_data(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + + let post_stake_account = &process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Ok(()), + )[0]; + + assert_eq!( + stake_from(post_stake_account) + .unwrap() + .delegation + .deactivation_epoch, + current_epoch + ); + + // `reference_vote_account` has consistently voted and `vote_account` has not voted for the + // last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. Try to deactivate an unrelated stake + // account. Instruction will fail + let unrelated_vote_address = Pubkey::new_unique(); + let unrelated_stake_address = Pubkey::new_unique(); + let mut unrelated_stake_account = stake_account.clone(); + assert_ne!(unrelated_vote_address, vote_address); + unrelated_stake_account + .serialize_data(&StakeState::Stake( + Meta::default(), + new_stake( + 1, /* stake */ + &unrelated_vote_address, + &VoteState::default(), + 1, /* activation_epoch */ + &stake_config::Config::default(), + ), + )) + .unwrap(); + + process_instruction_deactivate_delinquent( + &unrelated_stake_address, + &unrelated_stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::VoteAddressMismatch.into()), + ); + + // `reference_vote_account` has consistently voted and `vote_account` voted once + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` ago. + // Instruction will succeed + let mut vote_state = VoteState::default(); + vote_state.increment_credits( + current_epoch - MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch, + 1, + ); + vote_account + .serialize_data(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Ok(()), + ); + + // `reference_vote_account` has consistently voted and `vote_account` voted once + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` - 1 epochs ago + // Instruction will fail + let mut vote_state = VoteState::default(); + vote_state.increment_credits( + current_epoch - (MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION - 1) as Epoch, + 1, + ); + vote_account + .serialize_data(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()), + ); + } + + mod old_behavior { + use super::*; + + fn new_feature_set() -> FeatureSet { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&feature_set::stake_raise_minimum_delegation_to_1_sol::id()); + feature_set + } + + #[test] + fn test_stake_process_instruction() { + do_test_stake_process_instruction(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_decode_bail() { + do_test_stake_process_instruction_decode_bail(new_feature_set()); + } + #[test] + fn test_stake_checked_instructions() { + do_test_stake_checked_instructions(new_feature_set()); + } + #[test] + fn test_stake_initialize() { + do_test_stake_initialize(new_feature_set()); + } + #[test] + fn test_authorize() { + do_test_authorize(new_feature_set()); + } + #[test] + fn test_authorize_override() { + do_test_authorize_override(new_feature_set()); + } + #[test] + fn test_authorize_with_seed() { + do_test_authorize_with_seed(new_feature_set()); + } + #[test] + fn test_authorize_delegated_stake() { + do_test_authorize_delegated_stake(new_feature_set()); + } + #[test] + fn test_stake_delegate() { + do_test_stake_delegate(new_feature_set()); + } + #[test] + fn test_redelegate_consider_balance_changes() { + do_test_redelegate_consider_balance_changes(new_feature_set()); + } + #[test] + fn test_split() { + do_test_split(new_feature_set()); + } + #[test] + fn test_withdraw_stake() { + do_test_withdraw_stake(new_feature_set()); + } + #[test] + fn test_withdraw_stake_before_warmup() { + do_test_withdraw_stake_before_warmup(new_feature_set()); + } + #[test] + fn test_withdraw_lockup() { + do_test_withdraw_lockup(new_feature_set()); + } + #[test] + fn test_withdraw_rent_exempt() { + do_test_withdraw_rent_exempt(new_feature_set()); + } + #[test] + fn test_deactivate() { + do_test_deactivate(new_feature_set()); + } + #[test] + fn test_set_lockup() { + do_test_set_lockup(new_feature_set()); + } + #[test] + fn test_initialize_minimum_balance() { + do_test_initialize_minimum_balance(new_feature_set()); + } + #[test] + fn test_delegate_minimum_stake_delegation() { + do_test_delegate_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_minimum_stake_delegation() { + do_test_split_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_full_amount_minimum_stake_delegation() { + do_test_split_full_amount_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_initialized_split_destination_minimum_balance() { + do_test_initialized_split_destination_minimum_balance(new_feature_set()); + } + #[test] + fn test_staked_split_destination_minimum_balance() { + do_test_staked_split_destination_minimum_balance(new_feature_set(), &[Ok(()), Ok(())]); + } + #[test] + fn test_withdraw_minimum_stake_delegation() { + do_test_withdraw_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() { + let mut feature_set = new_feature_set(); + // The "old" behavior relies on `validate_delegated_amount()` *not* checking if the + // stake amount meets the minimum delegation. Once the + // `stake_allow_zero_undelegated_amount` feature is activated, `the expected_result` + // parameter can be removed and consolidated. + feature_set.deactivate(&feature_set::stake_allow_zero_undelegated_amount::id()); + do_test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + feature_set, + Ok(()), + ); + } + #[test] + fn test_split_source_uninitialized() { + do_test_split_source_uninitialized(new_feature_set()); + } + #[test] + fn test_split_split_not_uninitialized() { + do_test_split_split_not_uninitialized(new_feature_set()); + } + #[test] + fn test_split_more_than_staked() { + do_test_split_more_than_staked(new_feature_set()); + } + #[test] + fn test_split_with_rent() { + do_test_split_with_rent(new_feature_set()); + } + #[test] + fn test_split_to_account_with_rent_exempt_reserve() { + do_test_split_to_account_with_rent_exempt_reserve(new_feature_set()); + } + #[test] + fn test_split_from_larger_sized_account() { + do_test_split_from_larger_sized_account(new_feature_set()); + } + #[test] + fn test_split_from_smaller_sized_account() { + do_test_split_from_smaller_sized_account(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source() { + do_test_split_100_percent_of_source(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source_to_account_with_lamports() { + do_test_split_100_percent_of_source_to_account_with_lamports(new_feature_set()); + } + #[test] + fn test_split_rent_exemptness() { + do_test_split_rent_exemptness(new_feature_set()); + } + #[test] + fn test_merge() { + do_test_merge(new_feature_set()); + } + #[test] + fn test_merge_self_fails() { + do_test_merge_self_fails(new_feature_set()); + } + #[test] + fn test_merge_incorrect_authorized_staker() { + do_test_merge_incorrect_authorized_staker(new_feature_set()); + } + #[test] + fn test_merge_invalid_account_data() { + do_test_merge_invalid_account_data(new_feature_set()); + } + #[test] + fn test_merge_fake_stake_source() { + do_test_merge_fake_stake_source(new_feature_set()); + } + #[test] + fn test_merge_active_stake() { + do_test_merge_active_stake(new_feature_set()); + } + #[test] + fn test_stake_get_minimum_delegation() { + do_test_stake_get_minimum_delegation(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_error_ordering() { + do_test_stake_process_instruction_error_ordering(new_feature_set()); + } + #[test] + fn test_deactivate_delinquent() { + do_test_deactivate_delinquent(new_feature_set()); + } + } + + mod new_behavior { + use super::*; + + fn new_feature_set() -> FeatureSet { + FeatureSet::all_enabled() + } + + #[test] + fn test_stake_process_instruction() { + do_test_stake_process_instruction(new_feature_set()); + } + #[test] + fn test_spoofed_stake_accounts() { + do_test_spoofed_stake_accounts(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_decode_bail() { + do_test_stake_process_instruction_decode_bail(new_feature_set()); + } + #[test] + fn test_stake_checked_instructions() { + do_test_stake_checked_instructions(new_feature_set()); + } + #[test] + fn test_stake_initialize() { + do_test_stake_initialize(new_feature_set()); + } + #[test] + fn test_authorize() { + do_test_authorize(new_feature_set()); + } + #[test] + fn test_authorize_override() { + do_test_authorize_override(new_feature_set()); + } + #[test] + fn test_authorize_with_seed() { + do_test_authorize_with_seed(new_feature_set()); + } + #[test] + fn test_authorize_delegated_stake() { + do_test_authorize_delegated_stake(new_feature_set()); + } + #[test] + fn test_stake_delegate() { + do_test_stake_delegate(new_feature_set()); + } + #[test] + fn test_redelegate_consider_balance_changes() { + do_test_redelegate_consider_balance_changes(new_feature_set()); + } + #[test] + fn test_split() { + do_test_split(new_feature_set()); + } + #[test] + fn test_withdraw_stake() { + do_test_withdraw_stake(new_feature_set()); + } + #[test] + fn test_withdraw_stake_before_warmup() { + do_test_withdraw_stake_before_warmup(new_feature_set()); + } + #[test] + fn test_withdraw_lockup() { + do_test_withdraw_lockup(new_feature_set()); + } + #[test] + fn test_withdraw_rent_exempt() { + do_test_withdraw_rent_exempt(new_feature_set()); + } + #[test] + fn test_deactivate() { + do_test_deactivate(new_feature_set()); + } + #[test] + fn test_set_lockup() { + do_test_set_lockup(new_feature_set()); + } + #[test] + fn test_initialize_minimum_balance() { + do_test_initialize_minimum_balance(new_feature_set()); + } + #[test] + fn test_delegate_minimum_stake_delegation() { + do_test_delegate_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_minimum_stake_delegation() { + do_test_split_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_split_full_amount_minimum_stake_delegation() { + do_test_split_full_amount_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_initialized_split_destination_minimum_balance() { + do_test_initialized_split_destination_minimum_balance(new_feature_set()); + } + #[test] + fn test_staked_split_destination_minimum_balance() { + do_test_staked_split_destination_minimum_balance( + new_feature_set(), + &[ + Err(InstructionError::InsufficientFunds), + Err(InstructionError::InsufficientFunds), + ], + ); + } + #[test] + fn test_withdraw_minimum_stake_delegation() { + do_test_withdraw_minimum_stake_delegation(new_feature_set()); + } + #[test] + fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation() { + do_test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + new_feature_set(), + Err(StakeError::InsufficientDelegation.into()), + ); + } + #[test] + fn test_split_source_uninitialized() { + do_test_split_source_uninitialized(new_feature_set()); + } + #[test] + fn test_split_split_not_uninitialized() { + do_test_split_split_not_uninitialized(new_feature_set()); + } + #[test] + fn test_split_more_than_staked() { + do_test_split_more_than_staked(new_feature_set()); + } + #[test] + fn test_split_with_rent() { + do_test_split_with_rent(new_feature_set()); + } + #[test] + fn test_split_to_account_with_rent_exempt_reserve() { + do_test_split_to_account_with_rent_exempt_reserve(new_feature_set()); + } + #[test] + fn test_split_from_larger_sized_account() { + do_test_split_from_larger_sized_account(new_feature_set()); + } + #[test] + fn test_split_from_smaller_sized_account() { + do_test_split_from_smaller_sized_account(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source() { + do_test_split_100_percent_of_source(new_feature_set()); + } + #[test] + fn test_split_100_percent_of_source_to_account_with_lamports() { + do_test_split_100_percent_of_source_to_account_with_lamports(new_feature_set()); + } + #[test] + fn test_split_rent_exemptness() { + do_test_split_rent_exemptness(new_feature_set()); + } + #[test] + fn test_merge() { + do_test_merge(new_feature_set()); + } + #[test] + fn test_merge_self_fails() { + do_test_merge_self_fails(new_feature_set()); + } + #[test] + fn test_merge_incorrect_authorized_staker() { + do_test_merge_incorrect_authorized_staker(new_feature_set()); + } + #[test] + fn test_merge_invalid_account_data() { + do_test_merge_invalid_account_data(new_feature_set()); + } + #[test] + fn test_merge_fake_stake_source() { + do_test_merge_fake_stake_source(new_feature_set()); + } + #[test] + fn test_merge_active_stake() { + do_test_merge_active_stake(new_feature_set()); + } + #[test] + fn test_stake_get_minimum_delegation() { + do_test_stake_get_minimum_delegation(new_feature_set()); + } + #[test] + fn test_stake_process_instruction_error_ordering() { + do_test_stake_process_instruction_error_ordering(new_feature_set()); + } + #[test] + fn test_deactivate_delinquent() { + do_test_deactivate_delinquent(new_feature_set()); + } + } +>>>>>>> cbb0f07d5 (Award one credit per dequeued vote when processing VoteStateUpdate in… (#25743)) } diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 71f6fd31ee14da..a0e7381a4b8752 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -2162,8 +2162,8 @@ mod tests { ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0); - vote_state.increment_credits(0); + vote_state.increment_credits(0, 1); + vote_state.increment_credits(0, 1); // this one should be able to collect exactly 2 assert_eq!( @@ -2224,7 +2224,7 @@ mod tests { // put 193,536,000 credits in at epoch 0, typical for a 14-day epoch // this loop takes a few seconds... for _ in 0..epoch_slots { - vote_state.increment_credits(0); + vote_state.increment_credits(0, 1); } // no overflow on points @@ -2265,8 +2265,8 @@ mod tests { ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0); - vote_state.increment_credits(0); + vote_state.increment_credits(0, 1); + vote_state.increment_credits(0, 1); // this one should be able to collect exactly 2 assert_eq!( @@ -2304,7 +2304,7 @@ mod tests { ); // put 1 credit in epoch 1 - vote_state.increment_credits(1); + vote_state.increment_credits(1, 1); stake.credits_observed = 2; // this one should be able to collect the one just added @@ -2325,7 +2325,7 @@ mod tests { ); // put 1 credit in epoch 2 - vote_state.increment_credits(2); + vote_state.increment_credits(2, 1); // this one should be able to collect 2 now assert_eq!( Some((stake.delegation.stake * 2, 0, 4)), diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index 46da34fc0421b4..7e19bf05bf59cb 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -101,6 +101,7 @@ pub fn process_instruction( &clock, vote_state_update, &signers, + &invoke_context.feature_set, ) } else { Err(InstructionError::InvalidInstructionData) diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index fe5587a2ace6c2..54644898fc9c20 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -763,6 +763,7 @@ impl VoteState { new_root: Option, timestamp: Option, epoch: Epoch, + feature_set: Option<&FeatureSet>, ) -> Result<(), VoteError> { assert!(!new_state.is_empty()); if new_state.len() > MAX_LOCKOUT_HISTORY { @@ -822,12 +823,23 @@ impl VoteState { let mut current_vote_state_index = 0; let mut new_vote_state_index = 0; + // Count the number of slots at and before the new root within the current vote state lockouts. Start with 1 + // for the new root. The purpose of this is to know how many slots were rooted by this state update: + // - The new root was rooted + // - As were any slots that were in the current state but are not in the new state. The only slots which + // can be in this set are those oldest slots in the current vote state that are not present in the + // new vote state; these have been "popped off the back" of the tower and thus represent finalized slots + let mut finalized_slot_count = 1_u64; + for current_vote in &self.votes { // Find the first vote in the current vote state for a slot greater // than the new proposed root if let Some(new_root) = new_root { if current_vote.slot <= new_root { current_vote_state_index += 1; + if current_vote.slot != new_root { + finalized_slot_count += 1; + } continue; } } @@ -871,9 +883,19 @@ impl VoteState { // `new_vote_state` passed all the checks, finalize the change by rewriting // our state. if self.root_slot != new_root { - // TODO to think about: Note, people may be incentivized to set more - // roots to get more credits, but I think they can already do this... - self.increment_credits(epoch); + // Award vote credits based on the number of slots that were voted on and have reached finality + if feature_set + .map(|feature_set| { + feature_set.is_active(&feature_set::vote_state_update_credit_per_dequeue::id()) + }) + .unwrap_or(false) + { + // For each finalized slot, there was one voted-on slot in the new vote state that was responsible for + // finalizing it. Each of those votes is awarded 1 credit. + self.increment_credits(epoch, finalized_slot_count); + } else { + self.increment_credits(epoch, 1); + } } if let Some(timestamp) = timestamp { let last_slot = new_state.back().unwrap().slot; @@ -941,14 +963,14 @@ impl VoteState { let vote = self.votes.pop_front().unwrap(); self.root_slot = Some(vote.slot); - self.increment_credits(epoch); + self.increment_credits(epoch, 1); } self.votes.push_back(vote); self.double_lockouts(); } /// increment credits, record credits for last epoch if new epoch - pub fn increment_credits(&mut self, epoch: Epoch) { + pub fn increment_credits(&mut self, epoch: Epoch, credits: u64) { // increment credits, record by epoch // never seen a credit @@ -972,7 +994,7 @@ impl VoteState { } } - self.epoch_credits.last_mut().unwrap().1 += 1; + self.epoch_credits.last_mut().unwrap().1 += credits; } /// "unchecked" functions used by tests and Tower @@ -1384,6 +1406,7 @@ pub fn process_vote_state_update( clock: &Clock, mut vote_state_update: VoteStateUpdate, signers: &HashSet, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?; vote_state.check_update_vote_state_slots_are_valid(&mut vote_state_update, slot_hashes)?; @@ -1392,6 +1415,7 @@ pub fn process_vote_state_update( vote_state_update.root, vote_state_update.timestamp, clock.epoch, + Some(feature_set), )?; vote_account.set_state(&VoteStateVersions::new_current(vote_state)) } @@ -1849,7 +1873,7 @@ mod tests { let epochs = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; for epoch in 0..epochs { for _j in 0..epoch { - vote_state.increment_credits(epoch); + vote_state.increment_credits(epoch, 1); credits += 1; } expected.push((epoch, credits, credits - epoch)); @@ -1868,10 +1892,10 @@ mod tests { let mut vote_state = VoteState::default(); assert_eq!(vote_state.epoch_credits().len(), 0); - vote_state.increment_credits(1); + vote_state.increment_credits(1, 1); assert_eq!(vote_state.epoch_credits().len(), 1); - vote_state.increment_credits(2); + vote_state.increment_credits(2, 1); assert_eq!(vote_state.epoch_credits().len(), 2); } @@ -1881,12 +1905,89 @@ mod tests { let credits = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; for i in 0..credits { - vote_state.increment_credits(i as u64); + vote_state.increment_credits(i as u64, 1); } assert_eq!(vote_state.credits(), credits); assert!(vote_state.epoch_credits().len() <= MAX_EPOCH_CREDITS_HISTORY); } + // Test vote credit updates after "one credit per slot" feature is enabled + #[test] + fn test_vote_state_update_increment_credits() { + // Create a new Votestate + let mut vote_state = VoteState::new(&VoteInit::default(), &Clock::default()); + + // 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![ + // Initial set of votes that don't dequeue any slots, so no credits earned + vec![1, 2, 3, 4, 5, 6, 7, 8], + vec![ + 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, + ], + // Now a single vote which should result in the first root and first credit earned + vec![32], + // Now another vote, should earn one credit + vec![33], + // Two votes in sequence + vec![34, 35], + // 3 votes in sequence + vec![36, 37, 38], + // 30 votes in sequence + vec![ + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, + ], + // 31 votes in sequence + vec![ + 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + ], + // Votes with expiry + vec![100, 101, 106, 107, 112, 116, 120, 121, 122, 124], + // More votes with expiry of a large number of votes + vec![200, 201], + vec![ + 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, + 218, 219, 220, 221, 222, 223, 224, 225, 226, + ], + vec![227, 228, 229, 230, 231, 232, 233, 234, 235, 236], + ]; + + let mut feature_set = FeatureSet::default(); + feature_set.activate(&feature_set::vote_state_update_credit_per_dequeue::id(), 1); + + for vote_group in test_vote_groups { + // Duplicate vote_state so that the new vote can be applied + let mut vote_state_after_vote = vote_state.clone(); + + vote_state_after_vote.process_vote_unchecked(Vote { + slots: vote_group.clone(), + hash: Hash::new_unique(), + timestamp: None, + }); + + // Now use the resulting new vote state to perform a vote state update on vote_state + assert_eq!( + vote_state.process_new_vote_state( + vote_state_after_vote.votes, + vote_state_after_vote.root_slot, + None, + 0, + Some(&feature_set) + ), + Ok(()) + ); + + // And ensure that the credits earned were the same + assert_eq!( + vote_state.epoch_credits, + vote_state_after_vote.epoch_credits + ); + } + } + #[test] fn test_vote_process_timestamp() { let (slot, timestamp) = (15, 1_575_412_285); @@ -2228,7 +2329,13 @@ mod tests { .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None, + ), Err(VoteError::TooManyVotes) ); } @@ -2255,6 +2362,7 @@ mod tests { lesser_root, None, vote_state2.current_epoch(), + None, ), Err(VoteError::RootRollBack) ); @@ -2267,6 +2375,7 @@ mod tests { none_root, None, vote_state2.current_epoch(), + None, ), Err(VoteError::RootRollBack) ); @@ -2289,7 +2398,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None, + ), Err(VoteError::ZeroConfirmations) ); @@ -2306,7 +2421,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None, + ), Err(VoteError::ZeroConfirmations) ); } @@ -2323,7 +2444,7 @@ mod tests { .collect(); vote_state1 - .process_new_vote_state(good_votes, None, None, vote_state1.current_epoch()) + .process_new_vote_state(good_votes, None, None, vote_state1.current_epoch(), None) .unwrap(); let mut vote_state1 = VoteState::default(); @@ -2334,7 +2455,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::ConfirmationTooLarge) ); } @@ -2362,6 +2489,7 @@ mod tests { Some(root_slot), None, vote_state1.current_epoch(), + None, ), Err(VoteError::SlotSmallerThanRoot) ); @@ -2384,6 +2512,7 @@ mod tests { Some(root_slot), None, vote_state1.current_epoch(), + None, ), Err(VoteError::SlotSmallerThanRoot) ); @@ -2406,7 +2535,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::SlotsNotOrdered) ); @@ -2423,7 +2558,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::SlotsNotOrdered) ); } @@ -2445,7 +2586,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::ConfirmationsNotOrdered) ); @@ -2462,7 +2609,13 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::ConfirmationsNotOrdered) ); } @@ -2486,7 +2639,13 @@ mod tests { // Slot 7 should have expired slot 0 assert_eq!( - vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + None, + None, + vote_state1.current_epoch(), + None, + ), Err(VoteError::NewVoteStateLockoutMismatch) ); } @@ -2507,7 +2666,7 @@ mod tests { .into_iter() .collect(); vote_state1 - .process_new_vote_state(votes, None, None, vote_state1.current_epoch()) + .process_new_vote_state(votes, None, None, vote_state1.current_epoch(), None) .unwrap(); let votes: VecDeque = vec![ @@ -2530,7 +2689,13 @@ mod tests { // Should error because newer vote state should not have lower confirmation the same slot // 1 assert_eq!( - vote_state1.process_new_vote_state(votes, None, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + votes, + None, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::ConfirmationRollBack) ); } @@ -2561,6 +2726,7 @@ mod tests { vote_state2.root_slot, None, vote_state2.current_epoch(), + None, ) .unwrap(); @@ -2618,6 +2784,7 @@ mod tests { vote_state2.root_slot, None, vote_state2.current_epoch(), + None, ) .unwrap(); @@ -2658,6 +2825,7 @@ mod tests { vote_state2.root_slot, None, vote_state2.current_epoch(), + None ), Err(VoteError::LockoutConflict) ); @@ -2698,6 +2866,7 @@ mod tests { vote_state2.root_slot, None, vote_state2.current_epoch(), + None ), Err(VoteError::LockoutConflict) ); @@ -2742,6 +2911,7 @@ mod tests { vote_state2.root_slot, None, vote_state2.current_epoch(), + None, ) .unwrap(); assert_eq!(vote_state1, vote_state2,); @@ -2777,7 +2947,13 @@ mod tests { let root = Some(1); assert_eq!( - vote_state1.process_new_vote_state(bad_votes, root, None, vote_state1.current_epoch(),), + vote_state1.process_new_vote_state( + bad_votes, + root, + None, + vote_state1.current_epoch(), + None + ), Err(VoteError::LockoutConflict) ); @@ -2795,7 +2971,13 @@ mod tests { .collect(); vote_state1 - .process_new_vote_state(good_votes.clone(), root, None, vote_state1.current_epoch()) + .process_new_vote_state( + good_votes.clone(), + root, + None, + vote_state1.current_epoch(), + None, + ) .unwrap(); assert_eq!(vote_state1.votes, good_votes); } diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index b3662ec971fea9..fd3e461e16f46a 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -375,8 +375,13 @@ pub mod enable_durable_nonce { solana_sdk::declare_id!("4EJQtF2pkRyawwcTVfQutzq4Sa5hRhibF6QAK1QXhtEX"); } +<<<<<<< HEAD pub mod executables_incur_cpi_data_cost { solana_sdk::declare_id!("7GUcYgq4tVtaqNCKT3dho9r4665Qp5TxCZ27Qgjx3829"); +======= +pub mod vote_state_update_credit_per_dequeue { + solana_sdk::declare_id!("CveezY6FDLVBToHDcvJRmtMouqzsmj4UXYh5ths5G5Uv"); +>>>>>>> cbb0f07d5 (Award one credit per dequeued vote when processing VoteStateUpdate in… (#25743)) } lazy_static! { @@ -466,7 +471,11 @@ lazy_static! { (warp_timestamp_with_a_vengeance::id(), "warp timestamp again, adjust bounding to 150% slow #25666"), (separate_nonce_from_blockhash::id(), "separate durable nonce and blockhash domains #25744"), (enable_durable_nonce::id(), "enable durable nonce #25744"), +<<<<<<< HEAD (executables_incur_cpi_data_cost::id(), "Executables incure CPI data costs"), +======= + (vote_state_update_credit_per_dequeue::id(), "Calculate vote credits for VoteStateUpdate per vote dequeue to match credit awards for Vote instruction"), +>>>>>>> cbb0f07d5 (Award one credit per dequeued vote when processing VoteStateUpdate in… (#25743)) /*************** ADD NEW FEATURES HERE ***************/ ] .iter()