Skip to content

Commit

Permalink
reimplemnt stake test with program-test
Browse files Browse the repository at this point in the history
  • Loading branch information
HaoranYi committed Jul 29, 2023
1 parent 1a2de7b commit 87ef81a
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 123 deletions.
190 changes: 190 additions & 0 deletions program-test/tests/stake.rs
Original file line number Diff line number Diff line change
@@ -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
}
150 changes: 150 additions & 0 deletions program-test/tests/test_utils.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
Loading

0 comments on commit 87ef81a

Please sign in to comment.