diff --git a/program-test/tests/stake.rs b/program-test/tests/stake.rs new file mode 100644 index 00000000000000..6d19d7df02224f --- /dev/null +++ b/program-test/tests/stake.rs @@ -0,0 +1,190 @@ +#![allow(clippy::integer_arithmetic)] + +mod test_utils; + +use { + solana_program_test::ProgramTest, + solana_sdk::{ + signature::{Keypair, Signer}, + stake::instruction as stake_instruction, + transaction::Transaction, + }, + test_utils::{setup_stake, setup_vote, setup_vote_no_warp}, +}; + +#[derive(PartialEq)] +enum PendingStakeActivationTestFlag { + MergeActive, + MergeInactive, + NoMerge, +} + +async fn test_stake_redelegation_pending_activation(merge_flag: PendingStakeActivationTestFlag) { + let program_test = ProgramTest::default(); + let mut context = program_test.start_with_context().await; + + // 1. create first vote accounts + let vote_address = setup_vote(&mut context).await; + + // 1.1 advance to normal epoch + let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; + let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; + let mut current_slot = first_normal_slot + slots_per_epoch; + context.warp_to_slot(current_slot).unwrap(); + context.warp_forward_force_reward_interval_end().unwrap(); + + // 2. create first stake account and delegate to first vote_address + let stake_lamports = 50_000_000_000; + let user_keypair = Keypair::new(); + let stake_address = + setup_stake(&mut context, &user_keypair, &vote_address, stake_lamports).await; + + // 2.1 advance to new epoch so that the stake is activated. + current_slot += slots_per_epoch; + context.warp_to_slot(current_slot).unwrap(); + context.warp_forward_force_reward_interval_end().unwrap(); + + // 2.2 stake is now activated and can't withdrawal directly + let transaction = Transaction::new_signed_with_payer( + &[stake_instruction::withdraw( + &stake_address, + &user_keypair.pubkey(), + &solana_sdk::pubkey::new_rand(), + 1, + None, + )], + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair], + context.last_blockhash, + ); + let r = context.banks_client.process_transaction(transaction).await; + assert!(format!("{}", r.err().unwrap()).contains("insufficient funds for instruction")); + + // 3. create 2nd vote account + let vote_address2 = setup_vote_no_warp(&mut context).await; + + // 3.1 relegate 1st stake account to 2nd stake account. + let stake_keypair2 = Keypair::new(); + let stake_address2 = stake_keypair2.pubkey(); + let transaction = Transaction::new_signed_with_payer( + &stake_instruction::redelegate( + &stake_address, + &user_keypair.pubkey(), + &vote_address2, + &stake_address2, + ), + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair, &stake_keypair2], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + if merge_flag != PendingStakeActivationTestFlag::NoMerge { + // 3.2 create 3rd to-merge stake account + let stake_address3 = + setup_stake(&mut context, &user_keypair, &vote_address2, stake_lamports).await; + + // 3.2.1 deactivate merge stake account + if merge_flag == PendingStakeActivationTestFlag::MergeInactive { + let transaction = Transaction::new_signed_with_payer( + &[stake_instruction::deactivate_stake( + &stake_address3, + &user_keypair.pubkey(), + )], + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + } + + // 3.2.2 merge 3rd stake account to 2nd stake account. However, it should not clear the pending stake activation flags on stake_account2. + let transaction = Transaction::new_signed_with_payer( + &stake_instruction::merge(&stake_address2, &stake_address3, &user_keypair.pubkey()), + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + } + + // 3.3 deactivate 2nd stake account should fail because of pending stake activation. + let transaction = Transaction::new_signed_with_payer( + &[stake_instruction::deactivate_stake( + &stake_address2, + &user_keypair.pubkey(), + )], + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair], + context.last_blockhash, + ); + let r = context.banks_client.process_transaction(transaction).await; + assert_eq!( + format!("{}", r.err().unwrap()), + "transport transaction error: Error processing Instruction 0: custom program error: 0xf" + ); + + // 3.4 withdraw from 2nd stake account should also fail because of pending stake activation. + let transaction = Transaction::new_signed_with_payer( + &[stake_instruction::withdraw( + &stake_address2, + &user_keypair.pubkey(), + &solana_sdk::pubkey::new_rand(), + 1, + None, + )], + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair], + context.last_blockhash, + ); + let r = context.banks_client.process_transaction(transaction).await; + assert!(format!("{}", r.err().unwrap()).contains("insufficient funds for instruction")); + + // 4. advance to new epoch so that the 2nd stake account is fully activated + current_slot += slots_per_epoch; + context.warp_to_slot(current_slot).unwrap(); + context.warp_forward_force_reward_interval_end().unwrap(); + + // 4.1 Now deactivate 2nd stake account should succeed because there is no pending stake activation. + let transaction = Transaction::new_signed_with_payer( + &[stake_instruction::deactivate_stake( + &stake_address2, + &user_keypair.pubkey(), + )], + Some(&context.payer.pubkey()), + &vec![&context.payer, &user_keypair], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_stake_redelegation_then_deactivation_withdraw_not_permitted() { + test_stake_redelegation_pending_activation(PendingStakeActivationTestFlag::NoMerge).await +} + +#[tokio::test] +async fn test_stake_redelegation_then_merge_active_then_deactivation_withdraw_not_permitted() { + test_stake_redelegation_pending_activation(PendingStakeActivationTestFlag::MergeActive).await +} + +#[tokio::test] +async fn test_stake_redelegation_then_merge_inactive_then_deactivation_withdraw_not_permitted() { + test_stake_redelegation_pending_activation(PendingStakeActivationTestFlag::MergeInactive).await +} diff --git a/program-test/tests/test_utils.rs b/program-test/tests/test_utils.rs new file mode 100644 index 00000000000000..eee3dc75d404a1 --- /dev/null +++ b/program-test/tests/test_utils.rs @@ -0,0 +1,150 @@ +#![allow(clippy::integer_arithmetic)] +use { + bincode::deserialize, + solana_banks_client::BanksClient, + solana_program_test::ProgramTestContext, + solana_sdk::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + signature::{Keypair, Signer}, + stake::{ + instruction as stake_instruction, + state::{Authorized, Lockup, StakeState}, + }, + system_instruction, system_program, + sysvar::Sysvar, + transaction::Transaction, + }, + solana_vote_program::{ + vote_instruction, + vote_state::{self, VoteInit, VoteState}, + }, + std::convert::TryInto, +}; + +// Use a big number to be sure that we get the right error +#[allow(dead_code)] +pub const WRONG_SLOT_ERROR: u32 = 123456; + +pub async fn setup_stake( + context: &mut ProgramTestContext, + user: &Keypair, + vote_address: &Pubkey, + stake_lamports: u64, +) -> Pubkey { + let stake_keypair = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &stake_instruction::create_account_and_delegate_stake( + &context.payer.pubkey(), + &stake_keypair.pubkey(), + vote_address, + &Authorized::auto(&user.pubkey()), + &Lockup::default(), + stake_lamports, + ), + Some(&context.payer.pubkey()), + &vec![&context.payer, &stake_keypair, user], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + stake_keypair.pubkey() +} + +pub async fn setup_vote(context: &mut ProgramTestContext) -> Pubkey { + _setup_vote(context, true).await +} + +#[allow(dead_code)] +pub async fn setup_vote_no_warp(context: &mut ProgramTestContext) -> Pubkey { + _setup_vote(context, false).await +} + +async fn _setup_vote(context: &mut ProgramTestContext, do_warp: bool) -> Pubkey { + if do_warp { + // warp once to make sure stake config doesn't get rent-collected + context.warp_to_slot(100).unwrap(); + } + let mut instructions = vec![]; + let validator_keypair = Keypair::new(); + instructions.push(system_instruction::create_account( + &context.payer.pubkey(), + &validator_keypair.pubkey(), + Rent::default().minimum_balance(0), + 0, + &system_program::id(), + )); + let vote_lamports = Rent::default().minimum_balance(VoteState::size_of()); + let vote_keypair = Keypair::new(); + let user_keypair = Keypair::new(); + instructions.append(&mut vote_instruction::create_account_with_config( + &context.payer.pubkey(), + &vote_keypair.pubkey(), + &VoteInit { + node_pubkey: validator_keypair.pubkey(), + authorized_voter: user_keypair.pubkey(), + ..VoteInit::default() + }, + vote_lamports, + vote_instruction::CreateVoteAccountConfig { + space: vote_state::VoteStateVersions::vote_state_size_of(true) as u64, + ..vote_instruction::CreateVoteAccountConfig::default() + }, + )); + + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &vec![&context.payer, &validator_keypair, &vote_keypair], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + vote_keypair.pubkey() +} + +#[allow(dead_code)] +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let clock_info = next_account_info(account_info_iter)?; + let clock = &Clock::from_account_info(clock_info)?; + let expected_slot = u64::from_le_bytes(input.try_into().unwrap()); + if clock.slot == expected_slot { + Ok(()) + } else { + Err(ProgramError::Custom(WRONG_SLOT_ERROR)) + } +} + +#[allow(dead_code)] +pub async fn check_credits_observed( + banks_client: &mut BanksClient, + stake_address: Pubkey, + expected_credits: u64, +) { + let stake_account = banks_client + .get_account(stake_address) + .await + .unwrap() + .unwrap(); + let stake_state: StakeState = deserialize(&stake_account.data).unwrap(); + assert_eq!( + stake_state.stake().unwrap().credits_observed, + expected_credits + ); +} diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs index 5da577acdc7c3b..21cc5092f32b14 100644 --- a/program-test/tests/warp.rs +++ b/program-test/tests/warp.rs @@ -1,133 +1,35 @@ #![allow(clippy::integer_arithmetic)] + +mod test_utils; + use { bincode::deserialize, log::debug, - solana_banks_client::BanksClient, - solana_program_test::{ - processor, ProgramTest, ProgramTestBanksClientExt, ProgramTestContext, ProgramTestError, - }, + solana_program_test::{processor, ProgramTest, ProgramTestBanksClientExt, ProgramTestError}, solana_sdk::{ account::Account, - account_info::{next_account_info, AccountInfo}, clock::Clock, - entrypoint::ProgramResult, instruction::{AccountMeta, Instruction, InstructionError}, - program_error::ProgramError, pubkey::Pubkey, rent::Rent, signature::{Keypair, Signer}, stake::{ instruction as stake_instruction, - state::{Authorized, Lockup, StakeActivationStatus, StakeState}, + state::{StakeActivationStatus, StakeState}, }, - system_instruction, system_program, sysvar::{ clock, stake_history::{self, StakeHistory}, - Sysvar, }, transaction::{Transaction, TransactionError}, }, solana_stake_program::stake_state, - solana_vote_program::{ - vote_instruction, - vote_state::{self, VoteInit, VoteState}, + solana_vote_program::vote_state::{self}, + test_utils::{ + check_credits_observed, process_instruction, setup_stake, setup_vote, WRONG_SLOT_ERROR, }, - std::convert::TryInto, }; -// Use a big number to be sure that we get the right error -const WRONG_SLOT_ERROR: u32 = 123456; - -async fn setup_stake( - context: &mut ProgramTestContext, - user: &Keypair, - vote_address: &Pubkey, - stake_lamports: u64, -) -> Pubkey { - let stake_keypair = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &stake_instruction::create_account_and_delegate_stake( - &context.payer.pubkey(), - &stake_keypair.pubkey(), - vote_address, - &Authorized::auto(&user.pubkey()), - &Lockup::default(), - stake_lamports, - ), - Some(&context.payer.pubkey()), - &vec![&context.payer, &stake_keypair, user], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - stake_keypair.pubkey() -} - -async fn setup_vote(context: &mut ProgramTestContext) -> Pubkey { - // warp once to make sure stake config doesn't get rent-collected - context.warp_to_slot(100).unwrap(); - let mut instructions = vec![]; - let validator_keypair = Keypair::new(); - instructions.push(system_instruction::create_account( - &context.payer.pubkey(), - &validator_keypair.pubkey(), - Rent::default().minimum_balance(0), - 0, - &system_program::id(), - )); - let vote_lamports = Rent::default().minimum_balance(VoteState::size_of()); - let vote_keypair = Keypair::new(); - let user_keypair = Keypair::new(); - instructions.append(&mut vote_instruction::create_account_with_config( - &context.payer.pubkey(), - &vote_keypair.pubkey(), - &VoteInit { - node_pubkey: validator_keypair.pubkey(), - authorized_voter: user_keypair.pubkey(), - ..VoteInit::default() - }, - vote_lamports, - vote_instruction::CreateVoteAccountConfig { - space: vote_state::VoteStateVersions::vote_state_size_of(true) as u64, - ..vote_instruction::CreateVoteAccountConfig::default() - }, - )); - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &vec![&context.payer, &validator_keypair, &vote_keypair], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - vote_keypair.pubkey() -} - -fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let expected_slot = u64::from_le_bytes(input.try_into().unwrap()); - if clock.slot == expected_slot { - Ok(()) - } else { - Err(ProgramError::Custom(WRONG_SLOT_ERROR)) - } -} - #[tokio::test] async fn clock_sysvar_updated_from_warp() { let program_id = Pubkey::new_unique(); @@ -399,23 +301,6 @@ async fn stake_rewards_filter_bench_core(num_stake_accounts: u64) { ); } -async fn check_credits_observed( - banks_client: &mut BanksClient, - stake_address: Pubkey, - expected_credits: u64, -) { - let stake_account = banks_client - .get_account(stake_address) - .await - .unwrap() - .unwrap(); - let stake_state: StakeState = deserialize(&stake_account.data).unwrap(); - assert_eq!( - stake_state.stake().unwrap().credits_observed, - expected_credits - ); -} - #[tokio::test] async fn stake_merge_immediately_after_activation() { let program_test = ProgramTest::default();