diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dbc46776..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: @@ -156,7 +157,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 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/programs/steward/src/instructions/auto_remove_validator_from_pool.rs b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs index 84e01606..c3ff1fab 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; @@ -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 @@ -267,23 +269,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/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/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/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index c1b0e146..d631d062 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -465,7 +465,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)?; @@ -474,8 +474,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) @@ -487,8 +495,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)?; @@ -502,7 +508,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)?)?; @@ -556,9 +562,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/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index c39345c3..8e58837e 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -4,7 +4,12 @@ use std::ops::{Deref, Not}; use anchor_lang::idl::types::*; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; -use spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64, solana_program::program_pack::Pack}; +use spl_pod::solana_program::clock::Epoch; +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}, @@ -312,6 +317,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 65d53e61..076ee770 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,28 @@ 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, + compute_budget::ComputeBudgetInstruction, + epoch_schedule::EpochSchedule, + hash::Hash, + instruction::Instruction, + native_token::LAMPORTS_PER_SOL, + rent::Rent, + signature::Keypair, + signer::Signer, + stake::{ + self, + state::{Lockup, StakeStateV2}, + }, + system_program, sysvar, + 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 +158,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 +647,854 @@ 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 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, + extra_validator_accounts: &[ExtraValidatorAccounts], + 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: &[ExtraValidatorAccounts], + 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: &[ExtraValidatorAccounts], + 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: &[ExtraValidatorAccounts], + 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: &[ExtraValidatorAccounts], + 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: &[ExtraValidatorAccounts], + 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, + 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, + validator_list: stake_pool_meta.validator_list, + 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(), + }; + 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), + ) + }) + .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 +1730,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 +1780,7 @@ impl Default for StateMachineFixtures { // Setup Sysvars: Clock, EpochSchedule - let epoch_schedule = EpochSchedule::custom(1000, 1000, false); + let epoch_schedule = EpochSchedule::default(); let clock = Clock { epoch: current_epoch, @@ -877,51 +1789,69 @@ 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, is_superminority: 0, - vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), + activated_stake_lamports: 10 * LAMPORTS_PER_SOL, + vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i), ..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, is_superminority: 1, - vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), + activated_stake_lamports: 10 * LAMPORTS_PER_SOL, + vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i), ..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, is_superminority: 0, - vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i.into()), + activated_stake_lamports: 10 * LAMPORTS_PER_SOL, + vote_account_last_update_slot: epoch_schedule.get_last_slot_in_epoch(i), ..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 +1920,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..e7f88120 100644 --- a/tests/tests/steward/mod.rs +++ b/tests/tests/steward/mod.rs @@ -1,4 +1,6 @@ mod test_algorithms; +mod test_cycle; +mod test_epoch_maintenance; mod test_integration; mod test_parameters; mod test_scoring; diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 2b07210b..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, }, @@ -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 } @@ -1594,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_cycle.rs b/tests/tests/steward/test_cycle.rs new file mode 100644 index 00000000..9981bb25 --- /dev/null +++ b/tests/tests/steward/test_cycle.rs @@ -0,0 +1,632 @@ +#![allow(clippy::await_holding_refcell_ref)] +use std::collections::HashMap; + +use anchor_lang::{ + solana_program::{instruction::Instruction, pubkey::Pubkey, stake}, + InstructionData, ToAccountMetas, +}; +use jito_steward::{utils::ValidatorList, StewardStateAccount, UpdateParametersArgs}; +use solana_program_test::*; +use solana_sdk::{ + 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, + 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; + +#[tokio::test] +async fn test_cycle() { + let mut fixture_accounts = FixtureDefaultAccounts::default(); + + 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) + .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; + fixture.realloc_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() { + 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 extra_accounts in extra_validator_accounts.iter() { + auto_add_validator(&fixture, extra_accounts).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!(state.validators_to_remove.is_empty()); + // assert_eq!(state.status_flags, 3); // TODO + + // All other values are reset + + drop(fixture); +} + +#[tokio::test] +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. + */ + + 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; + fixture.realloc_steward_state().await; + + let mut extra_validator_accounts = vec![]; + 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(), + ); + + 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 validators 2 and 3 + for extra_accounts in extra_validator_accounts.iter().take(3) { + auto_add_validator(&fixture, extra_accounts).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_for_immediate_removal.count(), 1); + assert!(state.validators_for_immediate_removal.get(2).unwrap()); + 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() + .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); + + // 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() + .any(|v| v.vote_account_address == extra_validator_accounts[2].vote_account)); + 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, + &extra_validator_accounts, + &[2], + ) + .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; + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + assert!(!validator_list + .validators + .iter() + .any(|v| v.vote_account_address == extra_validator_accounts[2].vote_account)); + assert!(validator_list.validators.len() == 2); + + 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_to_remove.count(), 0); + assert_eq!(state.validators_for_immediate_removal.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; + 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 validators 2 and 3 + for extra_accounts in extra_validator_accounts.iter().take(2) { + auto_add_validator(&fixture, extra_accounts).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() + .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 + 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!(state.validators_to_remove.is_empty()); + 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); +} diff --git a/tests/tests/steward/test_epoch_maintenance.rs b/tests/tests/steward/test_epoch_maintenance.rs new file mode 100644 index 00000000..77fbde7e --- /dev/null +++ b/tests/tests/steward/test_epoch_maintenance.rs @@ -0,0 +1,231 @@ +#![allow(clippy::await_holding_refcell_ref)] +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::::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 extra_accounts in extra_validator_accounts + .iter() + .take(unit_test_fixtures.validators.len()) + { + 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!(state.validators_to_remove.is_empty()); + assert!(state.validators_for_immediate_removal.is_empty()); + assert!(state.has_flag(EPOCH_MAINTENANCE)); +} diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index 7b049001..4cbd7464 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -8,10 +8,10 @@ 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, + Delegation, StewardState, StewardStateEnum, }; use solana_sdk::native_token::LAMPORTS_PER_SOL; use spl_stake_pool::big_vec::BigVec; @@ -751,3 +751,171 @@ 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"); + } +} + +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]); + 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 +} +#[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); + 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)); + + // test basic case - remove immediate_removal validator + let mut state = _test_remove_validator_setup(&fixtures); + + 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 = _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; + // both validators were removed from pool and now the validator list is down to 3 + let res = state.remove_validator(3); + assert!(res.is_ok()); + + assert_eq!(state.num_pool_validators, 3); + assert!(state.validators_for_immediate_removal.get(3).unwrap()); + assert!(!state.validators_for_immediate_removal.get(4).unwrap()); +} + +#[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..8a66cec7 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -1,16 +1,30 @@ +#![allow(clippy::await_holding_refcell_ref)] /// Basic integration test use anchor_lang::{ solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}, 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 +240,431 @@ 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) +} + +#[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, + ..Default::default() + }, + 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, + ..Default::default() + }, + 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, + ..Default::default() + }, + 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;