From c0a780c7f5fbe2492554318ef0189acaa04cb96f Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 23 Dec 2022 17:27:16 +0100 Subject: [PATCH] stake-pool: Add `DecreaseAdditionalValidatorStake` instruction (#3925) * stake-pool: Add `DecreaseAdditionalValidatorStake` instruction * Update checks for deduping --- stake-pool/program/src/instruction.rs | 115 ++++++ stake-pool/program/src/processor.rs | 195 +++++++--- stake-pool/program/tests/decrease.rs | 456 ++++++++++++++++-------- stake-pool/program/tests/helpers/mod.rs | 86 +++++ 4 files changed, 654 insertions(+), 198 deletions(-) diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 6d3d6fd5beb..ad468443fe8 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -442,6 +442,37 @@ pub enum StakePoolInstruction { /// seed used to create ephemeral account. ephemeral_stake_seed: u64, }, + + /// (Staker only) Decrease active stake again from a validator, eventually moving it to the reserve + /// + /// Works regardless if the transient stake account already exists. + /// + /// Internally, this instruction splits a validator stake account into an + /// ephemeral stake account, deactivates it, then merges or splits it into + /// the transient stake account delegated to the appropriate validator. + /// + /// The amount of lamports to move must be at least rent-exemption plus + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. + /// + /// 0. `[]` Stake pool + /// 1. `[s]` Stake pool staker + /// 2. `[]` Stake pool withdraw authority + /// 3. `[w]` Validator list + /// 4. `[w]` Canonical stake account to split from + /// 5. `[w]` Uninitialized ephemeral stake account to receive stake + /// 6. `[w]` Transient stake account + /// 7. `[]` Clock sysvar + /// 8. '[]' Stake history sysvar + /// 9. `[]` System program + /// 10. `[]` Stake program + DecreaseAdditionalValidatorStake { + /// amount of lamports to split into the transient stake account + lamports: u64, + /// seed used to create transient stake account + transient_stake_seed: u64, + /// seed used to create ephemeral account. + ephemeral_stake_seed: u64, + }, } /// Creates an 'initialize' instruction. @@ -595,6 +626,47 @@ pub fn decrease_validator_stake( } } +/// Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from +/// validator account to transient account) +pub fn decrease_additional_validator_stake( + program_id: &Pubkey, + stake_pool: &Pubkey, + staker: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + validator_list: &Pubkey, + validator_stake: &Pubkey, + ephemeral_stake: &Pubkey, + transient_stake: &Pubkey, + lamports: u64, + transient_stake_seed: u64, + ephemeral_stake_seed: u64, +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*stake_pool, false), + AccountMeta::new_readonly(*staker, true), + AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), + AccountMeta::new(*validator_list, false), + AccountMeta::new(*validator_stake, false), + AccountMeta::new(*ephemeral_stake, false), + AccountMeta::new(*transient_stake, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(stake::program::id(), false), + ]; + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::DecreaseAdditionalValidatorStake { + lamports, + transient_stake_seed, + ephemeral_stake_seed, + } + .try_to_vec() + .unwrap(), + } +} + /// Creates `IncreaseValidatorStake` instruction (rebalance from reserve account to /// transient account) pub fn increase_validator_stake( @@ -895,6 +967,49 @@ pub fn decrease_validator_stake_with_vote( ) } +/// Create a `DecreaseAdditionalValidatorStake` instruction given an existing +/// stake pool and vote account +pub fn decrease_additional_validator_stake_with_vote( + program_id: &Pubkey, + stake_pool: &StakePool, + stake_pool_address: &Pubkey, + vote_account_address: &Pubkey, + lamports: u64, + validator_stake_seed: Option, + transient_stake_seed: u64, + ephemeral_stake_seed: u64, +) -> Instruction { + let pool_withdraw_authority = + find_withdraw_authority_program_address(program_id, stake_pool_address).0; + let (validator_stake_address, _) = find_stake_program_address( + program_id, + vote_account_address, + stake_pool_address, + validator_stake_seed, + ); + let (ephemeral_stake_address, _) = + find_ephemeral_stake_program_address(program_id, stake_pool_address, ephemeral_stake_seed); + let (transient_stake_address, _) = find_transient_stake_program_address( + program_id, + vote_account_address, + stake_pool_address, + transient_stake_seed, + ); + decrease_additional_validator_stake( + program_id, + stake_pool_address, + &stake_pool.staker, + &pool_withdraw_authority, + &stake_pool.validator_list, + &validator_stake_address, + &ephemeral_stake_address, + &transient_stake_address, + lamports, + transient_stake_seed, + ephemeral_stake_seed, + ) +} + /// Creates `UpdateValidatorListBalance` instruction (update validator stake account balances) pub fn update_validator_list_balance( program_id: &Pubkey, diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 6aa9c67b870..761272e1a6e 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -1124,6 +1124,7 @@ impl Processor { accounts: &[AccountInfo], lamports: u64, transient_stake_seed: u64, + maybe_ephemeral_stake_seed: Option, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; @@ -1131,11 +1132,23 @@ impl Processor { let withdraw_authority_info = next_account_info(account_info_iter)?; let validator_list_info = next_account_info(account_info_iter)?; let validator_stake_account_info = next_account_info(account_info_iter)?; + let maybe_ephemeral_stake_account_info = maybe_ephemeral_stake_seed + .map(|_| next_account_info(account_info_iter)) + .transpose()?; let transient_stake_account_info = next_account_info(account_info_iter)?; let clock_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(clock_info)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; + let rent = if maybe_ephemeral_stake_seed.is_some() { + // instruction with ephemeral account doesn't take the rent account + Rent::get()? + } else { + // legacy instruction takes the rent account + let rent_info = next_account_info(account_info_iter)?; + Rent::from_account_info(rent_info)? + }; + let maybe_stake_history_info = maybe_ephemeral_stake_seed + .map(|_| next_account_info(account_info_iter)) + .transpose()?; let system_program_info = next_account_info(account_info_iter)?; let stake_program_info = next_account_info(account_info_iter)?; @@ -1191,24 +1204,21 @@ impl Processor { NonZeroU32::new(validator_stake_info.validator_seed_suffix), )?; if validator_stake_info.transient_stake_lamports > 0 { - return Err(StakePoolError::TransientAccountInUse.into()); + if maybe_ephemeral_stake_seed.is_none() { + msg!("Attempting to decrease stake on a validator with pending transient stake, use DecreaseAdditionalValidatorStake with the existing seed"); + return Err(StakePoolError::TransientAccountInUse.into()); + } + if transient_stake_seed != validator_stake_info.transient_seed_suffix { + msg!( + "Transient stake already exists with seed {}, you must use that one", + validator_stake_info.transient_seed_suffix + ); + return Err(ProgramError::InvalidSeeds); + } + // Let the runtime check to see if the merge is valid, so there's no + // explicit check here that the transient stake is decreasing } - let transient_stake_bump_seed = check_transient_stake_address( - program_id, - stake_pool_info.key, - transient_stake_account_info.key, - &vote_account_address, - transient_stake_seed, - )?; - let transient_stake_account_signer_seeds: &[&[_]] = &[ - TRANSIENT_STAKE_SEED_PREFIX, - &vote_account_address.to_bytes(), - &stake_pool_info.key.to_bytes(), - &transient_stake_seed.to_le_bytes(), - &[transient_stake_bump_seed], - ]; - let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let stake_rent = rent.minimum_balance(std::mem::size_of::()); let current_minimum_lamports = @@ -1236,38 +1246,126 @@ impl Processor { return Err(ProgramError::InsufficientFunds); } - create_stake_account( - transient_stake_account_info.clone(), - transient_stake_account_signer_seeds, - system_program_info.clone(), - )?; + let source_stake_account_info = + if let Some((ephemeral_stake_seed, ephemeral_stake_account_info)) = + maybe_ephemeral_stake_seed.zip(maybe_ephemeral_stake_account_info) + { + let ephemeral_stake_bump_seed = check_ephemeral_stake_address( + program_id, + stake_pool_info.key, + ephemeral_stake_account_info.key, + ephemeral_stake_seed, + )?; + let ephemeral_stake_account_signer_seeds: &[&[_]] = &[ + EPHEMERAL_STAKE_SEED_PREFIX, + &stake_pool_info.key.to_bytes(), + &ephemeral_stake_seed.to_le_bytes(), + &[ephemeral_stake_bump_seed], + ]; + create_stake_account( + ephemeral_stake_account_info.clone(), + ephemeral_stake_account_signer_seeds, + system_program_info.clone(), + )?; - // split into transient stake account - Self::stake_split( - stake_pool_info.key, - validator_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - lamports, - transient_stake_account_info.clone(), - )?; + // split into ephemeral stake account + Self::stake_split( + stake_pool_info.key, + validator_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + lamports, + ephemeral_stake_account_info.clone(), + )?; - // deactivate transient stake - Self::stake_deactivate( - transient_stake_account_info.clone(), - clock_info.clone(), - withdraw_authority_info.clone(), + Self::stake_deactivate( + ephemeral_stake_account_info.clone(), + clock_info.clone(), + withdraw_authority_info.clone(), + stake_pool_info.key, + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + )?; + + ephemeral_stake_account_info + } else { + // if no ephemeral account is provided, split everything from the + // validator stake account, into the transient stake account + validator_stake_account_info + }; + + let transient_stake_bump_seed = check_transient_stake_address( + program_id, stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, + transient_stake_account_info.key, + &vote_account_address, + transient_stake_seed, )?; + if validator_stake_info.transient_stake_lamports > 0 { + let stake_history_info = maybe_stake_history_info.unwrap(); + // transient stake exists, try to merge from the source account, + // which is always an ephemeral account + Self::stake_merge( + stake_pool_info.key, + source_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + transient_stake_account_info.clone(), + clock_info.clone(), + stake_history_info.clone(), + stake_program_info.clone(), + )?; + } else { + let transient_stake_account_signer_seeds: &[&[_]] = &[ + TRANSIENT_STAKE_SEED_PREFIX, + &vote_account_address.to_bytes(), + &stake_pool_info.key.to_bytes(), + &transient_stake_seed.to_le_bytes(), + &[transient_stake_bump_seed], + ]; + + create_stake_account( + transient_stake_account_info.clone(), + transient_stake_account_signer_seeds, + system_program_info.clone(), + )?; + + // split into transient stake account + Self::stake_split( + stake_pool_info.key, + source_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + lamports, + transient_stake_account_info.clone(), + )?; + + // Deactivate transient stake if necessary + let (_, stake) = get_stake_state(transient_stake_account_info)?; + if stake.delegation.deactivation_epoch == Epoch::MAX { + Self::stake_deactivate( + transient_stake_account_info.clone(), + clock_info.clone(), + withdraw_authority_info.clone(), + stake_pool_info.key, + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + )?; + } + } + validator_stake_info.active_stake_lamports = validator_stake_info .active_stake_lamports .checked_sub(lamports) .ok_or(StakePoolError::CalculationFailure)?; - validator_stake_info.transient_stake_lamports = lamports; + validator_stake_info.transient_stake_lamports = validator_stake_info + .transient_stake_lamports + .checked_add(lamports) + .ok_or(StakePoolError::CalculationFailure)?; validator_stake_info.transient_seed_suffix = transient_stake_seed; Ok(()) @@ -3248,6 +3346,21 @@ impl Processor { accounts, lamports, transient_stake_seed, + None, + ) + } + StakePoolInstruction::DecreaseAdditionalValidatorStake { + lamports, + transient_stake_seed, + ephemeral_stake_seed, + } => { + msg!("Instruction: DecreaseAdditionalValidatorStake"); + Self::process_decrease_validator_stake( + program_id, + accounts, + lamports, + transient_stake_seed, + Some(ephemeral_stake_seed), ) } StakePoolInstruction::IncreaseValidatorStake { diff --git a/stake-pool/program/tests/decrease.rs b/stake-pool/program/tests/decrease.rs index 0c698efbfa8..7d263597635 100644 --- a/stake-pool/program/tests/decrease.rs +++ b/stake-pool/program/tests/decrease.rs @@ -6,72 +6,70 @@ mod helpers; use { bincode::deserialize, helpers::*, - solana_program::{ - clock::Epoch, hash::Hash, instruction::InstructionError, pubkey::Pubkey, stake, - }, + solana_program::{clock::Epoch, instruction::InstructionError, pubkey::Pubkey, stake}, solana_program_test::*, solana_sdk::{ signature::{Keypair, Signer}, transaction::{Transaction, TransactionError}, }, spl_stake_pool::{ - error::StakePoolError, find_transient_stake_program_address, id, instruction, - MINIMUM_RESERVE_LAMPORTS, + error::StakePoolError, find_ephemeral_stake_program_address, + find_transient_stake_program_address, id, instruction, MINIMUM_RESERVE_LAMPORTS, }, + test_case::test_case, }; async fn setup() -> ( - BanksClient, - Keypair, - Hash, + ProgramTestContext, StakePoolAccounts, ValidatorStakeAccount, DepositStakeAccount, u64, ) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let rent = banks_client.get_rent().await.unwrap(); + let mut context = program_test().start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = - stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; let stake_pool_accounts = StakePoolAccounts::default(); stake_pool_accounts .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + MINIMUM_RESERVE_LAMPORTS + stake_rent + current_minimum_delegation, ) .await .unwrap(); let validator_stake_account = simple_add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &stake_pool_accounts, None, ) .await; + let decrease_lamports = (current_minimum_delegation + stake_rent) * 3; let deposit_info = simple_deposit_stake( - &mut banks_client, - &payer, - &recent_blockhash, + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &stake_pool_accounts, &validator_stake_account, - current_minimum_delegation * 2 + stake_rent, + decrease_lamports, ) .await .unwrap(); - let decrease_lamports = current_minimum_delegation + stake_rent; - ( - banks_client, - payer, - recent_blockhash, + context, stake_pool_accounts, validator_stake_account, deposit_info, @@ -79,45 +77,42 @@ async fn setup() -> ( ) } +#[test_case(true; "additional")] +#[test_case(false; "no-additional")] #[tokio::test] -async fn success() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - _deposit_info, - decrease_lamports, - ) = setup().await; +async fn success(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + setup().await; // Save validator stake let pre_validator_stake_account = - get_account(&mut banks_client, &validator_stake.stake_account).await; + get_account(&mut context.banks_client, &validator_stake.stake_account).await; // Check no transient stake - let transient_account = banks_client + let transient_account = context + .banks_client .get_account(validator_stake.transient_stake_account) .await .unwrap(); assert!(transient_account.is_none()); let error = stake_pool_accounts - .decrease_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.stake_account, &validator_stake.transient_stake_account, decrease_lamports, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await; assert!(error.is_none()); // Check validator stake account balance let validator_stake_account = - get_account(&mut banks_client, &validator_stake.stake_account).await; + get_account(&mut context.banks_client, &validator_stake.stake_account).await; let validator_stake_state = deserialize::(&validator_stake_account.data).unwrap(); assert_eq!( @@ -133,8 +128,11 @@ async fn success() { ); // Check transient stake account state and balance - let transient_stake_account = - get_account(&mut banks_client, &validator_stake.transient_stake_account).await; + let transient_stake_account = get_account( + &mut context.banks_client, + &validator_stake.transient_stake_account, + ) + .await; let transient_stake_state = deserialize::(&transient_stake_account.data).unwrap(); assert_eq!(transient_stake_account.lamports, decrease_lamports); @@ -149,15 +147,8 @@ async fn success() { #[tokio::test] async fn fail_with_wrong_withdraw_authority() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - _deposit_info, - decrease_lamports, - ) = setup().await; + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + setup().await; let wrong_authority = Pubkey::new_unique(); @@ -173,11 +164,12 @@ async fn fail_with_wrong_withdraw_authority() { decrease_lamports, validator_stake.transient_stake_seed, )], - Some(&payer.pubkey()), - &[&payer, &stake_pool_accounts.staker], - recent_blockhash, + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.staker], + context.last_blockhash, ); - let error = banks_client + let error = context + .banks_client .process_transaction(transaction) .await .err() @@ -195,15 +187,8 @@ async fn fail_with_wrong_withdraw_authority() { #[tokio::test] async fn fail_with_wrong_validator_list() { - let ( - mut banks_client, - payer, - recent_blockhash, - mut stake_pool_accounts, - validator_stake, - _deposit_info, - decrease_lamports, - ) = setup().await; + let (mut context, mut stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + setup().await; stake_pool_accounts.validator_list = Keypair::new(); @@ -219,11 +204,12 @@ async fn fail_with_wrong_validator_list() { decrease_lamports, validator_stake.transient_stake_seed, )], - Some(&payer.pubkey()), - &[&payer, &stake_pool_accounts.staker], - recent_blockhash, + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.staker], + context.last_blockhash, ); - let error = banks_client + let error = context + .banks_client .process_transaction(transaction) .await .err() @@ -241,20 +227,13 @@ async fn fail_with_wrong_validator_list() { #[tokio::test] async fn fail_with_unknown_validator() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - _validator_stake, - _deposit_info, - decrease_lamports, - ) = setup().await; + let (mut context, stake_pool_accounts, _validator_stake, _deposit_info, decrease_lamports) = + setup().await; let unknown_stake = create_unknown_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &stake_pool_accounts.stake_pool.pubkey(), ) .await; @@ -271,11 +250,12 @@ async fn fail_with_unknown_validator() { decrease_lamports, unknown_stake.transient_stake_seed, )], - Some(&payer.pubkey()), - &[&payer, &stake_pool_accounts.staker], - recent_blockhash, + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.staker], + context.last_blockhash, ); - let error = banks_client + let error = context + .banks_client .process_transaction(transaction) .await .err() @@ -291,27 +271,23 @@ async fn fail_with_unknown_validator() { ); } +#[test_case(true; "additional")] +#[test_case(false; "no-additional")] #[tokio::test] -async fn fail_decrease_twice() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - _deposit_info, - decrease_lamports, - ) = setup().await; +async fn fail_twice_diff_seed(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + setup().await; let error = stake_pool_accounts - .decrease_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.stake_account, &validator_stake.transient_stake_account, - decrease_lamports, + decrease_lamports / 3, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await; assert!(error.is_none()); @@ -325,51 +301,161 @@ async fn fail_decrease_twice() { ) .0; let error = stake_pool_accounts - .decrease_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.stake_account, &transient_stake_address, - decrease_lamports, + decrease_lamports / 2, transient_stake_seed, + use_additional_instruction, ) .await .unwrap() .unwrap(); - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::TransientAccountInUse as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error"), + if use_additional_instruction { + assert_eq!( + error, + TransactionError::InstructionError(0, InstructionError::InvalidSeeds) + ); + } else { + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::TransientAccountInUse as u32) + ) + ); } } +#[test_case(true, true, true; "success-all-additional")] +#[test_case(true, false, true; "success-with-additional")] +#[test_case(false, true, false; "fail-without-additional")] +#[test_case(false, false, false; "fail-no-additional")] #[tokio::test] -async fn fail_with_small_lamport_amount() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - _deposit_info, - _decrease_lamports, - ) = setup().await; +async fn twice(success: bool, use_additional_first_time: bool, use_additional_second_time: bool) { + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + setup().await; + + let pre_stake_account = + get_account(&mut context.banks_client, &validator_stake.stake_account).await; + + let first_decrease = decrease_lamports / 3; + let second_decrease = decrease_lamports / 2; + let total_decrease = first_decrease + second_decrease; + let error = stake_pool_accounts + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &validator_stake.stake_account, + &validator_stake.transient_stake_account, + first_decrease, + validator_stake.transient_stake_seed, + use_additional_first_time, + ) + .await; + assert!(error.is_none()); + + let error = stake_pool_accounts + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &validator_stake.stake_account, + &validator_stake.transient_stake_account, + second_decrease, + validator_stake.transient_stake_seed, + use_additional_second_time, + ) + .await; + + if success { + assert!(error.is_none()); + // no ephemeral account + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + 0, + ) + .0; + let ephemeral_account = context + .banks_client + .get_account(ephemeral_stake) + .await + .unwrap(); + assert!(ephemeral_account.is_none()); + + // Check validator stake account balance + let stake_account = + get_account(&mut context.banks_client, &validator_stake.stake_account).await; + let stake_state = deserialize::(&stake_account.data).unwrap(); + assert_eq!( + pre_stake_account.lamports - total_decrease, + stake_account.lamports + ); + assert_eq!( + stake_state.delegation().unwrap().deactivation_epoch, + Epoch::MAX + ); + + // Check transient stake account state and balance + let transient_stake_account = get_account( + &mut context.banks_client, + &validator_stake.transient_stake_account, + ) + .await; + let transient_stake_state = + deserialize::(&transient_stake_account.data).unwrap(); + assert_eq!(transient_stake_account.lamports, total_decrease); + assert_ne!( + transient_stake_state + .delegation() + .unwrap() + .deactivation_epoch, + Epoch::MAX + ); + + // marked correctly in the list + let validator_list = stake_pool_accounts + .get_validator_list(&mut context.banks_client) + .await; + let entry = validator_list.find(&validator_stake.vote.pubkey()).unwrap(); + assert_eq!(entry.transient_stake_lamports, total_decrease); + } else { + let error = error.unwrap().unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::TransientAccountInUse as u32) + ) + ); + } +} + +#[test_case(true; "additional")] +#[test_case(false; "no-additional")] +#[tokio::test] +async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, _decrease_lamports) = + setup().await; - let rent = banks_client.get_rent().await.unwrap(); + let rent = context.banks_client.get_rent().await.unwrap(); let lamports = rent.minimum_balance(std::mem::size_of::()); let error = stake_pool_accounts - .decrease_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.stake_account, &validator_stake.transient_stake_account, lamports, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await .unwrap() @@ -383,21 +469,14 @@ async fn fail_with_small_lamport_amount() { #[tokio::test] async fn fail_big_overdraw() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - deposit_info, - _decrease_lamports, - ) = setup().await; + let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports) = + setup().await; let error = stake_pool_accounts .decrease_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.stake_account, &validator_stake.transient_stake_account, deposit_info.stake_lamports * 1_000_000, @@ -413,30 +492,26 @@ async fn fail_big_overdraw() { ); } +#[test_case(true; "additional")] +#[test_case(false; "no-additional")] #[tokio::test] -async fn fail_overdraw() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - deposit_info, - _decrease_lamports, - ) = setup().await; +async fn fail_overdraw(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports) = + setup().await; - let rent = banks_client.get_rent().await.unwrap(); + let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); let error = stake_pool_accounts - .decrease_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.stake_account, &validator_stake.transient_stake_account, deposit_info.stake_lamports + stake_rent + 1, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await .unwrap() @@ -447,3 +522,70 @@ async fn fail_overdraw() { TransactionError::InstructionError(0, InstructionError::InsufficientFunds) ); } + +#[tokio::test] +async fn fail_additional_with_increasing() { + let (mut context, stake_pool_accounts, validator_stake, _, decrease_lamports) = setup().await; + + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + + // warp forward to activation + let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; + context.warp_to_slot(first_normal_slot).unwrap(); + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[validator_stake.vote.pubkey()], + false, + ) + .await; + + let error = stake_pool_accounts + .increase_validator_stake( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &validator_stake.transient_stake_account, + &validator_stake.stake_account, + &validator_stake.vote.pubkey(), + current_minimum_delegation, + validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + let error = stake_pool_accounts + .decrease_validator_stake_either( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &validator_stake.stake_account, + &validator_stake.transient_stake_account, + decrease_lamports / 2, + validator_stake.transient_stake_seed, + true, + ) + .await + .unwrap() + .unwrap(); + + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(stake::instruction::StakeError::MergeTransientStake as u32) + ) + ); +} diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index dd032e7c217..07e9ff617d3 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -1462,6 +1462,92 @@ impl StakePoolAccounts { .err() } + #[allow(clippy::too_many_arguments)] + pub async fn decrease_additional_validator_stake( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + validator_stake: &Pubkey, + ephemeral_stake: &Pubkey, + transient_stake: &Pubkey, + lamports: u64, + transient_stake_seed: u64, + ephemeral_stake_seed: u64, + ) -> Option { + let mut instructions = vec![instruction::decrease_additional_validator_stake( + &id(), + &self.stake_pool.pubkey(), + &self.staker.pubkey(), + &self.withdraw_authority, + &self.validator_list.pubkey(), + validator_stake, + ephemeral_stake, + transient_stake, + lamports, + transient_stake_seed, + ephemeral_stake_seed, + )]; + self.maybe_add_compute_budget_instruction(&mut instructions); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &self.staker], + *recent_blockhash, + ); + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + + #[allow(clippy::too_many_arguments)] + pub async fn decrease_validator_stake_either( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + validator_stake: &Pubkey, + transient_stake: &Pubkey, + lamports: u64, + transient_stake_seed: u64, + use_additional_instruction: bool, + ) -> Option { + if use_additional_instruction { + let ephemeral_stake_seed = 0; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &self.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + self.decrease_additional_validator_stake( + banks_client, + payer, + recent_blockhash, + validator_stake, + &ephemeral_stake, + transient_stake, + lamports, + transient_stake_seed, + ephemeral_stake_seed, + ) + .await + } else { + self.decrease_validator_stake( + banks_client, + payer, + recent_blockhash, + validator_stake, + transient_stake, + lamports, + transient_stake_seed, + ) + .await + } + } + #[allow(clippy::too_many_arguments)] pub async fn increase_validator_stake( &self,