#![allow(clippy::arithmetic_side_effects)]
#![allow(clippy::items_after_test_module)]
#![cfg(feature = "test-sbf")]
mod helpers;
use {
bincode::deserialize,
helpers::*,
solana_program::{clock::Epoch, instruction::InstructionError, pubkey::Pubkey},
solana_program_test::*,
solana_sdk::{
signature::Signer,
stake::instruction::StakeError,
transaction::{Transaction, TransactionError},
},
solana_stake_interface as stake,
spl_stake_pool::{
error::StakePoolError, find_ephemeral_stake_program_address,
find_transient_stake_program_address, id, instruction, MINIMUM_RESERVE_LAMPORTS,
},
test_case::test_case,
};
async fn setup() -> (
ProgramTestContext,
StakePoolAccounts,
ValidatorStakeAccount,
u64,
) {
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 context.banks_client,
&context.payer,
&context.last_blockhash,
reserve_lamports,
)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
None,
)
.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::<stake::state::StakeStateV2>());
let _deposit_info = simple_deposit_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
&validator_stake_account,
current_minimum_delegation * 2 + stake_rent,
)
.await
.unwrap();
(
context,
stake_pool_accounts,
validator_stake_account,
reserve_lamports,
)
}
#[test_case(true; "additional")]
#[test_case(false; "non-additional")]
#[tokio::test]
async fn success(use_additional_instruction: 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 transient_account = context
.banks_client
.get_account(validator_stake.transient_stake_account)
.await
.unwrap();
assert!(transient_account.is_none());
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeStateV2>());
let increase_amount = reserve_lamports - stake_rent - MINIMUM_RESERVE_LAMPORTS;
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(),
increase_amount,
validator_stake.transient_stake_seed,
use_additional_instruction,
)
.await;
assert!(error.is_none(), "{:?}", error);
let reserve_stake_account = get_account(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
)
.await;
let reserve_stake_state =
deserialize::<stake::state::StakeStateV2>(&reserve_stake_account.data).unwrap();
assert_eq!(
pre_reserve_stake_account.lamports - increase_amount - stake_rent,
reserve_stake_account.lamports
);
assert!(reserve_stake_state.delegation().is_none());
let transient_stake_account = get_account(
&mut context.banks_client,
&validator_stake.transient_stake_account,
)
.await;
let transient_stake_state =
deserialize::<stake::state::StakeStateV2>(&transient_stake_account.data).unwrap();
assert_eq!(
transient_stake_account.lamports,
increase_amount + stake_rent
);
assert_ne!(
transient_stake_state.delegation().unwrap().activation_epoch,
Epoch::MAX
);
}
#[tokio::test]
async fn fail_with_wrong_withdraw_authority() {
let (context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await;
let wrong_authority = Pubkey::new_unique();
let transaction = Transaction::new_signed_with_payer(
&[instruction::increase_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&wrong_authority,
&stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&validator_stake.transient_stake_account,
&validator_stake.stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports / 2,
validator_stake.transient_stake_seed,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &stake_pool_accounts.staker],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::InvalidProgramAddress as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_with_wrong_validator_list() {
let (context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await;
let wrong_validator_list = Pubkey::new_unique();
let transaction = Transaction::new_signed_with_payer(
&[instruction::increase_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority,
&wrong_validator_list,
&stake_pool_accounts.reserve_stake.pubkey(),
&validator_stake.transient_stake_account,
&validator_stake.stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports / 2,
validator_stake.transient_stake_seed,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &stake_pool_accounts.staker],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_with_unknown_validator() {
let (mut context, stake_pool_accounts, _validator_stake, reserve_lamports) = setup().await;
let unknown_stake = create_unknown_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts.stake_pool.pubkey(),
0,
)
.await;
let transaction = Transaction::new_signed_with_payer(
&[instruction::increase_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&unknown_stake.transient_stake_account,
&unknown_stake.stake_account,
&unknown_stake.vote.pubkey(),
reserve_lamports / 2,
unknown_stake.transient_stake_seed,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &stake_pool_accounts.staker],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::ValidatorNotFound as u32)
)
);
}
#[test_case(true; "additional")]
#[test_case(false; "non-additional")]
#[tokio::test]
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_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_instruction,
)
.await;
assert!(error.is_none(), "{:?}", error);
let transient_stake_seed = validator_stake.transient_stake_seed * 100;
let transient_stake_address = find_transient_stake_program_address(
&id(),
&validator_stake.vote.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(),
transient_stake_seed,
)
.0;
let error = stake_pool_accounts
.increase_validator_stake_either(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&transient_stake_address,
&validator_stake.stake_account,
&validator_stake.vote.pubkey(),
second_increase,
transient_stake_seed,
use_additional_instruction,
)
.await
.unwrap()
.unwrap();
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(), "{:?}", error);
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(), "{:?}", error);
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeStateV2>());
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());
let reserve_stake_account = get_account(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
)
.await;
let reserve_stake_state =
deserialize::<stake::state::StakeStateV2>(&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());
let transient_stake_account = get_account(
&mut context.banks_client,
&validator_stake.transient_stake_account,
)
.await;
let transient_stake_state =
deserialize::<stake::state::StakeStateV2>(&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
);
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!(
u64::from(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"),
}
}
}
#[test_case(true; "additional")]
#[test_case(false; "non-additional")]
#[tokio::test]
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 context.banks_client,
&context.payer,
&context.last_blockhash,
)
.await;
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(),
current_minimum_delegation - 1,
validator_stake.transient_stake_seed,
use_additional_instruction,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakeError::InsufficientDelegation as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[test_case(true; "additional")]
#[test_case(false; "non-additional")]
#[tokio::test]
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_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()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::InsufficientFunds) => {}
_ => panic!("Wrong error occurs while overdrawing reserve stake"),
}
}
#[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::<stake::state::StakeStateV2>());
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
context.warp_to_slot(first_normal_slot + 1).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,
false,
)
.await;
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,
current_minimum_delegation + stake_rent,
validator_stake.transient_stake_seed,
DecreaseInstruction::Reserve,
)
.await;
assert!(error.is_none(), "{:?}", error);
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(StakePoolError::WrongStakeStake as u32)
)
);
}
#[tokio::test]
async fn fail_with_force_destaked_validator() {}