From 8d98aefd7a1477c4076c418a3c724a26cdc130e1 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 16 Dec 2022 02:26:15 +0100 Subject: [PATCH 1/3] stake-pool: Add "IncreaseAdditionalValidatorStake" instruction --- stake-pool/program/src/instruction.rs | 137 +++++++- stake-pool/program/src/lib.rs | 19 + stake-pool/program/src/processor.rs | 233 ++++++++++--- stake-pool/program/tests/helpers/mod.rs | 98 +++++- stake-pool/program/tests/increase.rs | 440 ++++++++++++++++-------- 5 files changed, 740 insertions(+), 187 deletions(-) diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index e3c5e9650fe..6d3d6fd5beb 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -4,8 +4,9 @@ use { crate::{ - find_deposit_authority_program_address, find_stake_program_address, - find_transient_stake_program_address, find_withdraw_authority_program_address, + find_deposit_authority_program_address, find_ephemeral_stake_program_address, + find_stake_program_address, find_transient_stake_program_address, + find_withdraw_authority_program_address, state::{Fee, FeeType, StakePool, ValidatorList}, MAX_VALIDATORS_TO_UPDATE, }, @@ -401,6 +402,46 @@ pub enum StakePoolInstruction { /// URI of the uploaded metadata of the spl-token uri: String, }, + + /// (Staker only) Increase stake on a validator again in an epoch. + /// + /// Works regardless if the transient stake account exists. + /// + /// Internally, this instruction splits reserve stake into an ephemeral stake + /// account, activates it, then merges or splits it into the transient stake + /// account delegated to the appropriate validator. `UpdateValidatorListBalance` + /// will do the work of merging once it's ready. + /// + /// The minimum amount to move is 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]` Stake pool reserve stake + /// 5. `[w]` Uninitialized ephemeral stake account to receive stake + /// 6. `[w]` Transient stake account + /// 7. `[]` Validator stake account + /// 8. `[]` Validator vote account to delegate to + /// 9. '[]' Clock sysvar + /// 10. `[]` Stake History sysvar + /// 11. `[]` Stake Config sysvar + /// 12. `[]` System program + /// 13. `[]` Stake program + /// userdata: amount of lamports to increase on the given validator. + /// The actual amount split into the transient stake account is: + /// `lamports + stake_rent_exemption` + /// The rent-exemption of the stake account is withdrawn back to the reserve + /// after it is merged. + IncreaseAdditionalValidatorStake { + /// amount of lamports to increase on the given validator + 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. @@ -597,6 +638,52 @@ pub fn increase_validator_stake( } } +/// Creates `IncreaseAdditionalValidatorStake` instruction (rebalance from reserve account to +/// transient account) +pub fn increase_additional_validator_stake( + program_id: &Pubkey, + stake_pool: &Pubkey, + staker: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + validator_list: &Pubkey, + reserve_stake: &Pubkey, + ephemeral_stake: &Pubkey, + transient_stake: &Pubkey, + validator_stake: &Pubkey, + validator: &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(*reserve_stake, false), + AccountMeta::new(*ephemeral_stake, false), + AccountMeta::new(*transient_stake, false), + AccountMeta::new_readonly(*validator_stake, false), + AccountMeta::new_readonly(*validator, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(stake::config::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(stake::program::id(), false), + ]; + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::IncreaseAdditionalValidatorStake { + lamports, + transient_stake_seed, + ephemeral_stake_seed, + } + .try_to_vec() + .unwrap(), + } +} + /// Creates `SetPreferredDepositValidator` instruction pub fn set_preferred_validator( program_id: &Pubkey, @@ -724,6 +811,52 @@ pub fn increase_validator_stake_with_vote( ) } +/// Create an `IncreaseAdditionalValidatorStake` instruction given an existing +/// stake pool and vote account +pub fn increase_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 (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, + ); + let (validator_stake_address, _) = find_stake_program_address( + program_id, + vote_account_address, + stake_pool_address, + validator_stake_seed, + ); + + increase_additional_validator_stake( + program_id, + stake_pool_address, + &stake_pool.staker, + &pool_withdraw_authority, + &stake_pool.validator_list, + &stake_pool.reserve_stake, + &ephemeral_stake_address, + &transient_stake_address, + &validator_stake_address, + vote_account_address, + lamports, + transient_stake_seed, + ephemeral_stake_seed, + ) +} + /// Create a `DecreaseValidatorStake` instruction given an existing stake pool and /// vote account pub fn decrease_validator_stake_with_vote( diff --git a/stake-pool/program/src/lib.rs b/stake-pool/program/src/lib.rs index e5fbcfaa588..66c9dd2c2d9 100644 --- a/stake-pool/program/src/lib.rs +++ b/stake-pool/program/src/lib.rs @@ -28,6 +28,9 @@ const AUTHORITY_WITHDRAW: &[u8] = b"withdraw"; /// Seed for transient stake account const TRANSIENT_STAKE_SEED_PREFIX: &[u8] = b"transient"; +/// Seed for ephemeral stake account +const EPHEMERAL_STAKE_SEED_PREFIX: &[u8] = b"ephemeral"; + /// Minimum amount of staked lamports required in a validator stake account to allow /// for merges without a mismatch on credits observed pub const MINIMUM_ACTIVE_STAKE: u64 = 1_000_000; @@ -138,6 +141,22 @@ pub fn find_transient_stake_program_address( ) } +/// Generates the ephemeral program address for stake pool redelegation +pub fn find_ephemeral_stake_program_address( + program_id: &Pubkey, + stake_pool_address: &Pubkey, + seed: u64, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[ + EPHEMERAL_STAKE_SEED_PREFIX, + stake_pool_address.as_ref(), + &seed.to_le_bytes(), + ], + program_id, + ) +} + solana_program::declare_id!("SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy"); #[cfg(test)] diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 08caa1cc719..a2da16cdf7d 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -10,7 +10,8 @@ use { is_extension_supported_for_mint, AccountType, Fee, FeeType, StakePool, StakeStatus, StakeWithdrawSource, ValidatorList, ValidatorListHeader, ValidatorStakeInfo, }, - AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, TRANSIENT_STAKE_SEED_PREFIX, + AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, EPHEMERAL_STAKE_SEED_PREFIX, + TRANSIENT_STAKE_SEED_PREFIX, }, borsh::{BorshDeserialize, BorshSerialize}, mpl_token_metadata::{ @@ -99,6 +100,23 @@ fn check_transient_stake_address( } } +/// Check address validity for an ephemeral stake account +fn check_ephemeral_stake_address( + program_id: &Pubkey, + stake_pool_address: &Pubkey, + stake_account_address: &Pubkey, + seed: u64, +) -> Result { + // Check stake account address validity + let (ephemeral_stake_address, bump_seed) = + crate::find_ephemeral_stake_program_address(program_id, stake_pool_address, seed); + if ephemeral_stake_address != *stake_account_address { + Err(StakePoolError::InvalidStakeAccountAddress.into()) + } else { + Ok(bump_seed) + } +} + /// Check mpl metadata account address for the pool mint fn check_mpl_metadata_account_address( metadata_address: &Pubkey, @@ -1262,6 +1280,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)?; @@ -1269,13 +1288,22 @@ impl Processor { let withdraw_authority_info = next_account_info(account_info_iter)?; let validator_list_info = next_account_info(account_info_iter)?; let reserve_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 validator_stake_account_info = next_account_info(account_info_iter)?; let validator_vote_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 stake_history_info = next_account_info(account_info_iter)?; let stake_config_info = next_account_info(account_info_iter)?; let system_program_info = next_account_info(account_info_iter)?; @@ -1327,7 +1355,19 @@ impl Processor { } let mut validator_stake_info = maybe_validator_stake_info.unwrap(); if validator_stake_info.transient_stake_lamports > 0 { - return Err(StakePoolError::TransientAccountInUse.into()); + if maybe_ephemeral_stake_seed.is_none() { + msg!("Attempting to increase stake on a validator with transient stake, use IncreaseAdditionalValidatorStake 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 increasing } // Check that the validator stake account is actually delegated to the right @@ -1357,21 +1397,6 @@ impl Processor { } } - 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], - ]; - if validator_stake_info.status != StakeStatus::Active { msg!("Validator is marked for removal and no longer allows increases"); return Err(StakePoolError::ValidatorNotFound.into()); @@ -1410,37 +1435,136 @@ impl Processor { return Err(ProgramError::InsufficientFunds); } - create_stake_account( - transient_stake_account_info.clone(), - transient_stake_account_signer_seeds, - system_program_info.clone(), - )?; + let maybe_split_from_account_info = + if let Some(ephemeral_stake_seed) = maybe_ephemeral_stake_seed { + let ephemeral_stake_account_info = maybe_ephemeral_stake_account_info.unwrap(); + 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, - reserve_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - total_lamports, - transient_stake_account_info.clone(), - )?; + // split into ephemeral stake account + Self::stake_split( + stake_pool_info.key, + reserve_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + total_lamports, + ephemeral_stake_account_info.clone(), + )?; - // activate transient stake to validator - Self::stake_delegate( - transient_stake_account_info.clone(), - validator_vote_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - stake_config_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; + // activate stake to validator + Self::stake_delegate( + ephemeral_stake_account_info.clone(), + validator_vote_account_info.clone(), + clock_info.clone(), + stake_history_info.clone(), + stake_config_info.clone(), + withdraw_authority_info.clone(), + stake_pool_info.key, + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + )?; + if validator_stake_info.transient_stake_lamports > 0 { + // transient stake exists, try to merge + Self::stake_merge( + stake_pool_info.key, + ephemeral_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(), + )?; + None + } else { + // otherwise, split everything from the ephemeral stake, into the transient + Some(ephemeral_stake_account_info) + } + } else { + // if no ephemeral account is provided, split everything from the + // reserve account, into the transient stake account + Some(reserve_stake_account_info) + }; - validator_stake_info.transient_stake_lamports = total_lamports; + if let Some(split_from_account_info) = maybe_split_from_account_info { + 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], + ]; + + 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, + split_from_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + total_lamports, + transient_stake_account_info.clone(), + )?; + + // Activate transient stake to validator if necessary + let stake_state = try_from_slice_unchecked::( + &transient_stake_account_info.data.borrow(), + )?; + match stake_state { + // if it was delegated on or before this epoch, we're good + stake::state::StakeState::Stake(_, stake) + if stake.delegation.activation_epoch <= clock.epoch => {} + // all other situations, delegate! + _ => { + Self::stake_delegate( + transient_stake_account_info.clone(), + validator_vote_account_info.clone(), + clock_info.clone(), + stake_history_info.clone(), + stake_config_info.clone(), + withdraw_authority_info.clone(), + stake_pool_info.key, + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + )?; + } + } + } + + validator_stake_info.transient_stake_lamports = validator_stake_info + .transient_stake_lamports + .checked_add(total_lamports) + .ok_or(StakePoolError::CalculationFailure)?; validator_stake_info.transient_seed_suffix = transient_stake_seed; Ok(()) @@ -3136,6 +3260,21 @@ impl Processor { accounts, lamports, transient_stake_seed, + None, + ) + } + StakePoolInstruction::IncreaseAdditionalValidatorStake { + lamports, + transient_stake_seed, + ephemeral_stake_seed, + } => { + msg!("Instruction: IncreaseAdditionalValidatorStake"); + Self::process_increase_validator_stake( + program_id, + accounts, + lamports, + transient_stake_seed, + Some(ephemeral_stake_seed), ) } StakePoolInstruction::SetPreferredValidator { diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index a9db0f78a15..dd032e7c217 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -26,9 +26,9 @@ use { vote_state::{VoteInit, VoteState, VoteStateVersions}, }, spl_stake_pool::{ - find_deposit_authority_program_address, find_stake_program_address, - find_transient_stake_program_address, find_withdraw_authority_program_address, id, - instruction, minimum_delegation, + find_deposit_authority_program_address, find_ephemeral_stake_program_address, + find_stake_program_address, find_transient_stake_program_address, + find_withdraw_authority_program_address, id, instruction, minimum_delegation, processor::Processor, state::{self, FeeType, StakePool, ValidatorList}, MINIMUM_RESERVE_LAMPORTS, @@ -1501,6 +1501,98 @@ impl StakePoolAccounts { .err() } + #[allow(clippy::too_many_arguments)] + pub async fn increase_additional_validator_stake( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + ephemeral_stake: &Pubkey, + transient_stake: &Pubkey, + validator_stake: &Pubkey, + validator: &Pubkey, + lamports: u64, + transient_stake_seed: u64, + ephemeral_stake_seed: u64, + ) -> Option { + let mut instructions = vec![instruction::increase_additional_validator_stake( + &id(), + &self.stake_pool.pubkey(), + &self.staker.pubkey(), + &self.withdraw_authority, + &self.validator_list.pubkey(), + &self.reserve_stake.pubkey(), + ephemeral_stake, + transient_stake, + validator_stake, + validator, + 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 increase_validator_stake_either( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + transient_stake: &Pubkey, + validator_stake: &Pubkey, + validator: &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.increase_additional_validator_stake( + banks_client, + payer, + recent_blockhash, + &ephemeral_stake, + transient_stake, + validator_stake, + validator, + lamports, + transient_stake_seed, + ephemeral_stake_seed, + ) + .await + } else { + self.increase_validator_stake( + banks_client, + payer, + recent_blockhash, + transient_stake, + validator_stake, + validator, + lamports, + transient_stake_seed, + ) + .await + } + } + pub async fn set_preferred_validator( &self, banks_client: &mut BanksClient, diff --git a/stake-pool/program/tests/increase.rs b/stake-pool/program/tests/increase.rs index 4bc69f751b4..7967907c956 100644 --- a/stake-pool/program/tests/increase.rs +++ b/stake-pool/program/tests/increase.rs @@ -6,120 +6,118 @@ 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}, + signature::Signer, stake::instruction::StakeError, 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, u64, ) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + let mut context = program_test().start_with_context().await; let stake_pool_accounts = StakePoolAccounts::default(); let reserve_lamports = 100_000_000_000 + MINIMUM_RESERVE_LAMPORTS; stake_pool_accounts .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, + &mut context.banks_client, + &context.payer, + &context.last_blockhash, reserve_lamports, ) .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 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 rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); 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, + current_minimum_delegation * 2 + stake_rent, ) .await .unwrap(); ( - banks_client, - payer, - recent_blockhash, + context, stake_pool_accounts, validator_stake_account, reserve_lamports, ) } +#[test_case(true; "additional")] +#[test_case(false; "non-additional")] #[tokio::test] -async fn success() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - reserve_lamports, - ) = setup().await; +async fn success(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; // Save reserve stake let pre_reserve_stake_account = get_account( - &mut banks_client, + &mut context.banks_client, &stake_pool_accounts.reserve_stake.pubkey(), ) .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 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 increase_amount = reserve_lamports - stake_rent - MINIMUM_RESERVE_LAMPORTS; let error = stake_pool_accounts - .increase_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.transient_stake_account, &validator_stake.stake_account, &validator_stake.vote.pubkey(), increase_amount, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await; assert!(error.is_none()); // Check reserve stake account balance let reserve_stake_account = get_account( - &mut banks_client, + &mut context.banks_client, &stake_pool_accounts.reserve_stake.pubkey(), ) .await; @@ -132,8 +130,11 @@ async fn success() { assert!(reserve_stake_state.delegation().is_none()); // 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!( @@ -148,14 +149,7 @@ async fn success() { #[tokio::test] async fn fail_with_wrong_withdraw_authority() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - reserve_lamports, - ) = setup().await; + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; let wrong_authority = Pubkey::new_unique(); @@ -173,11 +167,12 @@ async fn fail_with_wrong_withdraw_authority() { reserve_lamports / 2, 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,14 +190,7 @@ async fn fail_with_wrong_withdraw_authority() { #[tokio::test] async fn fail_with_wrong_validator_list() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - reserve_lamports, - ) = setup().await; + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; let wrong_validator_list = Pubkey::new_unique(); @@ -220,11 +208,12 @@ async fn fail_with_wrong_validator_list() { reserve_lamports / 2, 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() @@ -242,19 +231,12 @@ 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, - reserve_lamports, - ) = setup().await; + let (mut context, stake_pool_accounts, _validator_stake, reserve_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; @@ -273,11 +255,12 @@ async fn fail_with_unknown_validator() { reserve_lamports / 2, 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() @@ -293,27 +276,25 @@ async fn fail_with_unknown_validator() { ); } +#[test_case(true; "additional")] +#[test_case(false; "non-additional")] #[tokio::test] -async fn fail_increase_twice() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - reserve_lamports, - ) = setup().await; +async fn fail_twice_diff_seed(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; + let first_increase = reserve_lamports / 3; + let second_increase = reserve_lamports / 4; let error = stake_pool_accounts - .increase_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.transient_stake_account, &validator_stake.stake_account, &validator_stake.vote.pubkey(), - reserve_lamports / 3, + first_increase, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await; assert!(error.is_none()); @@ -327,52 +308,176 @@ async fn fail_increase_twice() { ) .0; let error = stake_pool_accounts - .increase_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &transient_stake_address, &validator_stake.stake_account, &validator_stake.vote.pubkey(), - reserve_lamports / 4, + second_increase, 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); + + 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 twice(success: bool, use_additional_first_time: bool, use_additional_second_time: bool) { + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; + + let pre_reserve_stake_account = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await; + + let first_increase = reserve_lamports / 3; + let second_increase = reserve_lamports / 4; + let total_increase = first_increase + second_increase; + let error = stake_pool_accounts + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &validator_stake.transient_stake_account, + &validator_stake.stake_account, + &validator_stake.vote.pubkey(), + first_increase, + validator_stake.transient_stake_seed, + use_additional_first_time, + ) + .await; + assert!(error.is_none()); + + let error = stake_pool_accounts + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &validator_stake.transient_stake_account, + &validator_stake.stake_account, + &validator_stake.vote.pubkey(), + second_increase, + validator_stake.transient_stake_seed, + use_additional_second_time, + ) + .await; + + if success { + assert!(error.is_none()); + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + // 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 reserve stake account balance + let reserve_stake_account = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await; + let reserve_stake_state = + deserialize::(&reserve_stake_account.data).unwrap(); + assert_eq!( + pre_reserve_stake_account.lamports - total_increase - stake_rent * 2, + reserve_stake_account.lamports + ); + assert!(reserve_stake_state.delegation().is_none()); + + // 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_increase + stake_rent * 2 + ); + assert_ne!( + transient_stake_state.delegation().unwrap().activation_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_increase + stake_rent * 2 + ); + } else { + let error = error.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"), } - _ => panic!("Wrong error"), } } +#[test_case(true; "additional")] +#[test_case(false; "non-additional")] #[tokio::test] -async fn fail_with_small_lamport_amount() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - _reserve_lamports, - ) = setup().await; +async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, _reserve_lamports) = setup().await; - 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 error = stake_pool_accounts - .increase_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.transient_stake_account, &validator_stake.stake_account, &validator_stake.vote.pubkey(), current_minimum_delegation - 1, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await .unwrap() @@ -387,27 +492,23 @@ async fn fail_with_small_lamport_amount() { } } +#[test_case(true; "additional")] +#[test_case(false; "non-additional")] #[tokio::test] -async fn fail_overdraw_reserve() { - let ( - mut banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - reserve_lamports, - ) = setup().await; +async fn fail_overdraw_reserve(use_additional_instruction: bool) { + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; let error = stake_pool_accounts - .increase_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, &validator_stake.transient_stake_account, &validator_stake.stake_account, &validator_stake.vote.pubkey(), reserve_lamports, validator_stake.transient_stake_seed, + use_additional_instruction, ) .await .unwrap() @@ -419,5 +520,74 @@ async fn fail_overdraw_reserve() { } } +#[tokio::test] +async fn fail_additional_with_decreasing() { + let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; + + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + + // 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 + .decrease_validator_stake( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &validator_stake.stake_account, + &validator_stake.transient_stake_account, + current_minimum_delegation + stake_rent, + validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + let error = stake_pool_accounts + .increase_validator_stake_either( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &validator_stake.transient_stake_account, + &validator_stake.stake_account, + &validator_stake.vote.pubkey(), + reserve_lamports / 2, + validator_stake.transient_stake_seed, + true, + ) + .await + .unwrap() + .unwrap(); + + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakeError::MergeTransientStake as u32) + ) + ); +} + #[tokio::test] async fn fail_with_force_destaked_validator() {} From a964a56417be4dd3a1b19ec1a8d284837475dbc5 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Wed, 21 Dec 2022 21:34:04 +0100 Subject: [PATCH 2/3] Address feedback --- stake-pool/program/src/processor.rs | 48 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index a2da16cdf7d..72e13ae54bb 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -1356,7 +1356,7 @@ impl Processor { let mut validator_stake_info = maybe_validator_stake_info.unwrap(); if validator_stake_info.transient_stake_lamports > 0 { if maybe_ephemeral_stake_seed.is_none() { - msg!("Attempting to increase stake on a validator with transient stake, use IncreaseAdditionalValidatorStake with the existing seed"); + msg!("Attempting to increase stake on a validator with pending transient stake, use IncreaseAdditionalValidatorStake with the existing seed"); return Err(StakePoolError::TransientAccountInUse.into()); } if transient_stake_seed != validator_stake_info.transient_seed_suffix { @@ -1435,9 +1435,10 @@ impl Processor { return Err(ProgramError::InsufficientFunds); } - let maybe_split_from_account_info = - if let Some(ephemeral_stake_seed) = maybe_ephemeral_stake_seed { - let ephemeral_stake_account_info = maybe_ephemeral_stake_account_info.unwrap(); + 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, @@ -1479,31 +1480,28 @@ impl Processor { AUTHORITY_WITHDRAW, stake_pool.stake_withdraw_bump_seed, )?; - if validator_stake_info.transient_stake_lamports > 0 { - // transient stake exists, try to merge - Self::stake_merge( - stake_pool_info.key, - ephemeral_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(), - )?; - None - } else { - // otherwise, split everything from the ephemeral stake, into the transient - Some(ephemeral_stake_account_info) - } + ephemeral_stake_account_info } else { // if no ephemeral account is provided, split everything from the // reserve account, into the transient stake account - Some(reserve_stake_account_info) + reserve_stake_account_info }; - if let Some(split_from_account_info) = maybe_split_from_account_info { + if validator_stake_info.transient_stake_lamports > 0 { + // transient stake exists, try to merge from the source 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 { + // no transient stake, split let transient_stake_bump_seed = check_transient_stake_address( program_id, stake_pool_info.key, @@ -1528,7 +1526,7 @@ impl Processor { // split into transient stake account Self::stake_split( stake_pool_info.key, - split_from_account_info.clone(), + source_stake_account_info.clone(), withdraw_authority_info.clone(), AUTHORITY_WITHDRAW, stake_pool.stake_withdraw_bump_seed, From f0b1096c9402ee477d33788a7b56750bf727c0f9 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 23 Dec 2022 12:59:22 +0100 Subject: [PATCH 3/3] Always check transient stake account address --- stake-pool/program/src/processor.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 72e13ae54bb..6aa9c67b870 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -1487,8 +1487,17 @@ impl Processor { reserve_stake_account_info }; + 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, + )?; + if validator_stake_info.transient_stake_lamports > 0 { - // transient stake exists, try to merge from the source account + // 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(), @@ -1502,13 +1511,6 @@ impl Processor { )?; } else { // no transient stake, split - 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(),