-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
reimplemnt stake test with program-test
- Loading branch information
HaoranYi
committed
Jul 29, 2023
1 parent
1a2de7b
commit 87ef81a
Showing
3 changed files
with
348 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} |
Oops, something went wrong.