From 841c92a3c51688a7518df88549f1b84b6fb5a846 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 17 Sep 2024 15:03:57 -0400 Subject: [PATCH 01/13] Start of rebased multi epoch tests --- tests/src/steward_fixtures.rs | 417 +++++++++++- tests/tests/steward/mod.rs | 1 + tests/tests/steward/test_cycle.rs | 1034 +++++++++++++++++++++++++++++ 3 files changed, 1437 insertions(+), 15 deletions(-) create mode 100644 tests/tests/steward/test_cycle.rs diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 65d53e61..5501db9b 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -1,5 +1,5 @@ #![allow(clippy::await_holding_refcell_ref)] -use std::{cell::RefCell, rc::Rc, str::FromStr, vec}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr, vec}; use crate::spl_stake_pool_cli; use anchor_lang::{ @@ -21,13 +21,23 @@ use jito_steward::{ }; use solana_program_test::*; use solana_sdk::{ - account::Account, epoch_schedule::EpochSchedule, hash::Hash, instruction::Instruction, - native_token::LAMPORTS_PER_SOL, rent::Rent, signature::Keypair, signer::Signer, - stake::state::StakeStateV2, transaction::Transaction, + account::Account, + epoch_schedule::EpochSchedule, + hash::Hash, + instruction::Instruction, + native_token::LAMPORTS_PER_SOL, + rent::Rent, + signature::Keypair, + signer::Signer, + stake::state::{Lockup, StakeStateV2}, + transaction::Transaction, }; use spl_stake_pool::{ find_stake_program_address, find_transient_stake_program_address, minimum_delegation, - state::{Fee, StakeStatus, ValidatorList as SPLValidatorList, ValidatorStakeInfo}, + state::{ + AccountType, Fee, FutureEpoch, StakeStatus, ValidatorList as SPLValidatorList, + ValidatorStakeInfo, + }, }; use validator_history::{ self, constants::MAX_ALLOC_BYTES, CircBuf, CircBufCluster, ClusterHistory, ClusterHistoryEntry, @@ -143,6 +153,54 @@ impl TestFixture { } } + pub async fn new_from_accounts( + accounts_fixture: FixtureDefaultAccounts, + additional_accounts: HashMap, + ) -> Self { + let mut program = ProgramTest::new("jito_steward", jito_steward::ID, None); + program.add_program("validator_history", validator_history::id(), None); + program.add_program("spl_stake_pool", spl_stake_pool::id(), None); + + for (key, account) in accounts_fixture.to_accounts_vec() { + // Skip keys that are overriden by additional_accounts + if !additional_accounts.contains_key(&key) { + program.add_account(key, account); + } + } + for (key, account) in additional_accounts { + program.add_account(key, account); + } + + program.deactivate_feature( + Pubkey::from_str("9onWzzvCzNC2jfhxxeqRgs5q7nFAAKpCUvkj6T6GJK9i").unwrap(), + ); + let ctx = Rc::new(RefCell::new(program.start_with_context().await)); + + let steward_config_address = accounts_fixture.steward_config_keypair.pubkey(); + + Self { + ctx, + stake_pool_meta: accounts_fixture.stake_pool_meta, + steward_config: accounts_fixture.steward_config_keypair, + steward_state: Pubkey::find_program_address( + &[StewardStateAccount::SEED, steward_config_address.as_ref()], + &jito_steward::id(), + ) + .0, + cluster_history_account: Pubkey::find_program_address( + &[ClusterHistory::SEED], + &validator_history::id(), + ) + .0, + validator_history_config: Pubkey::find_program_address( + &[validator_history::state::Config::SEED], + &validator_history::id(), + ) + .0, + keypair: accounts_fixture.keypair, + } + } + pub async fn load_and_deserialize( &self, address: &Pubkey, @@ -584,6 +642,315 @@ impl TestFixture { } } +pub struct ValidatorEntry { + pub vote_address: Pubkey, + pub vote_account: VoteStateVersions, + pub validator_history: ValidatorHistory, +} + +impl Default for ValidatorEntry { + fn default() -> Self { + let vote_address = Pubkey::new_unique(); + let vote_account = new_vote_state_versions(vote_address, vote_address, 0, None); + let validator_history = validator_history_default(vote_address, 0); + + Self { + vote_address, + vote_account, + validator_history, + } + } +} + +pub struct FixtureDefaultAccounts { + pub stake_pool_meta: StakePoolMetadata, + pub stake_pool: spl_stake_pool::state::StakePool, + pub validator_list: SPLValidatorList, + pub steward_config_keypair: Keypair, + pub steward_config: Config, + pub steward_state_address: Pubkey, + pub steward_state: StewardStateAccount, + pub validator_history_config: validator_history::state::Config, + pub cluster_history: ClusterHistory, + pub validators: Vec, + pub keypair: Keypair, +} + +impl Default for FixtureDefaultAccounts { + fn default() -> Self { + let keypair = Keypair::new(); + + // For each main thing to add to runtime, create a default account + let stake_pool_meta = StakePoolMetadata::default(); + let stake_pool = + FixtureDefaultAccounts::stake_pool_default(&stake_pool_meta, keypair.pubkey()); + + let validator_list = SPLValidatorList::new(MAX_VALIDATORS as u32); + + let steward_config_keypair = Keypair::new(); + let steward_config = Config { + stake_pool: stake_pool_meta.stake_pool.clone(), + validator_list: stake_pool_meta.validator_list.clone(), + blacklist_authority: keypair.pubkey(), + parameters_authority: keypair.pubkey(), + admin: keypair.pubkey(), + validator_history_blacklist: LargeBitMask::default(), + parameters: Parameters::default(), + _padding: [0; 1023], + paused: false.into(), + }; + + let (steward_state_address, steward_state_bump) = Pubkey::find_program_address( + &[ + StewardStateAccount::SEED, + steward_config_keypair.pubkey().as_ref(), + ], + &jito_steward::id(), + ); + + let steward_state = StewardState { + state_tag: StewardStateEnum::ComputeScores, + validator_lamport_balances: [0; MAX_VALIDATORS], + scores: [0; MAX_VALIDATORS], + sorted_score_indices: [SORTED_INDEX_DEFAULT; MAX_VALIDATORS], + yield_scores: [0; MAX_VALIDATORS], + sorted_yield_score_indices: [SORTED_INDEX_DEFAULT; MAX_VALIDATORS], + delegations: [Delegation::default(); MAX_VALIDATORS], + instant_unstake: BitMask::default(), + progress: BitMask::default(), + validators_to_remove: BitMask::default(), + validators_for_immediate_removal: BitMask::default(), + start_computing_scores_slot: 0, + current_epoch: 0, + next_cycle_epoch: 10, + num_pool_validators: 0, + scoring_unstake_total: 0, + instant_unstake_total: 0, + stake_deposit_unstake_total: 0, + validators_added: 0, + status_flags: 0, + _padding0: [0; STATE_PADDING_0_SIZE], + }; + let steward_state_account = StewardStateAccount { + state: steward_state, + is_initialized: true.into(), + bump: steward_state_bump, + _padding: [0; 6], + }; + + let validator_history_config_bump = Pubkey::find_program_address( + &[validator_history::state::Config::SEED], + &validator_history::id(), + ) + .1; + let validator_history_config = validator_history::state::Config { + bump: validator_history_config_bump, + counter: 1, + admin: keypair.pubkey(), + oracle_authority: keypair.pubkey(), + tip_distribution_program: jito_tip_distribution::id(), + ..Default::default() + }; + let cluster_history = cluster_history_default(); + + Self { + stake_pool_meta, + stake_pool, + validator_list, + steward_config_keypair, + steward_config, + steward_state_address, + steward_state: steward_state_account, + validator_history_config, + cluster_history, + validators: vec![], + keypair, + } + } +} + +impl FixtureDefaultAccounts { + fn to_accounts_vec(&self) -> Vec<(Pubkey, Account)> { + let validator_entry_accounts = self + .validators + .iter() + .map(|ve| { + let validator_history_address = Pubkey::find_program_address( + &[ValidatorHistory::SEED, ve.vote_address.as_ref()], + &validator_history::id(), + ) + .0; + ( + validator_history_address, + serialized_validator_history_account(ve.validator_history.clone()), + ) + }) + .collect::>(); + let vote_accounts_and_addresses = self + .validators + .iter() + .map(|ve| { + let vote_address = ve.vote_address; + let mut data = vec![0; VoteState::size_of()]; + VoteState::serialize(&ve.vote_account, &mut data).unwrap(); + + let vote_account = Account { + lamports: 1000000, + data, + owner: anchor_lang::solana_program::vote::program::ID, + ..Account::default() + }; + (vote_address, vote_account) + }) + .collect::>(); + + let cluster_history_address = + Pubkey::find_program_address(&[ClusterHistory::SEED], &validator_history::id()).0; + let steward_state_address = Pubkey::find_program_address( + &[ + StewardStateAccount::SEED, + self.steward_config_keypair.pubkey().as_ref(), + ], + &jito_steward::id(), + ) + .0; + + let validator_history_config_address = Pubkey::find_program_address( + &[validator_history::state::Config::SEED], + &validator_history::id(), + ) + .0; + + // For each account, serialize and return as a tuple + let mut accounts = vec![ + ( + self.steward_config_keypair.pubkey(), + serialized_config(self.steward_config), + ), + ( + steward_state_address, + serialized_steward_state_account(self.steward_state), + ), + ( + validator_history_config_address, + serialized_validator_history_config(self.validator_history_config.clone()), + ), + // ( + // self.stake_pool_meta.stake_pool, + // serialized_stake_pool_account( + // self.stake_pool.clone(), + // std::mem::size_of::(), + // ), + // ), + // ( + // self.stake_pool_meta.validator_list, + // serialized_validator_list_account( + // self.validator_list.clone(), + // Some(std::mem::size_of_val(&self.validator_list)), + // ), + // ), + ( + cluster_history_address, + serialized_cluster_history_account(self.cluster_history), + ), + (self.keypair.pubkey(), system_account(100_000_000_000)), + ]; + accounts.extend(validator_entry_accounts); + accounts.extend(vote_accounts_and_addresses); + accounts + } + + fn stake_pool_default( + stake_pool_meta: &StakePoolMetadata, + admin: Pubkey, + ) -> spl_stake_pool::state::StakePool { + let stake_pool_address = stake_pool_meta.stake_pool; + let validator_list = stake_pool_meta.validator_list; + let reserve_stake = stake_pool_meta.reserve; + let stake_deposit_authority = Pubkey::find_program_address( + &[&stake_pool_address.as_ref(), b"deposit"], + &spl_stake_pool::id(), + ) + .0; + let stake_withdraw_bump_seed = Pubkey::find_program_address( + &[&stake_pool_address.as_ref(), b"withdrawal"], + &spl_stake_pool::id(), + ) + .1; + let epoch_fee = Fee { + numerator: 1, + denominator: 100, + }; + let withdrawal_fee = Fee { + numerator: 1, + denominator: 100, + }; + let deposit_fee = Fee { + numerator: 1, + denominator: 100, + }; + // Use default values from stake pool initialization + spl_stake_pool::state::StakePool { + account_type: AccountType::StakePool, + manager: admin, + staker: admin, + stake_deposit_authority, + stake_withdraw_bump_seed, + validator_list, + reserve_stake, + pool_mint: Pubkey::new_unique(), + manager_fee_account: Pubkey::new_unique(), + token_program_id: spl_token::id(), + total_lamports: 0, + pool_token_supply: 0, + last_update_epoch: 0, + lockup: Lockup::default(), + epoch_fee, + next_epoch_fee: FutureEpoch::None, + preferred_deposit_validator_vote_address: None, + preferred_withdraw_validator_vote_address: None, + stake_deposit_fee: deposit_fee, + stake_withdrawal_fee: withdrawal_fee, + next_stake_withdrawal_fee: FutureEpoch::None, + stake_referral_fee: 0, + sol_deposit_authority: None, + sol_deposit_fee: deposit_fee, + sol_withdraw_authority: None, + sol_referral_fee: 0, + sol_withdrawal_fee: withdrawal_fee, + next_sol_withdrawal_fee: FutureEpoch::None, + last_epoch_pool_token_supply: 0, + last_epoch_total_lamports: 0, + } + } +} + +pub fn new_vote_state_versions( + node_pubkey: Pubkey, + vote_pubkey: Pubkey, + commission: u8, + maybe_epoch_credits: Option>, +) -> VoteStateVersions { + let vote_init = VoteInit { + node_pubkey, + authorized_voter: vote_pubkey, + authorized_withdrawer: vote_pubkey, + commission, + }; + let clock = Clock { + epoch: 0, + slot: 0, + unix_timestamp: 0, + leader_schedule_epoch: 0, + epoch_start_timestamp: 0, + }; + let mut vote_state = VoteState::new(&vote_init, &clock); + if let Some(epoch_credits) = maybe_epoch_credits { + vote_state.epoch_credits = epoch_credits; + } + VoteStateVersions::new_current(vote_state) +} + pub fn validator_history_config_account(bump: u8, num_validators: u32) -> Account { let config = validator_history::state::Config { bump, @@ -819,6 +1186,7 @@ pub struct StateMachineFixtures { pub clock: Clock, pub epoch_schedule: EpochSchedule, pub validators: Vec, + pub vote_accounts: Vec, pub cluster_history: ClusterHistory, pub config: Config, pub validator_list: Vec, @@ -868,7 +1236,10 @@ impl Default for StateMachineFixtures { // Setup Sysvars: Clock, EpochSchedule - let epoch_schedule = EpochSchedule::custom(1000, 1000, false); + let epoch_schedule = EpochSchedule::default(); + + // TODO support both types + // let epoch_schedule = EpochSchedule::custom(1000, 1000, false); let clock = Clock { epoch: current_epoch, @@ -877,15 +1248,18 @@ impl Default for StateMachineFixtures { }; // Setup ValidatorHistory accounts - let vote_account_1 = Pubkey::new_unique(); - let vote_account_2 = Pubkey::new_unique(); - let vote_account_3 = Pubkey::new_unique(); + let vote_address_1 = Pubkey::new_unique(); + let vote_address_2 = Pubkey::new_unique(); + let vote_address_3 = Pubkey::new_unique(); // First one: Good validator - let mut validator_history_1 = validator_history_default(vote_account_1, 0); + let mut validator_history_1 = validator_history_default(vote_address_1, 0); + let mut epoch_credits: Vec<(u64, u64, u64)> = vec![]; + for i in 0..=20 { + epoch_credits.push((i, (i + 1) * 1000, i * 1000)); validator_history_1.history.push(ValidatorHistoryEntry { - epoch: i, + epoch: i as u16, epoch_credits: 1000, commission: 0, mev_commission: 0, @@ -894,12 +1268,17 @@ impl Default for StateMachineFixtures { ..ValidatorHistoryEntry::default() }); } + let vote_account_1 = + new_vote_state_versions(vote_address_1, vote_address_1, 0, Some(epoch_credits)); // Second one: Bad validator - let mut validator_history_2 = validator_history_default(vote_account_2, 1); + let mut validator_history_2 = validator_history_default(vote_address_2, 1); + let mut epoch_credits: Vec<(u64, u64, u64)> = vec![]; for i in 0..=20 { + epoch_credits.push((i, (i + 1) * 200, i * 200)); + validator_history_2.history.push(ValidatorHistoryEntry { - epoch: i, + epoch: i as u16, epoch_credits: 200, commission: 99, mev_commission: 10000, @@ -908,12 +1287,17 @@ impl Default for StateMachineFixtures { ..ValidatorHistoryEntry::default() }); } + let vote_account_2 = + new_vote_state_versions(vote_address_2, vote_address_2, 99, Some(epoch_credits)); // Third one: Good validator - let mut validator_history_3 = validator_history_default(vote_account_3, 2); + let mut validator_history_3 = validator_history_default(vote_address_3, 2); + let mut epoch_credits: Vec<(u64, u64, u64)> = vec![]; for i in 0..=20 { + epoch_credits.push((i, (i + 1) * 1000, i * 1000)); + validator_history_3.history.push(ValidatorHistoryEntry { - epoch: i, + epoch: i as u16, epoch_credits: 1000, commission: 5, mev_commission: 500, @@ -922,6 +1306,8 @@ impl Default for StateMachineFixtures { ..ValidatorHistoryEntry::default() }); } + let vote_account_3 = + new_vote_state_versions(vote_address_3, vote_address_3, 5, Some(epoch_credits)); // Setup ClusterHistory let mut cluster_history = cluster_history_default(); @@ -990,6 +1376,7 @@ impl Default for StateMachineFixtures { validator_history_2, validator_history_3, ], + vote_accounts: vec![vote_account_1, vote_account_2, vote_account_3], cluster_history, config, validator_list, diff --git a/tests/tests/steward/mod.rs b/tests/tests/steward/mod.rs index d79627a5..d2d23974 100644 --- a/tests/tests/steward/mod.rs +++ b/tests/tests/steward/mod.rs @@ -1,4 +1,5 @@ mod test_algorithms; +mod test_cycle; mod test_integration; mod test_parameters; mod test_scoring; diff --git a/tests/tests/steward/test_cycle.rs b/tests/tests/steward/test_cycle.rs new file mode 100644 index 00000000..16c9facc --- /dev/null +++ b/tests/tests/steward/test_cycle.rs @@ -0,0 +1,1034 @@ +use std::collections::HashMap; + +use anchor_lang::{ + solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}, + InstructionData, ToAccountMetas, +}; +use jito_steward::{ + utils::{StakePool, ValidatorList}, + StewardStateAccount, UpdateParametersArgs, +}; +use solana_program_test::*; +use solana_sdk::{ + clock::Clock, compute_budget::ComputeBudgetInstruction, epoch_schedule::EpochSchedule, + signature::Keypair, signer::Signer, system_program, transaction::Transaction, +}; +use tests::steward_fixtures::{ + FixtureDefaultAccounts, StateMachineFixtures, TestFixture, ValidatorEntry, +}; +use validator_history::ValidatorHistory; + +pub struct ExtraValidatorAccounts { + vote_account: Pubkey, + validator_history_address: Pubkey, + stake_account_address: Pubkey, + transient_stake_account_address: Pubkey, + withdraw_authority: Pubkey, +} + +async fn _crank_stake_pool(fixture: &TestFixture) { + let stake_pool: StakePool = fixture + .load_and_deserialize(&fixture.stake_pool_meta.stake_pool) + .await; + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let (initial_ixs, final_ixs) = spl_stake_pool::instruction::update_stake_pool( + &spl_stake_pool::id(), + &stake_pool.as_ref(), + &validator_list.as_ref(), + &fixture.stake_pool_meta.stake_pool, + false, + ); + + let tx = Transaction::new_signed_with_payer( + &initial_ixs, + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(), + ); + fixture.submit_transaction_assert_success(tx).await; + + let tx = Transaction::new_signed_with_payer( + &final_ixs, + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(), + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _crank_epoch_maintenance(fixture: &TestFixture, remove_indices: Option<&[usize]>) { + let ctx = &fixture.ctx; + // Epoch Maintenence + if let Some(indices) = remove_indices { + for i in indices { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::EpochMaintenance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove: Some(*i as u64), + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } + } else { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::EpochMaintenance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove: None, + } + .data(), + }; + + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +async fn _auto_add_validator(fixture: &TestFixture, extra_accounts: &ExtraValidatorAccounts) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::AutoAddValidator { + validator_history_account: extra_accounts.validator_history_address, + steward_state: fixture.steward_state, + config: fixture.steward_config.pubkey(), + stake_pool_program: spl_stake_pool::id(), + stake_pool: fixture.stake_pool_meta.stake_pool, + reserve_stake: fixture.stake_pool_meta.reserve, + withdraw_authority: extra_accounts.withdraw_authority, + validator_list: fixture.stake_pool_meta.validator_list, + stake_account: extra_accounts.stake_account_address, + vote_account: extra_accounts.vote_account, + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + system_program: system_program::id(), + stake_program: stake::program::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _crank_compute_score( + fixture: &TestFixture, + unit_test_fixtures: &StateMachineFixtures, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let ctx = &fixture.ctx; + + for &i in indices { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::ComputeScore { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + validator_history: extra_validator_accounts[i].validator_history_address, + cluster_history: fixture.cluster_history_account, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeScore { + validator_list_index: i as u64, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +async fn _crank_compute_delegations(fixture: &TestFixture) { + let ctx = &fixture.ctx; + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::ComputeDelegations { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeDelegations {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _crank_idle(fixture: &TestFixture) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::Idle { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + } + .to_account_metas(None), + data: jito_steward::instruction::Idle {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _crank_compute_instant_unstake( + fixture: &TestFixture, + unit_test_fixtures: &StateMachineFixtures, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let ctx = &fixture.ctx; + + for &i in indices { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::ComputeInstantUnstake { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_history: extra_validator_accounts[i].validator_history_address, + validator_list: fixture.stake_pool_meta.validator_list, + cluster_history: fixture.cluster_history_account, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeInstantUnstake { + validator_list_index: i as u64, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +async fn _crank_rebalance( + fixture: &TestFixture, + _unit_test_fixtures: &StateMachineFixtures, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let ctx = &fixture.ctx; + + for &i in indices { + let extra_accounts = &extra_validator_accounts[i]; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::Rebalance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_history: extra_accounts.validator_history_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: fixture.stake_pool_meta.stake_pool, + withdraw_authority: extra_accounts.withdraw_authority, + validator_list: fixture.stake_pool_meta.validator_list, + reserve_stake: fixture.stake_pool_meta.reserve, + stake_account: extra_accounts.stake_account_address, + transient_stake_account: extra_accounts.transient_stake_account_address, + vote_account: extra_accounts.vote_account, + system_program: system_program::id(), + stake_program: stake::program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + } + .to_account_metas(None), + data: jito_steward::instruction::Rebalance { + validator_list_index: i as u64, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +async fn _copy_vote_account( + fixture: &TestFixture, + extra_validator_accounts: &Vec, + index: usize, +) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account: extra_validator_accounts[index].validator_history_address, + vote_account: extra_validator_accounts[index].vote_account, + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::CopyVoteAccount {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _update_stake_history( + fixture: &TestFixture, + extra_validator_accounts: &Vec, + index: usize, + epoch: u64, + lamports: u64, + rank: u32, + is_superminority: bool, +) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::UpdateStakeHistory { + validator_history_account: extra_validator_accounts[index].validator_history_address, + vote_account: extra_validator_accounts[index].vote_account, + config: fixture.validator_history_config, + oracle_authority: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::UpdateStakeHistory { + epoch, + is_superminority, + lamports, + rank, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _copy_cluster_info(fixture: &TestFixture) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::CopyClusterInfo { + cluster_history_account: fixture.cluster_history_account, + slot_history: sysvar::slot_history::id(), + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::CopyClusterInfo {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::request_heap_frame(1024 * 256), + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ix, + ], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _crank_validator_history_accounts( + fixture: &TestFixture, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let clock: Clock = fixture + .ctx + .borrow_mut() + .banks_client + .get_sysvar() + .await + .unwrap(); + for &i in indices { + fixture + .ctx + .borrow_mut() + .increment_vote_account_credits(&extra_validator_accounts[i].vote_account, 1000); + _copy_vote_account(&fixture, &extra_validator_accounts, i).await; + // only field that's relevant to score is is_superminority + _update_stake_history( + &fixture, + &extra_validator_accounts, + i, + clock.epoch, + 1_000_000, + 1_000, + false, + ) + .await; + } + _copy_cluster_info(&fixture).await; +} + +#[tokio::test] +async fn test_cycle() { + let mut fixture_accounts = FixtureDefaultAccounts::default(); + + let unit_test_fixtures = StateMachineFixtures::default(); + + fixture_accounts.steward_config.parameters = unit_test_fixtures.config.parameters; + + fixture_accounts.validators = (0..3) + .map(|i| ValidatorEntry { + validator_history: unit_test_fixtures.validators[i], + vote_account: unit_test_fixtures.vote_accounts[i].clone(), + vote_address: unit_test_fixtures.validators[i].vote_account, + }) + .collect(); + fixture_accounts.cluster_history = unit_test_fixtures.cluster_history; + + // Modify validator history account with desired values + + let mut fixture = TestFixture::new_from_accounts(fixture_accounts, HashMap::new()).await; + let ctx = &fixture.ctx; + + fixture.steward_config = Keypair::new(); + fixture.steward_state = Pubkey::find_program_address( + &[ + StewardStateAccount::SEED, + fixture.steward_config.pubkey().as_ref(), + ], + &jito_steward::id(), + ) + .0; + + fixture.advance_num_epochs(20, 10).await; + fixture.initialize_stake_pool().await; + fixture + .initialize_steward(Some(UpdateParametersArgs { + mev_commission_range: Some(10), // Set to pass validation, where epochs starts at 0 + epoch_credits_range: Some(20), // Set to pass validation, where epochs starts at 0 + commission_range: Some(20), // Set to pass validation, where epochs starts at 0 + scoring_delinquency_threshold_ratio: Some(0.85), + instant_unstake_delinquency_threshold_ratio: Some(0.70), + mev_commission_bps_threshold: Some(1000), + commission_threshold: Some(5), + historical_commission_threshold: Some(50), + num_delegation_validators: Some(200), + scoring_unstake_cap_bps: Some(750), + instant_unstake_cap_bps: Some(10), + stake_deposit_unstake_cap_bps: Some(10), + instant_unstake_epoch_progress: Some(0.00), + compute_score_slot_range: Some(1000), + instant_unstake_inputs_epoch_progress: Some(0.50), + num_epochs_between_scoring: Some(2), // 2 epoch cycle + minimum_stake_lamports: Some(5_000_000_000), + minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 + })) + .await; + let steward: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; + + let mut extra_validator_accounts = vec![]; + for i in 0..unit_test_fixtures.validators.len() { + let vote_account = unit_test_fixtures.validator_list[i].vote_account_address; + let (validator_history_address, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, vote_account.as_ref()], + &validator_history::id(), + ); + + let (stake_account_address, transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + extra_validator_accounts.push(ExtraValidatorAccounts { + vote_account, + validator_history_address, + stake_account_address, + transient_stake_account_address, + withdraw_authority, + }) + } + + // Auto add validator - adds to validator list + for i in 0..unit_test_fixtures.validators.len() { + let extra_accounts = &extra_validator_accounts[i]; + _auto_add_validator(&fixture, extra_accounts).await; + } + + _crank_epoch_maintenance(&fixture, None).await; + + _crank_compute_score( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + _crank_compute_delegations(&fixture).await; + + let epoch_schedule: EpochSchedule = ctx.borrow_mut().banks_client.get_sysvar().await.unwrap(); + let clock: Clock = ctx.borrow_mut().banks_client.get_sysvar().await.unwrap(); + + _crank_idle(&fixture).await; + + _crank_compute_instant_unstake( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + _crank_rebalance( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + fixture.advance_num_epochs(1, 10).await; + + _crank_stake_pool(&fixture).await; + + _crank_epoch_maintenance(&fixture, None).await; + + _crank_idle(&fixture).await; + + // Advance to instant_unstake_inputs_epoch_progress + fixture + .advance_num_epochs(0, epoch_schedule.get_slots_in_epoch(clock.epoch) / 2 + 1) + .await; + + // Update validator history values + _crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; + + _crank_compute_instant_unstake( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + _crank_rebalance( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + fixture.advance_num_epochs(1, 10).await; + + _crank_stake_pool(&fixture).await; + + _crank_epoch_maintenance(&fixture, None).await; + + // Update validator history values + _crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; + + // In new cycle + _crank_compute_score( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + let clock: Clock = ctx.borrow_mut().banks_client.get_sysvar().await.unwrap(); + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::ComputeDelegations + )); + assert_eq!(state.current_epoch, clock.epoch); + assert_eq!(state.next_cycle_epoch, clock.epoch + 2); + assert_eq!(state.instant_unstake_total, 0); + assert_eq!(state.scoring_unstake_total, 0); + assert_eq!(state.stake_deposit_unstake_total, 0); + assert_eq!(state.validators_added, 0); + assert_eq!(state.validators_to_remove.is_empty(), true); + // assert_eq!(state.status_flags, 3); // TODO + + // All other values are reset + + drop(fixture); +} + +#[tokio::test] +async fn test_remove_validator_next_epoch() { + /* + Tests that a validator removed at an arbitrary point in the cycle is not included in the current cycle's consideration, + even though it is still in the validator list, and the next epoch, it is removed from the validator list. + */ + + let mut fixture_accounts = FixtureDefaultAccounts::default(); + + let unit_test_fixtures = StateMachineFixtures::default(); + + fixture_accounts.steward_config.parameters = unit_test_fixtures.config.parameters; + + fixture_accounts.validators = (0..3) + .map(|i| ValidatorEntry { + validator_history: unit_test_fixtures.validators[i], + vote_account: unit_test_fixtures.vote_accounts[i].clone(), + vote_address: unit_test_fixtures.validators[i].vote_account, + }) + .collect(); + fixture_accounts.cluster_history = unit_test_fixtures.cluster_history; + + let mut fixture = TestFixture::new_from_accounts(fixture_accounts, HashMap::new()).await; + let ctx = &fixture.ctx; + + fixture.steward_config = Keypair::new(); + fixture.steward_state = Pubkey::find_program_address( + &[ + StewardStateAccount::SEED, + fixture.steward_config.pubkey().as_ref(), + ], + &jito_steward::id(), + ) + .0; + + fixture.advance_num_epochs(20, 10).await; + fixture.initialize_stake_pool().await; + fixture + .initialize_steward(Some(UpdateParametersArgs { + mev_commission_range: Some(10), // Set to pass validation, where epochs starts at 0 + epoch_credits_range: Some(20), // Set to pass validation, where epochs starts at 0 + commission_range: Some(20), // Set to pass validation, where epochs starts at 0 + scoring_delinquency_threshold_ratio: Some(0.85), + instant_unstake_delinquency_threshold_ratio: Some(0.70), + mev_commission_bps_threshold: Some(1000), + commission_threshold: Some(5), + historical_commission_threshold: Some(50), + num_delegation_validators: Some(200), + scoring_unstake_cap_bps: Some(750), + instant_unstake_cap_bps: Some(10), + stake_deposit_unstake_cap_bps: Some(10), + instant_unstake_epoch_progress: Some(0.00), + compute_score_slot_range: Some(1000), + instant_unstake_inputs_epoch_progress: Some(0.50), + num_epochs_between_scoring: Some(2), // 2 epoch cycle + minimum_stake_lamports: Some(5_000_000_000), + minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 + })) + .await; + + let mut extra_validator_accounts = vec![]; + for i in 0..unit_test_fixtures.validators.len() { + let vote_account = unit_test_fixtures.validator_list[i].vote_account_address; + let (validator_history_address, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, vote_account.as_ref()], + &validator_history::id(), + ); + + let (stake_account_address, transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + extra_validator_accounts.push(ExtraValidatorAccounts { + vote_account, + validator_history_address, + stake_account_address, + transient_stake_account_address, + withdraw_authority, + }) + } + + // Auto add validator - adds validators 2 and 3 + for i in 0..3 { + let extra_accounts = &extra_validator_accounts[i]; + _auto_add_validator(&fixture, extra_accounts).await; + } + + _crank_epoch_maintenance(&fixture, None).await; + _crank_compute_score( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + _crank_compute_delegations(&fixture).await; + + _crank_idle(&fixture).await; + + _crank_compute_instant_unstake( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1], + ) + .await; + + // Remove validator 2 in the middle of compute instant unstake + let remove_validator_from_pool_ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::RemoveValidatorFromPool { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + stake_pool_program: spl_stake_pool::id(), + stake_pool: fixture.stake_pool_meta.stake_pool, + withdraw_authority: extra_validator_accounts[2].withdraw_authority, + validator_list: fixture.stake_pool_meta.validator_list, + stake_account: extra_validator_accounts[2].stake_account_address, + transient_stake_account: extra_validator_accounts[2].transient_stake_account_address, + clock: solana_sdk::sysvar::clock::id(), + system_program: system_program::id(), + stake_program: stake::program::id(), + admin: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::RemoveValidatorFromPool { + validator_list_index: 2, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[remove_validator_from_pool_ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::ComputeInstantUnstake + )); + assert_eq!(state.validators_to_remove.count(), 1); + assert_eq!(state.validators_to_remove.get(2).unwrap(), true); + assert_eq!(state.num_pool_validators, 3); + + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + assert!(validator_list + .validators + .iter() + .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) + .is_some()); + assert!(validator_list.validators.len() == 3); + println!("Stake Status: {:?}", validator_list.validators[2].status); + + // Still need to crank? + _crank_compute_instant_unstake( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[2], + ) + .await; + + _crank_rebalance( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1, 2], + ) + .await; + + fixture.advance_num_epochs(1, 10).await; + _crank_stake_pool(&fixture).await; + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + assert!(validator_list + .validators + .iter() + .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) + .is_none()); + assert!(validator_list.validators.len() == 2); + + _crank_epoch_maintenance(&fixture, Some(&[2])).await; + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::Idle + )); + assert_eq!(state.validators_to_remove.count(), 0); + assert_eq!(state.num_pool_validators, 2); + + drop(fixture); +} + +#[tokio::test] +async fn test_add_validator_next_cycle() { + /* + Tests that a validator added at an arbitrary point during the cycle does not get included in the + current cycle's consideration, but is included in the next cycle's scoring after ComputeScores is run. + */ + + let mut fixture_accounts = FixtureDefaultAccounts::default(); + + let unit_test_fixtures = StateMachineFixtures::default(); + + fixture_accounts.steward_config.parameters = unit_test_fixtures.config.parameters; + + fixture_accounts.validators = (0..3) + .map(|i| ValidatorEntry { + validator_history: unit_test_fixtures.validators[i], + vote_account: unit_test_fixtures.vote_accounts[i].clone(), + vote_address: unit_test_fixtures.validators[i].vote_account, + }) + .collect(); + fixture_accounts.cluster_history = unit_test_fixtures.cluster_history; + + // Modify validator history account with desired values + + let mut fixture = TestFixture::new_from_accounts(fixture_accounts, HashMap::new()).await; + + fixture.steward_config = Keypair::new(); + fixture.steward_state = Pubkey::find_program_address( + &[ + StewardStateAccount::SEED, + fixture.steward_config.pubkey().as_ref(), + ], + &jito_steward::id(), + ) + .0; + + fixture.advance_num_epochs(20, 10).await; + fixture.initialize_stake_pool().await; + fixture + .initialize_steward(Some(UpdateParametersArgs { + mev_commission_range: Some(10), // Set to pass validation, where epochs starts at 0 + epoch_credits_range: Some(20), // Set to pass validation, where epochs starts at 0 + commission_range: Some(20), // Set to pass validation, where epochs starts at 0 + scoring_delinquency_threshold_ratio: Some(0.85), + instant_unstake_delinquency_threshold_ratio: Some(0.70), + mev_commission_bps_threshold: Some(1000), + commission_threshold: Some(5), + historical_commission_threshold: Some(50), + num_delegation_validators: Some(200), + scoring_unstake_cap_bps: Some(750), + instant_unstake_cap_bps: Some(10), + stake_deposit_unstake_cap_bps: Some(10), + instant_unstake_epoch_progress: Some(0.00), + compute_score_slot_range: Some(1000), + instant_unstake_inputs_epoch_progress: Some(0.50), + num_epochs_between_scoring: Some(1), // 1 epoch cycle + minimum_stake_lamports: Some(5_000_000_000), + minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 + })) + .await; + + let mut extra_validator_accounts = vec![]; + for i in 0..unit_test_fixtures.validators.len() { + let vote_account = unit_test_fixtures.validator_list[i].vote_account_address; + let (validator_history_address, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, vote_account.as_ref()], + &validator_history::id(), + ); + + let (stake_account_address, transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + extra_validator_accounts.push(ExtraValidatorAccounts { + vote_account, + validator_history_address, + stake_account_address, + transient_stake_account_address, + withdraw_authority, + }) + } + + // Auto add validator - adds validators 2 and 3 + for i in 0..2 { + let extra_accounts = &extra_validator_accounts[i]; + _auto_add_validator(&fixture, extra_accounts).await; + } + + _crank_epoch_maintenance(&fixture, None).await; + _crank_compute_score( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1], + ) + .await; + + // Add in validator 2 at random time + _auto_add_validator(&fixture, &extra_validator_accounts[2]).await; + + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + assert!(validator_list + .validators + .iter() + .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) + .is_some()); + assert!(validator_list.validators.len() == 3); + + // Ensure that num_pool_validators isn't updated but validators_added is + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::ComputeDelegations + )); + assert_eq!(state.validators_added, 1); + assert_eq!(state.num_pool_validators, 2); + + _crank_compute_delegations(&fixture).await; + _crank_idle(&fixture).await; + _crank_compute_instant_unstake( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1], + ) + .await; + _crank_rebalance( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0, 1], + ) + .await; + + fixture.advance_num_epochs(1, 10).await; + + _crank_stake_pool(&fixture).await; + _crank_epoch_maintenance(&fixture, None).await; + + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::Idle + )); + assert_eq!(state.validators_added, 1); + assert_eq!(state.num_pool_validators, 2); + + _crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; + + // Ensure we're in the next cycle + _crank_compute_score( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[0], + ) + .await; + + // Ensure that num_pool_validators is updated and validators_added is reset + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::ComputeScores + )); + + assert_eq!(state.validators_added, 0); + assert_eq!(state.validators_to_remove.is_empty(), true); + assert_eq!(state.num_pool_validators, 3); + + // Ensure we can crank the new validator + _crank_compute_score( + &fixture, + &unit_test_fixtures, + &extra_validator_accounts, + &[2], + ) + .await; + + drop(fixture); +} From 01a875a6fce30640b2f705003127e36e1f3c1f4c Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 17 Sep 2024 16:17:58 -0400 Subject: [PATCH 02/13] Fix tests --- .../auto_remove_validator_from_pool.rs | 24 +--- .../src/instructions/spl_passthrough.rs | 33 ++++- programs/steward/src/utils.rs | 22 +++ run_tests.sh | 2 +- tests/src/steward_fixtures.rs | 6 +- tests/tests/steward/test_algorithms.rs | 30 +++-- tests/tests/steward/test_cycle.rs | 126 ++++++++++++++++-- 7 files changed, 189 insertions(+), 54 deletions(-) diff --git a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs index 84e01606..d56d9167 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -6,11 +6,11 @@ use crate::events::AutoRemoveValidatorEvent; use crate::state::Config; use crate::utils::{ deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, - remove_validator_check, + remove_validator_check, stake_is_inactive_without_history, stake_is_usable_by_pool, }; use crate::StewardStateAccount; use anchor_lang::prelude::*; -use anchor_lang::solana_program::{clock::Epoch, program::invoke_signed, stake, sysvar, vote}; +use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; use spl_pod::solana_program::borsh1::try_from_slice_unchecked; use spl_pod::solana_program::stake::state::StakeStateV2; use spl_stake_pool::state::StakeStatus; @@ -267,23 +267,3 @@ pub fn handler(ctx: Context, validator_list_index: usize) - Ok(()) } - -// CHECKS FROM spl_stake_pool::processor::update_validator_list_balance - -/// Checks if a stake account can be managed by the pool -fn stake_is_usable_by_pool( - meta: &stake::state::Meta, - expected_authority: &Pubkey, - expected_lockup: &stake::state::Lockup, -) -> bool { - meta.authorized.staker == *expected_authority - && meta.authorized.withdrawer == *expected_authority - && meta.lockup == *expected_lockup -} - -/// Checks if a stake account is active, without taking into account cooldowns -fn stake_is_inactive_without_history(stake: &stake::state::Stake, epoch: Epoch) -> bool { - stake.delegation.deactivation_epoch < epoch - || (stake.delegation.activation_epoch == epoch - && stake.delegation.deactivation_epoch == epoch) -} diff --git a/programs/steward/src/instructions/spl_passthrough.rs b/programs/steward/src/instructions/spl_passthrough.rs index 2f7d315c..e6876891 100644 --- a/programs/steward/src/instructions/spl_passthrough.rs +++ b/programs/steward/src/instructions/spl_passthrough.rs @@ -9,11 +9,13 @@ use crate::errors::StewardError; use crate::state::Config; use crate::utils::{ deserialize_stake_pool, get_config_admin, get_stake_pool_address, - get_validator_stake_info_at_index, + get_validator_stake_info_at_index, stake_is_inactive_without_history, stake_is_usable_by_pool, }; use crate::StewardStateAccount; use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; +use spl_pod::solana_program::borsh1::try_from_slice_unchecked; +use spl_pod::solana_program::stake::state::StakeStateV2; use spl_stake_pool::find_stake_program_address; use spl_stake_pool::instruction::PreferredValidatorType; use spl_stake_pool::state::{StakeStatus, ValidatorListHeader}; @@ -180,9 +182,9 @@ pub fn remove_validator_from_pool_handler( ctx: Context, validator_list_index: usize, ) -> Result<()> { + let epoch = Clock::get()?.epoch; { let state_account = ctx.accounts.state_account.load_mut()?; - let epoch = Clock::get()?.epoch; // Should not be able to remove a validator if update is not complete require!( @@ -245,12 +247,37 @@ pub fn remove_validator_from_pool_handler( let stake_status = StakeStatus::try_from(validator_stake_info.status)?; + let stake_pool = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + match stake_status { StakeStatus::Active => { // Should never happen return Err(StewardError::ValidatorMarkedActive.into()); } - StakeStatus::DeactivatingValidator | StakeStatus::ReadyForRemoval => { + StakeStatus::DeactivatingValidator => { + let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); + let (meta, stake) = + match try_from_slice_unchecked::(stake_account_data)? { + StakeStateV2::Stake(meta, stake, _stake_flags) => (meta, stake), + _ => return Err(StewardError::StakeStateIsNotStake.into()), + }; + + if stake_is_usable_by_pool( + &meta, + ctx.accounts.withdraw_authority.key, + &stake_pool.lockup, + ) && stake_is_inactive_without_history(&stake, epoch) + { + state_account + .state + .mark_validator_for_immediate_removal(validator_list_index)?; + } else { + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } + } + StakeStatus::ReadyForRemoval => { state_account .state .mark_validator_for_immediate_removal(validator_list_index)?; diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index 1539e99a..c3399d1f 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -1,7 +1,9 @@ use std::ops::{Deref, Not}; +use anchor_lang::solana_program::stake; use anchor_lang::{idl::types::*, prelude::*}; use borsh::{BorshDeserialize, BorshSerialize}; +use spl_pod::solana_program::clock::Epoch; use spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64, solana_program::program_pack::Pack}; use spl_stake_pool::{ big_vec::BigVec, @@ -310,6 +312,26 @@ pub fn check_validator_list_has_stake_status_other_than( Ok(false) } +/// Checks if a stake account can be managed by the pool +/// FROM spl_stake_pool::processor::update_validator_list_balance +pub fn stake_is_usable_by_pool( + meta: &stake::state::Meta, + expected_authority: &Pubkey, + expected_lockup: &stake::state::Lockup, +) -> bool { + meta.authorized.staker == *expected_authority + && meta.authorized.withdrawer == *expected_authority + && meta.lockup == *expected_lockup +} + +/// Checks if a stake account is active, without taking into account cooldowns +/// FROM spl_stake_pool::processor::update_validator_list_balance +pub fn stake_is_inactive_without_history(stake: &stake::state::Stake, epoch: Epoch) -> bool { + stake.delegation.deactivation_epoch < epoch + || (stake.delegation.activation_epoch == epoch + && stake.delegation.deactivation_epoch == epoch) +} + pub fn get_validator_list_length(validator_list_account_info: &AccountInfo) -> Result { let mut validator_list_data = validator_list_account_info.try_borrow_mut_data()?; let (header, validator_list) = ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; diff --git a/run_tests.sh b/run_tests.sh index 3f16abc7..117e9254 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -5,5 +5,5 @@ cargo build-sbf --manifest-path programs/steward/Cargo.toml; cargo build-sbf --manifest-path programs/validator-history/Cargo.toml; # Run all tests -SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=5000000 cargo test --package tests --all-features --color auto +SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=10000000 cargo test --package tests --all-features --color auto diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 5501db9b..1e9dd97d 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -1238,9 +1238,6 @@ impl Default for StateMachineFixtures { let epoch_schedule = EpochSchedule::default(); - // TODO support both types - // let epoch_schedule = EpochSchedule::custom(1000, 1000, false); - let clock = Clock { epoch: current_epoch, slot: epoch_schedule.get_last_slot_in_epoch(current_epoch), @@ -1264,6 +1261,7 @@ impl Default for StateMachineFixtures { commission: 0, mev_commission: 0, is_superminority: 0, + activated_stake_lamports: 10 * LAMPORTS_PER_SOL, vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), ..ValidatorHistoryEntry::default() }); @@ -1283,6 +1281,7 @@ impl Default for StateMachineFixtures { commission: 99, mev_commission: 10000, is_superminority: 1, + activated_stake_lamports: 10 * LAMPORTS_PER_SOL, vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), ..ValidatorHistoryEntry::default() }); @@ -1302,6 +1301,7 @@ impl Default for StateMachineFixtures { commission: 5, mev_commission: 500, is_superminority: 0, + activated_stake_lamports: 10 * LAMPORTS_PER_SOL, vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), ..ValidatorHistoryEntry::default() }); diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 2b07210b..824fc036 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -620,6 +620,8 @@ fn test_instant_unstake() { .parameters .instant_unstake_delinquency_threshold_ratio = 0.25; let start_slot = epoch_schedule.get_first_slot_in_epoch(current_epoch); + let end_slot = epoch_schedule.get_last_slot_in_epoch(current_epoch); + let slot_index = end_slot - start_slot; let current_epoch = current_epoch as u16; // Non-instant-unstake validator @@ -646,9 +648,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 1000, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 1000, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 0, mev_commission: 0 } @@ -681,9 +683,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 1000, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 1000, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 0, mev_commission: 0 } @@ -713,9 +715,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 200, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 1000, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 99, mev_commission: 10000 } @@ -762,9 +764,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 0, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 1000, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 0, mev_commission: 0 } @@ -811,9 +813,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 1000, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 1000, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 100, mev_commission: 0 } @@ -843,9 +845,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 1000, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 1000, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 0, mev_commission: 0 } @@ -875,9 +877,9 @@ fn test_instant_unstake() { epoch: current_epoch, details: InstantUnstakeDetails { epoch_credits_latest: 1000, - vote_account_last_update_slot: start_slot + 999, + vote_account_last_update_slot: end_slot, total_blocks_latest: 0, - cluster_history_slot_index: 999, + cluster_history_slot_index: slot_index, commission: 0, mev_commission: 0 } diff --git a/tests/tests/steward/test_cycle.rs b/tests/tests/steward/test_cycle.rs index 16c9facc..f7b5fcc2 100644 --- a/tests/tests/steward/test_cycle.rs +++ b/tests/tests/steward/test_cycle.rs @@ -159,6 +159,80 @@ async fn _auto_add_validator(fixture: &TestFixture, extra_accounts: &ExtraValida fixture.submit_transaction_assert_success(tx).await; } +async fn _auto_remove_validator( + fixture: &TestFixture, + extra_accounts: &ExtraValidatorAccounts, + index: u64, +) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::AutoRemoveValidator { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + stake_account: extra_accounts.stake_account_address, + withdraw_authority: extra_accounts.withdraw_authority, + validator_history_account: extra_accounts.validator_history_address, + reserve_stake: fixture.stake_pool_meta.reserve, + transient_stake_account: extra_accounts.transient_stake_account_address, + vote_account: extra_accounts.vote_account, + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + stake_program: stake::program::id(), + stake_pool_program: spl_stake_pool::id(), + system_program: system_program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoRemoveValidatorFromPool { + validator_list_index: index, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +async fn _instant_remove_validator(fixture: &TestFixture, index: usize) { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::InstantRemoveValidator { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::InstantRemoveValidator { + validator_index_to_remove: index as u64, + } + .data(), + }; + let blockhash = fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + async fn _crank_compute_score( fixture: &TestFixture, unit_test_fixtures: &StateMachineFixtures, @@ -241,7 +315,7 @@ async fn _crank_idle(fixture: &TestFixture) { async fn _crank_compute_instant_unstake( fixture: &TestFixture, - unit_test_fixtures: &StateMachineFixtures, + _unit_test_fixtures: &StateMachineFixtures, extra_validator_accounts: &Vec, indices: &[usize], ) { @@ -454,6 +528,7 @@ async fn test_cycle() { let unit_test_fixtures = StateMachineFixtures::default(); + // Note that these parameters are overriden in initialize_steward, just included here for completeness fixture_accounts.steward_config.parameters = unit_test_fixtures.config.parameters; fixture_accounts.validators = (0..3) @@ -504,6 +579,8 @@ async fn test_cycle() { minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 })) .await; + fixture.realloc_steward_state().await; + let steward: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; let mut extra_validator_accounts = vec![]; @@ -526,14 +603,13 @@ async fn test_cycle() { }) } + _crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds to validator list for i in 0..unit_test_fixtures.validators.len() { let extra_accounts = &extra_validator_accounts[i]; _auto_add_validator(&fixture, extra_accounts).await; } - _crank_epoch_maintenance(&fixture, None).await; - _crank_compute_score( &fixture, &unit_test_fixtures, @@ -639,7 +715,7 @@ async fn test_cycle() { } #[tokio::test] -async fn test_remove_validator_next_epoch() { +async fn test_remove_validator_mid_epoch() { /* Tests that a validator removed at an arbitrary point in the cycle is not included in the current cycle's consideration, even though it is still in the validator list, and the next epoch, it is removed from the validator list. @@ -697,6 +773,7 @@ async fn test_remove_validator_next_epoch() { minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 })) .await; + fixture.realloc_steward_state().await; let mut extra_validator_accounts = vec![]; for i in 0..unit_test_fixtures.validators.len() { @@ -718,13 +795,13 @@ async fn test_remove_validator_next_epoch() { }) } + _crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds validators 2 and 3 for i in 0..3 { let extra_accounts = &extra_validator_accounts[i]; _auto_add_validator(&fixture, extra_accounts).await; } - _crank_epoch_maintenance(&fixture, None).await; _crank_compute_score( &fixture, &unit_test_fixtures, @@ -784,8 +861,8 @@ async fn test_remove_validator_next_epoch() { state.state_tag, jito_steward::StewardStateEnum::ComputeInstantUnstake )); - assert_eq!(state.validators_to_remove.count(), 1); - assert_eq!(state.validators_to_remove.get(2).unwrap(), true); + assert_eq!(state.validators_for_immediate_removal.count(), 1); + assert_eq!(state.validators_for_immediate_removal.get(2).unwrap(), true); assert_eq!(state.num_pool_validators, 3); let validator_list: ValidatorList = fixture @@ -799,7 +876,32 @@ async fn test_remove_validator_next_epoch() { assert!(validator_list.validators.len() == 3); println!("Stake Status: {:?}", validator_list.validators[2].status); - // Still need to crank? + // crank stake pool to remove validator from list + _crank_stake_pool(&fixture).await; + + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + assert!(validator_list + .validators + .iter() + .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) + .is_none()); + assert!(validator_list.validators.len() == 2); + + _instant_remove_validator(&fixture, 2).await; + let state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let state = state_account.state; + assert!(matches!( + state.state_tag, + jito_steward::StewardStateEnum::ComputeInstantUnstake + )); + assert_eq!(state.validators_to_remove.count(), 0); + assert_eq!(state.validators_for_immediate_removal.count(), 0); + assert_eq!(state.num_pool_validators, 2); + + // Compute instant unstake transitions to Rebalance _crank_compute_instant_unstake( &fixture, &unit_test_fixtures, @@ -812,7 +914,7 @@ async fn test_remove_validator_next_epoch() { &fixture, &unit_test_fixtures, &extra_validator_accounts, - &[0, 1, 2], + &[0, 1], ) .await; @@ -828,7 +930,7 @@ async fn test_remove_validator_next_epoch() { .is_none()); assert!(validator_list.validators.len() == 2); - _crank_epoch_maintenance(&fixture, Some(&[2])).await; + _crank_epoch_maintenance(&fixture, None).await; let state_account: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; let state = state_account.state; @@ -837,6 +939,7 @@ async fn test_remove_validator_next_epoch() { jito_steward::StewardStateEnum::Idle )); assert_eq!(state.validators_to_remove.count(), 0); + assert_eq!(state.validators_for_immediate_removal.count(), 0); assert_eq!(state.num_pool_validators, 2); drop(fixture); @@ -902,6 +1005,7 @@ async fn test_add_validator_next_cycle() { minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 })) .await; + fixture.realloc_steward_state().await; let mut extra_validator_accounts = vec![]; for i in 0..unit_test_fixtures.validators.len() { @@ -923,13 +1027,13 @@ async fn test_add_validator_next_cycle() { }) } + _crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds validators 2 and 3 for i in 0..2 { let extra_accounts = &extra_validator_accounts[i]; _auto_add_validator(&fixture, extra_accounts).await; } - _crank_epoch_maintenance(&fixture, None).await; _crank_compute_score( &fixture, &unit_test_fixtures, From d17617552fbfb506230771ade10a0c32c0568b43 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Thu, 19 Sep 2024 15:29:25 -0400 Subject: [PATCH 03/13] Tests for lamport balance default --- programs/steward/src/delegation.rs | 6 +- tests/tests/steward/test_algorithms.rs | 42 ++++++++++- tests/tests/steward/test_state_methods.rs | 85 ++++++++++++++++++++++- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/programs/steward/src/delegation.rs b/programs/steward/src/delegation.rs index 27fa58d4..78c02886 100644 --- a/programs/steward/src/delegation.rs +++ b/programs/steward/src/delegation.rs @@ -241,7 +241,7 @@ impl UnstakeState { }) } - fn stake_deposit_unstake( + pub fn stake_deposit_unstake( &self, state: &StewardState, index: usize, @@ -276,7 +276,7 @@ impl UnstakeState { Ok(0) } - fn instant_unstake( + pub fn instant_unstake( &self, state: &StewardState, index: usize, @@ -303,7 +303,7 @@ impl UnstakeState { Ok(0) } - fn scoring_unstake(&self, current_lamports: u64, target_lamports: u64) -> Result { + pub fn scoring_unstake(&self, current_lamports: u64, target_lamports: u64) -> Result { // If there are additional lamports to unstake on this validator and the total unstaked lamports is below the cap, destake to the target if self.scoring_unstake_total < self.scoring_unstake_cap { let lamports_above_target = current_lamports diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 824fc036..d200f33f 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -1,7 +1,7 @@ // Unit tests for scoring, instant unstake, and delegation methods use anchor_lang::AnchorSerialize; use jito_steward::{ - constants::{EPOCH_DEFAULT, SORTED_INDEX_DEFAULT}, + constants::{EPOCH_DEFAULT, LAMPORT_BALANCE_DEFAULT, SORTED_INDEX_DEFAULT}, delegation::{ decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState, }, @@ -1596,3 +1596,43 @@ fn test_decrease_stake_calculation() { _ => false, }); } + +#[test] +fn test_decrease_stake_default_lamports() { + // Given internal lamport balance set to default, test that no changes happen when doing stake deposit unstake + + let mut state = StateMachineFixtures::default().state; + + state.validator_lamport_balances[0] = LAMPORT_BALANCE_DEFAULT; + + let mut unstake_state = UnstakeState { + stake_deposit_unstake_total: 0, + stake_deposit_unstake_cap: 1000 * LAMPORTS_PER_SOL, + ..Default::default() + }; + + let test_cases = vec![ + // current_lamports, target_lamports + (1500 * LAMPORTS_PER_SOL, 1000 * LAMPORTS_PER_SOL), + (3000 * LAMPORTS_PER_SOL, 500 * LAMPORTS_PER_SOL), + ]; + + for (current_lamports, target_lamports) in test_cases { + let result = unstake_state + .stake_deposit_unstake(&state, 0, current_lamports, target_lamports) + .unwrap(); + + assert_eq!(result, 0, "Expected 0 unstake lamports, but got {}", result); + } + + // Test when stake_deposit_unstake_total reaches stake_deposit_unstake_cap + unstake_state.stake_deposit_unstake_total = unstake_state.stake_deposit_unstake_cap; + let result = unstake_state + .stake_deposit_unstake(&state, 0, 2000 * LAMPORTS_PER_SOL, 1000 * LAMPORTS_PER_SOL) + .unwrap(); + assert_eq!( + result, 0, + "Expected 0 unstake lamports when cap is reached, but got {}", + result + ); +} diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index 7b049001..8c41251b 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -8,7 +8,7 @@ use anchor_lang::{error::Error, AnchorSerialize}; use jito_steward::{ - constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, + constants::{LAMPORT_BALANCE_DEFAULT, MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, delegation::RebalanceType, errors::StewardError, Delegation, StewardStateEnum, @@ -751,3 +751,86 @@ fn test_rebalance() { } } } + +#[test] +fn test_rebalance_default_lamports() { + let fixtures = StateMachineFixtures::default(); + let mut state = fixtures.state; + let mut validator_list = fixtures.validator_list.clone(); + + // Case 1: Lamports default, has transient stake + state.validator_lamport_balances[0] = LAMPORT_BALANCE_DEFAULT; + state.state_tag = StewardStateEnum::Rebalance; + state.delegations[0..3].copy_from_slice(&[ + Delegation::new(1, 1), + Delegation::default(), + Delegation::default(), + ]); + state.scores[0..3].copy_from_slice(&[1_000_000_000, 0, 0]); + state.sorted_score_indices[0..3].copy_from_slice(&[0, 1, 2]); + + validator_list[0].transient_stake_lamports = 1000.into(); + let validator_list_bigvec = BigVec { + data: &mut validator_list.try_to_vec().unwrap(), + }; + + let res = state.rebalance( + fixtures.current_epoch, + 0, + &validator_list_bigvec, + 3000 * LAMPORTS_PER_SOL, + 0, + u64::from(validator_list[0].active_stake_lamports), + 0, + 0, + &fixtures.config.parameters, + ); + + println!("{:?}", res); + assert!(res.is_ok()); + match res.unwrap() { + RebalanceType::None => {} + _ => panic!("Expected RebalanceType::Increase"), + } + assert_eq!(state.validator_lamport_balances[0], LAMPORT_BALANCE_DEFAULT); + + // Case 2: Lamports not default, no transient stake + let mut state = fixtures.state; + state.state_tag = StewardStateEnum::Rebalance; + state.delegations[0..3].copy_from_slice(&[ + Delegation::new(1, 1), + Delegation::default(), + Delegation::default(), + ]); + state.scores[0..3].copy_from_slice(&[1_000_000_000, 0, 0]); + state.sorted_score_indices[0..3].copy_from_slice(&[0, 1, 2]); + + state.validator_lamport_balances[0] = LAMPORT_BALANCE_DEFAULT; + validator_list[0].transient_stake_lamports = 0.into(); + let validator_list_bigvec = BigVec { + data: &mut validator_list.try_to_vec().unwrap(), + }; + + let res = state.rebalance( + fixtures.current_epoch, + 0, + &validator_list_bigvec, + 4000 * LAMPORTS_PER_SOL, + 1000 * LAMPORTS_PER_SOL, + u64::from(validator_list[0].active_stake_lamports), + 0, + 0, + &fixtures.config.parameters, + ); + + assert!(res.is_ok()); + println!("{:?}", res); + if let RebalanceType::Increase(increase_amount) = res.unwrap() { + assert_eq!( + state.validator_lamport_balances[0], + 1000 * LAMPORTS_PER_SOL + increase_amount + ); + } else { + panic!("Expected RebalanceType::Increase"); + } +} From 4ba61dbfdfdf3fd93e1c6c66e899a94d970d2b37 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 24 Sep 2024 16:50:28 -0400 Subject: [PATCH 04/13] Refactor so setup is easier --- tests/src/steward_fixtures.rs | 511 ++++++++++++++- tests/tests/steward/test_cycle.rs | 591 ++---------------- .../tests/steward/test_validator_list_sync.rs | 0 3 files changed, 555 insertions(+), 547 deletions(-) create mode 100644 tests/tests/steward/test_validator_list_sync.rs diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 1e9dd97d..7d8457e5 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -22,6 +22,7 @@ use jito_steward::{ use solana_program_test::*; use solana_sdk::{ account::Account, + compute_budget::ComputeBudgetInstruction, epoch_schedule::EpochSchedule, hash::Hash, instruction::Instruction, @@ -29,7 +30,11 @@ use solana_sdk::{ rent::Rent, signature::Keypair, signer::Signer, - stake::state::{Lockup, StakeStateV2}, + stake::{ + self, + state::{Lockup, StakeStateV2}, + }, + system_program, sysvar, transaction::Transaction, }; use spl_stake_pool::{ @@ -642,6 +647,510 @@ impl TestFixture { } } +pub struct ExtraValidatorAccounts { + pub vote_account: Pubkey, + pub validator_history_address: Pubkey, + pub stake_account_address: Pubkey, + pub transient_stake_account_address: Pubkey, + pub withdraw_authority: Pubkey, +} + +pub async fn crank_stake_pool(fixture: &TestFixture) { + let stake_pool: StakePool = fixture + .load_and_deserialize(&fixture.stake_pool_meta.stake_pool) + .await; + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let (initial_ixs, final_ixs) = spl_stake_pool::instruction::update_stake_pool( + &spl_stake_pool::id(), + &stake_pool.as_ref(), + &validator_list.as_ref(), + &fixture.stake_pool_meta.stake_pool, + false, + ); + + let tx = Transaction::new_signed_with_payer( + &initial_ixs, + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(), + ); + fixture.submit_transaction_assert_success(tx).await; + + let tx = Transaction::new_signed_with_payer( + &final_ixs, + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(), + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn crank_epoch_maintenance(fixture: &TestFixture, remove_indices: Option<&[usize]>) { + let ctx = &fixture.ctx; + // Epoch Maintenence + if let Some(indices) = remove_indices { + for i in indices { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::EpochMaintenance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove: Some(*i as u64), + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } + } else { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::EpochMaintenance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove: None, + } + .data(), + }; + + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +pub async fn auto_add_validator(fixture: &TestFixture, extra_accounts: &ExtraValidatorAccounts) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::AutoAddValidator { + validator_history_account: extra_accounts.validator_history_address, + steward_state: fixture.steward_state, + config: fixture.steward_config.pubkey(), + stake_pool_program: spl_stake_pool::id(), + stake_pool: fixture.stake_pool_meta.stake_pool, + reserve_stake: fixture.stake_pool_meta.reserve, + withdraw_authority: extra_accounts.withdraw_authority, + validator_list: fixture.stake_pool_meta.validator_list, + stake_account: extra_accounts.stake_account_address, + vote_account: extra_accounts.vote_account, + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + system_program: system_program::id(), + stake_program: stake::program::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn auto_remove_validator( + fixture: &TestFixture, + extra_accounts: &ExtraValidatorAccounts, + index: u64, +) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::AutoRemoveValidator { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + stake_account: extra_accounts.stake_account_address, + withdraw_authority: extra_accounts.withdraw_authority, + validator_history_account: extra_accounts.validator_history_address, + reserve_stake: fixture.stake_pool_meta.reserve, + transient_stake_account: extra_accounts.transient_stake_account_address, + vote_account: extra_accounts.vote_account, + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + stake_program: stake::program::id(), + stake_pool_program: spl_stake_pool::id(), + system_program: system_program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoRemoveValidatorFromPool { + validator_list_index: index, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn instant_remove_validator(fixture: &TestFixture, index: usize) { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::InstantRemoveValidator { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::InstantRemoveValidator { + validator_index_to_remove: index as u64, + } + .data(), + }; + let blockhash = fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn crank_compute_score( + fixture: &TestFixture, + unit_test_fixtures: &StateMachineFixtures, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let ctx = &fixture.ctx; + + for &i in indices { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::ComputeScore { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + validator_history: extra_validator_accounts[i].validator_history_address, + cluster_history: fixture.cluster_history_account, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeScore { + validator_list_index: i as u64, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +pub async fn crank_compute_delegations(fixture: &TestFixture) { + let ctx = &fixture.ctx; + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::ComputeDelegations { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeDelegations {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn crank_idle(fixture: &TestFixture) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::Idle { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + } + .to_account_metas(None), + data: jito_steward::instruction::Idle {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn crank_compute_instant_unstake( + fixture: &TestFixture, + _unit_test_fixtures: &StateMachineFixtures, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let ctx = &fixture.ctx; + + for &i in indices { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::ComputeInstantUnstake { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_history: extra_validator_accounts[i].validator_history_address, + validator_list: fixture.stake_pool_meta.validator_list, + cluster_history: fixture.cluster_history_account, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeInstantUnstake { + validator_list_index: i as u64, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +pub async fn crank_rebalance( + fixture: &TestFixture, + _unit_test_fixtures: &StateMachineFixtures, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let ctx = &fixture.ctx; + + for &i in indices { + let extra_accounts = &extra_validator_accounts[i]; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::Rebalance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_history: extra_accounts.validator_history_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: fixture.stake_pool_meta.stake_pool, + withdraw_authority: extra_accounts.withdraw_authority, + validator_list: fixture.stake_pool_meta.validator_list, + reserve_stake: fixture.stake_pool_meta.reserve, + stake_account: extra_accounts.stake_account_address, + transient_stake_account: extra_accounts.transient_stake_account_address, + vote_account: extra_accounts.vote_account, + system_program: system_program::id(), + stake_program: stake::program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + } + .to_account_metas(None), + data: jito_steward::instruction::Rebalance { + validator_list_index: i as u64, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; + } +} + +pub async fn copy_vote_account( + fixture: &TestFixture, + extra_validator_accounts: &Vec, + index: usize, +) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account: extra_validator_accounts[index].validator_history_address, + vote_account: extra_validator_accounts[index].vote_account, + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::CopyVoteAccount {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn update_stake_history( + fixture: &TestFixture, + extra_validator_accounts: &Vec, + index: usize, + epoch: u64, + lamports: u64, + rank: u32, + is_superminority: bool, +) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::UpdateStakeHistory { + validator_history_account: extra_validator_accounts[index].validator_history_address, + vote_account: extra_validator_accounts[index].vote_account, + config: fixture.validator_history_config, + oracle_authority: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::UpdateStakeHistory { + epoch, + is_superminority, + lamports, + rank, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn copy_cluster_info(fixture: &TestFixture) { + let ctx = &fixture.ctx; + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::CopyClusterInfo { + cluster_history_account: fixture.cluster_history_account, + slot_history: sysvar::slot_history::id(), + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::CopyClusterInfo {}.data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::request_heap_frame(1024 * 256), + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ix, + ], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + +pub async fn crank_validator_history_accounts( + fixture: &TestFixture, + extra_validator_accounts: &Vec, + indices: &[usize], +) { + let clock: Clock = fixture + .ctx + .borrow_mut() + .banks_client + .get_sysvar() + .await + .unwrap(); + for &i in indices { + fixture + .ctx + .borrow_mut() + .increment_vote_account_credits(&extra_validator_accounts[i].vote_account, 1000); + copy_vote_account(&fixture, &extra_validator_accounts, i).await; + // only field that's relevant to score is is_superminority + update_stake_history( + &fixture, + &extra_validator_accounts, + i, + clock.epoch, + 1_000_000, + 1_000, + false, + ) + .await; + } + copy_cluster_info(&fixture).await; +} + pub struct ValidatorEntry { pub vote_address: Pubkey, pub vote_account: VoteStateVersions, diff --git a/tests/tests/steward/test_cycle.rs b/tests/tests/steward/test_cycle.rs index f7b5fcc2..1bde50fc 100644 --- a/tests/tests/steward/test_cycle.rs +++ b/tests/tests/steward/test_cycle.rs @@ -14,514 +14,13 @@ use solana_sdk::{ signature::Keypair, signer::Signer, system_program, transaction::Transaction, }; use tests::steward_fixtures::{ + auto_add_validator, crank_compute_delegations, crank_compute_instant_unstake, + crank_compute_score, crank_epoch_maintenance, crank_idle, crank_rebalance, crank_stake_pool, + crank_validator_history_accounts, instant_remove_validator, ExtraValidatorAccounts, FixtureDefaultAccounts, StateMachineFixtures, TestFixture, ValidatorEntry, }; use validator_history::ValidatorHistory; -pub struct ExtraValidatorAccounts { - vote_account: Pubkey, - validator_history_address: Pubkey, - stake_account_address: Pubkey, - transient_stake_account_address: Pubkey, - withdraw_authority: Pubkey, -} - -async fn _crank_stake_pool(fixture: &TestFixture) { - let stake_pool: StakePool = fixture - .load_and_deserialize(&fixture.stake_pool_meta.stake_pool) - .await; - let validator_list: ValidatorList = fixture - .load_and_deserialize(&fixture.stake_pool_meta.validator_list) - .await; - let (initial_ixs, final_ixs) = spl_stake_pool::instruction::update_stake_pool( - &spl_stake_pool::id(), - &stake_pool.as_ref(), - &validator_list.as_ref(), - &fixture.stake_pool_meta.stake_pool, - false, - ); - - let tx = Transaction::new_signed_with_payer( - &initial_ixs, - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - fixture - .ctx - .borrow_mut() - .get_new_latest_blockhash() - .await - .unwrap(), - ); - fixture.submit_transaction_assert_success(tx).await; - - let tx = Transaction::new_signed_with_payer( - &final_ixs, - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - fixture - .ctx - .borrow_mut() - .get_new_latest_blockhash() - .await - .unwrap(), - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _crank_epoch_maintenance(fixture: &TestFixture, remove_indices: Option<&[usize]>) { - let ctx = &fixture.ctx; - // Epoch Maintenence - if let Some(indices) = remove_indices { - for i in indices { - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::EpochMaintenance { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - stake_pool: fixture.stake_pool_meta.stake_pool, - } - .to_account_metas(None), - data: jito_steward::instruction::EpochMaintenance { - validator_index_to_remove: Some(*i as u64), - } - .data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; - } - } else { - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::EpochMaintenance { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - stake_pool: fixture.stake_pool_meta.stake_pool, - } - .to_account_metas(None), - data: jito_steward::instruction::EpochMaintenance { - validator_index_to_remove: None, - } - .data(), - }; - - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; - } -} - -async fn _auto_add_validator(fixture: &TestFixture, extra_accounts: &ExtraValidatorAccounts) { - let ctx = &fixture.ctx; - - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::AutoAddValidator { - validator_history_account: extra_accounts.validator_history_address, - steward_state: fixture.steward_state, - config: fixture.steward_config.pubkey(), - stake_pool_program: spl_stake_pool::id(), - stake_pool: fixture.stake_pool_meta.stake_pool, - reserve_stake: fixture.stake_pool_meta.reserve, - withdraw_authority: extra_accounts.withdraw_authority, - validator_list: fixture.stake_pool_meta.validator_list, - stake_account: extra_accounts.stake_account_address, - vote_account: extra_accounts.vote_account, - rent: solana_sdk::sysvar::rent::id(), - clock: solana_sdk::sysvar::clock::id(), - stake_history: solana_sdk::sysvar::stake_history::id(), - stake_config: stake::config::ID, - system_program: system_program::id(), - stake_program: stake::program::id(), - } - .to_account_metas(None), - data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _auto_remove_validator( - fixture: &TestFixture, - extra_accounts: &ExtraValidatorAccounts, - index: u64, -) { - let ctx = &fixture.ctx; - - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::AutoRemoveValidator { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - stake_pool: fixture.stake_pool_meta.stake_pool, - stake_account: extra_accounts.stake_account_address, - withdraw_authority: extra_accounts.withdraw_authority, - validator_history_account: extra_accounts.validator_history_address, - reserve_stake: fixture.stake_pool_meta.reserve, - transient_stake_account: extra_accounts.transient_stake_account_address, - vote_account: extra_accounts.vote_account, - stake_history: solana_sdk::sysvar::stake_history::id(), - stake_config: stake::config::ID, - stake_program: stake::program::id(), - stake_pool_program: spl_stake_pool::id(), - system_program: system_program::id(), - rent: solana_sdk::sysvar::rent::id(), - clock: solana_sdk::sysvar::clock::id(), - } - .to_account_metas(None), - data: jito_steward::instruction::AutoRemoveValidatorFromPool { - validator_list_index: index, - } - .data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _instant_remove_validator(fixture: &TestFixture, index: usize) { - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::InstantRemoveValidator { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - stake_pool: fixture.stake_pool_meta.stake_pool, - } - .to_account_metas(None), - data: jito_steward::instruction::InstantRemoveValidator { - validator_index_to_remove: index as u64, - } - .data(), - }; - let blockhash = fixture - .ctx - .borrow_mut() - .get_new_latest_blockhash() - .await - .unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _crank_compute_score( - fixture: &TestFixture, - unit_test_fixtures: &StateMachineFixtures, - extra_validator_accounts: &Vec, - indices: &[usize], -) { - let ctx = &fixture.ctx; - - for &i in indices { - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::ComputeScore { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - validator_history: extra_validator_accounts[i].validator_history_address, - cluster_history: fixture.cluster_history_account, - } - .to_account_metas(None), - data: jito_steward::instruction::ComputeScore { - validator_list_index: i as u64, - } - .data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; - } -} - -async fn _crank_compute_delegations(fixture: &TestFixture) { - let ctx = &fixture.ctx; - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::ComputeDelegations { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - } - .to_account_metas(None), - data: jito_steward::instruction::ComputeDelegations {}.data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _crank_idle(fixture: &TestFixture) { - let ctx = &fixture.ctx; - - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::Idle { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_list: fixture.stake_pool_meta.validator_list, - } - .to_account_metas(None), - data: jito_steward::instruction::Idle {}.data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _crank_compute_instant_unstake( - fixture: &TestFixture, - _unit_test_fixtures: &StateMachineFixtures, - extra_validator_accounts: &Vec, - indices: &[usize], -) { - let ctx = &fixture.ctx; - - for &i in indices { - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::ComputeInstantUnstake { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_history: extra_validator_accounts[i].validator_history_address, - validator_list: fixture.stake_pool_meta.validator_list, - cluster_history: fixture.cluster_history_account, - } - .to_account_metas(None), - data: jito_steward::instruction::ComputeInstantUnstake { - validator_list_index: i as u64, - } - .data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; - } -} - -async fn _crank_rebalance( - fixture: &TestFixture, - _unit_test_fixtures: &StateMachineFixtures, - extra_validator_accounts: &Vec, - indices: &[usize], -) { - let ctx = &fixture.ctx; - - for &i in indices { - let extra_accounts = &extra_validator_accounts[i]; - - let ix = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::Rebalance { - config: fixture.steward_config.pubkey(), - state_account: fixture.steward_state, - validator_history: extra_accounts.validator_history_address, - stake_pool_program: spl_stake_pool::id(), - stake_pool: fixture.stake_pool_meta.stake_pool, - withdraw_authority: extra_accounts.withdraw_authority, - validator_list: fixture.stake_pool_meta.validator_list, - reserve_stake: fixture.stake_pool_meta.reserve, - stake_account: extra_accounts.stake_account_address, - transient_stake_account: extra_accounts.transient_stake_account_address, - vote_account: extra_accounts.vote_account, - system_program: system_program::id(), - stake_program: stake::program::id(), - rent: solana_sdk::sysvar::rent::id(), - clock: solana_sdk::sysvar::clock::id(), - stake_history: solana_sdk::sysvar::stake_history::id(), - stake_config: stake::config::ID, - } - .to_account_metas(None), - data: jito_steward::instruction::Rebalance { - validator_list_index: i as u64, - } - .data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; - } -} - -async fn _copy_vote_account( - fixture: &TestFixture, - extra_validator_accounts: &Vec, - index: usize, -) { - let ctx = &fixture.ctx; - - let ix = Instruction { - program_id: validator_history::id(), - accounts: validator_history::accounts::CopyVoteAccount { - validator_history_account: extra_validator_accounts[index].validator_history_address, - vote_account: extra_validator_accounts[index].vote_account, - signer: fixture.keypair.pubkey(), - } - .to_account_metas(None), - data: validator_history::instruction::CopyVoteAccount {}.data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _update_stake_history( - fixture: &TestFixture, - extra_validator_accounts: &Vec, - index: usize, - epoch: u64, - lamports: u64, - rank: u32, - is_superminority: bool, -) { - let ctx = &fixture.ctx; - - let ix = Instruction { - program_id: validator_history::id(), - accounts: validator_history::accounts::UpdateStakeHistory { - validator_history_account: extra_validator_accounts[index].validator_history_address, - vote_account: extra_validator_accounts[index].vote_account, - config: fixture.validator_history_config, - oracle_authority: fixture.keypair.pubkey(), - } - .to_account_metas(None), - data: validator_history::instruction::UpdateStakeHistory { - epoch, - is_superminority, - lamports, - rank, - } - .data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _copy_cluster_info(fixture: &TestFixture) { - let ctx = &fixture.ctx; - - let ix = Instruction { - program_id: validator_history::id(), - accounts: validator_history::accounts::CopyClusterInfo { - cluster_history_account: fixture.cluster_history_account, - slot_history: sysvar::slot_history::id(), - signer: fixture.keypair.pubkey(), - } - .to_account_metas(None), - data: validator_history::instruction::CopyClusterInfo {}.data(), - }; - let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &[ - ComputeBudgetInstruction::request_heap_frame(1024 * 256), - ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), - ix, - ], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - blockhash, - ); - fixture.submit_transaction_assert_success(tx).await; -} - -async fn _crank_validator_history_accounts( - fixture: &TestFixture, - extra_validator_accounts: &Vec, - indices: &[usize], -) { - let clock: Clock = fixture - .ctx - .borrow_mut() - .banks_client - .get_sysvar() - .await - .unwrap(); - for &i in indices { - fixture - .ctx - .borrow_mut() - .increment_vote_account_credits(&extra_validator_accounts[i].vote_account, 1000); - _copy_vote_account(&fixture, &extra_validator_accounts, i).await; - // only field that's relevant to score is is_superminority - _update_stake_history( - &fixture, - &extra_validator_accounts, - i, - clock.epoch, - 1_000_000, - 1_000, - false, - ) - .await; - } - _copy_cluster_info(&fixture).await; -} - #[tokio::test] async fn test_cycle() { let mut fixture_accounts = FixtureDefaultAccounts::default(); @@ -603,14 +102,14 @@ async fn test_cycle() { }) } - _crank_epoch_maintenance(&fixture, None).await; + crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds to validator list for i in 0..unit_test_fixtures.validators.len() { let extra_accounts = &extra_validator_accounts[i]; - _auto_add_validator(&fixture, extra_accounts).await; + auto_add_validator(&fixture, extra_accounts).await; } - _crank_compute_score( + crank_compute_score( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -618,14 +117,14 @@ async fn test_cycle() { ) .await; - _crank_compute_delegations(&fixture).await; + crank_compute_delegations(&fixture).await; let epoch_schedule: EpochSchedule = ctx.borrow_mut().banks_client.get_sysvar().await.unwrap(); let clock: Clock = ctx.borrow_mut().banks_client.get_sysvar().await.unwrap(); - _crank_idle(&fixture).await; + crank_idle(&fixture).await; - _crank_compute_instant_unstake( + crank_compute_instant_unstake( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -633,7 +132,7 @@ async fn test_cycle() { ) .await; - _crank_rebalance( + crank_rebalance( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -643,11 +142,11 @@ async fn test_cycle() { fixture.advance_num_epochs(1, 10).await; - _crank_stake_pool(&fixture).await; + crank_stake_pool(&fixture).await; - _crank_epoch_maintenance(&fixture, None).await; + crank_epoch_maintenance(&fixture, None).await; - _crank_idle(&fixture).await; + crank_idle(&fixture).await; // Advance to instant_unstake_inputs_epoch_progress fixture @@ -655,9 +154,9 @@ async fn test_cycle() { .await; // Update validator history values - _crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; + crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; - _crank_compute_instant_unstake( + crank_compute_instant_unstake( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -665,7 +164,7 @@ async fn test_cycle() { ) .await; - _crank_rebalance( + crank_rebalance( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -675,15 +174,15 @@ async fn test_cycle() { fixture.advance_num_epochs(1, 10).await; - _crank_stake_pool(&fixture).await; + crank_stake_pool(&fixture).await; - _crank_epoch_maintenance(&fixture, None).await; + crank_epoch_maintenance(&fixture, None).await; // Update validator history values - _crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; + crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; // In new cycle - _crank_compute_score( + crank_compute_score( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -795,14 +294,14 @@ async fn test_remove_validator_mid_epoch() { }) } - _crank_epoch_maintenance(&fixture, None).await; + crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds validators 2 and 3 for i in 0..3 { let extra_accounts = &extra_validator_accounts[i]; - _auto_add_validator(&fixture, extra_accounts).await; + auto_add_validator(&fixture, extra_accounts).await; } - _crank_compute_score( + crank_compute_score( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -810,11 +309,11 @@ async fn test_remove_validator_mid_epoch() { ) .await; - _crank_compute_delegations(&fixture).await; + crank_compute_delegations(&fixture).await; - _crank_idle(&fixture).await; + crank_idle(&fixture).await; - _crank_compute_instant_unstake( + crank_compute_instant_unstake( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -877,7 +376,7 @@ async fn test_remove_validator_mid_epoch() { println!("Stake Status: {:?}", validator_list.validators[2].status); // crank stake pool to remove validator from list - _crank_stake_pool(&fixture).await; + crank_stake_pool(&fixture).await; let validator_list: ValidatorList = fixture .load_and_deserialize(&fixture.stake_pool_meta.validator_list) @@ -889,7 +388,7 @@ async fn test_remove_validator_mid_epoch() { .is_none()); assert!(validator_list.validators.len() == 2); - _instant_remove_validator(&fixture, 2).await; + instant_remove_validator(&fixture, 2).await; let state_account: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; let state = state_account.state; @@ -902,7 +401,7 @@ async fn test_remove_validator_mid_epoch() { assert_eq!(state.num_pool_validators, 2); // Compute instant unstake transitions to Rebalance - _crank_compute_instant_unstake( + crank_compute_instant_unstake( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -910,7 +409,7 @@ async fn test_remove_validator_mid_epoch() { ) .await; - _crank_rebalance( + crank_rebalance( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -919,7 +418,7 @@ async fn test_remove_validator_mid_epoch() { .await; fixture.advance_num_epochs(1, 10).await; - _crank_stake_pool(&fixture).await; + crank_stake_pool(&fixture).await; let validator_list: ValidatorList = fixture .load_and_deserialize(&fixture.stake_pool_meta.validator_list) .await; @@ -930,7 +429,7 @@ async fn test_remove_validator_mid_epoch() { .is_none()); assert!(validator_list.validators.len() == 2); - _crank_epoch_maintenance(&fixture, None).await; + crank_epoch_maintenance(&fixture, None).await; let state_account: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; let state = state_account.state; @@ -1027,14 +526,14 @@ async fn test_add_validator_next_cycle() { }) } - _crank_epoch_maintenance(&fixture, None).await; + crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds validators 2 and 3 for i in 0..2 { let extra_accounts = &extra_validator_accounts[i]; - _auto_add_validator(&fixture, extra_accounts).await; + auto_add_validator(&fixture, extra_accounts).await; } - _crank_compute_score( + crank_compute_score( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -1043,7 +542,7 @@ async fn test_add_validator_next_cycle() { .await; // Add in validator 2 at random time - _auto_add_validator(&fixture, &extra_validator_accounts[2]).await; + auto_add_validator(&fixture, &extra_validator_accounts[2]).await; let validator_list: ValidatorList = fixture .load_and_deserialize(&fixture.stake_pool_meta.validator_list) @@ -1067,16 +566,16 @@ async fn test_add_validator_next_cycle() { assert_eq!(state.validators_added, 1); assert_eq!(state.num_pool_validators, 2); - _crank_compute_delegations(&fixture).await; - _crank_idle(&fixture).await; - _crank_compute_instant_unstake( + crank_compute_delegations(&fixture).await; + crank_idle(&fixture).await; + crank_compute_instant_unstake( &fixture, &unit_test_fixtures, &extra_validator_accounts, &[0, 1], ) .await; - _crank_rebalance( + crank_rebalance( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -1086,8 +585,8 @@ async fn test_add_validator_next_cycle() { fixture.advance_num_epochs(1, 10).await; - _crank_stake_pool(&fixture).await; - _crank_epoch_maintenance(&fixture, None).await; + crank_stake_pool(&fixture).await; + crank_epoch_maintenance(&fixture, None).await; let state_account: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; @@ -1100,10 +599,10 @@ async fn test_add_validator_next_cycle() { assert_eq!(state.validators_added, 1); assert_eq!(state.num_pool_validators, 2); - _crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; + crank_validator_history_accounts(&fixture, &extra_validator_accounts, &[0, 1, 2]).await; // Ensure we're in the next cycle - _crank_compute_score( + crank_compute_score( &fixture, &unit_test_fixtures, &extra_validator_accounts, @@ -1126,7 +625,7 @@ async fn test_add_validator_next_cycle() { assert_eq!(state.num_pool_validators, 3); // Ensure we can crank the new validator - _crank_compute_score( + crank_compute_score( &fixture, &unit_test_fixtures, &extra_validator_accounts, diff --git a/tests/tests/steward/test_validator_list_sync.rs b/tests/tests/steward/test_validator_list_sync.rs new file mode 100644 index 00000000..e69de29b From 33322bb62b19d368f61daecee197de7c2148180c Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Mon, 30 Sep 2024 15:35:59 -0400 Subject: [PATCH 05/13] Final tests --- .../auto_remove_validator_from_pool.rs | 2 + .../src/instructions/epoch_maintenance.rs | 2 +- .../instructions/instant_remove_validator.rs | 2 +- programs/steward/src/state/steward_state.rs | 21 +- tests/src/steward_fixtures.rs | 38 +- tests/tests/steward/mod.rs | 1 + tests/tests/steward/test_cycle.rs | 13 +- tests/tests/steward/test_epoch_maintenance.rs | 227 +++++++++ tests/tests/steward/test_state_methods.rs | 107 +++++ tests/tests/steward/test_steward.rs | 447 +++++++++++++++++- .../tests/steward/test_validator_list_sync.rs | 0 11 files changed, 838 insertions(+), 22 deletions(-) create mode 100644 tests/tests/steward/test_epoch_maintenance.rs delete mode 100644 tests/tests/steward/test_validator_list_sync.rs diff --git a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs index d56d9167..c3ff1fab 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -243,12 +243,14 @@ pub fn handler(ctx: Context, validator_list_index: usize) - } } StakeStatus::ReadyForRemoval => { + // Should never happen but this is logical action marked_for_immediate_removal = true; state_account .state .mark_validator_for_immediate_removal(validator_list_index)?; } StakeStatus::DeactivatingAll | StakeStatus::DeactivatingTransient => { + // DeactivatingTransient should not be possible but this is the logical action marked_for_immediate_removal = false; state_account .state diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 893be118..2b3cafe7 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -85,7 +85,7 @@ pub fn handler( if let Some(validator_index_to_remove) = validator_index_to_remove { state_account .state - .remove_validator(validator_index_to_remove, validators_in_list)?; + .remove_validator(validator_index_to_remove)?; } } diff --git a/programs/steward/src/instructions/instant_remove_validator.rs b/programs/steward/src/instructions/instant_remove_validator.rs index 5daadf4f..bc4dc1a0 100644 --- a/programs/steward/src/instructions/instant_remove_validator.rs +++ b/programs/steward/src/instructions/instant_remove_validator.rs @@ -88,7 +88,7 @@ pub fn handler( state_account .state - .remove_validator(validator_index_to_remove, validators_in_list)?; + .remove_validator(validator_index_to_remove)?; Ok(()) } diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 88da95f7..d6193532 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -460,7 +460,7 @@ impl StewardState { } /// Update internal state when a validator is removed from the pool - pub fn remove_validator(&mut self, index: usize, validator_list_len: usize) -> Result<()> { + pub fn remove_validator(&mut self, index: usize) -> Result<()> { let marked_for_regular_removal = self.validators_to_remove.get(index)?; let marked_for_immediate_removal = self.validators_for_immediate_removal.get(index)?; @@ -469,8 +469,16 @@ impl StewardState { StewardError::ValidatorNotMarkedForRemoval ); + let num_pool_validators = self.num_pool_validators as usize; + let num_pool_validators_plus_added = num_pool_validators + self.validators_added as usize; + + require!( + index < num_pool_validators_plus_added, + StewardError::ValidatorIndexOutOfBounds + ); + // If the validator was marked for removal in the current cycle, decrement validators_added - if index >= self.num_pool_validators as usize { + if index >= num_pool_validators { self.validators_added = self .validators_added .checked_sub(1) @@ -482,8 +490,6 @@ impl StewardState { .ok_or(StewardError::ArithmeticError)?; } - let num_pool_validators = self.num_pool_validators as usize; - // Shift all validator state to the left for i in index..num_pool_validators { let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; @@ -497,7 +503,7 @@ impl StewardState { } // For state that can be valid past num_pool_validators, we still need to shift the values - for i in index..validator_list_len { + for i in index..num_pool_validators_plus_added { let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; self.validators_to_remove .set(i, self.validators_to_remove.get(next_i)?)?; @@ -551,9 +557,10 @@ impl StewardState { self.delegations[num_pool_validators] = Delegation::default(); self.instant_unstake.set(num_pool_validators, false)?; self.progress.set(num_pool_validators, false)?; - self.validators_to_remove.set(validator_list_len, false)?; + self.validators_to_remove + .set(num_pool_validators_plus_added, false)?; self.validators_for_immediate_removal - .set(validator_list_len, false)?; + .set(num_pool_validators_plus_added, false)?; Ok(()) } diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 7d8457e5..1c091d1a 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -862,9 +862,45 @@ pub async fn instant_remove_validator(fixture: &TestFixture, index: usize) { fixture.submit_transaction_assert_success(tx).await; } +pub async fn manual_remove_validator( + fixture: &TestFixture, + index: usize, + mark_for_removal: bool, + immediate: bool, +) { + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::AdminMarkForRemoval { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + authority: fixture.keypair.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::AdminMarkForRemoval { + validator_list_index: index as u64, + mark_for_removal: if mark_for_removal { 1 } else { 0 }, + immediate: if immediate { 1 } else { 0 }, + } + .data(), + }; + let blockhash = fixture + .ctx + .borrow_mut() + .get_new_latest_blockhash() + .await + .unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(tx).await; +} + pub async fn crank_compute_score( fixture: &TestFixture, - unit_test_fixtures: &StateMachineFixtures, + _unit_test_fixtures: &StateMachineFixtures, extra_validator_accounts: &Vec, indices: &[usize], ) { diff --git a/tests/tests/steward/mod.rs b/tests/tests/steward/mod.rs index d2d23974..e66b30f6 100644 --- a/tests/tests/steward/mod.rs +++ b/tests/tests/steward/mod.rs @@ -7,3 +7,4 @@ mod test_spl_passthrough; mod test_state_methods; mod test_state_transitions; mod test_steward; +mod test_epoch_maintenance; diff --git a/tests/tests/steward/test_cycle.rs b/tests/tests/steward/test_cycle.rs index 1bde50fc..6b41a5f4 100644 --- a/tests/tests/steward/test_cycle.rs +++ b/tests/tests/steward/test_cycle.rs @@ -1,17 +1,14 @@ use std::collections::HashMap; use anchor_lang::{ - solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}, + solana_program::{instruction::Instruction, pubkey::Pubkey, stake}, InstructionData, ToAccountMetas, }; -use jito_steward::{ - utils::{StakePool, ValidatorList}, - StewardStateAccount, UpdateParametersArgs, -}; +use jito_steward::{utils::ValidatorList, StewardStateAccount, UpdateParametersArgs}; use solana_program_test::*; use solana_sdk::{ - clock::Clock, compute_budget::ComputeBudgetInstruction, epoch_schedule::EpochSchedule, - signature::Keypair, signer::Signer, system_program, transaction::Transaction, + clock::Clock, epoch_schedule::EpochSchedule, signature::Keypair, signer::Signer, + system_program, transaction::Transaction, }; use tests::steward_fixtures::{ auto_add_validator, crank_compute_delegations, crank_compute_instant_unstake, @@ -80,7 +77,7 @@ async fn test_cycle() { .await; fixture.realloc_steward_state().await; - let steward: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; + let _steward: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; let mut extra_validator_accounts = vec![]; for i in 0..unit_test_fixtures.validators.len() { diff --git a/tests/tests/steward/test_epoch_maintenance.rs b/tests/tests/steward/test_epoch_maintenance.rs new file mode 100644 index 00000000..522ce6ac --- /dev/null +++ b/tests/tests/steward/test_epoch_maintenance.rs @@ -0,0 +1,227 @@ +use std::collections::HashMap; + +use anchor_lang::{ + solana_program::{instruction::Instruction, pubkey::Pubkey}, + InstructionData, ToAccountMetas, +}; +use jito_steward::{ + utils::ValidatorList, StewardStateAccount, UpdateParametersArgs, EPOCH_MAINTENANCE, +}; +use solana_program_test::*; +use solana_sdk::{clock::Clock, signature::Keypair, signer::Signer, transaction::Transaction}; +use spl_stake_pool::state::{StakeStatus, ValidatorList as SPLValidatorList}; +use tests::steward_fixtures::{ + auto_add_validator, crank_epoch_maintenance, crank_stake_pool, manual_remove_validator, + serialized_validator_list_account, ExtraValidatorAccounts, FixtureDefaultAccounts, + StateMachineFixtures, TestFixture, ValidatorEntry, +}; +use validator_history::ValidatorHistory; + +async fn _epoch_maintenance_tx( + fixture: &TestFixture, + validator_index_to_remove: Option, +) -> Transaction { + let ctx = &fixture.ctx; + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::EpochMaintenance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove, + } + .data(), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ) +} + +async fn _epoch_maintenance_setup() -> ( + Box, + Box, + Vec, +) { + // Setup pool and steward + let mut fixture_accounts = FixtureDefaultAccounts::default(); + + let unit_test_fixtures = Box::new(StateMachineFixtures::default()); + + // Note that these parameters are overriden in initialize_steward, just included here for completeness + fixture_accounts.steward_config.parameters = unit_test_fixtures.config.parameters; + + fixture_accounts.validators = (0..3) + .map(|i| ValidatorEntry { + validator_history: unit_test_fixtures.validators[i], + vote_account: unit_test_fixtures.vote_accounts[i].clone(), + vote_address: unit_test_fixtures.validators[i].vote_account, + }) + .collect(); + fixture_accounts.cluster_history = unit_test_fixtures.cluster_history; + + // Modify validator history account with desired values + + let mut fixture = TestFixture::new_from_accounts(fixture_accounts, HashMap::new()).await; + + fixture.steward_config = Keypair::new(); + fixture.steward_state = Pubkey::find_program_address( + &[ + StewardStateAccount::SEED, + fixture.steward_config.pubkey().as_ref(), + ], + &jito_steward::id(), + ) + .0; + + fixture.advance_num_epochs(20, 10).await; + fixture.initialize_stake_pool().await; + fixture + .initialize_steward(Some(UpdateParametersArgs { + mev_commission_range: Some(10), // Set to pass validation, where epochs starts at 0 + epoch_credits_range: Some(20), // Set to pass validation, where epochs starts at 0 + commission_range: Some(20), // Set to pass validation, where epochs starts at 0 + scoring_delinquency_threshold_ratio: Some(0.85), + instant_unstake_delinquency_threshold_ratio: Some(0.70), + mev_commission_bps_threshold: Some(1000), + commission_threshold: Some(5), + historical_commission_threshold: Some(50), + num_delegation_validators: Some(200), + scoring_unstake_cap_bps: Some(750), + instant_unstake_cap_bps: Some(10), + stake_deposit_unstake_cap_bps: Some(10), + instant_unstake_epoch_progress: Some(0.00), + compute_score_slot_range: Some(1000), + instant_unstake_inputs_epoch_progress: Some(0.50), + num_epochs_between_scoring: Some(2), // 2 epoch cycle + minimum_stake_lamports: Some(5_000_000_000), + minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 + })) + .await; + fixture.realloc_steward_state().await; + + let mut extra_validator_accounts = vec![]; + for i in 0..unit_test_fixtures.validators.len() { + let vote_account = unit_test_fixtures.validator_list[i].vote_account_address; + let (validator_history_address, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, vote_account.as_ref()], + &validator_history::id(), + ); + + let (stake_account_address, transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + extra_validator_accounts.push(ExtraValidatorAccounts { + vote_account, + validator_history_address, + stake_account_address, + transient_stake_account_address, + withdraw_authority, + }) + } + + crank_epoch_maintenance(&fixture, None).await; + // Auto add validator - adds to validator list + for i in 0..unit_test_fixtures.validators.len() { + let extra_accounts = &extra_validator_accounts[i]; + auto_add_validator(&fixture, extra_accounts).await; + } + + fixture.advance_num_epochs(1, 10).await; + + crank_stake_pool(&fixture).await; + + ( + Box::new(fixture), + unit_test_fixtures, + extra_validator_accounts, + ) +} + +#[tokio::test] +async fn test_epoch_maintenance_fails_status_check() { + // Setup pool and steward + let (fixture, _unit_test_fixtures, _extra_validator_accounts) = _epoch_maintenance_setup().await; + + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let mut spl_validator_list: SPLValidatorList = validator_list.as_ref().clone(); + + // Force validator list into deactivating state (overriding account) + spl_validator_list.validators[0].status = StakeStatus::ReadyForRemoval.into(); + fixture.ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account(spl_validator_list, None).into(), + ); + + // Tests fails state invariant checks + + let tx = _epoch_maintenance_tx(&fixture, Some(0)).await; + fixture + .submit_transaction_assert_error(tx, "ValidatorsHaveNotBeenRemoved") + .await; +} + +#[tokio::test] +async fn test_epoch_maintenance_fails_invariant_check() { + // Setup pool and steward + let (fixture, _unit_test_fixtures, _extra_validator_accounts) = _epoch_maintenance_setup().await; + + // Mark validator to remove without actually removing it from list + manual_remove_validator(&fixture, 0, true, false).await; + + // Try to remove validator 0 but it's not removed from spl ValidatorList + let tx = _epoch_maintenance_tx(&fixture, Some(0)).await; + fixture + .submit_transaction_assert_error(tx, "ListStateMismatch") + .await; +} + +#[tokio::test] +async fn test_epoch_maintenance_removes_validators() { + // Setup pool and steward + let (fixture, _unit_test_fixtures, _extra_validator_accounts) = _epoch_maintenance_setup().await; + + // Mark validator to remove with admin fn (delayed removal) + manual_remove_validator(&fixture, 0, true, false).await; + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + // Override validator list to actually remove validator + let mut spl_validator_list: SPLValidatorList = validator_list.as_ref().clone(); + spl_validator_list.validators.remove(0); + fixture.ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account(spl_validator_list, None).into(), + ); + + // Test removes validator_to_remove + crank_epoch_maintenance(&fixture, Some(&[0])).await; + + // Checks new validators_added and epoch maintenance state updated + let clock: Clock = fixture + .ctx + .borrow_mut() + .banks_client + .get_sysvar() + .await + .unwrap(); + let state_account: Box = + Box::new(fixture.load_and_deserialize(&fixture.steward_state).await); + let state = &state_account.state; + assert_eq!(state.validators_added, 2); + assert_eq!(state.current_epoch, clock.epoch); + assert_eq!(state.validators_to_remove.is_empty(), true); + assert_eq!(state.validators_for_immediate_removal.is_empty(), true); + assert!(state.has_flag(EPOCH_MAINTENANCE)); + + println!("Woo"); +} diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index 8c41251b..7ce0198c 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -834,3 +834,110 @@ fn test_rebalance_default_lamports() { panic!("Expected RebalanceType::Increase"); } } + +// TODO +#[test] +fn test_remove_validator() { + // Setup: create steward state based off StewardStateFixtures + // mark index 1 to removal + let fixtures = StateMachineFixtures::default(); + let mut state = fixtures.state; + // Set values for all of the values that are gonna get shifted + state.validator_lamport_balances[0..3].copy_from_slice(&[0, 1, 2]); + state.scores[0..3].copy_from_slice(&[0, 1, 2]); + state.yield_scores[0..3].copy_from_slice(&[0, 1, 2]); + state.delegations[0..3].copy_from_slice(&[ + Delegation::new(0, 1), + Delegation::new(1, 1), + Delegation::new(2, 1), + ]); + state.instant_unstake.reset(); + state.instant_unstake.set(0, true).unwrap(); + state.instant_unstake.set(1, false).unwrap(); + state.instant_unstake.set(2, true).unwrap(); + + // test basic case - remove validator_to_remove + state.validators_to_remove.set(1, true).unwrap(); + state.remove_validator(1); + assert_eq!(state.num_pool_validators, 2); + // Assert that values were shifted left + assert_eq!(state.yield_scores[1], 2); + assert_eq!(state.scores[1], 2); + assert!(state.delegations[1] == Delegation::new(2, 1)); + + // test basic case - remove immediate_removal validator + let mut state = fixtures.state; + // Set values for all of the values that are gonna get shifted + state.validator_lamport_balances[0..3].copy_from_slice(&[0, 1, 2]); + state.scores[0..3].copy_from_slice(&[0, 1, 2]); + state.yield_scores[0..3].copy_from_slice(&[0, 1, 2]); + state.delegations[0..3].copy_from_slice(&[ + Delegation::new(0, 1), + Delegation::new(1, 1), + Delegation::new(2, 1), + ]); + state.instant_unstake.reset(); + state.instant_unstake.set(0, true).unwrap(); + state.instant_unstake.set(1, false).unwrap(); + state.instant_unstake.set(2, true).unwrap(); + + state.validators_for_immediate_removal.set(1, true).unwrap(); + let res = state.remove_validator(1); + assert!(res.is_ok()); + assert_eq!(state.num_pool_validators, 2); + // Assert that values were shifted left + assert_eq!(state.yield_scores[1], 2); + assert_eq!(state.scores[1], 2); + assert!(state.delegations[1] == Delegation::new(2, 1)); + + // Setup: mark an index for removal that's higher than num_pool_validators + // Remember this is always gonna be run after actual removals have taken place, so could validator_list_len be kind of a red herring? do we need to go further? + + let mut state = fixtures.state; + // Set values for all of the values that are gonna get shifted + state.validator_lamport_balances[0..3].copy_from_slice(&[0, 1, 2]); + state.scores[0..3].copy_from_slice(&[0, 1, 2]); + state.yield_scores[0..3].copy_from_slice(&[0, 1, 2]); + state.delegations[0..3].copy_from_slice(&[ + Delegation::new(0, 1), + Delegation::new(1, 1), + Delegation::new(2, 1), + ]); + state.instant_unstake.reset(); + state.instant_unstake.set(0, true).unwrap(); + state.instant_unstake.set(1, false).unwrap(); + state.instant_unstake.set(2, true).unwrap(); + state.validators_for_immediate_removal.set(3, true).unwrap(); + state.validators_for_immediate_removal.set(4, true).unwrap(); + state.validators_added = 2; + // both validators were removed from pool and now the validator list is down to 3 + state.remove_validator(3); + + assert_eq!(state.num_pool_validators, 3); + assert_eq!(state.validators_for_immediate_removal.get(3).unwrap(), true); + assert_eq!( + state.validators_for_immediate_removal.get(4).unwrap(), + false + ); +} + +#[test] +fn test_remove_validator_fails() { + let fixtures = StateMachineFixtures::default(); + let mut state = fixtures.state; + + // Test fails if validator not marked to remove + state.validators_for_immediate_removal.reset(); + let res = state.remove_validator(0); + assert!(res.is_err()); + assert!(res == Err(Error::from(StewardError::ValidatorNotMarkedForRemoval))); + + // Test fails out of bounds + state + .validators_for_immediate_removal + .set(state.num_pool_validators as usize, true) + .unwrap(); + let res = state.remove_validator(state.num_pool_validators as usize); + assert!(res.is_err()); + assert!(res == Err(Error::from(StewardError::ValidatorIndexOutOfBounds))); +} diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 45e85a70..c869a00e 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -4,13 +4,26 @@ use anchor_lang::{ InstructionData, ToAccountMetas, }; use jito_steward::{ - instructions::AuthorityType, utils::ValidatorList, Config, StewardStateAccount, + instructions::AuthorityType, + utils::{StakePool, ValidatorList}, + Config, StewardStateAccount, }; use solana_program_test::*; -use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use solana_sdk::{ + clock::Clock, + signature::Keypair, + signer::Signer, + stake::{ + stake_flags::StakeFlags, + state::{Authorized, Delegation, Lockup, Meta, Stake, StakeStateV2}, + }, + transaction::Transaction, +}; +use spl_stake_pool::state::StakeStatus; use tests::steward_fixtures::{ - closed_vote_account, new_vote_account, serialized_validator_history_account, system_account, - validator_history_default, TestFixture, + closed_vote_account, crank_epoch_maintenance, crank_stake_pool, manual_remove_validator, + new_vote_account, serialized_stake_account, serialized_validator_history_account, + serialized_validator_list_account, system_account, validator_history_default, TestFixture, }; use validator_history::{ValidatorHistory, ValidatorHistoryEntry}; @@ -226,6 +239,432 @@ async fn test_auto_remove() { drop(fixture); } +async fn _auto_remove_validator_tx( + fixture: &TestFixture, + vote_account: Pubkey, + validator_index_to_remove: u64, +) -> Transaction { + let config = fixture.steward_config.pubkey(); + let state_account = fixture.steward_state; + let stake_pool = fixture.stake_pool_meta.stake_pool; + let reserve_stake = fixture.stake_pool_meta.reserve; + let validator_list = fixture.stake_pool_meta.validator_list; + let (stake_account, transient_stake_account, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + Transaction::new_signed_with_payer( + &[Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::AutoRemoveValidator { + config, + validator_history_account: Pubkey::find_program_address( + &[ValidatorHistory::SEED, vote_account.as_ref()], + &validator_history::id(), + ) + .0, + state_account, + stake_pool, + reserve_stake, + withdraw_authority, + validator_list, + stake_account, + transient_stake_account, + vote_account, + stake_history: sysvar::stake_history::id(), + stake_config: stake::config::id(), + stake_program: stake::program::id(), + stake_pool_program: spl_stake_pool::id(), + system_program: solana_program::system_program::id(), + rent: sysvar::rent::id(), + clock: sysvar::clock::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoRemoveValidatorFromPool { + validator_list_index: validator_index_to_remove, + } + .data(), + }], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture.ctx.borrow().last_blockhash, + ) +} + +async fn _setup_auto_remove_validator_test() -> (TestFixture, Pubkey) { + let fixture = TestFixture::new().await; + let _ctx = &fixture.ctx; + fixture.advance_num_epochs(1, 10).await; + fixture.initialize_stake_pool().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; + + crank_stake_pool(&fixture).await; + crank_epoch_maintenance(&fixture, None).await; + + let vote_account = Pubkey::new_unique(); + + _auto_add_validator_to_pool(&fixture, &vote_account).await; + + (fixture, vote_account) +} + +// TODO +#[tokio::test] +async fn test_auto_remove_validator_states() { + /* + This test requires specific setup of stake accounts to trigger different effects in spl_stake_pool::remove_validator_from_pool + Setting up all conditions via regular instruction calls is very difficult, so we are just testing the logic works as expected for + the different possible stake account states. + + - conditions of the stake accounts to pass `stake_is_usable_by_pool`: + meta.authorized.staker == *expected_authority + && meta.authorized.withdrawer == *expected_authority + && meta.lockup == *expected_lockup + - conditions of the stake accounts to pass `stake_is_inactive_without_history`: + stake.delegation.deactivation_epoch < epoch + || (stake.delegation.activation_epoch == epoch + && stake.delegation.deactivation_epoch == epoch) + */ + + // Status in DeactivatingValidator -> Immediate Removal + // Condition pt 1: get_stake_state on transient_stake_account retuns Err OR transient_stake_lamports == 0 (gets to DeactivatingValidator) + // Condition pt 2: (stake_is_usable_by_pool && stake_is_inactive_without_history) is TRUE + let (fixture, vote_account) = _setup_auto_remove_validator_test().await; + let ctx = &fixture.ctx; + let (stake_account_address, _transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + let stake_pool: StakePool = fixture + .load_and_deserialize(&fixture.stake_pool_meta.stake_pool) + .await; + + let current_epoch = ctx + .borrow_mut() + .banks_client + .get_sysvar::() + .await + .unwrap() + .epoch; + + // Manually set up stake account + let configured_stake_account = StakeStateV2::Stake( + Meta { + rent_exempt_reserve: 0, + authorized: Authorized { + staker: withdraw_authority, + withdrawer: withdraw_authority, + }, + lockup: stake_pool.lockup, + }, + Stake { + delegation: Delegation { + voter_pubkey: vote_account, + stake: 1_000_000_000, + activation_epoch: 0, + deactivation_epoch: current_epoch - 1, + warmup_cooldown_rate: 0.0, + }, + credits_observed: 0, + }, + StakeFlags::default(), + ); + + fixture.ctx.borrow_mut().set_account( + &stake_account_address, + &serialized_stake_account(configured_stake_account, 1_000_000_000).into(), + ); + + fixture + .submit_transaction_assert_success( + _auto_remove_validator_tx(&fixture, vote_account, 0).await, + ) + .await; + + // Get validator list and assert state + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + assert!(validator_list.validators[0].status == StakeStatus::DeactivatingValidator.into()); + assert!( + steward_state_account + .state + .validators_for_immediate_removal + .count() + == 1 + ); + assert!(steward_state_account.state.validators_to_remove.count() == 0); + + // Status in DeactivatingValidator -> Regular Removal + // Condition pt 1: get_stake_state on transient_stake_account retuns Err OR transient_stake_lamports == 0 (gets to DeactivatingValidator) + // Condition pt 2: (stake_is_usable_by_pool && stake_is_inactive_without_history is FALSE + let (fixture, vote_account) = _setup_auto_remove_validator_test().await; + let ctx = &fixture.ctx; + let (stake_account_address, _transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + let stake_pool: StakePool = fixture + .load_and_deserialize(&fixture.stake_pool_meta.stake_pool) + .await; + + let current_epoch = ctx + .borrow_mut() + .banks_client + .get_sysvar::() + .await + .unwrap() + .epoch; + + let mismatched_lockup = Lockup { + epoch: stake_pool.lockup.epoch + 1, + unix_timestamp: stake_pool.lockup.unix_timestamp + 1, + custodian: Pubkey::default(), + }; + + // Manually set up stake account + let configured_stake_account = StakeStateV2::Stake( + Meta { + rent_exempt_reserve: 0, + authorized: Authorized { + staker: withdraw_authority, + withdrawer: withdraw_authority, + }, + lockup: mismatched_lockup, // Not equal to stake pool lockup + }, + Stake { + delegation: Delegation { + voter_pubkey: vote_account, + stake: 1_000_000_000, + activation_epoch: 0, + deactivation_epoch: current_epoch - 1, + warmup_cooldown_rate: 0.0, + }, + credits_observed: 0, + }, + StakeFlags::default(), + ); + + fixture.ctx.borrow_mut().set_account( + &stake_account_address, + &serialized_stake_account(configured_stake_account, 1_000_000_000).into(), + ); + + fixture + .submit_transaction_assert_success( + _auto_remove_validator_tx(&fixture, vote_account, 0).await, + ) + .await; + + // Get validator list and assert state + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + assert!(validator_list.validators[0].status == StakeStatus::DeactivatingValidator.into()); + assert!( + steward_state_account + .state + .validators_for_immediate_removal + .count() + == 0 + ); + assert!(steward_state_account.state.validators_to_remove.count() == 1); + + // Status in DeactivatingAll -> Regular Removal + // If transient_stake_lamports > 0 and transient stake stake_is_usable_by_pool is true -> DeactivatingAll + let (fixture, vote_account) = _setup_auto_remove_validator_test().await; + let ctx = &fixture.ctx; + let (stake_account_address, transient_stake_account_address, withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + let stake_pool: StakePool = fixture + .load_and_deserialize(&fixture.stake_pool_meta.stake_pool) + .await; + + let current_epoch = ctx + .borrow_mut() + .banks_client + .get_sysvar::() + .await + .unwrap() + .epoch; + + // Manually set up stake account + let configured_stake_account = StakeStateV2::Stake( + Meta { + rent_exempt_reserve: 0, + authorized: Authorized { + staker: withdraw_authority, + withdrawer: withdraw_authority, + }, + lockup: stake_pool.lockup, + }, + Stake { + delegation: Delegation { + voter_pubkey: vote_account, + stake: 1_000_000_000, + activation_epoch: 0, + deactivation_epoch: current_epoch - 1, + warmup_cooldown_rate: 0.0, + }, + credits_observed: 0, + }, + StakeFlags::default(), + ); + + // Set custom transient stake account as well as validator list transient_stake_lamports + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let mut spl_validator_list = validator_list.as_ref().clone(); + spl_validator_list.validators[0].transient_stake_lamports = 1_000_000_000.into(); + fixture.ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account(spl_validator_list, None).into(), + ); + fixture.ctx.borrow_mut().set_account( + &stake_account_address, + &serialized_stake_account(configured_stake_account, 1_000_000_000).into(), + ); + fixture.ctx.borrow_mut().set_account( + &transient_stake_account_address, + &serialized_stake_account(configured_stake_account, 1_000_000_000).into(), + ); + + fixture + .submit_transaction_assert_success( + _auto_remove_validator_tx(&fixture, vote_account, 0).await, + ) + .await; + + // Get validator list and assert state + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + assert!(validator_list.validators[0].status == StakeStatus::DeactivatingAll.into()); + assert!( + steward_state_account + .state + .validators_for_immediate_removal + .count() + == 0 + ); + assert!(steward_state_account.state.validators_to_remove.count() == 1); + + // Remaining states not tested: + // Status in Active -> Error (not possible to get into this state from the instruction) + // Status in ReadyForRemoval -> Immediate Removal (not possible to get into this state from the instruction) + // Status in DeactivatingTransient -> Regular Removal (not possible to get into this state from the instruction) +} + +fn _instant_remove_validator_tx( + fixture: &TestFixture, + validator_index_to_remove: u64, +) -> Transaction { + Transaction::new_signed_with_payer( + &[Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::InstantRemoveValidator { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::InstantRemoveValidator { + validator_index_to_remove, + } + .data(), + }], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture.ctx.borrow().last_blockhash, + ) +} + +#[tokio::test] +async fn test_instant_remove_validator() { + // Setup + auto add validator to pool + let fixture = TestFixture::new().await; + let _ctx = &fixture.ctx; + fixture.initialize_stake_pool().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; + + let vote_account = Pubkey::new_unique(); + + let (_validator_history_account, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, vote_account.as_ref()], + &validator_history::id(), + ); + + let (_stake_account_address, _transient_stake_account_address, _withdraw_authority) = + fixture.stake_accounts_for_validator(vote_account).await; + + _auto_add_validator_to_pool(&fixture, &vote_account).await; + + //// Test checks //// + + // Default state + + // Test not marked for immediate removal (ValidatorNotInList) + let tx = _instant_remove_validator_tx(&fixture, 0); + fixture + .submit_transaction_assert_error(tx, "ValidatorNotInList") + .await; + + // Manually mark for removal and Force list ValidatorStakeInfo for removal - Ready for removal + manual_remove_validator(&fixture, 0, true, true).await; + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + let mut spl_validator_list = validator_list.as_ref().clone(); + spl_validator_list.validators[0].status = StakeStatus::ReadyForRemoval.into(); + fixture.ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account(spl_validator_list, None).into(), + ); + + // Test Validators have not been removed (ValidatorsHaveNotBeenRemoved) + let tx = _instant_remove_validator_tx(&fixture, 0); + fixture + .submit_transaction_assert_error(tx, "ValidatorsHaveNotBeenRemoved") + .await; + + // Actually remove validator + crank_stake_pool(&fixture).await; + + // Test passes and removes validator + let tx = _instant_remove_validator_tx(&fixture, 0); + fixture.submit_transaction_assert_success(tx).await; + + // Check that validator is removed + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + assert!( + steward_state_account + .state + .validators_for_immediate_removal + .count() + == 0 + ); + assert!( + steward_state_account.state.num_pool_validators + + steward_state_account.state.validators_added as u64 + == 0 + ); + + drop(fixture); +} + #[tokio::test] async fn test_pause() { let fixture = TestFixture::new().await; diff --git a/tests/tests/steward/test_validator_list_sync.rs b/tests/tests/steward/test_validator_list_sync.rs deleted file mode 100644 index e69de29b..00000000 From ae537bc2b7286f1a837385db6a1e9860f3879f09 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Mon, 30 Sep 2024 15:45:06 -0400 Subject: [PATCH 06/13] linti --- tests/src/steward_fixtures.rs | 35 +++++++++---------- tests/tests/steward/mod.rs | 2 +- tests/tests/steward/test_cycle.rs | 33 +++++++++-------- tests/tests/steward/test_epoch_maintenance.rs | 22 +++++++----- tests/tests/steward/test_state_methods.rs | 13 ++++--- tests/tests/steward/test_steward.rs | 1 - 6 files changed, 54 insertions(+), 52 deletions(-) diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 1c091d1a..12887aa1 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -664,8 +664,8 @@ pub async fn crank_stake_pool(fixture: &TestFixture) { .await; let (initial_ixs, final_ixs) = spl_stake_pool::instruction::update_stake_pool( &spl_stake_pool::id(), - &stake_pool.as_ref(), - &validator_list.as_ref(), + stake_pool.as_ref(), + validator_list.as_ref(), &fixture.stake_pool_meta.stake_pool, false, ); @@ -901,7 +901,7 @@ pub async fn manual_remove_validator( pub async fn crank_compute_score( fixture: &TestFixture, _unit_test_fixtures: &StateMachineFixtures, - extra_validator_accounts: &Vec, + extra_validator_accounts: &[ExtraValidatorAccounts], indices: &[usize], ) { let ctx = &fixture.ctx; @@ -981,7 +981,7 @@ pub async fn crank_idle(fixture: &TestFixture) { pub async fn crank_compute_instant_unstake( fixture: &TestFixture, _unit_test_fixtures: &StateMachineFixtures, - extra_validator_accounts: &Vec, + extra_validator_accounts: &[ExtraValidatorAccounts], indices: &[usize], ) { let ctx = &fixture.ctx; @@ -1016,7 +1016,7 @@ pub async fn crank_compute_instant_unstake( pub async fn crank_rebalance( fixture: &TestFixture, _unit_test_fixtures: &StateMachineFixtures, - extra_validator_accounts: &Vec, + extra_validator_accounts: &[ExtraValidatorAccounts], indices: &[usize], ) { let ctx = &fixture.ctx; @@ -1064,7 +1064,7 @@ pub async fn crank_rebalance( pub async fn copy_vote_account( fixture: &TestFixture, - extra_validator_accounts: &Vec, + extra_validator_accounts: &[ExtraValidatorAccounts], index: usize, ) { let ctx = &fixture.ctx; @@ -1091,7 +1091,7 @@ pub async fn copy_vote_account( pub async fn update_stake_history( fixture: &TestFixture, - extra_validator_accounts: &Vec, + extra_validator_accounts: &[ExtraValidatorAccounts], index: usize, epoch: u64, lamports: u64, @@ -1171,11 +1171,11 @@ pub async fn crank_validator_history_accounts( .ctx .borrow_mut() .increment_vote_account_credits(&extra_validator_accounts[i].vote_account, 1000); - copy_vote_account(&fixture, &extra_validator_accounts, i).await; + copy_vote_account(fixture, extra_validator_accounts, i).await; // only field that's relevant to score is is_superminority update_stake_history( - &fixture, - &extra_validator_accounts, + fixture, + extra_validator_accounts, i, clock.epoch, 1_000_000, @@ -1184,7 +1184,7 @@ pub async fn crank_validator_history_accounts( ) .await; } - copy_cluster_info(&fixture).await; + copy_cluster_info(fixture).await; } pub struct ValidatorEntry { @@ -1234,8 +1234,8 @@ impl Default for FixtureDefaultAccounts { let steward_config_keypair = Keypair::new(); let steward_config = Config { - stake_pool: stake_pool_meta.stake_pool.clone(), - validator_list: stake_pool_meta.validator_list.clone(), + stake_pool: stake_pool_meta.stake_pool, + validator_list: stake_pool_meta.validator_list, blacklist_authority: keypair.pubkey(), parameters_authority: keypair.pubkey(), admin: keypair.pubkey(), @@ -1294,7 +1294,6 @@ impl Default for FixtureDefaultAccounts { admin: keypair.pubkey(), oracle_authority: keypair.pubkey(), tip_distribution_program: jito_tip_distribution::id(), - ..Default::default() }; let cluster_history = cluster_history_default(); @@ -1327,7 +1326,7 @@ impl FixtureDefaultAccounts { .0; ( validator_history_address, - serialized_validator_history_account(ve.validator_history.clone()), + serialized_validator_history_account(ve.validator_history), ) }) .collect::>(); @@ -1807,7 +1806,7 @@ impl Default for StateMachineFixtures { mev_commission: 0, is_superminority: 0, activated_stake_lamports: 10 * LAMPORTS_PER_SOL, - vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), + vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i), ..ValidatorHistoryEntry::default() }); } @@ -1827,7 +1826,7 @@ impl Default for StateMachineFixtures { mev_commission: 10000, is_superminority: 1, activated_stake_lamports: 10 * LAMPORTS_PER_SOL, - vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), + vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i), ..ValidatorHistoryEntry::default() }); } @@ -1847,7 +1846,7 @@ impl Default for StateMachineFixtures { mev_commission: 500, is_superminority: 0, activated_stake_lamports: 10 * LAMPORTS_PER_SOL, - vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), + vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i), ..ValidatorHistoryEntry::default() }); } diff --git a/tests/tests/steward/mod.rs b/tests/tests/steward/mod.rs index e66b30f6..e7f88120 100644 --- a/tests/tests/steward/mod.rs +++ b/tests/tests/steward/mod.rs @@ -1,5 +1,6 @@ mod test_algorithms; mod test_cycle; +mod test_epoch_maintenance; mod test_integration; mod test_parameters; mod test_scoring; @@ -7,4 +8,3 @@ mod test_spl_passthrough; mod test_state_methods; mod test_state_transitions; mod test_steward; -mod test_epoch_maintenance; diff --git a/tests/tests/steward/test_cycle.rs b/tests/tests/steward/test_cycle.rs index 6b41a5f4..df16a81d 100644 --- a/tests/tests/steward/test_cycle.rs +++ b/tests/tests/steward/test_cycle.rs @@ -202,7 +202,7 @@ async fn test_cycle() { assert_eq!(state.scoring_unstake_total, 0); assert_eq!(state.stake_deposit_unstake_total, 0); assert_eq!(state.validators_added, 0); - assert_eq!(state.validators_to_remove.is_empty(), true); + assert!(state.validators_to_remove.is_empty()); // assert_eq!(state.status_flags, 3); // TODO // All other values are reset @@ -272,8 +272,12 @@ async fn test_remove_validator_mid_epoch() { fixture.realloc_steward_state().await; let mut extra_validator_accounts = vec![]; - for i in 0..unit_test_fixtures.validators.len() { - let vote_account = unit_test_fixtures.validator_list[i].vote_account_address; + for vote_account in unit_test_fixtures + .validator_list + .iter() + .take(unit_test_fixtures.validators.len()) + .map(|v| v.vote_account_address) + { let (validator_history_address, _) = Pubkey::find_program_address( &[ValidatorHistory::SEED, vote_account.as_ref()], &validator_history::id(), @@ -293,8 +297,7 @@ async fn test_remove_validator_mid_epoch() { crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds validators 2 and 3 - for i in 0..3 { - let extra_accounts = &extra_validator_accounts[i]; + for extra_accounts in extra_validator_accounts.iter().take(3) { auto_add_validator(&fixture, extra_accounts).await; } @@ -358,7 +361,7 @@ async fn test_remove_validator_mid_epoch() { jito_steward::StewardStateEnum::ComputeInstantUnstake )); assert_eq!(state.validators_for_immediate_removal.count(), 1); - assert_eq!(state.validators_for_immediate_removal.get(2).unwrap(), true); + assert!(state.validators_for_immediate_removal.get(2).unwrap()); assert_eq!(state.num_pool_validators, 3); let validator_list: ValidatorList = fixture @@ -367,8 +370,7 @@ async fn test_remove_validator_mid_epoch() { assert!(validator_list .validators .iter() - .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) - .is_some()); + .any(|v| v.vote_account_address == extra_validator_accounts[2].vote_account)); assert!(validator_list.validators.len() == 3); println!("Stake Status: {:?}", validator_list.validators[2].status); @@ -378,11 +380,10 @@ async fn test_remove_validator_mid_epoch() { let validator_list: ValidatorList = fixture .load_and_deserialize(&fixture.stake_pool_meta.validator_list) .await; - assert!(validator_list + assert!(!validator_list .validators .iter() - .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) - .is_none()); + .any(|v| v.vote_account_address == extra_validator_accounts[2].vote_account)); assert!(validator_list.validators.len() == 2); instant_remove_validator(&fixture, 2).await; @@ -419,11 +420,10 @@ async fn test_remove_validator_mid_epoch() { let validator_list: ValidatorList = fixture .load_and_deserialize(&fixture.stake_pool_meta.validator_list) .await; - assert!(validator_list + assert!(!validator_list .validators .iter() - .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) - .is_none()); + .any(|v| v.vote_account_address == extra_validator_accounts[2].vote_account)); assert!(validator_list.validators.len() == 2); crank_epoch_maintenance(&fixture, None).await; @@ -547,8 +547,7 @@ async fn test_add_validator_next_cycle() { assert!(validator_list .validators .iter() - .find(|v| v.vote_account_address == extra_validator_accounts[2].vote_account) - .is_some()); + .any(|v| v.vote_account_address == extra_validator_accounts[2].vote_account)); assert!(validator_list.validators.len() == 3); // Ensure that num_pool_validators isn't updated but validators_added is @@ -618,7 +617,7 @@ async fn test_add_validator_next_cycle() { )); assert_eq!(state.validators_added, 0); - assert_eq!(state.validators_to_remove.is_empty(), true); + assert!(state.validators_to_remove.is_empty()); assert_eq!(state.num_pool_validators, 3); // Ensure we can crank the new validator diff --git a/tests/tests/steward/test_epoch_maintenance.rs b/tests/tests/steward/test_epoch_maintenance.rs index 522ce6ac..dcac98fb 100644 --- a/tests/tests/steward/test_epoch_maintenance.rs +++ b/tests/tests/steward/test_epoch_maintenance.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::collections::HashMap; use anchor_lang::{ @@ -53,7 +54,7 @@ async fn _epoch_maintenance_setup() -> ( // Setup pool and steward let mut fixture_accounts = FixtureDefaultAccounts::default(); - let unit_test_fixtures = Box::new(StateMachineFixtures::default()); + let unit_test_fixtures = Box::::default(); // Note that these parameters are overriden in initialize_steward, just included here for completeness fixture_accounts.steward_config.parameters = unit_test_fixtures.config.parameters; @@ -129,8 +130,10 @@ async fn _epoch_maintenance_setup() -> ( crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds to validator list - for i in 0..unit_test_fixtures.validators.len() { - let extra_accounts = &extra_validator_accounts[i]; + for extra_accounts in extra_validator_accounts + .iter() + .take(unit_test_fixtures.validators.len()) + { auto_add_validator(&fixture, extra_accounts).await; } @@ -148,7 +151,8 @@ async fn _epoch_maintenance_setup() -> ( #[tokio::test] async fn test_epoch_maintenance_fails_status_check() { // Setup pool and steward - let (fixture, _unit_test_fixtures, _extra_validator_accounts) = _epoch_maintenance_setup().await; + let (fixture, _unit_test_fixtures, _extra_validator_accounts) = + _epoch_maintenance_setup().await; let validator_list: ValidatorList = fixture .load_and_deserialize(&fixture.stake_pool_meta.validator_list) @@ -173,7 +177,8 @@ async fn test_epoch_maintenance_fails_status_check() { #[tokio::test] async fn test_epoch_maintenance_fails_invariant_check() { // Setup pool and steward - let (fixture, _unit_test_fixtures, _extra_validator_accounts) = _epoch_maintenance_setup().await; + let (fixture, _unit_test_fixtures, _extra_validator_accounts) = + _epoch_maintenance_setup().await; // Mark validator to remove without actually removing it from list manual_remove_validator(&fixture, 0, true, false).await; @@ -188,7 +193,8 @@ async fn test_epoch_maintenance_fails_invariant_check() { #[tokio::test] async fn test_epoch_maintenance_removes_validators() { // Setup pool and steward - let (fixture, _unit_test_fixtures, _extra_validator_accounts) = _epoch_maintenance_setup().await; + let (fixture, _unit_test_fixtures, _extra_validator_accounts) = + _epoch_maintenance_setup().await; // Mark validator to remove with admin fn (delayed removal) manual_remove_validator(&fixture, 0, true, false).await; @@ -219,8 +225,8 @@ async fn test_epoch_maintenance_removes_validators() { let state = &state_account.state; assert_eq!(state.validators_added, 2); assert_eq!(state.current_epoch, clock.epoch); - assert_eq!(state.validators_to_remove.is_empty(), true); - assert_eq!(state.validators_for_immediate_removal.is_empty(), true); + assert!(state.validators_to_remove.is_empty()); + assert!(state.validators_for_immediate_removal.is_empty()); assert!(state.has_flag(EPOCH_MAINTENANCE)); println!("Woo"); diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index 7ce0198c..a4ade01c 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -858,7 +858,8 @@ fn test_remove_validator() { // test basic case - remove validator_to_remove state.validators_to_remove.set(1, true).unwrap(); - state.remove_validator(1); + let res = state.remove_validator(1); + assert!(res.is_ok()); assert_eq!(state.num_pool_validators, 2); // Assert that values were shifted left assert_eq!(state.yield_scores[1], 2); @@ -911,14 +912,12 @@ fn test_remove_validator() { state.validators_for_immediate_removal.set(4, true).unwrap(); state.validators_added = 2; // both validators were removed from pool and now the validator list is down to 3 - state.remove_validator(3); + let res = state.remove_validator(3); + assert!(res.is_ok()); assert_eq!(state.num_pool_validators, 3); - assert_eq!(state.validators_for_immediate_removal.get(3).unwrap(), true); - assert_eq!( - state.validators_for_immediate_removal.get(4).unwrap(), - false - ); + assert!(state.validators_for_immediate_removal.get(3).unwrap()); + assert!(!state.validators_for_immediate_removal.get(4).unwrap()); } #[test] diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index c869a00e..2779fe03 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -308,7 +308,6 @@ async fn _setup_auto_remove_validator_test() -> (TestFixture, Pubkey) { (fixture, vote_account) } -// TODO #[tokio::test] async fn test_auto_remove_validator_states() { /* From ca589cf9c06131453d74141327d4a294c7d2086c Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Mon, 30 Sep 2024 16:03:09 -0400 Subject: [PATCH 07/13] fix last things --- tests/tests/steward/test_epoch_maintenance.rs | 2 - tests/tests/steward/test_state_methods.rs | 49 ++++++------------- 2 files changed, 14 insertions(+), 37 deletions(-) diff --git a/tests/tests/steward/test_epoch_maintenance.rs b/tests/tests/steward/test_epoch_maintenance.rs index dcac98fb..77fbde7e 100644 --- a/tests/tests/steward/test_epoch_maintenance.rs +++ b/tests/tests/steward/test_epoch_maintenance.rs @@ -228,6 +228,4 @@ async fn test_epoch_maintenance_removes_validators() { assert!(state.validators_to_remove.is_empty()); assert!(state.validators_for_immediate_removal.is_empty()); assert!(state.has_flag(EPOCH_MAINTENANCE)); - - println!("Woo"); } diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index a4ade01c..4cbd7464 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -11,7 +11,7 @@ use jito_steward::{ constants::{LAMPORT_BALANCE_DEFAULT, MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, delegation::RebalanceType, errors::StewardError, - Delegation, StewardStateEnum, + Delegation, StewardState, StewardStateEnum, }; use solana_sdk::native_token::LAMPORTS_PER_SOL; use spl_stake_pool::big_vec::BigVec; @@ -835,12 +835,7 @@ fn test_rebalance_default_lamports() { } } -// TODO -#[test] -fn test_remove_validator() { - // Setup: create steward state based off StewardStateFixtures - // mark index 1 to removal - let fixtures = StateMachineFixtures::default(); +fn _test_remove_validator_setup(fixtures: &StateMachineFixtures) -> StewardState { let mut state = fixtures.state; // Set values for all of the values that are gonna get shifted state.validator_lamport_balances[0..3].copy_from_slice(&[0, 1, 2]); @@ -856,6 +851,15 @@ fn test_remove_validator() { state.instant_unstake.set(1, false).unwrap(); state.instant_unstake.set(2, true).unwrap(); + state +} +#[test] +fn test_remove_validator() { + // Setup: create steward state based off StewardStateFixtures + // mark index 1 to removal + let fixtures = StateMachineFixtures::default(); + let mut state = _test_remove_validator_setup(&fixtures); + // test basic case - remove validator_to_remove state.validators_to_remove.set(1, true).unwrap(); let res = state.remove_validator(1); @@ -867,20 +871,7 @@ fn test_remove_validator() { assert!(state.delegations[1] == Delegation::new(2, 1)); // test basic case - remove immediate_removal validator - let mut state = fixtures.state; - // Set values for all of the values that are gonna get shifted - state.validator_lamport_balances[0..3].copy_from_slice(&[0, 1, 2]); - state.scores[0..3].copy_from_slice(&[0, 1, 2]); - state.yield_scores[0..3].copy_from_slice(&[0, 1, 2]); - state.delegations[0..3].copy_from_slice(&[ - Delegation::new(0, 1), - Delegation::new(1, 1), - Delegation::new(2, 1), - ]); - state.instant_unstake.reset(); - state.instant_unstake.set(0, true).unwrap(); - state.instant_unstake.set(1, false).unwrap(); - state.instant_unstake.set(2, true).unwrap(); + let mut state = _test_remove_validator_setup(&fixtures); state.validators_for_immediate_removal.set(1, true).unwrap(); let res = state.remove_validator(1); @@ -894,20 +885,8 @@ fn test_remove_validator() { // Setup: mark an index for removal that's higher than num_pool_validators // Remember this is always gonna be run after actual removals have taken place, so could validator_list_len be kind of a red herring? do we need to go further? - let mut state = fixtures.state; - // Set values for all of the values that are gonna get shifted - state.validator_lamport_balances[0..3].copy_from_slice(&[0, 1, 2]); - state.scores[0..3].copy_from_slice(&[0, 1, 2]); - state.yield_scores[0..3].copy_from_slice(&[0, 1, 2]); - state.delegations[0..3].copy_from_slice(&[ - Delegation::new(0, 1), - Delegation::new(1, 1), - Delegation::new(2, 1), - ]); - state.instant_unstake.reset(); - state.instant_unstake.set(0, true).unwrap(); - state.instant_unstake.set(1, false).unwrap(); - state.instant_unstake.set(2, true).unwrap(); + let mut state = _test_remove_validator_setup(&fixtures); + state.validators_for_immediate_removal.set(3, true).unwrap(); state.validators_for_immediate_removal.set(4, true).unwrap(); state.validators_added = 2; From 892ad5dc36304a184547fa03ff0e7bac0817099d Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Mon, 30 Sep 2024 16:03:25 -0400 Subject: [PATCH 08/13] fix last things --- tests/tests/steward/test_steward.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 2779fe03..bc4e1f6e 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -271,7 +271,7 @@ async fn _auto_remove_validator_tx( transient_stake_account, vote_account, stake_history: sysvar::stake_history::id(), - stake_config: stake::config::id(), + stake_config: stake::config::ID, stake_program: stake::program::id(), stake_pool_program: spl_stake_pool::id(), system_program: solana_program::system_program::id(), @@ -361,7 +361,7 @@ async fn test_auto_remove_validator_states() { stake: 1_000_000_000, activation_epoch: 0, deactivation_epoch: current_epoch - 1, - warmup_cooldown_rate: 0.0, + ..Default::default() }, credits_observed: 0, }, @@ -439,7 +439,7 @@ async fn test_auto_remove_validator_states() { stake: 1_000_000_000, activation_epoch: 0, deactivation_epoch: current_epoch - 1, - warmup_cooldown_rate: 0.0, + ..Default::default() }, credits_observed: 0, }, @@ -509,7 +509,7 @@ async fn test_auto_remove_validator_states() { stake: 1_000_000_000, activation_epoch: 0, deactivation_epoch: current_epoch - 1, - warmup_cooldown_rate: 0.0, + ..Default::default() }, credits_observed: 0, }, From c479af7ed1452b531a0904d638d12dc979b6c436 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 4 Dec 2024 15:10:28 -0500 Subject: [PATCH 09/13] pls --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 166f5fd9..120c5943 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -156,7 +156,7 @@ jobs: env: RUST_LOG: trace SBF_OUT_DIR: ${{ github.workspace }}/target/deploy - RUST_MIN_STACK: 5000000 + RUST_MIN_STACK: 10000000 # release only runs on tagged commits # it should wait for all the other steps to finish, to ensure releases are the highest quality From 4d78984f3e45ed096f6d50fdd58d8937c67e4a05 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 4 Dec 2024 16:14:11 -0500 Subject: [PATCH 10/13] done --- programs/steward/src/utils.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index a2eb01d1..8e58837e 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -5,7 +5,11 @@ use anchor_lang::idl::types::*; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use spl_pod::solana_program::clock::Epoch; -use spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64, solana_program::program_pack::Pack}; +use spl_pod::{ + bytemuck::pod_from_bytes, + primitives::PodU64, + solana_program::{program_pack::Pack, stake}, +}; use spl_stake_pool::{ big_vec::BigVec, state::{StakeStatus, ValidatorListHeader, ValidatorStakeInfo}, From d119bd532fd377fc78efe2bffe0eef0a04558fda Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 4 Dec 2024 16:28:21 -0500 Subject: [PATCH 11/13] last --- tests/src/steward_fixtures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 12887aa1..076ee770 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -1156,7 +1156,7 @@ pub async fn copy_cluster_info(fixture: &TestFixture) { pub async fn crank_validator_history_accounts( fixture: &TestFixture, - extra_validator_accounts: &Vec, + extra_validator_accounts: &[ExtraValidatorAccounts], indices: &[usize], ) { let clock: Clock = fixture From c3f315e25765597dcf55419a3225ae7fb9ff0f23 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 4 Dec 2024 16:47:16 -0500 Subject: [PATCH 12/13] done done --- tests/tests/steward/test_cycle.rs | 7 +++---- tests/tests/steward/test_steward.rs | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/tests/steward/test_cycle.rs b/tests/tests/steward/test_cycle.rs index df16a81d..9981bb25 100644 --- a/tests/tests/steward/test_cycle.rs +++ b/tests/tests/steward/test_cycle.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::collections::HashMap; use anchor_lang::{ @@ -101,8 +102,7 @@ async fn test_cycle() { crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds to validator list - for i in 0..unit_test_fixtures.validators.len() { - let extra_accounts = &extra_validator_accounts[i]; + for extra_accounts in extra_validator_accounts.iter() { auto_add_validator(&fixture, extra_accounts).await; } @@ -525,8 +525,7 @@ async fn test_add_validator_next_cycle() { crank_epoch_maintenance(&fixture, None).await; // Auto add validator - adds validators 2 and 3 - for i in 0..2 { - let extra_accounts = &extra_validator_accounts[i]; + for extra_accounts in extra_validator_accounts.iter().take(2) { auto_add_validator(&fixture, extra_accounts).await; } diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index bc4e1f6e..8a66cec7 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] /// Basic integration test use anchor_lang::{ solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}, From ad04bb5374c7565c33bc7580ae3cb07ccdfc4666 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 4 Dec 2024 16:51:04 -0500 Subject: [PATCH 13/13] kms --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7c9321cd..f25d2d46 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -75,6 +75,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: solana-verify + version: "0.2.11" - name: Install anchor-cli from crates.io uses: baptiste0928/cargo-install@v3 with: