From 6b3ba23584ef424d89b2e4cd6b0ab81165415007 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 4 Jun 2024 16:58:24 -0400 Subject: [PATCH 01/18] Admin methods for testing --- .../instructions/close_steward_accounts.rs | 39 +++++++++++ programs/steward/src/instructions/mod.rs | 4 ++ .../src/instructions/reset_steward_state.rs | 66 +++++++++++++++++++ programs/steward/src/lib.rs | 14 ++++ 4 files changed, 123 insertions(+) create mode 100644 programs/steward/src/instructions/close_steward_accounts.rs create mode 100644 programs/steward/src/instructions/reset_steward_state.rs diff --git a/programs/steward/src/instructions/close_steward_accounts.rs b/programs/steward/src/instructions/close_steward_accounts.rs new file mode 100644 index 00000000..bbf00776 --- /dev/null +++ b/programs/steward/src/instructions/close_steward_accounts.rs @@ -0,0 +1,39 @@ +use crate::{ + state::{Config, StewardStateAccount}, + utils::get_config_authority, + Staker, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseStewardAccounts<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + close = authority, + seeds = [Staker::SEED, config.key().as_ref()], + bump, + )] + staker: Account<'info, Staker>, + + #[account( + mut, + close = authority, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + #[account(mut, address = get_config_authority(&config)?)] + pub authority: Signer<'info>, +} + +/* + Closes Steward PDA accounts associated with a given Config (StewardStateAccount, and Staker). + Config is not closed as it is a Keypair, so lamports can simply be withdrawn. + Reclaims lamports to authority +*/ +pub const fn handler(_ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/programs/steward/src/instructions/mod.rs b/programs/steward/src/instructions/mod.rs index 4ca9674e..ec849977 100644 --- a/programs/steward/src/instructions/mod.rs +++ b/programs/steward/src/instructions/mod.rs @@ -2,6 +2,7 @@ pub mod add_validator_to_blacklist; pub mod auto_add_validator_to_pool; pub mod auto_remove_validator_from_pool; +pub mod close_steward_accounts; pub mod compute_delegations; pub mod compute_instant_unstake; pub mod compute_score; @@ -12,6 +13,7 @@ pub mod pause_steward; pub mod realloc_state; pub mod rebalance; pub mod remove_validator_from_blacklist; +pub mod reset_steward_state; pub mod resume_steward; pub mod set_new_authority; pub mod spl_passthrough; @@ -20,6 +22,7 @@ pub mod update_parameters; pub use add_validator_to_blacklist::*; pub use auto_add_validator_to_pool::*; pub use auto_remove_validator_from_pool::*; +pub use close_steward_accounts::*; pub use compute_delegations::*; pub use compute_instant_unstake::*; pub use compute_score::*; @@ -30,6 +33,7 @@ pub use pause_steward::*; pub use realloc_state::*; pub use rebalance::*; pub use remove_validator_from_blacklist::*; +pub use reset_steward_state::*; pub use resume_steward::*; pub use set_new_authority::*; pub use spl_passthrough::*; diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs new file mode 100644 index 00000000..a2f84542 --- /dev/null +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -0,0 +1,66 @@ +use crate::{ + constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, + errors::StewardError, + state::{Config, StewardStateAccount}, + utils::{get_config_authority, get_stake_pool, StakePool}, + BitMask, Delegation, StewardStateEnum, +}; +use anchor_lang::prelude::*; +use spl_stake_pool::state::ValidatorListHeader; + +#[derive(Accounts)] +pub struct ResetStewardState<'info> { + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + pub config: AccountLoader<'info, Config>, + + pub system_program: Program<'info, System>, + + #[account(address = get_stake_pool(&config)?)] + pub stake_pool: Account<'info, StakePool>, + + #[account(address = stake_pool.validator_list)] + pub validator_list: AccountInfo<'info>, + + #[account(mut, address = get_config_authority(&config)?)] + pub authority: Signer<'info>, +} + +/* + Resets steward state account to its initial state. +*/ +pub fn handler(ctx: Context) -> Result<()> { + let mut state_account = ctx.accounts.state_account.load_mut()?; + + let clock = Clock::get()?; + state_account.is_initialized = true.into(); + state_account.bump = ctx.bumps.state_account; + + let config = ctx.accounts.config.load()?; + let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; + let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + + state_account.state.state_tag = StewardStateEnum::ComputeScores; + state_account.state.num_pool_validators = validator_list.len() as usize; + state_account.state.scores = [0; MAX_VALIDATORS]; + state_account.state.sorted_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; + state_account.state.yield_scores = [0; MAX_VALIDATORS]; + state_account.state.sorted_yield_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; + state_account.state.progress = BitMask::default(); + state_account.state.current_epoch = clock.epoch; + state_account.state.next_cycle_epoch = clock + .epoch + .checked_add(config.parameters.num_epochs_between_scoring) + .ok_or(StewardError::ArithmeticError)?; + state_account.state.delegations = [Delegation::default(); MAX_VALIDATORS]; + state_account.state.rebalance_completed = false.into(); + state_account.state.instant_unstake = BitMask::default(); + state_account.state.start_computing_scores_slot = clock.slot; + state_account.state._padding0 = [0; 6 + MAX_VALIDATORS * 8]; + Ok(()) +} diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 493e622e..3fcd5f6a 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -161,6 +161,20 @@ pub mod steward { instructions::update_parameters::handler(ctx, &update_parameters_args) } + /* TEMPORARY ADMIN INSTRUCTIONS for testing */ + + /// Resets steward state account to its initial state. + pub fn reset_steward_state(ctx: Context) -> Result<()> { + instructions::reset_steward_state::handler(ctx) + } + + /// Closes Steward PDA accounts associated with a given Config (StewardStateAccount, and Staker). + /// Config is not closed as it is a Keypair, so lamports can simply be withdrawn. + /// Reclaims lamports to authority + pub fn close_steward_accounts(ctx: Context) -> Result<()> { + instructions::close_steward_accounts::handler(ctx) + } + /* Passthrough instructions to spl-stake-pool, where the signer is Staker. Must be invoked by `config.authority` */ pub fn set_staker(ctx: Context) -> Result<()> { From 0d8debe4c436485119e55082ccd43ba57afcefd2 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 5 Jun 2024 16:53:49 -0400 Subject: [PATCH 02/18] new program id --- Anchor.toml | 2 +- programs/steward/src/instructions/reset_steward_state.rs | 2 -- programs/steward/src/lib.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index c99a48ce..970e14b9 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -3,7 +3,7 @@ resolution = false skip-lint = false [programs.localnet] validator_history = "HistoryJTGbKQD2mRgLZ3XhqHnN811Qpez8X9kCcGHoa" -steward = "Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8" +steward = "sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP" [workspace] members = [ diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs index a2f84542..6b9bdfa2 100644 --- a/programs/steward/src/instructions/reset_steward_state.rs +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -19,8 +19,6 @@ pub struct ResetStewardState<'info> { pub config: AccountLoader<'info, Config>, - pub system_program: Program<'info, System>, - #[account(address = get_stake_pool(&config)?)] pub stake_pool: Account<'info, StakePool>, diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 3fcd5f6a..bed7bea1 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -16,7 +16,7 @@ pub mod utils; pub use state::*; -declare_id!("Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8"); +declare_id!("sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP"); /* This program manages the selection of validators and delegation of stake for a SPL Stake Pool. From 98e44ff1a2f16d168eb75dd7e423409463fab2b0 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:13:06 -0600 Subject: [PATCH 03/18] FIX: Skip reordering scored indexes if index DNE (#45) Co-authored-by: Christian --- programs/steward/src/state/steward_state.rs | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index e141a9ed..eab3f46a 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -443,22 +443,24 @@ impl StewardState { let yield_score_index = self .sorted_yield_score_indices .iter() - .position(|&i| i == index as u16) - .ok_or(StewardError::ValidatorIndexOutOfBounds)?; + .position(|&i| i == index as u16); let score_index = self .sorted_score_indices .iter() - .position(|&i| i == index as u16) - .ok_or(StewardError::ValidatorIndexOutOfBounds)?; + .position(|&i| i == index as u16); - for i in yield_score_index..num_pool_validators { - let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; - self.sorted_yield_score_indices[i] = self.sorted_yield_score_indices[next_i]; + if let Some(yield_score_index) = yield_score_index { + for i in yield_score_index..self.num_pool_validators { + let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; + self.sorted_yield_score_indices[i] = self.sorted_yield_score_indices[next_i]; + } } - for i in score_index..num_pool_validators { - let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; - self.sorted_score_indices[i] = self.sorted_score_indices[next_i]; + if let Some(score_index) = score_index { + for i in score_index..self.num_pool_validators { + let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; + self.sorted_score_indices[i] = self.sorted_score_indices[next_i]; + } } for i in 0..num_pool_validators { From d3d3d873547eeb7e445f536c8ff37fd617d175f6 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:40:11 -0600 Subject: [PATCH 04/18] Christian/epoch update (#47) Starting to add the solution for keeping track of removed validators --------- Co-authored-by: Christian --- programs/steward/src/errors.rs | 10 ++ .../auto_add_validator_to_pool.rs | 22 ++- .../auto_remove_validator_from_pool.rs | 10 +- .../src/instructions/compute_delegations.rs | 5 + .../instructions/compute_instant_unstake.rs | 6 + .../steward/src/instructions/compute_score.rs | 16 ++- .../src/instructions/epoch_maintenance.rs | 95 +++++++++++++ programs/steward/src/instructions/idle.rs | 5 + programs/steward/src/instructions/mod.rs | 2 + .../steward/src/instructions/realloc_state.rs | 7 +- .../steward/src/instructions/rebalance.rs | 5 + .../src/instructions/reset_steward_state.rs | 7 +- .../src/instructions/spl_passthrough.rs | 36 ++++- programs/steward/src/lib.rs | 8 ++ programs/steward/src/state/steward_state.rs | 134 +++++++++++++++--- programs/steward/src/utils.rs | 39 ++++- tests/src/steward_fixtures.rs | 32 ++++- tests/tests/steward/test_integration.rs | 51 ++++++- tests/tests/steward/test_spl_passthrough.rs | 33 ++++- tests/tests/steward/test_state_methods.rs | 69 +++++++-- tests/tests/steward/test_steward.rs | 1 + 21 files changed, 532 insertions(+), 61 deletions(-) create mode 100644 programs/steward/src/instructions/epoch_maintenance.rs diff --git a/programs/steward/src/errors.rs b/programs/steward/src/errors.rs index 7380a4ef..6554baf9 100644 --- a/programs/steward/src/errors.rs +++ b/programs/steward/src/errors.rs @@ -50,4 +50,14 @@ pub enum StewardError { MaxValidatorsReached, #[msg("Validator history account does not match vote account")] ValidatorHistoryMismatch, + #[msg("Epoch Maintenance must be called before continuing")] + EpochMaintenanceNotComplete, + #[msg("The stake pool must be updated before continuing")] + StakePoolNotUpdated, + #[msg("Validator not marked for removal")] + ValidatorNotMarkedForRemoval, + #[msg("Validators have not been removed")] + ValidatorsHaveNotBeenRemoved, + #[msg("Validator List count does not match state machine")] + ListStateMismatch, } diff --git a/programs/steward/src/instructions/auto_add_validator_to_pool.rs b/programs/steward/src/instructions/auto_add_validator_to_pool.rs index 8ecb5b47..123e620f 100644 --- a/programs/steward/src/instructions/auto_add_validator_to_pool.rs +++ b/programs/steward/src/instructions/auto_add_validator_to_pool.rs @@ -1,6 +1,6 @@ use crate::constants::{MAX_VALIDATORS, STAKE_POOL_WITHDRAW_SEED}; use crate::errors::StewardError; -use crate::state::{Config, Staker}; +use crate::state::{Config, Staker, StewardStateAccount}; use crate::utils::{deserialize_stake_pool, get_stake_pool_address}; use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; @@ -10,6 +10,15 @@ use validator_history::state::ValidatorHistory; #[derive(Accounts)] pub struct AutoAddValidator<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub steward_state: AccountLoader<'info, StewardStateAccount>, + // Only adding validators where this exists #[account( seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -18,8 +27,6 @@ pub struct AutoAddValidator<'info> { )] pub validator_history_account: AccountLoader<'info, ValidatorHistory>, - pub config: AccountLoader<'info, Config>, - /// CHECK: CPI address #[account( address = spl_stake_pool::ID @@ -102,10 +109,17 @@ all the validators we want to be eligible for delegation, as well as to accept s Performs some eligibility checks in order to not fill up the validator list with offline or malicious validators. */ pub fn handler(ctx: Context) -> Result<()> { + let mut state_account = ctx.accounts.steward_state.load_mut()?; let config = ctx.accounts.config.load()?; let validator_history = ctx.accounts.validator_history_account.load()?; let epoch = Clock::get()?.epoch; + // Should not be able to add a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + { let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; @@ -140,6 +154,8 @@ pub fn handler(ctx: Context) -> Result<()> { return Err(StewardError::ValidatorBelowLivenessMinimum.into()); } + state_account.state.increment_validator_to_add()?; + invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), 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 15b8b67d..fa4bd358 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -143,6 +143,12 @@ pub fn handler(ctx: Context, validator_list_index: usize) - StewardError::ValidatorNotInList ); + // Should not be able to remove a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + // Checks state for deactivate delinquent status, preventing pool from merging stake with activating let stake_account_deactivated = { let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); @@ -164,7 +170,9 @@ pub fn handler(ctx: Context, validator_list_index: usize) - StewardError::ValidatorNotRemovable ); - state_account.state.remove_validator(validator_list_index)?; + state_account + .state + .mark_validator_for_removal(validator_list_index)?; invoke_signed( &spl_stake_pool::instruction::remove_validator_from_pool( diff --git a/programs/steward/src/instructions/compute_delegations.rs b/programs/steward/src/instructions/compute_delegations.rs index 679144df..33f29b78 100644 --- a/programs/steward/src/instructions/compute_delegations.rs +++ b/programs/steward/src/instructions/compute_delegations.rs @@ -28,6 +28,11 @@ pub fn handler(ctx: Context) -> Result<()> { let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + if config.is_paused() { return Err(StewardError::StateMachinePaused.into()); } diff --git a/programs/steward/src/instructions/compute_instant_unstake.rs b/programs/steward/src/instructions/compute_instant_unstake.rs index 9bcd64f4..75eb282f 100644 --- a/programs/steward/src/instructions/compute_instant_unstake.rs +++ b/programs/steward/src/instructions/compute_instant_unstake.rs @@ -49,6 +49,11 @@ pub fn handler(ctx: Context, validator_list_index: usize) StewardError::ValidatorNotInList ); + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + if config.is_paused() { return Err(StewardError::StateMachinePaused.into()); } @@ -61,6 +66,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) &cluster, &config, )?; + maybe_transition_and_emit( &mut state_account.state, &clock, diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index 81125003..c93f618a 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -1,8 +1,9 @@ use anchor_lang::prelude::*; -use spl_stake_pool::state::ValidatorListHeader; use crate::{ - errors::StewardError, maybe_transition_and_emit, utils::get_validator_stake_info_at_index, + errors::StewardError, + maybe_transition_and_emit, + utils::{get_validator_list_length, get_validator_stake_info_at_index}, Config, StewardStateAccount, StewardStateEnum, }; use validator_history::{ClusterHistory, ValidatorHistory}; @@ -51,11 +52,12 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul StewardError::ValidatorNotInList ); - let num_pool_validators = { - let mut validator_list_data = validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - validator_list.len() as u64 - }; + let num_pool_validators = get_validator_list_length(validator_list)?; + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); if config.is_paused() { return Err(StewardError::StateMachinePaused.into()); diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs new file mode 100644 index 00000000..480a3157 --- /dev/null +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -0,0 +1,95 @@ +use crate::{ + errors::StewardError, + utils::{ + check_validator_list_has_stake_status, get_stake_pool, get_validator_list_length, StakePool, + }, + Config, StewardStateAccount, +}; +use anchor_lang::prelude::*; +use spl_stake_pool::state::StakeStatus; + +#[derive(Accounts)] +pub struct EpochMaintenance<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + #[account(mut, address = stake_pool.validator_list)] + pub validator_list: AccountInfo<'info>, + + #[account( + address = get_stake_pool(&config)? + )] + pub stake_pool: Account<'info, StakePool>, +} + +/// Runs maintenance tasks at the start of each epoch, needs to be run multiple times +/// Routines: +/// - Remove delinquent validators +pub fn handler( + ctx: Context, + validator_index_to_remove: Option, +) -> Result<()> { + let stake_pool = &ctx.accounts.stake_pool; + let mut state_account = ctx.accounts.state_account.load_mut()?; + + let clock = Clock::get()?; + + require!( + clock.epoch == stake_pool.last_update_epoch, + StewardError::StakePoolNotUpdated + ); + + if (!state_account.state.checked_validators_removed_from_list).into() { + // Ensure there are no validators in the list that have not been removed, that should be + require!( + !check_validator_list_has_stake_status( + &ctx.accounts.validator_list, + StakeStatus::ReadyForRemoval + )?, + StewardError::ValidatorsHaveNotBeenRemoved + ); + state_account.state.checked_validators_removed_from_list = true.into(); + } + + { + // Routine - Remove marked validators + // We still want these checks to run even if we don't specify a validator to remove + let validators_in_list = get_validator_list_length(&ctx.accounts.validator_list)?; + let validators_to_remove = state_account.state.validators_to_remove.count(); + + // Ensure we have a 1-1 mapping between the number of validators in the list and the number of validators in the state + require!( + state_account.state.num_pool_validators + state_account.state.validators_added as usize + - validators_to_remove + == validators_in_list, + StewardError::ListStateMismatch + ); + + if let Some(validator_index_to_remove) = validator_index_to_remove { + state_account + .state + .remove_validator(validator_index_to_remove)?; + } + } + + { + // Routine - Update state + let okay_to_update = state_account.state.validators_to_remove.is_empty() + && state_account + .state + .checked_validators_removed_from_list + .into(); + if okay_to_update { + state_account.state.current_epoch = clock.epoch; + state_account.state.checked_validators_removed_from_list = false.into(); + } + } + + Ok(()) +} diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index 4d647612..47d594aa 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -33,6 +33,11 @@ pub fn handler(ctx: Context) -> Result<()> { StewardError::InvalidState ); + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + if config.is_paused() { return Err(StewardError::StateMachinePaused.into()); } diff --git a/programs/steward/src/instructions/mod.rs b/programs/steward/src/instructions/mod.rs index ec849977..ad9c7c2c 100644 --- a/programs/steward/src/instructions/mod.rs +++ b/programs/steward/src/instructions/mod.rs @@ -6,6 +6,7 @@ pub mod close_steward_accounts; pub mod compute_delegations; pub mod compute_instant_unstake; pub mod compute_score; +pub mod epoch_maintenance; pub mod idle; pub mod initialize_config; pub mod initialize_state; @@ -26,6 +27,7 @@ pub use close_steward_accounts::*; pub use compute_delegations::*; pub use compute_instant_unstake::*; pub use compute_score::*; +pub use epoch_maintenance::*; pub use idle::*; pub use initialize_config::*; pub use initialize_state::*; diff --git a/programs/steward/src/instructions/realloc_state.rs b/programs/steward/src/instructions/realloc_state.rs index fadaf40c..1f543815 100644 --- a/programs/steward/src/instructions/realloc_state.rs +++ b/programs/steward/src/instructions/realloc_state.rs @@ -3,7 +3,7 @@ use crate::{ constants::{MAX_ALLOC_BYTES, MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, errors::StewardError, state::{Config, StewardStateAccount}, - Delegation, StewardStateEnum, + Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, }; use anchor_lang::prelude::*; use spl_stake_pool::state::ValidatorListHeader; @@ -89,7 +89,10 @@ pub fn handler(ctx: Context) -> Result<()> { state_account.state.rebalance_completed = false.into(); state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; - state_account.state._padding0 = [0; 6 + MAX_VALIDATORS * 8]; + state_account.state.validators_to_remove = BitMask::default(); + state_account.state.validators_added = 0; + state_account.state.checked_validators_removed_from_list = false.into(); + state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; } Ok(()) diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index 92ef5a46..4f6e0ff6 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -161,6 +161,11 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( ); let transient_seed = u64::from(validator_stake_info.transient_seed_suffix); + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + if config.is_paused() { return Err(StewardError::StateMachinePaused.into()); } diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs index 6b9bdfa2..0140f7d6 100644 --- a/programs/steward/src/instructions/reset_steward_state.rs +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -3,7 +3,7 @@ use crate::{ errors::StewardError, state::{Config, StewardStateAccount}, utils::{get_config_authority, get_stake_pool, StakePool}, - BitMask, Delegation, StewardStateEnum, + BitMask, Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, }; use anchor_lang::prelude::*; use spl_stake_pool::state::ValidatorListHeader; @@ -59,6 +59,9 @@ pub fn handler(ctx: Context) -> Result<()> { state_account.state.rebalance_completed = false.into(); state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; - state_account.state._padding0 = [0; 6 + MAX_VALIDATORS * 8]; + state_account.state.validators_to_remove = BitMask::default(); + state_account.state.validators_added = 0; + state_account.state.checked_validators_removed_from_list = false.into(); + state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; Ok(()) } diff --git a/programs/steward/src/instructions/spl_passthrough.rs b/programs/steward/src/instructions/spl_passthrough.rs index ed4224a8..fb6771d0 100644 --- a/programs/steward/src/instructions/spl_passthrough.rs +++ b/programs/steward/src/instructions/spl_passthrough.rs @@ -23,6 +23,13 @@ use validator_history::ValidatorHistory; #[derive(Accounts)] pub struct AddValidatorToPool<'info> { pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub steward_state: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -73,6 +80,15 @@ pub fn add_validator_to_pool_handler( ctx: Context, validator_seed: Option, ) -> Result<()> { + let mut state_account = ctx.accounts.steward_state.load_mut()?; + let epoch = Clock::get()?.epoch; + + // Should not be able to add a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + { let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; @@ -81,6 +97,9 @@ pub fn add_validator_to_pool_handler( return Err(StewardError::MaxValidatorsReached.into()); } } + + state_account.state.increment_validator_to_add()?; + invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), @@ -168,6 +187,13 @@ pub fn remove_validator_from_pool_handler( validator_list_index: usize, ) -> Result<()> { let mut state_account = ctx.accounts.steward_state.load_mut()?; + let epoch = Clock::get()?.epoch; + + // Should not be able to remove a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); if validator_list_index < state_account.state.num_pool_validators as usize { let validator_list_stake_info = get_validator_stake_info_at_index( @@ -185,10 +211,12 @@ pub fn remove_validator_from_pool_handler( if validator_list_stake_account != ctx.accounts.stake_account.key() { return Err(StewardError::ValidatorNotInList.into()); } - - state_account.state.remove_validator(validator_list_index)?; } + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + invoke_signed( &spl_stake_pool::instruction::remove_validator_from_pool( &ctx.accounts.stake_pool_program.key(), @@ -307,7 +335,7 @@ pub struct IncreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut)] + #[account(mut, address = stake_pool.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account( @@ -436,7 +464,7 @@ pub struct DecreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut)] + #[account(mut, address = stake_pool.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account( diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index bed7bea1..4f9578b7 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -91,6 +91,14 @@ pub mod steward { instructions::auto_remove_validator_from_pool::handler(ctx, validator_list_index as usize) } + /// Housekeeping, run at the start of any new epoch before any other instructions + pub fn epoch_maintenance( + ctx: Context, + validator_index_to_remove: Option, + ) -> Result<()> { + instructions::epoch_maintenance::handler(ctx, validator_index_to_remove) + } + /// Computes score for a the validator at `validator_list_index` for the current cycle. pub fn compute_score(ctx: Context, validator_list_index: u64) -> Result<()> { instructions::compute_score::handler(ctx, validator_list_index as usize) diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index eab3f46a..1a575641 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -35,26 +35,28 @@ pub struct StateTransition { } pub fn maybe_transition_and_emit( - state_account: &mut StewardState, + steward_state: &mut StewardState, clock: &Clock, params: &Parameters, epoch_schedule: &EpochSchedule, ) -> Result<()> { - let initial_state = state_account.state_tag.to_string(); - state_account.transition(clock, params, epoch_schedule)?; - if initial_state != state_account.state_tag.to_string() { + let initial_state = steward_state.state_tag.to_string(); + steward_state.transition(clock, params, epoch_schedule)?; + + if initial_state != steward_state.state_tag.to_string() { emit!(StateTransition { epoch: clock.epoch, slot: clock.slot, previous_state: initial_state, - new_state: state_account.state_tag.to_string(), + new_state: steward_state.state_tag.to_string(), }); } Ok(()) } /// Tracks state of the stake pool. -/// Follow state transitions here: [TODO add link to github diagram] +/// Follow state transitions here: +/// https://github.com/jito-foundation/stakenet/blob/master/programs/steward/state-machine-diagram.png #[derive(BorshSerialize)] #[zero_copy] pub struct StewardState { @@ -87,6 +89,10 @@ pub struct StewardState { /// Tracks progress of states that require one instruction per validator pub progress: BitMask, + /// Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool + /// This is cleaned up in the next epoch + pub validators_to_remove: BitMask, + ////// Cycle metadata fields ////// /// Slot of the first ComputeScores instruction in the current cycle pub start_computing_scores_slot: u64, @@ -110,17 +116,25 @@ pub struct StewardState { /// Total lamports that have been due to stake deposits this cycle pub stake_deposit_unstake_total: u64, + /// Number of validators added to the pool in the current cycle + pub validators_added: u16, + /// Tracks whether delegation computation has been completed pub compute_delegations_completed: U8Bool, /// Tracks whether unstake and delegate steps have completed pub rebalance_completed: U8Bool, + /// So we only have to check the validator list once for `ReadyToRemove` + pub checked_validators_removed_from_list: U8Bool, + /// Future state and #[repr(C)] alignment - pub _padding0: [u8; 6 + MAX_VALIDATORS * 8], + pub _padding0: [u8; STATE_PADDING_0_SIZE], // TODO ADD MORE PADDING } +pub const STATE_PADDING_0_SIZE: usize = MAX_VALIDATORS * 8 + 3; + #[derive(Clone, Copy)] #[repr(u64)] pub enum StewardStateEnum { @@ -239,6 +253,7 @@ impl StewardState { let current_epoch = clock.epoch; let current_slot = clock.slot; let epoch_progress = epoch_progress(clock, epoch_schedule)?; + match self.state_tag { StewardStateEnum::ComputeScores => self.transition_compute_scores( current_epoch, @@ -308,7 +323,6 @@ impl StewardState { )?; } else if self.compute_delegations_completed.into() { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; self.rebalance_completed = false.into(); } Ok(()) @@ -356,7 +370,6 @@ impl StewardState { )?; } else if current_epoch > self.current_epoch { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; self.instant_unstake = BitMask::default(); self.progress = BitMask::default(); } else if self.progress.is_complete(self.num_pool_validators)? { @@ -382,12 +395,10 @@ impl StewardState { )?; } else if current_epoch > self.current_epoch { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; self.progress = BitMask::default(); self.rebalance_completed = false.into(); } else if self.progress.is_complete(self.num_pool_validators)? { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; self.rebalance_completed = true.into(); } Ok(()) @@ -404,7 +415,6 @@ impl StewardState { self.scores = [0; MAX_VALIDATORS]; self.yield_scores = [0; MAX_VALIDATORS]; self.progress = BitMask::default(); - self.current_epoch = current_epoch; self.next_cycle_epoch = current_epoch .checked_add(num_epochs_between_scoring) .ok_or(StewardError::ArithmeticError)?; @@ -421,6 +431,11 @@ impl StewardState { /// Update internal state when a validator is removed from the pool pub fn remove_validator(&mut self, index: usize) -> Result<()> { + require!( + self.validators_to_remove.get(index)?, + StewardError::ValidatorNotMarkedForRemoval + ); + self.num_pool_validators = self .num_pool_validators .checked_sub(1) @@ -437,6 +452,8 @@ impl StewardState { self.instant_unstake .set(i, self.instant_unstake.get(next_i)?)?; self.progress.set(i, self.progress.get(next_i)?)?; + self.validators_to_remove + .set(i, self.validators_to_remove.get(next_i)?)?; } // Update score indices @@ -477,18 +494,37 @@ impl StewardState { } // Clear values on empty last index - self.validator_lamport_balances[num_pool_validators] = 0; - self.scores[num_pool_validators] = 0; - self.yield_scores[num_pool_validators] = 0; - self.sorted_score_indices[num_pool_validators] = SORTED_INDEX_DEFAULT; - self.sorted_yield_score_indices[num_pool_validators] = SORTED_INDEX_DEFAULT; - self.delegations[num_pool_validators] = Delegation::default(); - self.instant_unstake.set(num_pool_validators, false)?; - self.progress.set(num_pool_validators, false)?; + self.validator_lamport_balances[self.num_pool_validators] = 0; + self.scores[self.num_pool_validators] = 0; + self.yield_scores[self.num_pool_validators] = 0; + self.sorted_score_indices[self.num_pool_validators] = SORTED_INDEX_DEFAULT; + self.sorted_yield_score_indices[self.num_pool_validators] = SORTED_INDEX_DEFAULT; + self.delegations[self.num_pool_validators] = Delegation::default(); + self.instant_unstake.set(self.num_pool_validators, false)?; + self.validators_to_remove + .set(self.num_pool_validators, false)?; + self.progress.set(self.num_pool_validators, false)?; Ok(()) } + /// Mark a validator for removal from the pool - this happens right after + /// `remove_validator_from_pool` has been called on the stake pool + /// This is cleaned up in the next epoch + pub fn mark_validator_for_removal(&mut self, index: usize) -> Result<()> { + self.validators_to_remove.set(index, true) + } + + /// Called when adding a validator to the pool so that we can ensure a 1-1 mapping between + /// the validator list and the steward state + pub fn increment_validator_to_add(&mut self) -> Result<()> { + self.validators_added = self + .validators_added + .checked_add(1) + .ok_or(StewardError::ArithmeticError)?; + Ok(()) + } + /// One instruction per validator. Can be done in any order. /// Computes score for a validator for the current epoch, stores score, and yield score component. /// Inserts this validator's index into sorted_score_indices and sorted_yield_score_indices, sorted by @@ -544,7 +580,42 @@ impl StewardState { config.parameters.num_epochs_between_scoring, )?; // Updates num_pool_validators at the start of the cycle so validator additions later won't be considered + + require!( + num_pool_validators + == self.num_pool_validators + self.validators_added as usize, + StewardError::ListStateMismatch + ); self.num_pool_validators = num_pool_validators; + self.validators_added = 0; + } + + // Skip scoring if already processed + if self.progress.get(index)? { + return Ok(()); + } + + // Skip scoring if marked for deletion + if self.validators_to_remove.get(index)? { + self.scores[index] = 0_u32; + self.yield_scores[index] = 0_u32; + + let num_scores_calculated = self.progress.count(); + insert_sorted_index( + &mut self.sorted_score_indices, + &self.scores, + index as u16, + self.scores[index], + num_scores_calculated, + )?; + insert_sorted_index( + &mut self.sorted_yield_score_indices, + &self.yield_scores, + index as u16, + self.yield_scores[index], + num_scores_calculated, + )?; + return Ok(()); } let score = validator_score(validator, index, cluster, config, current_epoch as u16)?; @@ -646,6 +717,17 @@ impl StewardState { return Err(StewardError::InstantUnstakeNotReady.into()); } + // Skip if already processed + if self.progress.get(index)? { + return Ok(()); + } + + // Skip if marked for deletion + if self.validators_to_remove.get(index)? { + self.progress.set(index, true)?; + return Ok(()); + } + let first_slot = epoch_schedule.get_first_slot_in_epoch(clock.epoch); // Epoch credits and cluster history must be updated in the current epoch and after the midpoint of the epoch @@ -715,6 +797,18 @@ impl StewardState { self.state_tag.to_string(), )); } + + // Skip if already processed + if self.progress.get(index)? { + return Ok(RebalanceType::None); + } + + // Skip if marked for deletion + if self.validators_to_remove.get(index)? { + self.progress.set(index, true)?; + return Ok(RebalanceType::None); + } + let base_lamport_balance = minimum_delegation .checked_add(stake_rent) .ok_or(StewardError::ArithmeticError)?; diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index 96f281fd..78d52735 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -5,7 +5,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64, solana_program::program_pack::Pack}; use spl_stake_pool::{ big_vec::BigVec, - state::{ValidatorListHeader, ValidatorStakeInfo}, + state::{StakeStatus, ValidatorListHeader, ValidatorStakeInfo}, }; use crate::{errors::StewardError, Config, Delegation}; @@ -86,6 +86,43 @@ pub fn get_validator_stake_info_at_index( Ok(validator_stake_info) } +pub fn check_validator_list_has_stake_status( + validator_list_account_info: &AccountInfo, + flag: StakeStatus, +) -> 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)?; + require!( + header.account_type == spl_stake_pool::state::AccountType::ValidatorList, + StewardError::ValidatorListTypeMismatch + ); + + for index in 0..validator_list.len() as usize { + let stake_status_index = VEC_SIZE_BYTES + .saturating_add(index.saturating_mul(ValidatorStakeInfo::LEN)) + .checked_add(40) + .ok_or(StewardError::ArithmeticError)?; + + let stake_status = validator_list.data[stake_status_index]; + + if stake_status == flag as u8 { + return Ok(true); + } + } + + Ok(false) +} + +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)?; + require!( + header.account_type == spl_stake_pool::state::AccountType::ValidatorList, + StewardError::ValidatorListTypeMismatch + ); + Ok(validator_list.len() as usize) +} + /// A boolean type stored as a u8. #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] #[zero_copy] diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index f7a97489..1ca223b4 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::{cell::RefCell, rc::Rc, str::FromStr, vec}; use crate::spl_stake_pool_cli; @@ -15,7 +16,7 @@ use jito_steward::{ constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT, STAKE_POOL_WITHDRAW_SEED}, utils::StakePool, Config, Delegation, Parameters, Staker, StewardState, StewardStateAccount, StewardStateEnum, - UpdateParametersArgs, + UpdateParametersArgs, STATE_PADDING_0_SIZE, }; use solana_program_test::*; use solana_sdk::{ @@ -178,6 +179,30 @@ impl TestFixture { account } + pub async fn simulate_stake_pool_update(&self) { + let stake_pool: StakePool = self + .load_and_deserialize(&self.stake_pool_meta.stake_pool) + .await; + + let mut stake_pool_spl = stake_pool.as_ref().clone(); + + let current_epoch = self + .ctx + .borrow_mut() + .banks_client + .get_sysvar::() + .await + .unwrap() + .epoch; + + stake_pool_spl.last_update_epoch = current_epoch; + + self.ctx.borrow_mut().set_account( + &self.stake_pool_meta.stake_pool, + &serialized_stake_pool_account(stake_pool_spl, std::mem::size_of::()).into(), + ); + } + pub async fn initialize_stake_pool(&self) { // Call command_create_pool and execute transactions responded let mint = Keypair::new(); @@ -929,7 +954,10 @@ impl Default for StateMachineFixtures { instant_unstake: BitMask::default(), compute_delegations_completed: false.into(), rebalance_completed: false.into(), - _padding0: [0; 6 + 8 * MAX_VALIDATORS], + validators_added: 0, + checked_validators_removed_from_list: false.into(), + validators_to_remove: BitMask::default(), + _padding0: [0; STATE_PADDING_0_SIZE], }; StateMachineFixtures { diff --git a/tests/tests/steward/test_integration.rs b/tests/tests/steward/test_integration.rs index 8e158ef6..e8dcbc00 100644 --- a/tests/tests/steward/test_integration.rs +++ b/tests/tests/steward/test_integration.rs @@ -245,6 +245,7 @@ async fn test_compute_scores() { steward_state_account.state.current_epoch = clock.epoch; steward_state_account.state.next_cycle_epoch = clock.epoch + steward_config.parameters.num_epochs_between_scoring; + // steward_state_account.state.validators_added = MAX_VALIDATORS as u16; // Setup validator list let mut validator_list_validators = (0..MAX_VALIDATORS) @@ -284,6 +285,23 @@ async fn test_compute_scores() { &serialized_config(steward_config).into(), ); + fixture.simulate_stake_pool_update().await; + + let epoch_maintenance_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(), + }; + // Basic test - test score computation that requires most compute let compute_scores_ix = Instruction { program_id: jito_steward::id(), @@ -305,8 +323,9 @@ async fn test_compute_scores() { let tx = Transaction::new_signed_with_payer( &[ // Only high because we are averaging 512 epochs - ComputeBudgetInstruction::set_compute_unit_limit(600_000), + ComputeBudgetInstruction::set_compute_unit_limit(800_000), ComputeBudgetInstruction::request_heap_frame(128 * 1024), + epoch_maintenance_ix.clone(), compute_scores_ix.clone(), ], Some(&fixture.keypair.pubkey()), @@ -316,6 +335,8 @@ async fn test_compute_scores() { fixture.submit_transaction_assert_success(tx).await; + println!("Okay!"); + let mut steward_state_account: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; @@ -332,6 +353,30 @@ async fn test_compute_scores() { // Transition out of this state // Reset current state, set progress[1] to true, progress[0] to false + + { + // Reset Validator List, such that there are only 2 validators + let mut validator_list_validators = (0..2) + .map(|_| ValidatorStakeInfo { + vote_account_address: Pubkey::new_unique(), + ..ValidatorStakeInfo::default() + }) + .collect::>(); + validator_list_validators[0].vote_account_address = vote_account; + let validator_list = spl_stake_pool::state::ValidatorList { + header: ValidatorListHeader { + account_type: AccountType::ValidatorList, + max_validators: MAX_VALIDATORS as u32, + }, + validators: validator_list_validators, + }; + + fixture.ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account(validator_list, None).into(), + ); + } + steward_state_account.state.num_pool_validators = 2; steward_state_account.state.scores[..2].copy_from_slice(&[0, 0]); steward_state_account.state.yield_scores[..2].copy_from_slice(&[0, 0]); @@ -919,6 +964,7 @@ async fn test_rebalance_increase() { let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, validator_history_account: validator_history_address, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), @@ -1151,6 +1197,8 @@ async fn test_rebalance_decrease() { let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, + validator_history_account: validator_history_address, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), @@ -1392,6 +1440,7 @@ async fn test_rebalance_other_cases() { let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, validator_history_account: validator_history_address, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), diff --git a/tests/tests/steward/test_spl_passthrough.rs b/tests/tests/steward/test_spl_passthrough.rs index a3448572..b0a7b080 100644 --- a/tests/tests/steward/test_spl_passthrough.rs +++ b/tests/tests/steward/test_spl_passthrough.rs @@ -158,10 +158,28 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { let (stake_account_address, _, withdraw_authority) = fixture.stake_accounts_for_validator(vote_account).await; + fixture.simulate_stake_pool_update().await; + + let epoch_maintenance_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(), + }; + // Add Validator let instruction = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AddValidatorToPool { + 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, @@ -189,7 +207,7 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { let latest_blockhash = _get_latest_blockhash(fixture).await; let transaction = Transaction::new_signed_with_payer( - &[instruction], + &[epoch_maintenance_ix, instruction], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], latest_blockhash, @@ -500,7 +518,7 @@ async fn test_add_validator_to_pool() { } { - // Add 5 validators + // Add 10 validators for _ in 0..10 { _add_test_validator(&fixture, Pubkey::new_unique()).await; } @@ -518,7 +536,7 @@ async fn test_remove_validator_from_pool() { fixture.initialize_steward_state().await; // Setup the steward state - _setup_test_steward_state(&fixture, MAX_VALIDATORS, 1_000_000_000).await; + // _setup_test_steward_state(&fixture, MAX_VALIDATORS, 1_000_000_000).await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -805,6 +823,7 @@ async fn test_decrease_additional_validator_stake() { let validator_list_account_raw = fixture .get_account(&fixture.stake_pool_meta.validator_list) .await; + let validator_list_account: ValidatorList = ValidatorList::try_deserialize_unchecked(&mut validator_list_account_raw.data.as_slice()) .expect("Failed to deserialize validator list account"); @@ -814,14 +833,20 @@ async fn test_decrease_additional_validator_stake() { .get(validator_list_index) .expect("Validator is not in list"); + println!("4"); + let vote_account = validator_to_increase_stake.vote_account_address; let (stake_account_address, transient_stake_account_address, withdraw_authority) = fixture.stake_accounts_for_validator(vote_account).await; + println!("5"); + _simulate_stake_deposit(&fixture, stake_account_address, 2_000_000_000).await; + println!("6"); let validator_history = fixture.initialize_validator_history_with_credits(vote_account, validator_list_index); + println!("7"); let (ephemeral_stake_account, _) = find_ephemeral_stake_program_address( &spl_stake_pool::id(), @@ -861,6 +886,7 @@ async fn test_decrease_additional_validator_stake() { }; let latest_blockhash = _get_latest_blockhash(&fixture).await; + println!("8"); let transaction = Transaction::new_signed_with_payer( &[instruction], @@ -869,6 +895,7 @@ async fn test_decrease_additional_validator_stake() { latest_blockhash, ); fixture.submit_transaction_assert_success(transaction).await; + println!("9"); drop(fixture); } diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index 6ec24a1e..da660681 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -71,6 +71,7 @@ fn test_compute_scores() { assert!(state.current_epoch == current_epoch); // Test invalid state + state.progress.reset(); state.state_tag = StewardStateEnum::Idle; let res = state.compute_score( clock, @@ -187,25 +188,26 @@ fn test_compute_scores() { assert!(res.is_ok()); assert!(state.start_computing_scores_slot == clock.slot); assert!(state.next_cycle_epoch == current_epoch + parameters.num_epochs_between_scoring); - assert!(state.current_epoch == current_epoch); assert!(state.num_pool_validators == 4); // 2) Progress stalled and time moved into next epoch // Conditions: clock.epoch > state.current_epoch and !state.progress.is_empty() - state.current_epoch = current_epoch - 1; - assert!(!state.progress.is_empty()); - assert!(state.current_epoch < clock.epoch); - let res = state.compute_score( - clock, - epoch_schedule, - &validators[0], - validators[0].index as usize, - cluster_history, - config, - state.num_pool_validators, - ); - assert!(res.is_ok()); - assert!(state.current_epoch == current_epoch); + // REDACTED: The epoch is now updated in the epoch_maintenance method + + // state.current_epoch = current_epoch - 1; + // assert!(!state.progress.is_empty()); + // assert!(state.current_epoch < clock.epoch); + // let res = state.compute_score( + // clock, + // epoch_schedule, + // &validators[0], + // validators[0].index as usize, + // cluster_history, + // config, + // state.num_pool_validators, + // ); + // assert!(res.is_ok()); + // assert!(state.current_epoch == current_epoch); // 3) Progress started, but took >1000 slots to complete // Conditions: start_computing_scores_slot > 1000 slots ago, !progress.is_empty(), and clock.epoch == state.current_epoch @@ -225,6 +227,8 @@ fn test_compute_scores() { state.num_pool_validators, ); assert!(res.is_ok()); + println!("{:?}", state.start_computing_scores_slot); + println!("{:?}", clock.slot); assert!(state.start_computing_scores_slot == clock.slot); } @@ -427,7 +431,19 @@ fn test_compute_instant_unstake_success() { .get(validators[0].index as usize) .unwrap()); + // Should skip validator since it's already been computed + let res = state.compute_instant_unstake( + clock, + epoch_schedule, + &validators[0], + validators[0].index as usize, + cluster_history, + config, + ); + assert!(res.is_ok()); + // Instant unstakeable validator + state.progress.reset(); state.instant_unstake.reset(); config .blacklist @@ -450,6 +466,7 @@ fn test_compute_instant_unstake_success() { // Instant unstakeable validator with no delegation amount state.delegations[validators[0].index as usize] = Delegation::new(0, 1); + state.progress.reset(); state.instant_unstake.reset(); let res = state.compute_instant_unstake( clock, @@ -571,6 +588,24 @@ fn test_rebalance() { _ => panic!("Expected RebalanceType::Decrease"), } + // Test that rebalance will be skipped if validator has already been run + let res = state.rebalance( + fixtures.current_epoch, + 1, + &validator_list_bigvec, + 4000 * LAMPORTS_PER_SOL, + 1000 * LAMPORTS_PER_SOL, + 0, + 0, + &fixtures.config.parameters, + ); + + assert!(res.is_ok()); + match res.unwrap() { + RebalanceType::None => {} + _ => panic!("Expected RebalanceType::None"), + } + // Instant unstake validator, but no delegation, so other delegations are not affected // Same scenario as above but out-of-band validator state.delegations[0..3].copy_from_slice(&[ @@ -589,6 +624,7 @@ fn test_rebalance() { // Validator index 1: 1000 SOL, 0.5 score, 0 delegation, -> Decrease stake, from "instant unstake" category, and set delegation to 0 // Validator index 2: 1000 SOL, 0 score, 0 delegation -> Decrease stake, from "regular unstake" category + state.progress.reset(); let res = state.rebalance( fixtures.current_epoch, 1, @@ -639,6 +675,8 @@ fn test_rebalance() { state.sorted_yield_score_indices[0..3].copy_from_slice(&[0, 1, 2]); state.instant_unstake.reset(); state.instant_unstake.set(0, true).unwrap(); + + state.progress.reset(); let res = state.rebalance( fixtures.current_epoch, 0, @@ -670,6 +708,7 @@ fn test_rebalance() { state.scores[0..3].copy_from_slice(&[1_000_000_000, 1_000_000_000, 1_000_000_000]); state.sorted_score_indices[0..3].copy_from_slice(&[0, 1, 2]); state.sorted_yield_score_indices[0..3].copy_from_slice(&[0, 1, 2]); + state.progress.reset(); let res = state.rebalance( fixtures.current_epoch, 0, diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 5e7673d1..6f759cb0 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -41,6 +41,7 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, validator_history_account, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), From 2124bbe10d50f2397f11011766cca1c53c955e7e Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:58:31 -0600 Subject: [PATCH 05/18] CLI + FIXES: Steward CLI (#49) This PR adds the Steward CLI that can create and crank through the entire Steward state machine. It also introduces some fixes to the Steward program. --- Cargo.lock | 35 +- Cargo.toml | 2 +- keepers/keeper-core/src/lib.rs | 1 + .../src/instructions/epoch_maintenance.rs | 1 + programs/steward/src/score.rs | 5 +- programs/steward/src/state/steward_state.rs | 37 +- utils/steward-cli/Cargo.toml | 26 ++ utils/steward-cli/initial_notes.md | 160 ++++++++ .../actions/auto_add_validator_from_pool.rs | 109 ++++++ .../auto_remove_validator_from_pool.rs | 119 ++++++ utils/steward-cli/src/commands/actions/mod.rs | 5 + .../commands/actions/remove_bad_validators.rs | 158 ++++++++ .../src/commands/actions/reset_state.rs | 75 ++++ .../src/commands/actions/update_config.rs | 73 ++++ .../steward-cli/src/commands/command_args.rs | 367 ++++++++++++++++++ .../commands/cranks/compute_delegations.rs | 79 ++++ .../cranks/compute_instant_unstake.rs | 119 ++++++ .../src/commands/cranks/compute_score.rs | 126 ++++++ .../src/commands/cranks/epoch_maintenance.rs | 79 ++++ utils/steward-cli/src/commands/cranks/idle.rs | 65 ++++ utils/steward-cli/src/commands/cranks/mod.rs | 6 + .../src/commands/cranks/rebalance.rs | 148 +++++++ utils/steward-cli/src/commands/info/mod.rs | 3 + .../src/commands/info/view_config.rs | 137 +++++++ .../info/view_next_index_to_remove.rs | 43 ++ .../src/commands/info/view_state.rs | 181 +++++++++ .../src/commands/init/init_config.rs | 104 +++++ .../src/commands/init/init_state.rs | 211 ++++++++++ utils/steward-cli/src/commands/init/mod.rs | 2 + utils/steward-cli/src/commands/mod.rs | 5 + utils/steward-cli/src/main.rs | 82 ++++ utils/steward-cli/src/utils/accounts.rs | 160 ++++++++ utils/steward-cli/src/utils/mod.rs | 2 + utils/steward-cli/src/utils/transactions.rs | 113 ++++++ 34 files changed, 2815 insertions(+), 23 deletions(-) create mode 100644 utils/steward-cli/Cargo.toml create mode 100644 utils/steward-cli/initial_notes.md create mode 100644 utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs create mode 100644 utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs create mode 100644 utils/steward-cli/src/commands/actions/mod.rs create mode 100644 utils/steward-cli/src/commands/actions/remove_bad_validators.rs create mode 100644 utils/steward-cli/src/commands/actions/reset_state.rs create mode 100644 utils/steward-cli/src/commands/actions/update_config.rs create mode 100644 utils/steward-cli/src/commands/command_args.rs create mode 100644 utils/steward-cli/src/commands/cranks/compute_delegations.rs create mode 100644 utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs create mode 100644 utils/steward-cli/src/commands/cranks/compute_score.rs create mode 100644 utils/steward-cli/src/commands/cranks/epoch_maintenance.rs create mode 100644 utils/steward-cli/src/commands/cranks/idle.rs create mode 100644 utils/steward-cli/src/commands/cranks/mod.rs create mode 100644 utils/steward-cli/src/commands/cranks/rebalance.rs create mode 100644 utils/steward-cli/src/commands/info/mod.rs create mode 100644 utils/steward-cli/src/commands/info/view_config.rs create mode 100644 utils/steward-cli/src/commands/info/view_next_index_to_remove.rs create mode 100644 utils/steward-cli/src/commands/info/view_state.rs create mode 100644 utils/steward-cli/src/commands/init/init_config.rs create mode 100644 utils/steward-cli/src/commands/init/init_state.rs create mode 100644 utils/steward-cli/src/commands/init/mod.rs create mode 100644 utils/steward-cli/src/commands/mod.rs create mode 100644 utils/steward-cli/src/main.rs create mode 100644 utils/steward-cli/src/utils/accounts.rs create mode 100644 utils/steward-cli/src/utils/mod.rs create mode 100644 utils/steward-cli/src/utils/transactions.rs diff --git a/Cargo.lock b/Cargo.lock index 13282434..09dcba52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,9 +360,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "aquamarine" @@ -1617,6 +1617,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "downcast" version = "0.11.0" @@ -6112,6 +6118,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "steward-cli" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anyhow", + "clap 4.4.18", + "dotenv", + "futures", + "futures-util", + "jito-steward", + "keeper-core", + "log", + "solana-account-decoder", + "solana-clap-utils", + "solana-client", + "solana-metrics", + "solana-program", + "solana-sdk", + "spl-stake-pool", + "thiserror", + "tokio", + "validator-history", +] + [[package]] name = "strsim" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index d30a17ab..d7498e84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,8 @@ members = [ "keepers/*", "programs/*", + "utils/*", "tests", - "utils/*" ] [profile.release] diff --git a/keepers/keeper-core/src/lib.rs b/keepers/keeper-core/src/lib.rs index dfd0cd58..91351ee0 100644 --- a/keepers/keeper-core/src/lib.rs +++ b/keepers/keeper-core/src/lib.rs @@ -394,6 +394,7 @@ pub async fn parallel_execute_transactions( // Future optimization: submit these in parallel batches and refresh blockhash for every batch match client.send_transaction(tx).await { Ok(signature) => { + println!("Submitted transaction: {:?}", signature); submitted_signatures.insert(signature, idx); } Err(e) => match e.get_transaction_error() { diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 480a3157..4cf1bd30 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -88,6 +88,7 @@ pub fn handler( if okay_to_update { state_account.state.current_epoch = clock.epoch; state_account.state.checked_validators_removed_from_list = false.into(); + state_account.state.rebalance_completed = false.into(); } } diff --git a/programs/steward/src/score.rs b/programs/steward/src/score.rs index d0e04e2c..d492110c 100644 --- a/programs/steward/src/score.rs +++ b/programs/steward/src/score.rs @@ -287,10 +287,7 @@ pub fn instant_unstake_validator( .checked_sub(epoch_start_slot) .ok_or(StewardError::ArithmeticError)?; - let vote_credits_rate = validator - .history - .epoch_credits_latest() - .ok_or(StewardError::VoteHistoryNotRecentEnough)? as f64 + let vote_credits_rate = validator.history.epoch_credits_latest().unwrap_or(0) as f64 / validator_history_slot_index as f64; let delinquency_check = if blocks_produced_rate > 0. { diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 1a575641..6ad1c68d 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -347,6 +347,8 @@ impl StewardState { } else if (!self.rebalance_completed).into() && epoch_progress >= min_epoch_progress_for_instant_unstake { + //NOTE: rebalance_completed is cleared on epoch change in `epoch_maintenance` + self.state_tag = StewardStateEnum::ComputeInstantUnstake; self.instant_unstake = BitMask::default(); self.progress = BitMask::default(); @@ -546,22 +548,6 @@ impl StewardState { let current_epoch = clock.epoch; let current_slot = clock.slot; - // Check that latest_update_slot is within the current epoch to guarantee previous epoch data is complete - let last_update_slot = validator - .history - .vote_account_last_update_slot_latest() - .ok_or(StewardError::VoteHistoryNotRecentEnough)?; - if last_update_slot < epoch_schedule.get_first_slot_in_epoch(current_epoch) { - return Err(StewardError::VoteHistoryNotRecentEnough.into()); - } - - // Check that cluster history is within current epoch to guarantee previous epoch data is complete - if cluster.cluster_history_last_update_slot - < epoch_schedule.get_first_slot_in_epoch(current_epoch) - { - return Err(StewardError::ClusterHistoryNotRecentEnough.into()); - } - /* Reset common state if: - it's a new delegation cycle - it's been more than `compute_score_slot_range` slots since compute scores started @@ -615,9 +601,28 @@ impl StewardState { self.yield_scores[index], num_scores_calculated, )?; + + self.progress.set(index, true)?; + return Ok(()); } + // Check that latest_update_slot is within the current epoch to guarantee previous epoch data is complete + let last_update_slot = validator + .history + .vote_account_last_update_slot_latest() + .ok_or(StewardError::VoteHistoryNotRecentEnough)?; + if last_update_slot < epoch_schedule.get_first_slot_in_epoch(current_epoch) { + return Err(StewardError::VoteHistoryNotRecentEnough.into()); + } + + // Check that cluster history is within current epoch to guarantee previous epoch data is complete + if cluster.cluster_history_last_update_slot + < epoch_schedule.get_first_slot_in_epoch(current_epoch) + { + return Err(StewardError::ClusterHistoryNotRecentEnough.into()); + } + let score = validator_score(validator, index, cluster, config, current_epoch as u16)?; emit!(score); diff --git a/utils/steward-cli/Cargo.toml b/utils/steward-cli/Cargo.toml new file mode 100644 index 00000000..4a9e9e70 --- /dev/null +++ b/utils/steward-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "steward-cli" +version = "0.1.0" +edition = "2021" +description = "CLI to manage the steward program" + +[dependencies] +anchor-lang = "0.30.0" +clap = { version = "4.3.0", features = ["derive", "env"] } +futures = "0.3.21" +futures-util = "0.3.21" +log = "0.4.18" +solana-account-decoder = "1.18" +solana-clap-utils = "1.18" +solana-client = "1.18" +solana-metrics = "1.18" +solana-program = "1.18" +solana-sdk = "1.18" +thiserror = "1.0.37" +spl-stake-pool = { features = ["no-entrypoint"], version = "1.0.0" } +tokio = { version = "1.36.0", features = ["full"] } +validator-history = { features = ["no-entrypoint"], path = "../../programs/validator-history" } +jito-steward = { features = ["no-entrypoint"], path = "../../programs/steward" } +keeper-core = { path = "../../keepers/keeper-core" } +dotenv = "0.15.0" +anyhow = "1.0.86" diff --git a/utils/steward-cli/initial_notes.md b/utils/steward-cli/initial_notes.md new file mode 100644 index 00000000..fe3ab76a --- /dev/null +++ b/utils/steward-cli/initial_notes.md @@ -0,0 +1,160 @@ + +# Accounts + +**Authority** +`aaaDerwdMyzNkoX1aSoTi3UtFe2W45vh5wCgQNhsjF8` + +**Steward Config** +`6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5` + +**Stake Pool** +`3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j` + +**Staker** +`4m64H5TbwAGtZVnxaGAVoTSwjZGV8BCLKRPr8agKQv4Z` + +**State** +`6SJrBTYSSu3jWmsPWWhMMHvrPxqKWXtLe9tRfYpU8EZa` + +# Initial Commands + +## Create Config +```bash +cargo run init-config \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config-keypair-path ../../credentials/steward_config.json \ + --stake-pool 3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j \ + --mev-commission-range 10 \ + --epoch-credits-range 30 \ + --commission-range 30 \ + --mev-commission-bps-threshold 1000 \ + --commission-threshold 5 \ + --historical-commission-threshold 50 \ + --scoring-delinquency-threshold-ratio 0.85 \ + --instant-unstake-delinquency-threshold-ratio 0.70 \ + --num-delegation-validators 200 \ + --scoring-unstake-cap-bps 750 \ + --instant-unstake-cap-bps 1000 \ + --stake-deposit-unstake-cap-bps 1000 \ + --compute-score-slot-range 50000 \ + --instant-unstake-epoch-progress 0.50 \ + --instant-unstake-inputs-epoch-progress 0.50 \ + --num-epochs-between-scoring 3 \ + --minimum-stake-lamports 100000000000 \ + --minimum-voting-epochs 5 +``` + +## Update Config +```bash +cargo run update-config \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 \ + --instant-unstake-inputs-epoch-progress 0.10 \ + --instant-unstake-epoch-progress 0.10 +``` + +## Create State +```bash +cargo run init-state --authority-keypair-path ../../credentials/stakenet_test.json --stake-pool 3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 +``` + +## View Config +```bash +cargo run view-config --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 +``` + +## View State +```bash +cargo run view-state --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 +``` + +## View State Per Validator +```bash +cargo run view-state-per-validator --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 +``` + +## View Next Index To Remove +```bash +cargo run view-next-index-to-remove --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 +``` + +## Auto Remove Validator +```bash +cargo run auto-remove-validator-from-pool --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json --validator-index-to-remove 1397 +``` + +## Auto Add Validator +```bash +cargo run auto-add-validator-from-pool --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json --vote-account 4m64H5TbwAGtZVnxaGAVoTSwjZGV8BCLKRPr8agKQv4Z +``` + +## Remove Bad Validators +```bash +cargo run remove-bad-validators --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Epoch Maintenance +```bash +cargo run crank-epoch-maintenance --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Compute Score +```bash +cargo run crank-compute-score --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Compute Delegations +```bash +cargo run crank-compute-delegations --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Idle +```bash +cargo run crank-idle --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Compute Instant Unstake +```bash +cargo run crank-compute-instant-unstake --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Rebalance +```bash +cargo run crank-rebalance --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json +``` + +# Deploy and Upgrade + +- upgrade solana cli to 1.18.16 +- make sure your configured keypair is `aaaDerwdMyzNkoX1aSoTi3UtFe2W45vh5wCgQNhsjF8` +- create a new keypair: `solana-keygen new -o credentials/temp-buffer.json` +- use anchor `0.30.0`: `avm install 0.30.0 && avm use 0.30.0` +- build .so file: `anchor build --no-idl` +- Write to buffer: `solana program write-buffer --use-rpc --buffer credentials/temp-buffer.json --url $(solana config get | grep "RPC URL" | awk '{print $3}') --with-compute-unit-price 10000 --max-sign-attempts 10000 target/deploy/jito_steward.so --keypair credentials/stakenet_test.json` +- Upgrade: `solana program upgrade $(solana address --keypair credentials/temp-buffer.json) sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP --keypair credentials/stakenet_test.json --url $(solana config get | grep "RPC URL" | awk '{print $3}')` +- Close Buffers: `solana program close --buffers --keypair credentials/stakenet_test.json` +- Upgrade Program Size: `solana program extend sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP 1000000 --keypair credentials/stakenet_test.json --url $(solana config get | grep "RPC URL" | awk '{print $3}')` + +# Initial Parameters + +```env +# Note - Do not use this .env when updating the parameters - this will update them all +MEV_COMMISSION_RANGE=10 +EPOCH_CREDITS_RANGE=30 +COMMISSION_RANGE=30 +MEV_COMMISSION_BPS_THRESHOLD=1000 +COMMISSION_THRESHOLD=5 +HISTORICAL_COMMISSION_THRESHOLD=50 +SCORING_DELINQUENCY_THRESHOLD_RATIO=0.85 +INSTANT_UNSTAKE_DELINQUENCY_THRESHOLD_RATIO=0.70 +NUM_DELEGATION_VALIDATORS=200 +SCORING_UNSTAKE_CAP_BPS=750 +INSTANT_UNSTAKE_CAP_BPS=1000 +STAKE_DEPOSIT_UNSTAKE_CAP_BPS=1000 +COMPUTE_SCORE_SLOT_RANGE=1000 +INSTANT_UNSTAKE_EPOCH_PROGRESS=0.50 +INSTANT_UNSTAKE_INPUTS_EPOCH_PROGRESS=0.50 +NUM_EPOCHS_BETWEEN_SCORING=3 +MINIMUM_STAKE_LAMPORTS=100000000000 +MINIMUM_VOTING_EPOCHS=5 +``` \ No newline at end of file diff --git a/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs new file mode 100644 index 00000000..e0b7206c --- /dev/null +++ b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use spl_stake_pool::find_stake_program_address; +use validator_history::id as validator_history_id; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, + transaction::Transaction, +}; + +use crate::{ + commands::command_args::AutoAddValidatorFromPool, + utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::configure_instruction, + }, +}; + +pub async fn command_auto_add_validator_from_pool( + args: AutoAddValidatorFromPool, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.permissionless_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let vote_account = args.vote_account; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_accounts.stake_pool_address, + None, + ); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::AutoAddValidator { + validator_history_account: history_account, + steward_state: steward_accounts.state_address, + config: args.permissionless_parameters.steward_config, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + staker: steward_accounts.staker_address, + reserve_stake: steward_accounts.stake_pool_account.reserve_stake, + withdraw_authority: steward_accounts.stake_pool_withdraw_authority, + validator_list: steward_accounts.validator_list_address, + stake_account: stake_address, + 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(), + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent blockhash"); + + let configured_ix = configure_instruction( + &[ix], + args.permissionless_parameters + .transaction_parameters + .priority_fee, + args.permissionless_parameters + .transaction_parameters + .compute_limit, + args.permissionless_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await + .expect("Failed to send transaction"); + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs new file mode 100644 index 00000000..6b80c248 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; +use validator_history::id as validator_history_id; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, + transaction::Transaction, +}; + +use crate::{ + commands::command_args::AutoRemoveValidatorFromPool, + utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::configure_instruction, + }, +}; + +pub async fn command_auto_remove_validator_from_pool( + args: AutoRemoveValidatorFromPool, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let validator_index = args.validator_index_to_remove; + let args = args.permissionless_parameters; + + // Creates config account + let payer = Arc::new( + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let vote_account = + steward_accounts.validator_list_account.validators[validator_index].vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_accounts.stake_pool_address, + None, + ); + + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_accounts.stake_pool_address, + steward_accounts.validator_list_account.validators[validator_index] + .transient_seed_suffix + .into(), + ); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::AutoRemoveValidator { + validator_history_account: history_account, + config: args.steward_config, + state_account: steward_accounts.state_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + staker: steward_accounts.staker_address, + reserve_stake: steward_accounts.stake_pool_account.reserve_stake, + withdraw_authority: steward_accounts.stake_pool_withdraw_authority, + validator_list: steward_accounts.validator_list_address, + stake_account: stake_address, + transient_stake_account: transient_stake_address, + 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(), + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoRemoveValidatorFromPool { + validator_list_index: validator_index, + } + .data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent blockhash"); + + let configured_ix = configure_instruction( + &[ix], + args.transaction_parameters.priority_fee, + args.transaction_parameters.compute_limit, + args.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await + .expect("Failed to send transaction"); + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/mod.rs b/utils/steward-cli/src/commands/actions/mod.rs new file mode 100644 index 00000000..26b60d27 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/mod.rs @@ -0,0 +1,5 @@ +pub mod auto_add_validator_from_pool; +pub mod auto_remove_validator_from_pool; +pub mod remove_bad_validators; +pub mod reset_state; +pub mod update_config; diff --git a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs new file mode 100644 index 00000000..40a6bb07 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use keeper_core::{get_multiple_accounts_batched, submit_transactions}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; +use validator_history::id as validator_history_id; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, sysvar, +}; + +use crate::{ + commands::command_args::RemoveBadValidators, + utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::package_instructions, + }, +}; + +pub async fn command_remove_bad_validators( + args: RemoveBadValidators, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Payer )"); + let arc_payer = Arc::new(payer); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.permissioned_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let validators_to_run = (0..steward_accounts.validator_list_account.validators.len()) + .filter_map(|validator_index| { + let has_been_scored = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_scored { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::>(); + + let history_accounts = validators_to_run + .iter() + .map(|(_, _, history_account)| *history_account) + .collect::>(); + + let validator_history_accounts = + get_multiple_accounts_batched(&history_accounts, client).await?; + + let bad_history_accounts = validator_history_accounts + .iter() + .zip(validators_to_run) + .filter_map( + |(account, (index, vote_account, history_account))| match account { + Some(_) => None, + None => Some((index, vote_account, history_account)), + }, + ) + .collect::>(); + + println!("Bad history accounts: {:?}", bad_history_accounts); + + let ixs_to_run = bad_history_accounts + .iter() + .map(|(validator_index, vote_account, history_account)| { + println!( + "index: {}, vote_account: {}, history_account: {}\n", + validator_index, vote_account, history_account + ); + + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + vote_account, + &steward_accounts.stake_pool_address, + None, + ); + + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + vote_account, + &steward_accounts.stake_pool_address, + steward_accounts.validator_list_account.validators[*validator_index] + .transient_seed_suffix + .into(), + ); + + Instruction { + program_id, + accounts: jito_steward::accounts::RemoveValidatorFromPool { + signer: arc_payer.pubkey(), + config: steward_config, + steward_state: steward_accounts.state_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + staker: steward_accounts.stake_pool_account.staker, + withdraw_authority: steward_accounts.stake_pool_withdraw_authority, + validator_list: steward_accounts.validator_list_address, + stake_account: stake_address, + transient_stake_account: transient_stake_address, + clock: sysvar::clock::id(), + system_program: system_program::id(), + stake_program: stake::program::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::RemoveValidatorFromPool { + validator_list_index: *validator_index, + } + .data(), + } + }) + .collect::>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissioned_parameters + .transaction_parameters + .chunk_size + .unwrap_or(1), + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit + .or(Some(1_400_000)), + args.permissioned_parameters + .transaction_parameters + .heap_size + .or(Some(256 * 1024)), + ); + + println!("Submitting {} instructions", ixs_to_run.len()); + + let submit_stats = submit_transactions(client, txs_to_run, &arc_payer).await?; + + println!("Submit stats: {:?}", submit_stats); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/reset_state.rs b/utils/steward-cli/src/commands/actions/reset_state.rs new file mode 100644 index 00000000..501f2b3a --- /dev/null +++ b/utils/steward-cli/src/commands/actions/reset_state.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::{ + commands::command_args::ResetState, + utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +}; + +pub async fn command_reset_state( + args: ResetState, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let all_steward_accounts = get_all_steward_accounts( + client, + &program_id, + &args.permissioned_parameters.steward_config, + ) + .await?; + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::ResetStewardState { + state_account: all_steward_accounts.state_address, + config: args.permissioned_parameters.steward_config, + stake_pool: all_steward_accounts.stake_pool_address, + validator_list: all_steward_accounts.validator_list_address, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ResetStewardState {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/update_config.rs b/utils/steward-cli/src/commands/actions/update_config.rs new file mode 100644 index 00000000..61bc74c0 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/update_config.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::UpdateParametersArgs; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::{commands::command_args::UpdateConfig, utils::transactions::configure_instruction}; + +pub async fn command_update_config( + args: UpdateConfig, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let steward_config = args.permissioned_parameters.steward_config; + + let update_parameters_args: UpdateParametersArgs = args.config_parameters.into(); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::UpdateParameters { + config: steward_config, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::UpdateParameters { + update_parameters_args, + } + .data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent blockhash"); + + let configured_ix = configure_instruction( + &[ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await + .expect("Failed to send transaction"); + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/command_args.rs b/utils/steward-cli/src/commands/command_args.rs new file mode 100644 index 00000000..7a3e6aea --- /dev/null +++ b/utils/steward-cli/src/commands/command_args.rs @@ -0,0 +1,367 @@ +use clap::{arg, command, Parser, Subcommand}; +use jito_steward::UpdateParametersArgs; +use solana_sdk::pubkey::Pubkey; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(about = "CLI for the steward program")] +pub struct Args { + /// RPC URL for the cluster + #[arg( + short, + long, + env, + default_value = "https://api.mainnet-beta.solana.com" + )] + pub json_rpc_url: String, + + /// Steward program ID + #[arg( + long, + env, + default_value_t = jito_steward::id() + )] + pub program_id: Pubkey, + + #[command(subcommand)] + pub commands: Commands, +} + +// ---------- Meta Parameters ------------ +#[derive(Parser)] +pub struct ConfigParameters { + /// Number of recent epochs used to evaluate MEV commissions and running Jito for scoring + #[arg(long, env)] + pub mev_commission_range: Option, + + /// Number of recent epochs used to evaluate yield + #[arg(long, env)] + pub epoch_credits_range: Option, + + /// Number of recent epochs used to evaluate commissions for scoring + #[arg(long, env)] + pub commission_range: Option, + + /// Minimum ratio of slots voted on for each epoch for a validator to be eligible for stake. Used as proxy for validator reliability/restart timeliness. Ratio is number of epoch_credits / blocks_produced + #[arg(long, env)] + pub scoring_delinquency_threshold_ratio: Option, + + /// Same as scoring_delinquency_threshold_ratio but evaluated every epoch + #[arg(long, env)] + pub instant_unstake_delinquency_threshold_ratio: Option, + + /// Maximum allowable MEV commission in mev_commission_range (stored in basis points) + #[arg(long, env)] + pub mev_commission_bps_threshold: Option, + + /// Maximum allowable validator commission in commission_range (stored in percent) + #[arg(long, env)] + pub commission_threshold: Option, + + /// Maximum allowable validator commission in all history (stored in percent) + #[arg(long, env)] + pub historical_commission_threshold: Option, + + /// Number of validators who are eligible for stake (validator set size) + #[arg(long, env)] + pub num_delegation_validators: Option, + + /// Percent of total pool lamports that can be unstaked due to new delegation set (in basis points) + #[arg(long, env)] + pub scoring_unstake_cap_bps: Option, + + /// Percent of total pool lamports that can be unstaked due to instant unstaking (in basis points) + #[arg(long, env)] + pub instant_unstake_cap_bps: Option, + + /// Percent of total pool lamports that can be unstaked due to stake deposits above target lamports (in basis points) + #[arg(long, env)] + pub stake_deposit_unstake_cap_bps: Option, + + /// Scoring window such that the validators are all scored within a similar timeframe (in slots) + #[arg(long, env)] + pub compute_score_slot_range: Option, + + /// Point in epoch progress before instant unstake can be computed + #[arg(long, env)] + pub instant_unstake_epoch_progress: Option, + + /// Inputs to “Compute Instant Unstake” need to be updated past this point in epoch progress + #[arg(long, env)] + pub instant_unstake_inputs_epoch_progress: Option, + + /// Cycle length - Number of epochs to run the Monitor->Rebalance loop + #[arg(long, env)] + pub num_epochs_between_scoring: Option, + + /// Minimum number of stake lamports for a validator to be considered for the pool + #[arg(long, env)] + pub minimum_stake_lamports: Option, + + /// Minimum number of consecutive epochs a validator has to vote before it can be considered for the pool + #[arg(long, env)] + pub minimum_voting_epochs: Option, +} + +impl From for UpdateParametersArgs { + fn from(config: ConfigParameters) -> Self { + UpdateParametersArgs { + mev_commission_range: config.mev_commission_range, + epoch_credits_range: config.epoch_credits_range, + commission_range: config.commission_range, + scoring_delinquency_threshold_ratio: config.scoring_delinquency_threshold_ratio, + instant_unstake_delinquency_threshold_ratio: config + .instant_unstake_delinquency_threshold_ratio, + mev_commission_bps_threshold: config.mev_commission_bps_threshold, + commission_threshold: config.commission_threshold, + historical_commission_threshold: config.historical_commission_threshold, + num_delegation_validators: config.num_delegation_validators, + scoring_unstake_cap_bps: config.scoring_unstake_cap_bps, + instant_unstake_cap_bps: config.instant_unstake_cap_bps, + stake_deposit_unstake_cap_bps: config.stake_deposit_unstake_cap_bps, + compute_score_slot_range: config.compute_score_slot_range, + instant_unstake_epoch_progress: config.instant_unstake_epoch_progress, + instant_unstake_inputs_epoch_progress: config.instant_unstake_inputs_epoch_progress, + num_epochs_between_scoring: config.num_epochs_between_scoring, + minimum_stake_lamports: config.minimum_stake_lamports, + minimum_voting_epochs: config.minimum_voting_epochs, + } + } +} + +#[derive(Parser)] +pub struct TransactionParameters { + /// priority fee in microlamports + #[arg(long, env)] + pub priority_fee: Option, + + /// CUs per transaction + #[arg(long, env)] + pub compute_limit: Option, + + /// Heap size for heap frame + #[arg(long, env)] + pub heap_size: Option, + + /// Amount of instructions to process in a single transaction + #[arg(long, env)] + pub chunk_size: Option, +} + +#[derive(Parser)] +pub struct PermissionlessParameters { + /// Path to keypair used to pay for the transaction + #[arg(short, long, env, default_value = "~/.config/solana/id.json")] + pub payer_keypair_path: PathBuf, + + /// Steward config account + #[arg(long, env)] + pub steward_config: Pubkey, + + #[command(flatten)] + pub transaction_parameters: TransactionParameters, +} + +#[derive(Parser)] +pub struct PermissionedParameters { + /// Authority keypair path, also used as payer + #[arg(short, long, env, default_value = "~/.config/solana/id.json")] + pub authority_keypair_path: PathBuf, + + // Steward config account + #[arg(long, env)] + pub steward_config: Pubkey, + + #[command(flatten)] + pub transaction_parameters: TransactionParameters, +} + +#[derive(Parser)] +pub struct ViewParameters { + /// Steward account + #[arg(long, env)] + pub steward_config: Pubkey, +} + +// ---------- COMMANDS ------------ +#[derive(Subcommand)] +pub enum Commands { + // Views + ViewState(ViewState), + ViewConfig(ViewConfig), + ViewNextIndexToRemove(ViewNextIndexToRemove), + + // Actions + InitConfig(InitConfig), + UpdateConfig(UpdateConfig), + + InitState(InitState), + ResetState(ResetState), + + RemoveBadValidators(RemoveBadValidators), + AutoRemoveValidatorFromPool(AutoRemoveValidatorFromPool), + AutoAddValidatorFromPool(AutoAddValidatorFromPool), + + // Cranks + CrankEpochMaintenance(CrankEpochMaintenance), + CrankComputeScore(CrankComputeScore), + CrankComputeDelegations(CrankComputeDelegations), + CrankIdle(CrankIdle), + CrankComputeInstantUnstake(CrankComputeInstantUnstake), + CrankRebalance(CrankRebalance), +} + +// ---------- VIEWS ------------ +#[derive(Parser)] +#[command(about = "View the steward state")] +pub struct ViewState { + #[command(flatten)] + pub view_parameters: ViewParameters, + + /// Views the steward state for all validators in the pool + #[arg(short, long)] + pub verbose: bool, +} + +#[derive(Parser)] +#[command(about = "View the current steward config account")] +pub struct ViewConfig { + #[command(flatten)] + pub view_parameters: ViewParameters, +} + +#[derive(Parser)] +#[command(about = "View the next index to remove in in the `epoch_maintenance` call")] +pub struct ViewNextIndexToRemove { + #[command(flatten)] + pub view_parameters: ViewParameters, +} + +// ---------- ACTIONS ------------ + +#[derive(Parser)] +#[command(about = "Initialize config account")] +pub struct InitConfig { + /// Path to keypair used to pay for account creation and execute transactions + #[arg(short, long, env, default_value = "~/.config/solana/id.json")] + pub authority_keypair_path: PathBuf, + + /// The current staker keypair path, defaults to the authority keypair path + #[arg(short, long, env)] + pub staker_keypair_path: Option, + + /// Optional path to Steward Config keypair, if not provided, a new keypair will be created + #[arg(long, env)] + pub steward_config_keypair_path: Option, + + /// Stake pool pubkey + #[arg(long, env)] + pub stake_pool: Pubkey, + + #[command(flatten)] + pub transaction_parameters: TransactionParameters, + + #[command(flatten)] + pub config_parameters: ConfigParameters, +} + +#[derive(Parser)] +#[command(about = "Updates config account parameters")] +pub struct UpdateConfig { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, + + #[command(flatten)] + pub config_parameters: ConfigParameters, +} + +#[derive(Parser)] +#[command(about = "Initialize state account")] +pub struct InitState { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Reset steward state")] +pub struct ResetState { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Removes bad validators from the pool")] +pub struct RemoveBadValidators { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Calls `auto_remove_validator_from_pool`")] +pub struct AutoRemoveValidatorFromPool { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator index of validator list to remove + #[arg(long, env)] + pub validator_index_to_remove: usize, +} + +#[derive(Parser)] +#[command(about = "Calls `auto_add_validator_from_pool`")] +pub struct AutoAddValidatorFromPool { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator vote account to add + #[arg(long, env)] + pub vote_account: Pubkey, +} + +// ---------- CRANKS ------------ + +#[derive(Parser)] +#[command(about = "Run epoch maintenance - needs to be run at the start of each epoch")] +pub struct CrankEpochMaintenance { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator index to remove, gotten from `validators_to_remove` Bitmask + #[arg(long, env)] + pub validator_index_to_remove: Option, +} + +#[derive(Parser)] +#[command(about = "Crank `compute_score` state")] +pub struct CrankComputeScore { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `compute_delegations` state")] +pub struct CrankComputeDelegations { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `idle` state")] +pub struct CrankIdle { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `compute_instant_unstake` state")] +pub struct CrankComputeInstantUnstake { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `rebalance` state")] +pub struct CrankRebalance { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} diff --git a/utils/steward-cli/src/commands/cranks/compute_delegations.rs b/utils/steward-cli/src/commands/cranks/compute_delegations.rs new file mode 100644 index 00000000..d26183cd --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/compute_delegations.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::{ + commands::command_args::CrankComputeDelegations, + utils::{accounts::get_steward_state_account, transactions::configure_instruction}, +}; + +pub async fn command_crank_compute_delegations( + args: CrankComputeDelegations, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let args = args.permissionless_parameters; + + // Creates config account + let payer = + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"); + + let steward_config = args.steward_config; + + let (state_account, state_address) = + get_steward_state_account(client, &program_id, &steward_config).await?; + + match state_account.state.state_tag { + StewardStateEnum::ComputeDelegations => { /* Continue */ } + _ => { + println!( + "State account is not in Compute Delegation state: {}", + state_account.state.state_tag + ); + return Ok(()); + } + } + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::ComputeDelegations { + config: steward_config, + state_account: state_address, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeDelegations {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[ix], + args.transaction_parameters.priority_fee, + args.transaction_parameters.compute_limit, + args.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs new file mode 100644 index 00000000..b9aad99b --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use validator_history::id as validator_history_id; + +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, signer::Signer}; + +use crate::{ + commands::command_args::CrankComputeInstantUnstake, + utils::{ + accounts::{ + get_all_steward_accounts, get_cluster_history_address, get_validator_history_address, + }, + transactions::{package_instructions, submit_packaged_transactions}, + }, +}; + +pub async fn command_crank_compute_instant_unstake( + args: CrankComputeInstantUnstake, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.permissionless_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::ComputeInstantUnstake => { /* Continue */ } + _ => { + println!( + "State account is not in Compute Instant Unstake state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) + .filter_map(|validator_index| { + let has_been_scored = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_scored { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::>(); + + let cluster_history = get_cluster_history_address(&validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|(validator_index, _, history_account)| Instruction { + program_id, + accounts: jito_steward::accounts::ComputeInstantUnstake { + config: steward_config, + state_account: steward_accounts.state_address, + validator_history: *history_account, + validator_list: steward_accounts.validator_list_address, + cluster_history, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeInstantUnstake { + validator_list_index: *validator_index, + } + .data(), + }) + .collect::>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissionless_parameters + .transaction_parameters + .chunk_size + .unwrap_or(15), + args.permissionless_parameters + .transaction_parameters + .priority_fee, + args.permissionless_parameters + .transaction_parameters + .compute_limit + .or(Some(1_400_000)), + args.permissionless_parameters + .transaction_parameters + .heap_size, + ); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let submit_stats = submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + + println!("Submit stats: {:?}", submit_stats); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/compute_score.rs b/utils/steward-cli/src/commands/cranks/compute_score.rs new file mode 100644 index 00000000..f8ce4dcb --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/compute_score.rs @@ -0,0 +1,126 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use validator_history::id as validator_history_id; + +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, signer::Signer}; + +use crate::{ + commands::command_args::CrankComputeScore, + utils::{ + accounts::{ + get_all_steward_accounts, get_cluster_history_address, get_validator_history_address, + }, + transactions::{package_instructions, submit_packaged_transactions}, + }, +}; + +pub async fn command_crank_compute_score( + args: CrankComputeScore, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.permissionless_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::ComputeScores => { /* Continue */ } + _ => { + println!( + "State account is not in ComputeScores state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) + .filter_map(|validator_index| { + let has_been_scored = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_scored { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::>(); + + let cluster_history = get_cluster_history_address(&validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|(validator_index, vote_account, history_account)| { + println!( + "index: {}, vote_account: {}, history_account: {}\n", + validator_index, vote_account, history_account + ); + + Instruction { + program_id, + accounts: jito_steward::accounts::ComputeScore { + config: steward_config, + state_account: steward_accounts.state_address, + validator_history: *history_account, + validator_list: steward_accounts.validator_list_address, + cluster_history, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeScore { + validator_list_index: *validator_index, + } + .data(), + } + }) + .collect::>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissionless_parameters + .transaction_parameters + .chunk_size + .unwrap_or(11), + args.permissionless_parameters + .transaction_parameters + .priority_fee, + args.permissionless_parameters + .transaction_parameters + .compute_limit + .or(Some(1_400_000)), + args.permissionless_parameters + .transaction_parameters + .heap_size, + ); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let submit_stats = submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + + println!("Submit stats: {:?}", submit_stats); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs b/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs new file mode 100644 index 00000000..e5a01644 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::{ + commands::command_args::CrankEpochMaintenance, + utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +}; + +pub async fn command_crank_epoch_maintenance( + args: CrankEpochMaintenance, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let validator_index_to_remove = args.validator_index_to_remove; + let args = args.permissionless_parameters; + + // Creates config account + let payer = + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"); + + let steward_config = args.steward_config; + + let all_steward_accounts = + get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let epoch = client.get_epoch_info().await?.epoch; + + if epoch == all_steward_accounts.state_account.state.current_epoch { + println!("Epoch is the same as the current epoch: {}", epoch); + return Ok(()); + } + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::EpochMaintenance { + config: steward_config, + state_account: all_steward_accounts.state_address, + validator_list: all_steward_accounts.validator_list_address, + stake_pool: all_steward_accounts.stake_pool_address, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove, + } + .data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[ix], + args.transaction_parameters.priority_fee, + args.transaction_parameters.compute_limit, + args.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/idle.rs b/utils/steward-cli/src/commands/cranks/idle.rs new file mode 100644 index 00000000..353cd5f2 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/idle.rs @@ -0,0 +1,65 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::{commands::command_args::CrankIdle, utils::accounts::get_steward_state_account}; + +pub async fn command_crank_idle( + args: CrankIdle, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let args = args.permissionless_parameters; + + // Creates config account + let payer = + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"); + + let steward_config = args.steward_config; + + let (state_account, state_address) = + get_steward_state_account(client, &program_id, &steward_config).await?; + + match state_account.state.state_tag { + StewardStateEnum::Idle => { /* Continue */ } + _ => { + println!( + "State account is not in Idle state: {}", + state_account.state.state_tag + ); + return Ok(()); + } + } + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::Idle { + config: steward_config, + state_account: state_address, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::Idle {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let transaction = + Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/mod.rs b/utils/steward-cli/src/commands/cranks/mod.rs new file mode 100644 index 00000000..5303e43e --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/mod.rs @@ -0,0 +1,6 @@ +pub mod compute_delegations; +pub mod compute_instant_unstake; +pub mod compute_score; +pub mod epoch_maintenance; +pub mod idle; +pub mod rebalance; diff --git a/utils/steward-cli/src/commands/cranks/rebalance.rs b/utils/steward-cli/src/commands/cranks/rebalance.rs new file mode 100644 index 00000000..e3e86481 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/rebalance.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; +use validator_history::id as validator_history_id; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, +}; + +use crate::{ + commands::command_args::CrankRebalance, + utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::{package_instructions, submit_packaged_transactions}, + }, +}; + +pub async fn command_crank_rebalance( + args: CrankRebalance, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.permissionless_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::Rebalance => { /* Continue */ } + _ => { + println!( + "State account is not in Rebalance state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) + .filter_map(|validator_index| { + let has_been_rebalanced = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_rebalanced { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::>(); + + let ixs_to_run = validators_to_run + .iter() + .map(|(validator_index, vote_account, history_account)| { + println!("vote_account ({}): {}", validator_index, vote_account); + + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + vote_account, + &steward_accounts.stake_pool_address, + None, + ); + + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + vote_account, + &steward_accounts.stake_pool_address, + steward_accounts.validator_list_account.validators[*validator_index] + .transient_seed_suffix + .into(), + ); + Instruction { + program_id, + accounts: jito_steward::accounts::Rebalance { + config: steward_config, + state_account: steward_accounts.state_address, + validator_history: *history_account, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + staker: steward_accounts.staker_address, + withdraw_authority: steward_accounts.stake_pool_withdraw_authority, + validator_list: steward_accounts.validator_list_address, + reserve_stake: steward_accounts.stake_pool_account.reserve_stake, + stake_account: stake_address, + transient_stake_account: transient_stake_address, + vote_account: *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, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::Rebalance { + validator_list_index: *validator_index, + } + .data(), + } + }) + .collect::>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissionless_parameters + .transaction_parameters + .chunk_size + .unwrap_or(1), + args.permissionless_parameters + .transaction_parameters + .priority_fee, + args.permissionless_parameters + .transaction_parameters + .compute_limit + .or(Some(1_400_000)), + None, + ); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let submit_stats = submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + + println!("Submit stats: {:?}", submit_stats); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/info/mod.rs b/utils/steward-cli/src/commands/info/mod.rs new file mode 100644 index 00000000..c4cb3b3b --- /dev/null +++ b/utils/steward-cli/src/commands/info/mod.rs @@ -0,0 +1,3 @@ +pub mod view_config; +pub mod view_next_index_to_remove; +pub mod view_state; diff --git a/utils/steward-cli/src/commands/info/view_config.rs b/utils/steward-cli/src/commands/info/view_config.rs new file mode 100644 index 00000000..75805309 --- /dev/null +++ b/utils/steward-cli/src/commands/info/view_config.rs @@ -0,0 +1,137 @@ +use std::sync::Arc; + +use anyhow::Result; +use jito_steward::{Config, Staker}; +use solana_client::nonblocking::rpc_client::RpcClient; + +use solana_sdk::pubkey::Pubkey; + +use crate::{ + commands::command_args::ViewConfig, + utils::accounts::{get_steward_config_account, get_steward_state_address}, +}; + +pub async fn command_view_config( + args: ViewConfig, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let steward_config = args.view_parameters.steward_config; + + let steward_config_account = get_steward_config_account(client, &steward_config).await?; + let steward_state = get_steward_state_address(&program_id, &steward_config); + let (steward_staker, _) = + Pubkey::find_program_address(&[Staker::SEED, steward_config.as_ref()], &program_id); + + // let mut output = String::new(); // Initialize the string directly + _print_default_config( + &steward_config, + &steward_state, + &steward_staker, + &steward_config_account, + ); + + Ok(()) +} + +fn _print_default_config( + steward_config: &Pubkey, + steward_state: &Pubkey, + steward_staker: &Pubkey, + config_account: &Config, +) { + let mut formatted_string = String::new(); + + formatted_string += "------- Config -------\n"; + formatted_string += "📚 Accounts 📚\n"; + formatted_string += &format!("Config: {}\n", steward_config); + formatted_string += &format!("Authority: {}\n", config_account.authority); + formatted_string += &format!("Staker: {}\n", steward_staker); + formatted_string += &format!("State: {}\n", steward_state); + formatted_string += &format!("Stake Pool: {}\n", config_account.stake_pool); + formatted_string += "\n↺ State ↺\n"; + formatted_string += &format!("Is Paused: {:?}\n", config_account.paused); + formatted_string += &format!("Blacklisted: {:?}\n", config_account.blacklist.count()); + formatted_string += "\n⚙️ Parameters ⚙️\n"; + formatted_string += &format!( + "Commission Range: {:?}\n", + config_account.parameters.commission_range + ); + formatted_string += &format!( + "MEV Commission Range: {:?}\n", + config_account.parameters.mev_commission_range + ); + formatted_string += &format!( + "Epoch Credits Range: {:?}\n", + config_account.parameters.epoch_credits_range + ); + formatted_string += &format!( + "MEV Commission BPS Threshold: {:?}\n", + config_account.parameters.mev_commission_bps_threshold + ); + formatted_string += &format!( + "Scoring Delinquency Threshold Ratio: {:?}\n", + config_account + .parameters + .scoring_delinquency_threshold_ratio + ); + formatted_string += &format!( + "Instant Unstake Delinquency Threshold Ratio: {:?}\n", + config_account + .parameters + .instant_unstake_delinquency_threshold_ratio + ); + formatted_string += &format!( + "Commission Threshold: {:?}\n", + config_account.parameters.commission_threshold + ); + formatted_string += &format!( + "Historical Commission Threshold: {:?}\n", + config_account.parameters.historical_commission_threshold + ); + formatted_string += &format!( + "Number of Delegation Validators: {:?}\n", + config_account.parameters.num_delegation_validators + ); + formatted_string += &format!( + "Scoring Unstake Cap BPS: {:?}\n", + config_account.parameters.scoring_unstake_cap_bps + ); + formatted_string += &format!( + "Instant Unstake Cap BPS: {:?}\n", + config_account.parameters.instant_unstake_cap_bps + ); + formatted_string += &format!( + "Stake Deposit Unstake Cap BPS: {:?}\n", + config_account.parameters.stake_deposit_unstake_cap_bps + ); + formatted_string += &format!( + "Compute Score Slot Range: {:?}\n", + config_account.parameters.compute_score_slot_range + ); + formatted_string += &format!( + "Instant Unstake Epoch Progress: {:?}\n", + config_account.parameters.instant_unstake_epoch_progress + ); + formatted_string += &format!( + "Instant Unstake Inputs Epoch Progress: {:?}\n", + config_account + .parameters + .instant_unstake_inputs_epoch_progress + ); + formatted_string += &format!( + "Number of Epochs Between Scoring: {:?}\n", + config_account.parameters.num_epochs_between_scoring + ); + formatted_string += &format!( + "Minimum Stake Lamports: {:?}\n", + config_account.parameters.minimum_stake_lamports + ); + formatted_string += &format!( + "Minimum Voting Epochs: {:?}\n", + config_account.parameters.minimum_voting_epochs + ); + formatted_string += "---------------------"; + + println!("{}", formatted_string) +} diff --git a/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs new file mode 100644 index 00000000..511b78c8 --- /dev/null +++ b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; + +use solana_sdk::pubkey::Pubkey; + +use crate::{ + commands::command_args::ViewNextIndexToRemove, + utils::accounts::{get_all_steward_accounts, UsefulStewardAccounts}, +}; + +pub async fn command_view_next_index_to_remove( + args: ViewNextIndexToRemove, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let all_steward_accounts = + get_all_steward_accounts(client, &program_id, &args.view_parameters.steward_config).await?; + + _print_next_index_to_remove(&all_steward_accounts); + + Ok(()) +} + +fn _print_next_index_to_remove(steward_state_accounts: &UsefulStewardAccounts) { + for i in 0..steward_state_accounts + .state_account + .state + .num_pool_validators + { + let value = steward_state_accounts + .state_account + .state + .validators_to_remove + .get_unsafe(i); + + if value { + println!("Validator {} is marked for removal", i); + return; + } + } +} diff --git a/utils/steward-cli/src/commands/info/view_state.rs b/utils/steward-cli/src/commands/info/view_state.rs new file mode 100644 index 00000000..1703de4f --- /dev/null +++ b/utils/steward-cli/src/commands/info/view_state.rs @@ -0,0 +1,181 @@ +use std::sync::Arc; + +use anyhow::Result; +use jito_steward::StewardStateAccount; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::pubkey::Pubkey; +use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; + +use crate::{ + commands::command_args::ViewState, + utils::accounts::{get_all_steward_accounts, UsefulStewardAccounts}, +}; + +pub async fn command_view_state( + args: ViewState, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let steward_config = args.view_parameters.steward_config; + + let steward_state_accounts = + get_all_steward_accounts(client, &program_id, &steward_config).await?; + + if args.verbose { + _print_verbose_state(&steward_state_accounts); + } else { + _print_default_state( + &steward_config, + &steward_state_accounts.state_address, + &steward_state_accounts.state_account, + ); + } + + Ok(()) +} + +fn _print_verbose_state(steward_state_accounts: &UsefulStewardAccounts) { + let mut formatted_string; + + for (index, validator) in steward_state_accounts + .validator_list_account + .validators + .iter() + .enumerate() + { + let vote_account = validator.vote_account_address; + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_state_accounts.stake_pool_address, + None, + ); + + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_state_accounts.stake_pool_address, + validator.transient_seed_suffix.into(), + ); + + let score_index = steward_state_accounts + .state_account + .state + .sorted_score_indices + .iter() + .position(|&i| i == index as u16); + let yield_score_index = steward_state_accounts + .state_account + .state + .sorted_yield_score_indices + .iter() + .position(|&i| i == index as u16); + + formatted_string = String::new(); + + formatted_string += &format!("Vote Account: {:?}\n", vote_account); + formatted_string += &format!("Stake Account: {:?}\n", stake_address); + formatted_string += &format!("Transient Stake Account: {:?}\n", transient_stake_address); + formatted_string += &format!( + "Validator Lamports: {:?}\n", + u64::from(validator.active_stake_lamports) + ); + formatted_string += &format!("Index: {:?}\n", index); + formatted_string += &format!( + "Is Blacklisted: {:?}\n", + steward_state_accounts.config_account.blacklist.get(index) + ); + formatted_string += &format!( + "Is Instant Unstake: {:?}\n", + steward_state_accounts + .state_account + .state + .instant_unstake + .get(index) + ); + formatted_string += &format!( + "Score: {:?}\n", + steward_state_accounts.state_account.state.scores.get(index) + ); + formatted_string += &format!( + "Yield Score: {:?}\n", + steward_state_accounts + .state_account + .state + .yield_scores + .get(index) + ); + formatted_string += &format!("Score Index: {:?}\n", score_index); + formatted_string += &format!("Yield Score Index: {:?}\n", yield_score_index); + + println!("{}", formatted_string); + } +} + +fn _print_default_state( + steward_config: &Pubkey, + steward_state: &Pubkey, + state_account: &StewardStateAccount, +) { + let state = &state_account.state; + + let mut formatted_string = String::new(); + + formatted_string += "------- State -------\n"; + formatted_string += "📚 Accounts 📚\n"; + formatted_string += &format!("Config: {}\n", steward_config); + formatted_string += &format!("State: {}\n", steward_state); + formatted_string += "\n"; + formatted_string += "↺ State ↺\n"; + formatted_string += &format!("State Tag: {}\n", state.state_tag); + formatted_string += &format!( + "Progress: {:?} / {} ({} remaining)\n", + state.progress.count(), + state.num_pool_validators, + state.num_pool_validators - state.progress.count() + ); + formatted_string += &format!( + "Validator Lamport Balances Count: {}\n", + state.validator_lamport_balances.len() + ); + formatted_string += &format!("Scores Count: {}\n", state.scores.len()); + formatted_string += &format!( + "Sorted Score Indices Count: {}\n", + state.sorted_score_indices.len() + ); + formatted_string += &format!("Yield Scores Count: {}\n", state.yield_scores.len()); + formatted_string += &format!( + "Sorted Yield Score Indices Count: {}\n", + state.sorted_yield_score_indices.len() + ); + formatted_string += &format!("Delegations Count: {}\n", state.delegations.len()); + formatted_string += &format!("Instant Unstake: {:?}\n", state.instant_unstake.count()); + formatted_string += &format!( + "Progress: {:?} / {} ( {} left )\n", + state.progress.count(), + state.num_pool_validators, + state.num_pool_validators - state.progress.count() + ); + formatted_string += &format!( + "Start Computing Scores Slot: {}\n", + state.start_computing_scores_slot + ); + formatted_string += &format!("Current Epoch: {}\n", state.current_epoch); + formatted_string += &format!("Next Cycle Epoch: {}\n", state.next_cycle_epoch); + formatted_string += &format!("Number of Pool Validators: {}\n", state.num_pool_validators); + formatted_string += &format!("Scoring Unstake Total: {}\n", state.scoring_unstake_total); + formatted_string += &format!("Instant Unstake Total: {}\n", state.instant_unstake_total); + formatted_string += &format!( + "Stake Deposit Unstake Total: {}\n", + state.stake_deposit_unstake_total + ); + formatted_string += &format!( + "Compute Delegations Completed: {:?}\n", + state.compute_delegations_completed + ); + formatted_string += &format!("Rebalance Completed: {:?}\n", state.rebalance_completed); + formatted_string += &format!("Padding0 Length: {}\n", state._padding0.len()); + formatted_string += "---------------------"; + + println!("{}", formatted_string) +} diff --git a/utils/steward-cli/src/commands/init/init_config.rs b/utils/steward-cli/src/commands/init/init_config.rs new file mode 100644 index 00000000..9aa80084 --- /dev/null +++ b/utils/steward-cli/src/commands/init/init_config.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::UpdateParametersArgs; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, + signature::{read_keypair_file, Keypair}, + signer::Signer, + transaction::Transaction, +}; + +use crate::{ + commands::command_args::InitConfig, + utils::{accounts::get_steward_staker_address, transactions::configure_instruction}, +}; + +pub async fn command_init_config( + args: InitConfig, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let staker_keypair = { + if let Some(staker_keypair_path) = args.staker_keypair_path { + read_keypair_file(staker_keypair_path).expect("Failed reading keypair file ( Staker )") + } else { + authority.insecure_clone() + } + }; + + let steward_config = { + if let Some(steward_config_keypair_path) = args.steward_config_keypair_path { + read_keypair_file(steward_config_keypair_path) + .expect("Failed reading keypair file ( Steward Config )") + } else { + Keypair::new() + } + }; + + let steward_staker = get_steward_staker_address(&program_id, &steward_config.pubkey()); + + let update_parameters_args: UpdateParametersArgs = args.config_parameters.into(); + + // Check if already created + match client.get_account(&steward_config.pubkey()).await { + Ok(config_account) => { + if config_account.owner == program_id { + println!("Config account already exists"); + return Ok(()); + } + } + Err(_) => { /* Account does not exist, continue */ } + } + + let init_ix = Instruction { + program_id, + accounts: jito_steward::accounts::InitializeConfig { + config: steward_config.pubkey(), + stake_pool: args.stake_pool, + staker: steward_staker, + stake_pool_program: spl_stake_pool::id(), + system_program: anchor_lang::solana_program::system_program::id(), + signer: staker_keypair.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::InitializeConfig { + authority: authority.pubkey(), + update_parameters_args, + } + .data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let ixs = configure_instruction( + &[init_ix], + args.transaction_parameters.priority_fee, + args.transaction_parameters.compute_limit, + args.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority, &steward_config, &staker_keypair], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + println!("Steward Config: {}", steward_config.pubkey()); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/init/init_state.rs b/utils/steward-cli/src/commands/init/init_state.rs new file mode 100644 index 00000000..76290bdf --- /dev/null +++ b/utils/steward-cli/src/commands/init/init_state.rs @@ -0,0 +1,211 @@ +use std::sync::Arc; + +use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::{constants::MAX_ALLOC_BYTES, StewardStateAccount}; +use solana_client::nonblocking::rpc_client::RpcClient; + +use solana_program::instruction::Instruction; +use solana_sdk::{ + pubkey::Pubkey, + signature::{read_keypair_file, Keypair, Signature}, + signer::Signer, + transaction::Transaction, +}; + +use crate::{ + commands::command_args::InitState, + utils::{ + accounts::{get_stake_pool_account, get_steward_config_account, get_steward_state_address}, + transactions::configure_instruction, + }, +}; + +const MAX_REALLOCS: usize = (StewardStateAccount::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; +const REALLOCS_PER_TX: usize = 10; + +pub async fn command_init_state( + args: InitState, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let steward_config = args.permissioned_parameters.steward_config; + let steward_config_account = + get_steward_config_account(client, &args.permissioned_parameters.steward_config).await?; + + let steward_state = get_steward_state_address(&program_id, &steward_config); + + let stake_pool_account = + get_stake_pool_account(client, &steward_config_account.stake_pool).await?; + + let validator_list = stake_pool_account.validator_list; + + let mut reallocs_left_to_run = MAX_REALLOCS; + let mut should_create = true; + + match client.get_account(&steward_state).await { + Ok(steward_state_account_raw) => { + if steward_state_account_raw.data.len() == StewardStateAccount::SIZE { + match StewardStateAccount::try_deserialize( + &mut steward_state_account_raw.data.as_slice(), + ) { + Ok(steward_state_account) => { + if steward_state_account.is_initialized.into() { + println!("State account already exists"); + return Ok(()); + } + } + Err(_) => { /* Account is not initialized, continue */ } + }; + } + + // if it already exists, we don't need to create it + should_create = false; + + let data_length = steward_state_account_raw.data.len(); + let whats_left = StewardStateAccount::SIZE - data_length.min(StewardStateAccount::SIZE); + + reallocs_left_to_run = + (whats_left.max(MAX_ALLOC_BYTES) - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; + } + Err(_) => { /* Account does not exist, continue */ } + } + + if should_create { + let signature = _create_state( + client, + &program_id, + &authority, + &steward_state, + &steward_config, + ) + .await?; + + println!("Created Steward State: {}", signature); + } + + let reallocs_to_run = reallocs_left_to_run; + let mut reallocs_ran = 0; + + while reallocs_left_to_run > 0 { + let reallocs_per_transaction = reallocs_left_to_run.min(REALLOCS_PER_TX); + + let signature = _realloc_x_times( + client, + &program_id, + &authority, + &steward_state, + &steward_config, + &validator_list, + reallocs_per_transaction, + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ) + .await?; + + reallocs_left_to_run -= reallocs_per_transaction; + reallocs_ran += reallocs_per_transaction; + + println!( + "{}/{}: Signature: {}", + reallocs_ran, reallocs_to_run, signature + ); + } + + println!("Steward State: {}", steward_state); + + Ok(()) +} + +async fn _create_state( + client: &RpcClient, + program_id: &Pubkey, + authority: &Keypair, + steward_state: &Pubkey, + steward_config: &Pubkey, +) -> Result { + let init_ix = Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::InitializeState { + state_account: *steward_state, + config: *steward_config, + system_program: anchor_lang::solana_program::system_program::id(), + signer: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::InitializeState {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let transaction = Transaction::new_signed_with_payer( + &[init_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + Ok(signature) +} + +#[allow(clippy::too_many_arguments)] +async fn _realloc_x_times( + client: &RpcClient, + program_id: &Pubkey, + authority: &Keypair, + steward_state: &Pubkey, + steward_config: &Pubkey, + validator_list: &Pubkey, + count: usize, + priority_fee: Option, + compute_limit: Option, + heap_size: Option, +) -> Result { + let ixs = vec![ + Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::ReallocState { + state_account: *steward_state, + config: *steward_config, + validator_list: *validator_list, + system_program: anchor_lang::solana_program::system_program::id(), + signer: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ReallocState {}.data(), + }; + count + ]; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ixs = configure_instruction(&ixs, priority_fee, compute_limit, heap_size); + + let transaction = Transaction::new_signed_with_payer( + &configured_ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + Ok(signature) +} diff --git a/utils/steward-cli/src/commands/init/mod.rs b/utils/steward-cli/src/commands/init/mod.rs new file mode 100644 index 00000000..07eea724 --- /dev/null +++ b/utils/steward-cli/src/commands/init/mod.rs @@ -0,0 +1,2 @@ +pub mod init_config; +pub mod init_state; diff --git a/utils/steward-cli/src/commands/mod.rs b/utils/steward-cli/src/commands/mod.rs new file mode 100644 index 00000000..4a5e08cd --- /dev/null +++ b/utils/steward-cli/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod actions; +pub mod command_args; +pub mod cranks; +pub mod info; +pub mod init; diff --git a/utils/steward-cli/src/main.rs b/utils/steward-cli/src/main.rs new file mode 100644 index 00000000..46a27416 --- /dev/null +++ b/utils/steward-cli/src/main.rs @@ -0,0 +1,82 @@ +use anyhow::Result; +use clap::Parser; +use commands::{ + actions::{ + auto_add_validator_from_pool::command_auto_add_validator_from_pool, + auto_remove_validator_from_pool::command_auto_remove_validator_from_pool, + remove_bad_validators::command_remove_bad_validators, reset_state::command_reset_state, + update_config::command_update_config, + }, + command_args::{Args, Commands}, + cranks::{ + compute_delegations::command_crank_compute_delegations, + compute_instant_unstake::command_crank_compute_instant_unstake, + compute_score::command_crank_compute_score, + epoch_maintenance::command_crank_epoch_maintenance, idle::command_crank_idle, + rebalance::command_crank_rebalance, + }, + info::{ + view_config::command_view_config, + view_next_index_to_remove::command_view_next_index_to_remove, + view_state::command_view_state, + }, + init::{init_config::command_init_config, init_state::command_init_state}, +}; +use dotenv::dotenv; +use solana_client::nonblocking::rpc_client::RpcClient; +use std::{sync::Arc, time::Duration}; + +pub mod commands; +pub mod utils; + +#[tokio::main] +async fn main() -> Result<()> { + dotenv().ok(); // Loads in .env file + let args = Args::parse(); + let client = Arc::new(RpcClient::new_with_timeout( + args.json_rpc_url.clone(), + Duration::from_secs(60), + )); + let program_id = args.program_id; + let _ = match args.commands { + // ---- Views ---- + Commands::ViewConfig(args) => command_view_config(args, &client, program_id).await, + Commands::ViewState(args) => command_view_state(args, &client, program_id).await, + Commands::ViewNextIndexToRemove(args) => { + command_view_next_index_to_remove(args, &client, program_id).await + } + + // --- Actions --- + Commands::InitConfig(args) => command_init_config(args, &client, program_id).await, + Commands::UpdateConfig(args) => command_update_config(args, &client, program_id).await, + Commands::InitState(args) => command_init_state(args, &client, program_id).await, + Commands::ResetState(args) => command_reset_state(args, &client, program_id).await, + Commands::AutoRemoveValidatorFromPool(args) => { + command_auto_remove_validator_from_pool(args, &client, program_id).await + } + Commands::AutoAddValidatorFromPool(args) => { + command_auto_add_validator_from_pool(args, &client, program_id).await + } + Commands::RemoveBadValidators(args) => { + command_remove_bad_validators(args, &client, program_id).await + } + + // --- Cranks --- + Commands::CrankEpochMaintenance(args) => { + command_crank_epoch_maintenance(args, &client, program_id).await + } + Commands::CrankComputeScore(args) => { + command_crank_compute_score(args, &client, program_id).await + } + Commands::CrankComputeDelegations(args) => { + command_crank_compute_delegations(args, &client, program_id).await + } + Commands::CrankIdle(args) => command_crank_idle(args, &client, program_id).await, + Commands::CrankComputeInstantUnstake(args) => { + command_crank_compute_instant_unstake(args, &client, program_id).await + } + Commands::CrankRebalance(args) => command_crank_rebalance(args, &client, program_id).await, + }; + + Ok(()) +} diff --git a/utils/steward-cli/src/utils/accounts.rs b/utils/steward-cli/src/utils/accounts.rs new file mode 100644 index 00000000..ea3c01ea --- /dev/null +++ b/utils/steward-cli/src/utils/accounts.rs @@ -0,0 +1,160 @@ +use anchor_lang::AccountDeserialize; +use anyhow::Result; +use jito_steward::{ + utils::{StakePool, ValidatorList}, + Config, Staker, StewardStateAccount, +}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::pubkey::Pubkey; +use spl_stake_pool::find_withdraw_authority_program_address; +use validator_history::{ClusterHistory, ValidatorHistory}; + +pub struct UsefulStewardAccounts { + pub config_account: Config, + pub staker_account: Staker, + pub staker_address: Pubkey, + pub state_account: StewardStateAccount, + pub state_address: Pubkey, + pub stake_pool_account: StakePool, + pub stake_pool_address: Pubkey, + pub stake_pool_withdraw_authority: Pubkey, + pub validator_list_account: ValidatorList, + pub validator_list_address: Pubkey, +} + +pub async fn get_all_steward_accounts( + client: &RpcClient, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result> { + let config_account = get_steward_config_account(client, steward_config).await?; + let (state_account, state_address) = + get_steward_state_account(client, program_id, steward_config).await?; + let stake_pool_address = config_account.stake_pool; + let stake_pool_account = get_stake_pool_account(client, &stake_pool_address).await?; + let (staker_account, staker_address) = + get_steward_staker_account(client, program_id, steward_config).await?; + let stake_pool_withdraw_authority = get_withdraw_authority_address(&stake_pool_address); + let validator_list_address = stake_pool_account.validator_list; + let validator_list_account = + get_validator_list_account(client, &validator_list_address).await?; + + Ok(Box::new(UsefulStewardAccounts { + config_account, + state_account, + state_address, + staker_account, + staker_address, + stake_pool_account, + stake_pool_address, + stake_pool_withdraw_authority, + validator_list_account, + validator_list_address, + })) +} + +pub async fn get_steward_config_account( + client: &RpcClient, + steward_config: &Pubkey, +) -> Result { + let config_raw_account = client.get_account(steward_config).await?; + + Ok(Config::try_deserialize( + &mut config_raw_account.data.as_slice(), + )?) +} + +pub fn get_steward_state_address(program_id: &Pubkey, steward_config: &Pubkey) -> Pubkey { + let (steward_state, _) = Pubkey::find_program_address( + &[StewardStateAccount::SEED, steward_config.as_ref()], + program_id, + ); + + steward_state +} + +pub async fn get_steward_state_account( + client: &RpcClient, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result<(StewardStateAccount, Pubkey)> { + let steward_state = get_steward_state_address(program_id, steward_config); + + let state_raw_account = client.get_account(&steward_state).await?; + Ok(( + StewardStateAccount::try_deserialize(&mut state_raw_account.data.as_slice())?, + steward_state, + )) +} + +pub async fn get_stake_pool_account(client: &RpcClient, stake_pool: &Pubkey) -> Result { + let stake_pool_account_raw = client.get_account(stake_pool).await?; + + Ok(StakePool::try_deserialize( + &mut stake_pool_account_raw.data.as_slice(), + )?) +} + +pub fn get_withdraw_authority_address(stake_pool_address: &Pubkey) -> Pubkey { + let (withdraw_authority, _) = + find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address); + + withdraw_authority +} + +pub fn get_steward_staker_address(program_id: &Pubkey, steward_config: &Pubkey) -> Pubkey { + let (steward_staker, _) = + Pubkey::find_program_address(&[Staker::SEED, steward_config.as_ref()], program_id); + + steward_staker +} + +pub async fn get_steward_staker_account( + client: &RpcClient, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result<(Staker, Pubkey)> { + let steward_staker = get_steward_staker_address(program_id, steward_config); + + let staker_raw_account = client.get_account(&steward_staker).await?; + + Ok(( + Staker::try_deserialize(&mut staker_raw_account.data.as_slice())?, + steward_staker, + )) +} + +pub async fn get_validator_list_account( + client: &RpcClient, + validator_list: &Pubkey, +) -> Result { + let validator_list_account_raw = client.get_account(validator_list).await?; + + Ok(ValidatorList::try_deserialize( + &mut validator_list_account_raw.data.as_slice(), + )?) +} + +pub fn get_cluster_history_address(validator_history_program_id: &Pubkey) -> Pubkey { + let (address, _) = + Pubkey::find_program_address(&[ClusterHistory::SEED], validator_history_program_id); + address +} + +pub fn get_validator_history_address( + vote_account: &Pubkey, + validator_history_program_id: &Pubkey, +) -> Pubkey { + let (address, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, &vote_account.to_bytes()], + validator_history_program_id, + ); + + address +} + +pub fn get_validator_history_config_address(validator_history_program_id: &Pubkey) -> Pubkey { + let (address, _) = Pubkey::find_program_address(&[Config::SEED], validator_history_program_id); + + address +} diff --git a/utils/steward-cli/src/utils/mod.rs b/utils/steward-cli/src/utils/mod.rs new file mode 100644 index 00000000..c0963f95 --- /dev/null +++ b/utils/steward-cli/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod accounts; +pub mod transactions; diff --git a/utils/steward-cli/src/utils/transactions.rs b/utils/steward-cli/src/utils/transactions.rs new file mode 100644 index 00000000..97becc0a --- /dev/null +++ b/utils/steward-cli/src/utils/transactions.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use anyhow::Result; +use keeper_core::{parallel_execute_transactions, SubmitStats, TransactionExecutionError}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, signature::Keypair, signer::Signer, + transaction::Transaction, +}; + +pub fn configure_instruction( + ixs: &[Instruction], + priority_fee: Option, + compute_limit: Option, + heap_size: Option, +) -> Vec { + let mut instructions = ixs.to_vec(); + if let Some(compute_limit) = compute_limit { + instructions.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_limit(compute_limit), + ); + } + if let Some(priority_fee) = priority_fee { + instructions.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_price(priority_fee), + ); + } + if let Some(heap_size) = heap_size { + instructions.insert(0, ComputeBudgetInstruction::request_heap_frame(heap_size)); + } + + instructions +} + +pub fn package_instructions( + ixs: &[Instruction], + chunk_size: usize, + priority_fee: Option, + compute_limit: Option, + heap_size: Option, +) -> Vec> { + ixs.chunks(chunk_size) + .map(|chunk: &[Instruction]| { + configure_instruction(chunk, priority_fee, compute_limit, heap_size) + }) + .collect::>>() +} + +pub async fn submit_packaged_transactions( + client: &Arc, + transactions: Vec>, + keypair: &Arc, + retry_count: Option, + retry_interval: Option, +) -> Result { + let mut stats = SubmitStats::default(); + let tx_slice = transactions + .iter() + .map(|t| t.as_slice()) + .collect::>(); + + match parallel_execute_transactions( + client, + &tx_slice, + keypair, + retry_count.unwrap_or(3), + retry_interval.unwrap_or(20), + ) + .await + { + Ok(results) => { + stats.successes = results.iter().filter(|&tx| tx.is_ok()).count() as u64; + stats.errors = results.len() as u64 - stats.successes; + stats.results = results; + Ok(stats) + } + Err(e) => Err(e), + } +} + +pub async fn debug_send_single_transaction( + client: &Arc, + payer: &Arc, + instructions: &[Instruction], + debug_print: Option, +) -> Result { + let transaction = Transaction::new_signed_with_payer( + instructions, + Some(&payer.pubkey()), + &[&payer], + client.get_latest_blockhash().await?, + ); + + let result = client.send_and_confirm_transaction(&transaction).await; + + if debug_print.unwrap_or(false) { + match &result { + Ok(signature) => { + println!("Signature: {}", signature); + } + Err(e) => { + println!("Accounts: {:?}", &instructions.last().unwrap().accounts); + println!("Error: {:?}", e); + } + } + } + + result +} From 235c0034182bc0520661e10fc9bcd5d96f8a0f42 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Mon, 24 Jun 2024 11:36:19 -0400 Subject: [PATCH 06/18] Fixes after rebase --- programs/steward/idl/steward.json | 187 +++++++++++++++++- .../steward/src/instructions/compute_score.rs | 2 +- .../src/instructions/epoch_maintenance.rs | 16 +- .../src/instructions/reset_steward_state.rs | 12 +- .../src/instructions/spl_passthrough.rs | 6 +- programs/steward/src/lib.rs | 4 +- programs/steward/src/state/steward_state.rs | 26 ++- .../auto_remove_validator_from_pool.rs | 2 +- .../commands/actions/remove_bad_validators.rs | 2 +- .../steward-cli/src/commands/command_args.rs | 4 +- .../cranks/compute_instant_unstake.rs | 4 +- .../src/commands/cranks/compute_score.rs | 4 +- .../src/commands/cranks/rebalance.rs | 4 +- .../info/view_next_index_to_remove.rs | 2 +- .../src/commands/info/view_state.rs | 4 +- 15 files changed, 230 insertions(+), 49 deletions(-) diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 9ea51b4a..37285e0e 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -1,5 +1,5 @@ { - "address": "Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8", + "address": "sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP", "metadata": { "name": "steward", "version": "0.1.0", @@ -56,6 +56,10 @@ { "name": "config" }, + { + "name": "steward_state", + "writable": true + }, { "name": "stake_pool_program" }, @@ -134,10 +138,14 @@ ], "accounts": [ { - "name": "validator_history_account" + "name": "config" }, { - "name": "config" + "name": "steward_state", + "writable": true + }, + { + "name": "validator_history_account" }, { "name": "stake_pool_program" @@ -285,6 +293,43 @@ } ] }, + { + "name": "close_steward_accounts", + "docs": [ + "Closes Steward PDA accounts associated with a given Config (StewardStateAccount, and Staker).", + "Config is not closed as it is a Keypair, so lamports can simply be withdrawn.", + "Reclaims lamports to authority" + ], + "discriminator": [ + 172, + 171, + 212, + 186, + 90, + 10, + 181, + 24 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "staker", + "writable": true + }, + { + "name": "state_account", + "writable": true + }, + { + "name": "authority", + "writable": true, + "signer": true + } + ], + "args": [] + }, { "name": "compute_delegations", "docs": [ @@ -587,6 +632,45 @@ } ] }, + { + "name": "epoch_maintenance", + "docs": [ + "Housekeeping, run at the start of any new epoch before any other instructions" + ], + "discriminator": [ + 208, + 225, + 211, + 82, + 219, + 242, + 58, + 200 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "state_account", + "writable": true + }, + { + "name": "validator_list" + }, + { + "name": "stake_pool" + } + ], + "args": [ + { + "name": "validator_index_to_remove", + "type": { + "option": "u64" + } + } + ] + }, { "name": "idle", "docs": [ @@ -1145,6 +1229,43 @@ } ] }, + { + "name": "reset_steward_state", + "docs": [ + "Resets steward state account to its initial state." + ], + "discriminator": [ + 84, + 248, + 158, + 46, + 200, + 205, + 234, + 86 + ], + "accounts": [ + { + "name": "state_account", + "writable": true + }, + { + "name": "config" + }, + { + "name": "stake_pool" + }, + { + "name": "validator_list" + }, + { + "name": "authority", + "writable": true, + "signer": true + } + ], + "args": [] + }, { "name": "resume_steward", "discriminator": [ @@ -1560,6 +1681,31 @@ "code": 6022, "name": "ValidatorHistoryMismatch", "msg": "Validator history account does not match vote account" + }, + { + "code": 6023, + "name": "EpochMaintenanceNotComplete", + "msg": "Epoch Maintenance must be called before continuing" + }, + { + "code": 6024, + "name": "StakePoolNotUpdated", + "msg": "The stake pool must be updated before continuing" + }, + { + "code": 6025, + "name": "ValidatorNotMarkedForRemoval", + "msg": "Validator not marked for removal" + }, + { + "code": 6026, + "name": "ValidatorsHaveNotBeenRemoved", + "msg": "Validators have not been removed" + }, + { + "code": 6027, + "name": "ListStateMismatch", + "msg": "Validator List count does not match state machine" } ], "types": [ @@ -2250,7 +2396,8 @@ "name": "StewardState", "docs": [ "Tracks state of the stake pool.", - "Follow state transitions here: [TODO add link to github diagram]" + "Follow state transitions here:", + "https://github.com/jito-foundation/stakenet/blob/master/programs/steward/state-machine-diagram.png" ], "serialization": "bytemuck", "repr": { @@ -2369,6 +2516,18 @@ } } }, + { + "name": "validators_to_remove", + "docs": [ + "Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool", + "This is cleaned up in the next epoch" + ], + "type": { + "defined": { + "name": "BitMask" + } + } + }, { "name": "start_computing_scores_slot", "docs": [ @@ -2419,6 +2578,13 @@ ], "type": "u64" }, + { + "name": "validators_added", + "docs": [ + "Number of validators added to the pool in the current cycle" + ], + "type": "u16" + }, { "name": "compute_delegations_completed", "docs": [ @@ -2441,6 +2607,17 @@ } } }, + { + "name": "checked_validators_removed_from_list", + "docs": [ + "So we only have to check the validator list once for `ReadyToRemove`" + ], + "type": { + "defined": { + "name": "U8Bool" + } + } + }, { "name": "_padding0", "docs": [ @@ -2449,7 +2626,7 @@ "type": { "array": [ "u8", - 40006 + 40003 ] } } diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index c93f618a..53d32d51 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -91,7 +91,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul validator_list_index, &cluster_history, &config, - num_pool_validators, + num_pool_validators as u64, )?; maybe_transition_and_emit( diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 4cf1bd30..037ec2d7 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -1,7 +1,8 @@ use crate::{ errors::StewardError, utils::{ - check_validator_list_has_stake_status, get_stake_pool, get_validator_list_length, StakePool, + check_validator_list_has_stake_status, deserialize_stake_pool, get_stake_pool_address, + get_validator_list_length, }, Config, StewardStateAccount, }; @@ -19,13 +20,15 @@ pub struct EpochMaintenance<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, - #[account(mut, address = stake_pool.validator_list)] + /// CHECK: Correct account guaranteed if address is correct + #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, + /// CHECK: Correct account guaranteed if address is correct #[account( - address = get_stake_pool(&config)? + address = get_stake_pool_address(&config)? )] - pub stake_pool: Account<'info, StakePool>, + pub stake_pool: AccountInfo<'info>, } /// Runs maintenance tasks at the start of each epoch, needs to be run multiple times @@ -35,7 +38,7 @@ pub fn handler( ctx: Context, validator_index_to_remove: Option, ) -> Result<()> { - let stake_pool = &ctx.accounts.stake_pool; + let stake_pool = deserialize_stake_pool(&ctx.accounts.stake_pool)?; let mut state_account = ctx.accounts.state_account.load_mut()?; let clock = Clock::get()?; @@ -65,7 +68,8 @@ pub fn handler( // Ensure we have a 1-1 mapping between the number of validators in the list and the number of validators in the state require!( - state_account.state.num_pool_validators + state_account.state.validators_added as usize + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize - validators_to_remove == validators_in_list, StewardError::ListStateMismatch diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs index 0140f7d6..9ec002ac 100644 --- a/programs/steward/src/instructions/reset_steward_state.rs +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -2,7 +2,7 @@ use crate::{ constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, errors::StewardError, state::{Config, StewardStateAccount}, - utils::{get_config_authority, get_stake_pool, StakePool}, + utils::{deserialize_stake_pool, get_config_authority, get_stake_pool_address}, BitMask, Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, }; use anchor_lang::prelude::*; @@ -19,10 +19,12 @@ pub struct ResetStewardState<'info> { pub config: AccountLoader<'info, Config>, - #[account(address = get_stake_pool(&config)?)] - pub stake_pool: Account<'info, StakePool>, + /// CHECK: Correct account guaranteed if address is correct + #[account(address = get_stake_pool_address(&config)?)] + pub stake_pool: AccountInfo<'info>, - #[account(address = stake_pool.validator_list)] + /// CHECK: Correct account guaranteed if address is correct + #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, #[account(mut, address = get_config_authority(&config)?)] @@ -44,7 +46,7 @@ pub fn handler(ctx: Context) -> Result<()> { let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; state_account.state.state_tag = StewardStateEnum::ComputeScores; - state_account.state.num_pool_validators = validator_list.len() as usize; + state_account.state.num_pool_validators = validator_list.len() as u64; state_account.state.scores = [0; MAX_VALIDATORS]; state_account.state.sorted_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; state_account.state.yield_scores = [0; MAX_VALIDATORS]; diff --git a/programs/steward/src/instructions/spl_passthrough.rs b/programs/steward/src/instructions/spl_passthrough.rs index fb6771d0..901d6b0b 100644 --- a/programs/steward/src/instructions/spl_passthrough.rs +++ b/programs/steward/src/instructions/spl_passthrough.rs @@ -165,7 +165,7 @@ pub struct RemoveValidatorFromPool<'info> { /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut)] + #[account(mut, address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut)] @@ -335,7 +335,7 @@ pub struct IncreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut, address = stake_pool.validator_list)] + #[account(mut, address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account( @@ -464,7 +464,7 @@ pub struct DecreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut, address = stake_pool.validator_list)] + #[account(mut, address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account( diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 4f9578b7..0e280f7b 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -94,9 +94,9 @@ pub mod steward { /// Housekeeping, run at the start of any new epoch before any other instructions pub fn epoch_maintenance( ctx: Context, - validator_index_to_remove: Option, + validator_index_to_remove: Option, ) -> Result<()> { - instructions::epoch_maintenance::handler(ctx, validator_index_to_remove) + instructions::epoch_maintenance::handler(ctx, validator_index_to_remove.map(|x| x as usize)) } /// Computes score for a the validator at `validator_list_index` for the current cycle. diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 6ad1c68d..3cf17131 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -469,14 +469,14 @@ impl StewardState { .position(|&i| i == index as u16); if let Some(yield_score_index) = yield_score_index { - for i in yield_score_index..self.num_pool_validators { + for i in yield_score_index..num_pool_validators { let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; self.sorted_yield_score_indices[i] = self.sorted_yield_score_indices[next_i]; } } if let Some(score_index) = score_index { - for i in score_index..self.num_pool_validators { + for i in score_index..num_pool_validators { let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; self.sorted_score_indices[i] = self.sorted_score_indices[next_i]; } @@ -496,16 +496,15 @@ impl StewardState { } // Clear values on empty last index - self.validator_lamport_balances[self.num_pool_validators] = 0; - self.scores[self.num_pool_validators] = 0; - self.yield_scores[self.num_pool_validators] = 0; - self.sorted_score_indices[self.num_pool_validators] = SORTED_INDEX_DEFAULT; - self.sorted_yield_score_indices[self.num_pool_validators] = SORTED_INDEX_DEFAULT; - self.delegations[self.num_pool_validators] = Delegation::default(); - self.instant_unstake.set(self.num_pool_validators, false)?; - self.validators_to_remove - .set(self.num_pool_validators, false)?; - self.progress.set(self.num_pool_validators, false)?; + self.validator_lamport_balances[num_pool_validators] = 0; + self.scores[num_pool_validators] = 0; + self.yield_scores[num_pool_validators] = 0; + self.sorted_score_indices[num_pool_validators] = SORTED_INDEX_DEFAULT; + self.sorted_yield_score_indices[num_pool_validators] = SORTED_INDEX_DEFAULT; + self.delegations[num_pool_validators] = Delegation::default(); + self.instant_unstake.set(num_pool_validators, false)?; + self.validators_to_remove.set(num_pool_validators, false)?; + self.progress.set(num_pool_validators, false)?; Ok(()) } @@ -568,8 +567,7 @@ impl StewardState { // Updates num_pool_validators at the start of the cycle so validator additions later won't be considered require!( - num_pool_validators - == self.num_pool_validators + self.validators_added as usize, + num_pool_validators == self.num_pool_validators + self.validators_added as u64, StewardError::ListStateMismatch ); self.num_pool_validators = num_pool_validators; diff --git a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs index 6b80c248..9fa2f442 100644 --- a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs +++ b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs @@ -85,7 +85,7 @@ pub async fn command_auto_remove_validator_from_pool( } .to_account_metas(None), data: jito_steward::instruction::AutoRemoveValidatorFromPool { - validator_list_index: validator_index, + validator_list_index: validator_index as u64, } .data(), }; diff --git a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs index 40a6bb07..a444ec42 100644 --- a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs +++ b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs @@ -122,7 +122,7 @@ pub async fn command_remove_bad_validators( } .to_account_metas(None), data: jito_steward::instruction::RemoveValidatorFromPool { - validator_list_index: *validator_index, + validator_list_index: *validator_index as u64, } .data(), } diff --git a/utils/steward-cli/src/commands/command_args.rs b/utils/steward-cli/src/commands/command_args.rs index 7a3e6aea..35786910 100644 --- a/utils/steward-cli/src/commands/command_args.rs +++ b/utils/steward-cli/src/commands/command_args.rs @@ -80,7 +80,7 @@ pub struct ConfigParameters { /// Scoring window such that the validators are all scored within a similar timeframe (in slots) #[arg(long, env)] - pub compute_score_slot_range: Option, + pub compute_score_slot_range: Option, /// Point in epoch progress before instant unstake can be computed #[arg(long, env)] @@ -328,7 +328,7 @@ pub struct CrankEpochMaintenance { /// Validator index to remove, gotten from `validators_to_remove` Bitmask #[arg(long, env)] - pub validator_index_to_remove: Option, + pub validator_index_to_remove: Option, } #[derive(Parser)] diff --git a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs index b9aad99b..c4f5f7eb 100644 --- a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs +++ b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs @@ -46,7 +46,7 @@ pub async fn command_crank_compute_instant_unstake( } } - let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) .filter_map(|validator_index| { let has_been_scored = steward_accounts .state_account @@ -84,7 +84,7 @@ pub async fn command_crank_compute_instant_unstake( } .to_account_metas(None), data: jito_steward::instruction::ComputeInstantUnstake { - validator_list_index: *validator_index, + validator_list_index: *validator_index as u64, } .data(), }) diff --git a/utils/steward-cli/src/commands/cranks/compute_score.rs b/utils/steward-cli/src/commands/cranks/compute_score.rs index f8ce4dcb..92d888a8 100644 --- a/utils/steward-cli/src/commands/cranks/compute_score.rs +++ b/utils/steward-cli/src/commands/cranks/compute_score.rs @@ -46,7 +46,7 @@ pub async fn command_crank_compute_score( } } - let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) .filter_map(|validator_index| { let has_been_scored = steward_accounts .state_account @@ -90,7 +90,7 @@ pub async fn command_crank_compute_score( } .to_account_metas(None), data: jito_steward::instruction::ComputeScore { - validator_list_index: *validator_index, + validator_list_index: *validator_index as u64, } .data(), } diff --git a/utils/steward-cli/src/commands/cranks/rebalance.rs b/utils/steward-cli/src/commands/cranks/rebalance.rs index e3e86481..62b94f4b 100644 --- a/utils/steward-cli/src/commands/cranks/rebalance.rs +++ b/utils/steward-cli/src/commands/cranks/rebalance.rs @@ -47,7 +47,7 @@ pub async fn command_crank_rebalance( } } - let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) .filter_map(|validator_index| { let has_been_rebalanced = steward_accounts .state_account @@ -114,7 +114,7 @@ pub async fn command_crank_rebalance( } .to_account_metas(None), data: jito_steward::instruction::Rebalance { - validator_list_index: *validator_index, + validator_list_index: *validator_index as u64, } .data(), } diff --git a/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs index 511b78c8..97bbc383 100644 --- a/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs +++ b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs @@ -27,7 +27,7 @@ fn _print_next_index_to_remove(steward_state_accounts: &UsefulStewardAccounts) { for i in 0..steward_state_accounts .state_account .state - .num_pool_validators + .num_pool_validators as usize { let value = steward_state_accounts .state_account diff --git a/utils/steward-cli/src/commands/info/view_state.rs b/utils/steward-cli/src/commands/info/view_state.rs index 1703de4f..a96d8073 100644 --- a/utils/steward-cli/src/commands/info/view_state.rs +++ b/utils/steward-cli/src/commands/info/view_state.rs @@ -132,7 +132,7 @@ fn _print_default_state( "Progress: {:?} / {} ({} remaining)\n", state.progress.count(), state.num_pool_validators, - state.num_pool_validators - state.progress.count() + state.num_pool_validators - state.progress.count() as u64 ); formatted_string += &format!( "Validator Lamport Balances Count: {}\n", @@ -154,7 +154,7 @@ fn _print_default_state( "Progress: {:?} / {} ( {} left )\n", state.progress.count(), state.num_pool_validators, - state.num_pool_validators - state.progress.count() + state.num_pool_validators - state.progress.count() as u64 ); formatted_string += &format!( "Start Computing Scores Slot: {}\n", From 4facad0eeee3b8f86a34470cce3be330bb65cbf3 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:58:24 -0600 Subject: [PATCH 07/18] PROGRAM TWEAKS (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [x] Separate authorities (blacklist, parameters, admin council: reset, pausing + passthrough?) - [x] Remove staker PDA in favor of State account - [x] Add ValidatorList address to Config so we can validate the list in instructions that don’t pass in the stake pool - [x] Test RemoveValidator for validators that don’t have score computed - [x] Adjust increase_validator_stake reserve_lamports to protect transient stake rent for other accounts - [x] Figure out a better system for persisting blacklist validator indices - [x] Add a check for Deactivate* state at the index marked for removal in epoch_maintenance --------- Co-authored-by: Evan Batsell --- .github/workflows/build.yaml | 2 + Cargo.toml | 2 +- programs/steward/idl/steward.json | 465 ++++++++++-------- programs/steward/src/delegation.rs | 6 +- programs/steward/src/errors.rs | 2 + .../add_validator_to_blacklist.rs | 10 +- .../auto_add_validator_to_pool.rs | 22 +- .../auto_remove_validator_from_pool.rs | 97 ++-- .../instructions/close_steward_accounts.rs | 13 +- .../src/instructions/compute_delegations.rs | 9 +- .../instructions/compute_instant_unstake.rs | 23 +- .../steward/src/instructions/compute_score.rs | 19 +- .../src/instructions/epoch_maintenance.rs | 10 +- programs/steward/src/instructions/idle.rs | 3 - .../src/instructions/initialize_state.rs | 32 -- ...ialize_config.rs => initialize_steward.rs} | 48 +- programs/steward/src/instructions/mod.rs | 6 +- .../steward/src/instructions/pause_steward.rs | 4 +- .../steward/src/instructions/realloc_state.rs | 7 +- .../steward/src/instructions/rebalance.rs | 135 ++++- .../remove_validator_from_blacklist.rs | 13 +- .../src/instructions/reset_steward_state.rs | 4 +- .../src/instructions/resume_steward.rs | 4 +- .../src/instructions/set_new_authority.rs | 77 ++- .../src/instructions/spl_passthrough.rs | 277 +++++------ .../src/instructions/update_parameters.rs | 4 +- programs/steward/src/lib.rs | 27 +- programs/steward/src/score.rs | 12 +- programs/steward/src/state/accounts.rs | 58 ++- programs/steward/src/state/large_bitmask.rs | 104 ++++ programs/steward/src/state/mod.rs | 2 + programs/steward/src/state/steward_state.rs | 85 ++-- programs/steward/src/utils.rs | 23 +- tests/src/steward_fixtures.rs | 48 +- tests/tests/steward/test_algorithms.rs | 196 ++------ tests/tests/steward/test_integration.rs | 53 +- tests/tests/steward/test_parameters.rs | 4 +- tests/tests/steward/test_spl_passthrough.rs | 90 ++-- tests/tests/steward/test_state_methods.rs | 4 +- tests/tests/steward/test_steward.rs | 119 ++++- utils/steward-cli/Cargo.toml | 10 +- .../actions/auto_add_validator_from_pool.rs | 2 - .../auto_remove_validator_from_pool.rs | 2 - .../commands/actions/remove_bad_validators.rs | 5 +- .../commands/cranks/compute_delegations.rs | 1 - .../cranks/compute_instant_unstake.rs | 3 +- .../src/commands/cranks/compute_score.rs | 3 +- utils/steward-cli/src/commands/cranks/idle.rs | 1 - .../src/commands/cranks/rebalance.rs | 6 +- .../src/commands/info/view_config.rs | 32 +- .../src/commands/info/view_state.rs | 4 - .../src/commands/init/init_state.rs | 90 +--- .../init/{init_config.rs => init_steward.rs} | 18 +- utils/steward-cli/src/commands/init/mod.rs | 2 +- utils/steward-cli/src/main.rs | 2 +- utils/steward-cli/src/utils/accounts.rs | 31 +- 56 files changed, 1224 insertions(+), 1107 deletions(-) delete mode 100644 programs/steward/src/instructions/initialize_state.rs rename programs/steward/src/instructions/{initialize_config.rs => initialize_steward.rs} (57%) create mode 100644 programs/steward/src/state/large_bitmask.rs rename utils/steward-cli/src/commands/init/{init_config.rs => init_steward.rs} (83%) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0cf0cc76..f61fdef7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,11 +3,13 @@ on: push: branches: - master + - steward-test-branch tags: - "v*" pull_request: branches: - master + - steward-test-branch jobs: security_audit: diff --git a/Cargo.toml b/Cargo.toml index d7498e84..af12b8b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,8 @@ members = [ "keepers/*", "programs/*", - "utils/*", "tests", + "utils/*", ] [profile.release] diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 37285e0e..57e49a14 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -35,7 +35,7 @@ ], "args": [ { - "name": "index", + "name": "validator_history_blacklist", "type": "u32" } ] @@ -57,7 +57,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -67,9 +67,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "reserve_stake", "writable": true @@ -107,7 +104,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -154,9 +151,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "reserve_stake", "writable": true @@ -192,11 +186,6 @@ }, { "name": "stake_program" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [] @@ -234,9 +223,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "reserve_stake", "writable": true @@ -279,11 +265,6 @@ }, { "name": "stake_program" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -314,10 +295,6 @@ { "name": "config" }, - { - "name": "staker", - "writable": true - }, { "name": "state_account", "writable": true @@ -353,11 +330,6 @@ { "name": "state_account", "writable": true - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [] @@ -393,11 +365,6 @@ }, { "name": "cluster_history" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -438,11 +405,6 @@ }, { "name": "cluster_history" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -469,7 +431,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -485,9 +447,6 @@ { "name": "stake_pool" }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -524,7 +483,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -561,7 +520,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -575,9 +534,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -616,7 +572,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -693,11 +649,6 @@ { "name": "state_account", "writable": true - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [] @@ -719,7 +670,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -732,9 +683,6 @@ { "name": "stake_pool" }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -776,7 +724,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -813,7 +761,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -827,9 +775,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -871,7 +816,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -888,16 +833,16 @@ ] }, { - "name": "initialize_config", + "name": "initialize_steward", "discriminator": [ - 208, - 127, - 21, - 1, - 194, - 190, - 196, - 70 + 195, + 182, + 16, + 84, + 217, + 58, + 220, + 175 ], "accounts": [ { @@ -906,7 +851,7 @@ "signer": true }, { - "name": "staker", + "name": "state_account", "writable": true }, { @@ -920,16 +865,12 @@ "name": "system_program" }, { - "name": "signer", + "name": "current_staker", "writable": true, "signer": true } ], "args": [ - { - "name": "authority", - "type": "pubkey" - }, { "name": "update_parameters_args", "type": { @@ -940,40 +881,6 @@ } ] }, - { - "name": "initialize_state", - "docs": [ - "Creates state account" - ], - "discriminator": [ - 190, - 171, - 224, - 219, - 217, - 72, - 199, - 176 - ], - "accounts": [ - { - "name": "state_account", - "writable": true - }, - { - "name": "config" - }, - { - "name": "system_program" - }, - { - "name": "signer", - "writable": true, - "signer": true - } - ], - "args": [] - }, { "name": "pause_steward", "discriminator": [ @@ -1069,10 +976,6 @@ { "name": "stake_pool" }, - { - "name": "staker", - "writable": true - }, { "name": "withdraw_authority" }, @@ -1115,11 +1018,6 @@ }, { "name": "stake_program" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -1157,7 +1055,7 @@ ], "args": [ { - "name": "index", + "name": "validator_history_blacklist", "type": "u32" } ] @@ -1179,7 +1077,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -1189,9 +1087,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -1217,7 +1112,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -1312,12 +1207,21 @@ "name": "new_authority" }, { - "name": "authority", + "name": "admin", "writable": true, "signer": true } ], - "args": [] + "args": [ + { + "name": "authority_type", + "type": { + "defined": { + "name": "AuthorityType" + } + } + } + ] }, { "name": "set_preferred_validator", @@ -1335,6 +1239,10 @@ { "name": "config" }, + { + "name": "state_account", + "writable": true + }, { "name": "stake_pool_program" }, @@ -1342,14 +1250,11 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "validator_list" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -1387,6 +1292,10 @@ { "name": "config" }, + { + "name": "state_account", + "writable": true + }, { "name": "stake_pool_program" }, @@ -1394,14 +1303,11 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "new_staker" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -1473,19 +1379,6 @@ 130 ] }, - { - "name": "Staker", - "discriminator": [ - 171, - 229, - 193, - 85, - 67, - 177, - 151, - 4 - ] - }, { "name": "StewardStateAccount", "discriminator": [ @@ -1540,6 +1433,19 @@ 77 ] }, + { + "name": "RebalanceEvent", + "discriminator": [ + 120, + 27, + 117, + 235, + 104, + 42, + 132, + 75 + ] + }, { "name": "ScoreComponents", "discriminator": [ @@ -1570,145 +1476,191 @@ "errors": [ { "code": 6000, + "name": "InvalidAuthorityType", + "msg": "Invalid set authority type: 0: SetAdmin, 1: SetBlacklistAuthority, 2: SetParametersAuthority" + }, + { + "code": 6001, "name": "ScoringNotComplete", "msg": "Scoring must be completed before any other steps can be taken" }, { - "code": 6001, + "code": 6002, "name": "ValidatorNotInList", "msg": "Validator does not exist at the ValidatorList index provided" }, { - "code": 6002, + "code": 6003, "name": "AddValidatorsNotComplete", "msg": "Add validators step must be completed before any other steps can be taken" }, { - "code": 6003, + "code": 6004, "name": "EpochNotOver", "msg": "Cannot reset state before epoch is over" }, { - "code": 6004, + "code": 6005, "name": "Unauthorized", "msg": "Unauthorized to perform this action" }, { - "code": 6005, + "code": 6006, "name": "BitmaskOutOfBounds", "msg": "Bitmask index out of bounds" }, { - "code": 6006, + "code": 6007, "name": "StateNotReset", "msg": "Epoch state not reset" }, { - "code": 6007, + "code": 6008, "name": "ValidatorOutOfRange", "msg": "Validator History created after epoch start, out of range" }, { - "code": 6008, + "code": 6009, "name": "InvalidState" }, { - "code": 6009, + "code": 6010, "name": "ValidatorBelowStakeMinimum", "msg": "Validator not eligible to be added to the pool. Must meet stake minimum" }, { - "code": 6010, + "code": 6011, "name": "ValidatorBelowLivenessMinimum", "msg": "Validator not eligible to be added to the pool. Must meet recent voting minimum" }, { - "code": 6011, + "code": 6012, "name": "VoteHistoryNotRecentEnough", "msg": "Validator History vote data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6012, + "code": 6013, "name": "StakeHistoryNotRecentEnough", "msg": "Validator History stake data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6013, + "code": 6014, "name": "ClusterHistoryNotRecentEnough", "msg": "Cluster History data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6014, + "code": 6015, "name": "StateMachinePaused", "msg": "Steward State Machine is paused. No state machine actions can be taken" }, { - "code": 6015, + "code": 6016, "name": "InvalidParameterValue", "msg": "Config parameter is out of range or otherwise invalid" }, { - "code": 6016, + "code": 6017, "name": "InstantUnstakeNotReady", "msg": "Instant unstake cannot be performed yet." }, { - "code": 6017, + "code": 6018, "name": "ValidatorIndexOutOfBounds", "msg": "Validator index out of bounds of state machine" }, { - "code": 6018, + "code": 6019, "name": "ValidatorListTypeMismatch", "msg": "ValidatorList account type mismatch" }, { - "code": 6019, + "code": 6020, "name": "ArithmeticError", "msg": "An operation caused an overflow/underflow" }, { - "code": 6020, + "code": 6021, "name": "ValidatorNotRemovable", "msg": "Validator not eligible for removal. Must be delinquent or have closed vote account" }, { - "code": 6021, + "code": 6022, "name": "MaxValidatorsReached", "msg": "Max validators reached" }, { - "code": 6022, + "code": 6023, "name": "ValidatorHistoryMismatch", "msg": "Validator history account does not match vote account" }, { - "code": 6023, + "code": 6024, "name": "EpochMaintenanceNotComplete", "msg": "Epoch Maintenance must be called before continuing" }, { - "code": 6024, + "code": 6025, "name": "StakePoolNotUpdated", "msg": "The stake pool must be updated before continuing" }, { - "code": 6025, + "code": 6026, "name": "ValidatorNotMarkedForRemoval", "msg": "Validator not marked for removal" }, { - "code": 6026, + "code": 6027, "name": "ValidatorsHaveNotBeenRemoved", "msg": "Validators have not been removed" }, { - "code": 6027, + "code": 6028, "name": "ListStateMismatch", "msg": "Validator List count does not match state machine" } ], "types": [ + { + "name": "AuthorityType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SetAdmin", + "fields": [ + { + "name": "SetAdmin", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "SetBlacklistAuthority", + "fields": [ + { + "name": "SetBlacklistAuthority", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "SetParameterAuthority", + "fields": [ + { + "name": "SetParameterAuthority", + "type": { + "option": "u8" + } + } + ] + } + ] + } + }, { "name": "BitMask", "docs": [ @@ -1958,20 +1910,49 @@ "type": "pubkey" }, { - "name": "authority", + "name": "validator_list", + "docs": [ + "Validator List" + ], + "type": "pubkey" + }, + { + "name": "admin", + "docs": [ + "Admin", + "- Update the `parameters_authority`", + "- Update the `blacklist_authority`", + "- Can call SPL Passthrough functions", + "- Can pause/reset the state machine" + ], + "type": "pubkey" + }, + { + "name": "parameters_authority", "docs": [ - "Authority for pool stewardship, can execute SPL Staker commands and adjust Delegation parameters" + "Parameters Authority", + "- Can update steward parameters" ], "type": "pubkey" }, { - "name": "blacklist", + "name": "blacklist_authority", "docs": [ - "Bitmask representing index of validators that are not allowed delegation" + "Blacklist Authority", + "- Can add to the blacklist", + "- Can remove from the blacklist" + ], + "type": "pubkey" + }, + { + "name": "validator_history_blacklist", + "docs": [ + "Bitmask representing index of validators that are not allowed delegation", + "NOTE: This is indexed off of the validator history, NOT the validator list" ], "type": { "defined": { - "name": "BitMask" + "name": "LargeBitMask" } } }, @@ -1987,26 +1968,26 @@ } }, { - "name": "_padding", + "name": "paused", "docs": [ - "Padding for future governance parameters" + "Halts any state machine progress" ], "type": { - "array": [ - "u8", - 1023 - ] + "defined": { + "name": "U8Bool" + } } }, { - "name": "paused", + "name": "_padding", "docs": [ - "Halts any state machine progress" + "Padding for future governance parameters" ], "type": { - "defined": { - "name": "U8Bool" - } + "array": [ + "u8", + 1023 + ] } } ] @@ -2107,6 +2088,33 @@ ] } }, + { + "name": "LargeBitMask", + "docs": [ + "Data structure used to efficiently pack a binary array, primarily used to store all validators.", + "Each validator has an index (its index in the spl_stake_pool::ValidatorList), corresponding to a bit in the bitmask.", + "When an operation is executed on a validator, the bit corresponding to that validator's index is set to 1.", + "When all bits are 1, the operation is complete." + ], + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "values", + "type": { + "array": [ + "u64", + 313 + ] + } + } + ] + } + }, { "name": "Parameters", "serialization": "bytemuck", @@ -2269,6 +2277,59 @@ ] } }, + { + "name": "RebalanceEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vote_account", + "type": "pubkey" + }, + { + "name": "epoch", + "type": "u16" + }, + { + "name": "rebalance_type_tag", + "type": { + "defined": { + "name": "RebalanceTypeTag" + } + } + }, + { + "name": "increase_lamports", + "type": "u64" + }, + { + "name": "decrease_components", + "type": { + "defined": { + "name": "DecreaseComponents" + } + } + } + ] + } + }, + { + "name": "RebalanceTypeTag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Increase" + }, + { + "name": "Decrease" + } + ] + } + }, { "name": "ScoreComponents", "type": { @@ -2356,18 +2417,6 @@ ] } }, - { - "name": "Staker", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - } - ] - } - }, { "name": "StateTransition", "type": { diff --git a/programs/steward/src/delegation.rs b/programs/steward/src/delegation.rs index 2ea078ae..14da68d6 100644 --- a/programs/steward/src/delegation.rs +++ b/programs/steward/src/delegation.rs @@ -1,4 +1,6 @@ +use anchor_lang::idl::*; use anchor_lang::prelude::*; +use borsh::BorshSerialize; use spl_stake_pool::big_vec::BigVec; use crate::{ @@ -7,7 +9,7 @@ use crate::{ StewardState, }; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RebalanceType { Increase(u64), Decrease(DecreaseComponents), @@ -15,7 +17,7 @@ pub enum RebalanceType { } #[event] -#[derive(Debug, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct DecreaseComponents { pub scoring_unstake_lamports: u64, pub instant_unstake_lamports: u64, diff --git a/programs/steward/src/errors.rs b/programs/steward/src/errors.rs index 6554baf9..de36d4ad 100644 --- a/programs/steward/src/errors.rs +++ b/programs/steward/src/errors.rs @@ -2,6 +2,8 @@ use anchor_lang::prelude::*; #[error_code] pub enum StewardError { + #[msg("Invalid set authority type: 0: SetAdmin, 1: SetBlacklistAuthority, 2: SetParametersAuthority")] + InvalidAuthorityType, #[msg("Scoring must be completed before any other steps can be taken")] ScoringNotComplete, #[msg("Validator does not exist at the ValidatorList index provided")] diff --git a/programs/steward/src/instructions/add_validator_to_blacklist.rs b/programs/steward/src/instructions/add_validator_to_blacklist.rs index d48774ae..404bee33 100644 --- a/programs/steward/src/instructions/add_validator_to_blacklist.rs +++ b/programs/steward/src/instructions/add_validator_to_blacklist.rs @@ -1,4 +1,4 @@ -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_blacklist_authority, Config}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -6,14 +6,16 @@ pub struct AddValidatorToBlacklist<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_blacklist_authority(&config)?)] pub authority: Signer<'info>, } // Removes ability for validator to receive delegation. Score will be set to 0 and instant unstaking will occur. // Index is the index of the validator from ValidatorHistory. -pub fn handler(ctx: Context, validator_list_index: u32) -> Result<()> { +pub fn handler(ctx: Context, validator_history_index: u32) -> Result<()> { let mut config = ctx.accounts.config.load_mut()?; - config.blacklist.set(validator_list_index as usize, true)?; + config + .validator_history_blacklist + .set(validator_history_index as usize, true)?; Ok(()) } diff --git a/programs/steward/src/instructions/auto_add_validator_to_pool.rs b/programs/steward/src/instructions/auto_add_validator_to_pool.rs index 123e620f..49a57626 100644 --- a/programs/steward/src/instructions/auto_add_validator_to_pool.rs +++ b/programs/steward/src/instructions/auto_add_validator_to_pool.rs @@ -1,6 +1,6 @@ use crate::constants::{MAX_VALIDATORS, STAKE_POOL_WITHDRAW_SEED}; use crate::errors::StewardError; -use crate::state::{Config, Staker, StewardStateAccount}; +use crate::state::{Config, StewardStateAccount}; use crate::utils::{deserialize_stake_pool, get_stake_pool_address}; use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; @@ -40,12 +40,6 @@ pub struct AutoAddValidator<'info> { )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut, address = deserialize_stake_pool(&stake_pool)?.reserve_stake)] pub reserve_stake: AccountInfo<'info>, @@ -98,9 +92,6 @@ pub struct AutoAddValidator<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - - #[account(mut)] - pub signer: Signer<'info>, } /* @@ -156,11 +147,14 @@ pub fn handler(ctx: Context) -> Result<()> { state_account.state.increment_validator_to_add()?; + // Have to drop the state account before calling the CPI + drop(state_account); + invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.steward_state.key(), &ctx.accounts.reserve_stake.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), @@ -170,7 +164,7 @@ pub fn handler(ctx: Context) -> Result<()> { ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.steward_state.to_account_info(), ctx.accounts.reserve_stake.to_owned(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), @@ -184,9 +178,9 @@ pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.steward_state], ]], )?; 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 fa4bd358..e18422e4 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -2,7 +2,7 @@ use std::num::NonZeroU32; use crate::constants::STAKE_POOL_WITHDRAW_SEED; use crate::errors::StewardError; -use crate::state::{Config, Staker}; +use crate::state::Config; use crate::utils::{ deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, }; @@ -46,12 +46,6 @@ pub struct AutoRemoveValidator<'info> { )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut, address = deserialize_stake_pool(&stake_pool)?.reserve_stake)] pub reserve_stake: AccountInfo<'info>, @@ -123,62 +117,63 @@ pub struct AutoRemoveValidator<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - - #[account(mut)] - pub signer: Signer<'info>, } /* */ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { - let mut state_account = ctx.accounts.state_account.load_mut()?; - let validator_list = &ctx.accounts.validator_list; - let epoch = Clock::get()?.epoch; - - let validator_stake_info = - get_validator_stake_info_at_index(validator_list, validator_list_index)?; - require!( - validator_stake_info.vote_account_address == ctx.accounts.vote_account.key(), - StewardError::ValidatorNotInList - ); - - // Should not be able to remove a validator if update is not complete - require!( - epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - // Checks state for deactivate delinquent status, preventing pool from merging stake with activating - let stake_account_deactivated = { - let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); - let stake_state: StakeStateV2 = - try_from_slice_unchecked::(stake_account_data)?; - - let deactivation_epoch = match stake_state { - StakeStateV2::Stake(_meta, stake, _stake_flags) => stake.delegation.deactivation_epoch, - _ => return Err(StewardError::InvalidState.into()), // TODO fix + { + let mut state_account = ctx.accounts.state_account.load_mut()?; + let validator_list = &ctx.accounts.validator_list; + let epoch = Clock::get()?.epoch; + + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + require!( + validator_stake_info.vote_account_address == ctx.accounts.vote_account.key(), + StewardError::ValidatorNotInList + ); + + // Should not be able to remove a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + // Checks state for deactivate delinquent status, preventing pool from merging stake with activating + let stake_account_deactivated = { + let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); + let stake_state: StakeStateV2 = + try_from_slice_unchecked::(stake_account_data)?; + + let deactivation_epoch = match stake_state { + StakeStateV2::Stake(_meta, stake, _stake_flags) => { + stake.delegation.deactivation_epoch + } + _ => return Err(StewardError::InvalidState.into()), // TODO fix + }; + deactivation_epoch < epoch }; - deactivation_epoch < epoch - }; - // Check if vote account closed - let vote_account_closed = ctx.accounts.vote_account.owner == &system_program::ID; + // Check if vote account closed + let vote_account_closed = ctx.accounts.vote_account.owner == &system_program::ID; - require!( - stake_account_deactivated || vote_account_closed, - StewardError::ValidatorNotRemovable - ); + require!( + stake_account_deactivated || vote_account_closed, + StewardError::ValidatorNotRemovable + ); - state_account - .state - .mark_validator_for_removal(validator_list_index)?; + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } invoke_signed( &spl_stake_pool::instruction::remove_validator_from_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.stake_account.key(), @@ -186,7 +181,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) - ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.reserve_stake.to_owned(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), @@ -201,9 +196,9 @@ pub fn handler(ctx: Context, validator_list_index: usize) - ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; diff --git a/programs/steward/src/instructions/close_steward_accounts.rs b/programs/steward/src/instructions/close_steward_accounts.rs index bbf00776..7c561bb8 100644 --- a/programs/steward/src/instructions/close_steward_accounts.rs +++ b/programs/steward/src/instructions/close_steward_accounts.rs @@ -1,7 +1,6 @@ use crate::{ state::{Config, StewardStateAccount}, - utils::get_config_authority, - Staker, + utils::get_config_admin, }; use anchor_lang::prelude::*; @@ -9,14 +8,6 @@ use anchor_lang::prelude::*; pub struct CloseStewardAccounts<'info> { pub config: AccountLoader<'info, Config>, - #[account( - mut, - close = authority, - seeds = [Staker::SEED, config.key().as_ref()], - bump, - )] - staker: Account<'info, Staker>, - #[account( mut, close = authority, @@ -25,7 +16,7 @@ pub struct CloseStewardAccounts<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_admin(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/instructions/compute_delegations.rs b/programs/steward/src/instructions/compute_delegations.rs index 33f29b78..005fe540 100644 --- a/programs/steward/src/instructions/compute_delegations.rs +++ b/programs/steward/src/instructions/compute_delegations.rs @@ -12,9 +12,6 @@ pub struct ComputeDelegations<'info> { bump )] pub state_account: AccountLoader<'info, StewardStateAccount>, - - #[account(mut)] - pub signer: Signer<'info>, } /* @@ -41,12 +38,14 @@ pub fn handler(ctx: Context) -> Result<()> { .state .compute_delegations(clock.epoch, &config)?; - maybe_transition_and_emit( + if let Some(event) = maybe_transition_and_emit( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/compute_instant_unstake.rs b/programs/steward/src/instructions/compute_instant_unstake.rs index 75eb282f..bfb50f44 100644 --- a/programs/steward/src/instructions/compute_instant_unstake.rs +++ b/programs/steward/src/instructions/compute_instant_unstake.rs @@ -1,5 +1,7 @@ use crate::{ - errors::StewardError, maybe_transition_and_emit, utils::get_validator_stake_info_at_index, + errors::StewardError, + maybe_transition_and_emit, + utils::{get_validator_list, get_validator_stake_info_at_index}, Config, StewardStateAccount, }; use anchor_lang::prelude::*; @@ -18,8 +20,8 @@ pub struct ComputeInstantUnstake<'info> { pub validator_history: AccountLoader<'info, ValidatorHistory>, - /// CHECK: TODO add validator list to config - #[account(owner = spl_stake_pool::id())] + #[account(address = get_validator_list(&config)?)] + /// CHECK: We check against the Config pub validator_list: AccountInfo<'info>, #[account( @@ -28,9 +30,6 @@ pub struct ComputeInstantUnstake<'info> { bump )] pub cluster_history: AccountLoader<'info, ClusterHistory>, - - #[account(mut)] - pub signer: Signer<'info>, } pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { @@ -58,21 +57,25 @@ pub fn handler(ctx: Context, validator_list_index: usize) return Err(StewardError::StateMachinePaused.into()); } - state_account.state.compute_instant_unstake( + if let Some(instant_unstake) = state_account.state.compute_instant_unstake( &clock, &epoch_schedule, &validator_history, validator_list_index, &cluster, &config, - )?; + )? { + emit!(instant_unstake); + } - maybe_transition_and_emit( + if let Some(event) = maybe_transition_and_emit( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index 53d32d51..585420ae 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use crate::{ errors::StewardError, maybe_transition_and_emit, - utils::{get_validator_list_length, get_validator_stake_info_at_index}, + utils::{get_validator_list, get_validator_list_length, get_validator_stake_info_at_index}, Config, StewardStateAccount, StewardStateEnum, }; use validator_history::{ClusterHistory, ValidatorHistory}; @@ -22,7 +22,7 @@ pub struct ComputeScore<'info> { pub validator_history: AccountLoader<'info, ValidatorHistory>, /// CHECK: Account owner checked, account type checked in get_validator_stake_info_at_index - #[account(owner = spl_stake_pool::id())] + #[account(address = get_validator_list(&config)?)] pub validator_list: AccountInfo<'info>, #[account( @@ -31,9 +31,6 @@ pub struct ComputeScore<'info> { bump )] pub cluster_history: AccountLoader<'info, ClusterHistory>, - - #[account(mut)] - pub signer: Signer<'info>, } pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { @@ -84,7 +81,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul StewardError::InvalidState ); - state_account.state.compute_score( + if let Some(score) = state_account.state.compute_score( &clock, &epoch_schedule, &validator_history, @@ -92,14 +89,18 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul &cluster_history, &config, num_pool_validators as u64, - )?; + )? { + emit!(score); + } - maybe_transition_and_emit( + if let Some(event) = maybe_transition_and_emit( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 037ec2d7..ff538c94 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -1,8 +1,8 @@ use crate::{ errors::StewardError, utils::{ - check_validator_list_has_stake_status, deserialize_stake_pool, get_stake_pool_address, - get_validator_list_length, + check_validator_list_has_stake_status_other_than, deserialize_stake_pool, + get_stake_pool_address, get_validator_list_length, }, Config, StewardStateAccount, }; @@ -51,12 +51,13 @@ pub fn handler( if (!state_account.state.checked_validators_removed_from_list).into() { // Ensure there are no validators in the list that have not been removed, that should be require!( - !check_validator_list_has_stake_status( + !check_validator_list_has_stake_status_other_than( &ctx.accounts.validator_list, - StakeStatus::ReadyForRemoval + StakeStatus::Active )?, StewardError::ValidatorsHaveNotBeenRemoved ); + state_account.state.checked_validators_removed_from_list = true.into(); } @@ -74,7 +75,6 @@ pub fn handler( == validators_in_list, StewardError::ListStateMismatch ); - if let Some(validator_index_to_remove) = validator_index_to_remove { state_account .state diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index 47d594aa..73173e5c 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -14,9 +14,6 @@ pub struct Idle<'info> { bump )] pub state_account: AccountLoader<'info, StewardStateAccount>, - - #[account(mut)] - pub signer: Signer<'info>, } /* diff --git a/programs/steward/src/instructions/initialize_state.rs b/programs/steward/src/instructions/initialize_state.rs deleted file mode 100644 index d637ba84..00000000 --- a/programs/steward/src/instructions/initialize_state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{ - constants::MAX_ALLOC_BYTES, - state::{Config, StewardStateAccount}, -}; -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct InitializeState<'info> { - #[account( - init, - payer = signer, - space = MAX_ALLOC_BYTES, - seeds = [StewardStateAccount::SEED, config.key().as_ref()], - bump - )] - pub state_account: AccountLoader<'info, StewardStateAccount>, - - pub config: AccountLoader<'info, Config>, - - pub system_program: Program<'info, System>, - - #[account(mut)] - pub signer: Signer<'info>, -} - -/* -Initializes steward state account, without assigning any values until it has been reallocated to desired size. -Split into multiple instructions due to 10240 byte allocation limit for PDAs. -*/ -pub const fn handler(_ctx: Context) -> Result<()> { - Ok(()) -} diff --git a/programs/steward/src/instructions/initialize_config.rs b/programs/steward/src/instructions/initialize_steward.rs similarity index 57% rename from programs/steward/src/instructions/initialize_config.rs rename to programs/steward/src/instructions/initialize_steward.rs index 12a5ccaa..7c146a0f 100644 --- a/programs/steward/src/instructions/initialize_config.rs +++ b/programs/steward/src/instructions/initialize_steward.rs @@ -1,27 +1,27 @@ use anchor_lang::{prelude::*, solana_program::program::invoke}; -use crate::{utils::deserialize_stake_pool, Config, Staker, UpdateParametersArgs}; +use crate::{ + constants::MAX_ALLOC_BYTES, utils::deserialize_stake_pool, Config, StewardStateAccount, + UpdateParametersArgs, +}; #[derive(Accounts)] -pub struct InitializeConfig<'info> { +pub struct InitializeSteward<'info> { #[account( init, - payer = signer, + payer = current_staker, space = Config::SIZE, )] pub config: AccountLoader<'info, Config>, - // Creates an account that will be used to sign instructions for the stake pool. - // The pool's "staker" keypair needs to be assigned to this address, and it has authority over - // adding validators, removing validators, and delegating stake to validators in the pool. #[account( init, - seeds = [Staker::SEED, config.key().as_ref()], - payer = signer, - space = Staker::SIZE, + payer = current_staker, + space = MAX_ALLOC_BYTES, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub staker: Account<'info, Staker>, + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut)] @@ -37,19 +37,26 @@ pub struct InitializeConfig<'info> { mut, address = deserialize_stake_pool(&stake_pool)?.staker )] - pub signer: Signer<'info>, + pub current_staker: Signer<'info>, } pub fn handler( - ctx: Context, - authority: Pubkey, + ctx: Context, update_parameters_args: &UpdateParametersArgs, ) -> Result<()> { // Confirm that the stake pool is valid - let _ = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + let stake_pool_account = deserialize_stake_pool(&ctx.accounts.stake_pool)?; let mut config = ctx.accounts.config.load_init()?; + + // Set the stake pool information config.stake_pool = ctx.accounts.stake_pool.key(); - config.authority = authority; + config.validator_list = stake_pool_account.validator_list; + + // Set all authorities to the current_staker + let admin = ctx.accounts.current_staker.key(); + config.admin = admin; + config.blacklist_authority = admin; + config.parameters_authority = admin; // Set Initial Parameters let max_slots_in_epoch = EpochSchedule::get()?.slots_per_epoch; @@ -63,19 +70,18 @@ pub fn handler( config.parameters = initial_parameters; - // Set the staker account - ctx.accounts.staker.bump = ctx.bumps.staker; + // The staker is the state account invoke( &spl_stake_pool::instruction::set_staker( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.signer.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.current_staker.key(), + &ctx.accounts.state_account.key(), ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.signer.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.current_staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ], )?; Ok(()) diff --git a/programs/steward/src/instructions/mod.rs b/programs/steward/src/instructions/mod.rs index ad9c7c2c..fdef8aa5 100644 --- a/programs/steward/src/instructions/mod.rs +++ b/programs/steward/src/instructions/mod.rs @@ -8,8 +8,7 @@ pub mod compute_instant_unstake; pub mod compute_score; pub mod epoch_maintenance; pub mod idle; -pub mod initialize_config; -pub mod initialize_state; +pub mod initialize_steward; pub mod pause_steward; pub mod realloc_state; pub mod rebalance; @@ -29,8 +28,7 @@ pub use compute_instant_unstake::*; pub use compute_score::*; pub use epoch_maintenance::*; pub use idle::*; -pub use initialize_config::*; -pub use initialize_state::*; +pub use initialize_steward::*; pub use pause_steward::*; pub use realloc_state::*; pub use rebalance::*; diff --git a/programs/steward/src/instructions/pause_steward.rs b/programs/steward/src/instructions/pause_steward.rs index 9d7f18de..94bc0ee7 100644 --- a/programs/steward/src/instructions/pause_steward.rs +++ b/programs/steward/src/instructions/pause_steward.rs @@ -1,13 +1,13 @@ use anchor_lang::prelude::*; -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_admin, Config}; #[derive(Accounts)] pub struct PauseSteward<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_admin(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/instructions/realloc_state.rs b/programs/steward/src/instructions/realloc_state.rs index 1f543815..1cc13bd8 100644 --- a/programs/steward/src/instructions/realloc_state.rs +++ b/programs/steward/src/instructions/realloc_state.rs @@ -3,6 +3,7 @@ use crate::{ constants::{MAX_ALLOC_BYTES, MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, errors::StewardError, state::{Config, StewardStateAccount}, + utils::get_validator_list, Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, }; use anchor_lang::prelude::*; @@ -43,10 +44,8 @@ pub struct ReallocState<'info> { pub config: AccountLoader<'info, Config>, - /// CHECK: TODO add validator_list address to config - #[account( - owner = spl_stake_pool::ID, - )] + /// CHECK: We check against the Config + #[account(address = get_validator_list(&config)?)] pub validator_list: AccountInfo<'info>, pub system_program: Program<'info, System>, diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index 4f6e0ff6..ab338ad7 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -1,6 +1,8 @@ use std::num::NonZeroU32; use anchor_lang::{ + idl::types::*, + idl::*, prelude::*, solana_program::{ program::invoke_signed, @@ -8,6 +10,7 @@ use anchor_lang::{ system_program, sysvar, vote, }, }; +use borsh::{BorshDeserialize, BorshSerialize}; use spl_pod::solana_program::stake::state::StakeStateV2; use spl_stake_pool::{ find_stake_program_address, find_transient_stake_program_address, minimum_delegation, @@ -17,11 +20,11 @@ use validator_history::ValidatorHistory; use crate::{ constants::STAKE_POOL_WITHDRAW_SEED, - delegation::RebalanceType, + delegation::{DecreaseComponents, RebalanceType}, errors::StewardError, maybe_transition_and_emit, utils::{deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index}, - Config, Staker, StewardStateAccount, + Config, StewardStateAccount, }; #[derive(Accounts)] @@ -51,13 +54,6 @@ pub struct Rebalance<'info> { #[account(address = get_stake_pool_address(&config)?)] pub stake_pool: AccountInfo<'info>, - #[account( - mut, - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account( seeds = [ @@ -140,9 +136,6 @@ pub struct Rebalance<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - - #[account(mut)] - pub signer: Signer<'info>, } pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { @@ -193,13 +186,15 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( )? }; - match result { + drop(state_account); + + match result.clone() { RebalanceType::Decrease(decrease_components) => { invoke_signed( &spl_stake_pool::instruction::decrease_validator_stake_with_reserve( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -210,7 +205,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -223,9 +218,9 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; } @@ -234,7 +229,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( &spl_stake_pool::instruction::increase_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -246,7 +241,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -261,21 +256,107 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; } RebalanceType::None => {} } - maybe_transition_and_emit( - &mut state_account.state, - &clock, - &config.parameters, - &epoch_schedule, - )?; + { + let mut state_account = ctx.accounts.state_account.load_mut()?; + + emit!(rebalance_to_event( + ctx.accounts.vote_account.key(), + clock.epoch as u16, + result + )); + + if let Some(event) = maybe_transition_and_emit( + &mut state_account.state, + &clock, + &config.parameters, + &epoch_schedule, + )? { + emit!(event); + } + } Ok(()) } + +#[event] +pub struct RebalanceEvent { + pub vote_account: Pubkey, + pub epoch: u16, + pub rebalance_type_tag: RebalanceTypeTag, + pub increase_lamports: u64, + pub decrease_components: DecreaseComponents, +} + +fn rebalance_to_event( + vote_account: Pubkey, + epoch: u16, + rebalance_type: RebalanceType, +) -> RebalanceEvent { + match rebalance_type { + RebalanceType::None => RebalanceEvent { + vote_account, + epoch, + rebalance_type_tag: RebalanceTypeTag::None, + increase_lamports: 0, + decrease_components: DecreaseComponents::default(), + }, + RebalanceType::Increase(lamports) => RebalanceEvent { + vote_account, + epoch, + rebalance_type_tag: RebalanceTypeTag::Increase, + increase_lamports: lamports, + decrease_components: DecreaseComponents::default(), + }, + RebalanceType::Decrease(decrease_components) => RebalanceEvent { + vote_account, + epoch, + rebalance_type_tag: RebalanceTypeTag::Decrease, + increase_lamports: 0, + decrease_components, + }, + } +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum RebalanceTypeTag { + None, + Increase, + Decrease, +} + +impl IdlBuild for RebalanceTypeTag { + fn create_type() -> Option { + Some(IdlTypeDef { + name: "RebalanceTypeTag".to_string(), + ty: IdlTypeDefTy::Enum { + variants: vec![ + IdlEnumVariant { + name: "None".to_string(), + fields: None, + }, + IdlEnumVariant { + name: "Increase".to_string(), + fields: None, + }, + IdlEnumVariant { + name: "Decrease".to_string(), + fields: None, + }, + ], + }, + docs: Default::default(), + generics: Default::default(), + serialization: Default::default(), + repr: Default::default(), + }) + } +} diff --git a/programs/steward/src/instructions/remove_validator_from_blacklist.rs b/programs/steward/src/instructions/remove_validator_from_blacklist.rs index 1c9c267e..c8e4c57e 100644 --- a/programs/steward/src/instructions/remove_validator_from_blacklist.rs +++ b/programs/steward/src/instructions/remove_validator_from_blacklist.rs @@ -1,4 +1,4 @@ -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_blacklist_authority, Config}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -6,14 +6,19 @@ pub struct RemoveValidatorFromBlacklist<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_blacklist_authority(&config)?)] pub authority: Signer<'info>, } // Removes validator from blacklist. Validator will be eligible to receive delegation again when scores are recomputed. // Index is the index of the validator from ValidatorHistory. -pub fn handler(ctx: Context, index: u32) -> Result<()> { +pub fn handler( + ctx: Context, + validator_history_index: u32, +) -> Result<()> { let mut config = ctx.accounts.config.load_mut()?; - config.blacklist.set(index as usize, false)?; + config + .validator_history_blacklist + .set(validator_history_index as usize, false)?; Ok(()) } diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs index 9ec002ac..2ed4d167 100644 --- a/programs/steward/src/instructions/reset_steward_state.rs +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -2,7 +2,7 @@ use crate::{ constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, errors::StewardError, state::{Config, StewardStateAccount}, - utils::{deserialize_stake_pool, get_config_authority, get_stake_pool_address}, + utils::{deserialize_stake_pool, get_config_admin, get_stake_pool_address}, BitMask, Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, }; use anchor_lang::prelude::*; @@ -27,7 +27,7 @@ pub struct ResetStewardState<'info> { #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_admin(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/instructions/resume_steward.rs b/programs/steward/src/instructions/resume_steward.rs index 0b4a6a23..d1b392ce 100644 --- a/programs/steward/src/instructions/resume_steward.rs +++ b/programs/steward/src/instructions/resume_steward.rs @@ -1,13 +1,13 @@ use anchor_lang::prelude::*; -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_admin, Config}; #[derive(Accounts)] pub struct ResumeSteward<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_admin(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/instructions/set_new_authority.rs b/programs/steward/src/instructions/set_new_authority.rs index cc268ca2..0e82fbbe 100644 --- a/programs/steward/src/instructions/set_new_authority.rs +++ b/programs/steward/src/instructions/set_new_authority.rs @@ -1,7 +1,65 @@ +use anchor_lang::idl::types::*; use anchor_lang::prelude::*; +use anchor_lang::IdlBuild; +use borsh::{BorshDeserialize, BorshSerialize}; use crate::{errors::StewardError, state::Config}; +#[repr(u8)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)] +pub enum AuthorityType { + SetAdmin = 0, + SetBlacklistAuthority = 1, + SetParametersAuthority = 2, +} + +impl AuthorityType { + pub fn to_u8(self) -> u8 { + self as u8 + } +} + +// Implement IdlBuild for AuthorityType +impl IdlBuild for AuthorityType { + fn create_type() -> Option { + Some(IdlTypeDef { + name: "AuthorityType".to_string(), + ty: IdlTypeDefTy::Enum { + variants: vec![ + IdlEnumVariant { + name: "SetAdmin".to_string(), + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "SetAdmin".to_string(), + docs: Default::default(), + ty: IdlType::Option(Box::new(IdlType::U8)), + }])), + }, + IdlEnumVariant { + name: "SetBlacklistAuthority".to_string(), + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "SetBlacklistAuthority".to_string(), + docs: Default::default(), + ty: IdlType::Option(Box::new(IdlType::U8)), + }])), + }, + IdlEnumVariant { + name: "SetParameterAuthority".to_string(), + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "SetParameterAuthority".to_string(), + docs: Default::default(), + ty: IdlType::Option(Box::new(IdlType::U8)), + }])), + }, + ], + }, + docs: Default::default(), + generics: Default::default(), + serialization: Default::default(), + repr: Default::default(), + }) + } +} + #[derive(Accounts)] pub struct SetNewAuthority<'info> { #[account(mut)] @@ -11,15 +69,26 @@ pub struct SetNewAuthority<'info> { pub new_authority: AccountInfo<'info>, #[account(mut)] - pub authority: Signer<'info>, + pub admin: Signer<'info>, } -pub fn handler(ctx: Context) -> Result<()> { +pub fn handler(ctx: Context, authority_type: AuthorityType) -> Result<()> { let mut config = ctx.accounts.config.load_mut()?; - if config.authority != *ctx.accounts.authority.key { + if config.admin != *ctx.accounts.admin.key { return Err(StewardError::Unauthorized.into()); } - config.authority = ctx.accounts.new_authority.key(); + match authority_type { + AuthorityType::SetAdmin => { + config.admin = ctx.accounts.new_authority.key(); + } + AuthorityType::SetBlacklistAuthority => { + config.blacklist_authority = ctx.accounts.new_authority.key(); + } + AuthorityType::SetParametersAuthority => { + config.parameters_authority = ctx.accounts.new_authority.key(); + } + } + Ok(()) } diff --git a/programs/steward/src/instructions/spl_passthrough.rs b/programs/steward/src/instructions/spl_passthrough.rs index 901d6b0b..5f985c74 100644 --- a/programs/steward/src/instructions/spl_passthrough.rs +++ b/programs/steward/src/instructions/spl_passthrough.rs @@ -6,9 +6,9 @@ use crate::constants::MAX_VALIDATORS; use crate::errors::StewardError; -use crate::state::{Config, Staker}; +use crate::state::Config; use crate::utils::{ - deserialize_stake_pool, get_config_authority, get_stake_pool_address, + deserialize_stake_pool, get_config_admin, get_stake_pool_address, get_validator_stake_info_at_index, }; use crate::StewardStateAccount; @@ -29,7 +29,7 @@ pub struct AddValidatorToPool<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -41,11 +41,7 @@ pub struct AddValidatorToPool<'info> { )] /// CHECK: passing through, checks are done by spl-stake-pool pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut)] pub reserve_stake: AccountInfo<'info>, @@ -72,39 +68,41 @@ pub struct AddValidatorToPool<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn add_validator_to_pool_handler( ctx: Context, validator_seed: Option, ) -> Result<()> { - let mut state_account = ctx.accounts.steward_state.load_mut()?; - let epoch = Clock::get()?.epoch; + { + let mut state_account = ctx.accounts.state_account.load_mut()?; + let epoch = Clock::get()?.epoch; - // Should not be able to add a validator if update is not complete - require!( - epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); + // Should not be able to add a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); - { - let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + { + let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; + let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; - if validator_list.len().checked_add(1).unwrap() > MAX_VALIDATORS as u32 { - return Err(StewardError::MaxValidatorsReached.into()); + if validator_list.len().checked_add(1).unwrap() > MAX_VALIDATORS as u32 { + return Err(StewardError::MaxValidatorsReached.into()); + } } - } - state_account.state.increment_validator_to_add()?; + state_account.state.increment_validator_to_add()?; + } invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.reserve_stake.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), @@ -114,7 +112,7 @@ pub fn add_validator_to_pool_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.reserve_stake.to_owned(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), @@ -128,9 +126,9 @@ pub fn add_validator_to_pool_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -144,7 +142,7 @@ pub struct RemoveValidatorFromPool<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( @@ -157,11 +155,7 @@ pub struct RemoveValidatorFromPool<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -178,50 +172,52 @@ pub struct RemoveValidatorFromPool<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn remove_validator_from_pool_handler( ctx: Context, validator_list_index: usize, ) -> Result<()> { - let mut state_account = ctx.accounts.steward_state.load_mut()?; - let epoch = Clock::get()?.epoch; - - // Should not be able to remove a validator if update is not complete - require!( - epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - if validator_list_index < state_account.state.num_pool_validators as usize { - let validator_list_stake_info = get_validator_stake_info_at_index( - &ctx.accounts.validator_list.to_account_info(), - validator_list_index, - )?; - - let (validator_list_stake_account, _) = find_stake_program_address( - &ctx.accounts.stake_pool_program.key(), - &validator_list_stake_info.vote_account_address, - &ctx.accounts.stake_pool.key(), - NonZeroU32::new(u32::from(validator_list_stake_info.validator_seed_suffix)), + { + let mut 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!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete ); - if validator_list_stake_account != ctx.accounts.stake_account.key() { - return Err(StewardError::ValidatorNotInList.into()); + if validator_list_index < state_account.state.num_pool_validators as usize { + let validator_list_stake_info = get_validator_stake_info_at_index( + &ctx.accounts.validator_list.to_account_info(), + validator_list_index, + )?; + + let (validator_list_stake_account, _) = find_stake_program_address( + &ctx.accounts.stake_pool_program.key(), + &validator_list_stake_info.vote_account_address, + &ctx.accounts.stake_pool.key(), + NonZeroU32::new(u32::from(validator_list_stake_info.validator_seed_suffix)), + ); + + if validator_list_stake_account != ctx.accounts.stake_account.key() { + return Err(StewardError::ValidatorNotInList.into()); + } } - } - state_account - .state - .mark_validator_for_removal(validator_list_index)?; + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } invoke_signed( &spl_stake_pool::instruction::remove_validator_from_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.stake_account.key(), @@ -229,7 +225,7 @@ pub fn remove_validator_from_pool_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.stake_account.to_account_info(), @@ -238,9 +234,9 @@ pub fn remove_validator_from_pool_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -249,6 +245,12 @@ pub fn remove_validator_from_pool_handler( #[derive(Accounts)] pub struct SetPreferredValidator<'info> { pub config: AccountLoader<'info, Config>, + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -260,16 +262,12 @@ pub struct SetPreferredValidator<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn set_preferred_validator_handler( @@ -281,20 +279,20 @@ pub fn set_preferred_validator_handler( &spl_stake_pool::instruction::set_preferred_validator( ctx.accounts.stake_pool_program.key, &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.validator_list.key(), validator_type.clone(), validator, ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.validator_list.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -308,7 +306,7 @@ pub struct IncreaseValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -327,11 +325,7 @@ pub struct IncreaseValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -364,19 +358,17 @@ pub struct IncreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } - pub fn increase_validator_stake_handler( ctx: Context, lamports: u64, transient_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -394,7 +386,7 @@ pub fn increase_validator_stake_handler( &spl_stake_pool::instruction::increase_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -406,7 +398,7 @@ pub fn increase_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -421,9 +413,9 @@ pub fn increase_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -437,7 +429,7 @@ pub struct DecreaseValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -456,11 +448,7 @@ pub struct DecreaseValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -490,8 +478,8 @@ pub struct DecreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn decrease_validator_stake_handler( @@ -499,10 +487,9 @@ pub fn decrease_validator_stake_handler( lamports: u64, transient_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -520,7 +507,7 @@ pub fn decrease_validator_stake_handler( &spl_stake_pool::instruction::decrease_validator_stake_with_reserve( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -531,7 +518,7 @@ pub fn decrease_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -544,9 +531,9 @@ pub fn decrease_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -560,7 +547,7 @@ pub struct IncreaseAdditionalValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -578,11 +565,7 @@ pub struct IncreaseAdditionalValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -614,8 +597,8 @@ pub struct IncreaseAdditionalValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn increase_additional_validator_stake_handler( @@ -624,10 +607,9 @@ pub fn increase_additional_validator_stake_handler( transient_seed: u64, ephemeral_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -645,7 +627,7 @@ pub fn increase_additional_validator_stake_handler( &spl_stake_pool::instruction::increase_additional_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -659,7 +641,7 @@ pub fn increase_additional_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -674,9 +656,9 @@ pub fn increase_additional_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -690,7 +672,7 @@ pub struct DecreaseAdditionalValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -713,11 +695,7 @@ pub struct DecreaseAdditionalValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -743,8 +721,8 @@ pub struct DecreaseAdditionalValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn decrease_additional_validator_stake_handler( @@ -753,10 +731,9 @@ pub fn decrease_additional_validator_stake_handler( transient_seed: u64, ephemeral_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -774,7 +751,7 @@ pub fn decrease_additional_validator_stake_handler( &spl_stake_pool::instruction::decrease_additional_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -787,7 +764,7 @@ pub fn decrease_additional_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -800,9 +777,9 @@ pub fn decrease_additional_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -811,6 +788,12 @@ pub fn decrease_additional_validator_stake_handler( #[derive(Accounts)] pub struct SetStaker<'info> { pub config: AccountLoader<'info, Config>, + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -821,15 +804,11 @@ pub struct SetStaker<'info> { mut, address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub new_staker: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } /// Note this function can only be called once by the Steward, as it will lose it's authority @@ -840,18 +819,18 @@ pub fn set_staker_handler(ctx: Context) -> Result<()> { &spl_stake_pool::instruction::set_staker( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.new_staker.key(), ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.new_staker.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) diff --git a/programs/steward/src/instructions/update_parameters.rs b/programs/steward/src/instructions/update_parameters.rs index 97032210..02e7f5d9 100644 --- a/programs/steward/src/instructions/update_parameters.rs +++ b/programs/steward/src/instructions/update_parameters.rs @@ -1,4 +1,4 @@ -use crate::{utils::get_config_authority, Config, UpdateParametersArgs}; +use crate::{utils::get_config_parameter_authority, Config, UpdateParametersArgs}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -6,7 +6,7 @@ pub struct UpdateParameters<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_parameter_authority(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 0e280f7b..138daf11 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -58,17 +58,11 @@ pub mod steward { // Initializes Config and Staker accounts. Must be called before any other instruction // Requires Pool to be initialized - pub fn initialize_config( - ctx: Context, - authority: Pubkey, + pub fn initialize_steward( + ctx: Context, update_parameters_args: UpdateParametersArgs, ) -> Result<()> { - instructions::initialize_config::handler(ctx, authority, &update_parameters_args) - } - - /// Creates state account - pub const fn initialize_state(ctx: Context) -> Result<()> { - instructions::initialize_state::handler(ctx) + instructions::initialize_steward::handler(ctx, &update_parameters_args) } /// Increases state account by 10KiB each ix until it reaches StewardStateAccount::SIZE @@ -133,8 +127,11 @@ pub mod steward { // If `new_authority` is not a pubkey you own, you cannot regain the authority, but you can // use the stake pool manager to set a new staker - pub fn set_new_authority(ctx: Context) -> Result<()> { - instructions::set_new_authority::handler(ctx) + pub fn set_new_authority( + ctx: Context, + authority_type: AuthorityType, + ) -> Result<()> { + instructions::set_new_authority::handler(ctx, authority_type) } pub fn pause_steward(ctx: Context) -> Result<()> { @@ -148,17 +145,17 @@ pub mod steward { /// Adds the validator at `index` to the blacklist. It will be instant unstaked and never receive delegations pub fn add_validator_to_blacklist( ctx: Context, - index: u32, + validator_history_blacklist: u32, ) -> Result<()> { - instructions::add_validator_to_blacklist::handler(ctx, index) + instructions::add_validator_to_blacklist::handler(ctx, validator_history_blacklist) } /// Removes the validator at `index` from the blacklist pub fn remove_validator_from_blacklist( ctx: Context, - index: u32, + validator_history_blacklist: u32, ) -> Result<()> { - instructions::remove_validator_from_blacklist::handler(ctx, index) + instructions::remove_validator_from_blacklist::handler(ctx, validator_history_blacklist) } /// For parameters that are present in args, the instruction checks that they are within sensible bounds and saves them to config struct diff --git a/programs/steward/src/score.rs b/programs/steward/src/score.rs index d492110c..d581e68f 100644 --- a/programs/steward/src/score.rs +++ b/programs/steward/src/score.rs @@ -51,7 +51,6 @@ pub struct ScoreComponents { pub fn validator_score( validator: &ValidatorHistory, - index: usize, cluster: &ClusterHistory, config: &Config, current_epoch: u16, @@ -195,7 +194,11 @@ pub fn validator_score( let superminority_score = if !is_superminority { 1.0 } else { 0.0 }; /////// Blacklist /////// - let blacklisted_score = if config.blacklist.get(index).unwrap_or(false) { + let blacklisted_score = if config + .validator_history_blacklist + .get(validator.index as usize) + .unwrap_or(false) + { 0.0 } else { 1.0 @@ -257,7 +260,6 @@ pub struct InstantUnstakeComponents { /// Before running, checks are needed on cluster and validator history to be updated this epoch past the halfway point of the epoch. pub fn instant_unstake_validator( validator: &ValidatorHistory, - index: usize, cluster: &ClusterHistory, config: &Config, epoch_start_slot: u64, @@ -321,7 +323,9 @@ pub fn instant_unstake_validator( let commission_check = commission > params.commission_threshold; /////// Blacklist /////// - let is_blacklisted = config.blacklist.get(index)?; + let is_blacklisted = config + .validator_history_blacklist + .get(validator.index as usize)?; let instant_unstake = delinquency_check || commission_check || mev_commission_check || is_blacklisted; diff --git a/programs/steward/src/state/accounts.rs b/programs/steward/src/state/accounts.rs index ee252adc..40460d65 100644 --- a/programs/steward/src/state/accounts.rs +++ b/programs/steward/src/state/accounts.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::*; use borsh::BorshSerialize; use type_layout::TypeLayout; -use crate::{bitmask::BitMask, parameters::Parameters, utils::U8Bool, StewardState}; +use crate::{parameters::Parameters, utils::U8Bool, LargeBitMask, StewardState}; /// Config is a user-provided keypair. /// This is so there can be multiple configs per stake pool, and one party can't @@ -15,20 +15,37 @@ pub struct Config { /// SPL Stake Pool address that this program is managing pub stake_pool: Pubkey, - /// Authority for pool stewardship, can execute SPL Staker commands and adjust Delegation parameters - pub authority: Pubkey, + /// Validator List + pub validator_list: Pubkey, + + /// Admin + /// - Update the `parameters_authority` + /// - Update the `blacklist_authority` + /// - Can call SPL Passthrough functions + /// - Can pause/reset the state machine + pub admin: Pubkey, + + /// Parameters Authority + /// - Can update steward parameters + pub parameters_authority: Pubkey, + + /// Blacklist Authority + /// - Can add to the blacklist + /// - Can remove from the blacklist + pub blacklist_authority: Pubkey, /// Bitmask representing index of validators that are not allowed delegation - pub blacklist: BitMask, + /// NOTE: This is indexed off of the validator history, NOT the validator list + pub validator_history_blacklist: LargeBitMask, /// Parameters for scoring, delegation, and state machine pub parameters: Parameters, - /// Padding for future governance parameters - pub _padding: [u8; 1023], - /// Halts any state machine progress pub paused: U8Bool, + + /// Padding for future governance parameters + pub _padding: [u8; 1023], } impl Config { @@ -44,26 +61,6 @@ impl Config { } } -// PDA that is used to sign instructions for the stake pool. -// The pool's "staker" account needs to be assigned to this address, -// and it has authority over adding validators, removing validators, and delegating stake. -#[account] -pub struct Staker { - pub bump: u8, -} -impl Staker { - pub const SIZE: usize = 8 + size_of::(); - pub const SEED: &'static [u8] = b"staker"; - - pub fn get_address(config: &Pubkey) -> Pubkey { - let (pubkey, _) = - Pubkey::find_program_address(&[Self::SEED, config.as_ref()], &crate::id()); - pubkey - } -} - -// static_assertions::const_assert_eq!(StewardStateAccount::SIZE, 162584); - #[derive(BorshSerialize)] #[account(zero_copy)] pub struct StewardStateAccount { @@ -78,3 +75,10 @@ impl StewardStateAccount { pub const SEED: &'static [u8] = b"steward_state"; pub const IS_INITIALIZED_BYTE_POSITION: usize = Self::SIZE - 8; } + +pub fn derive_steward_state_address(steward_config: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[StewardStateAccount::SEED, steward_config.as_ref()], + &crate::id(), + ) +} diff --git a/programs/steward/src/state/large_bitmask.rs b/programs/steward/src/state/large_bitmask.rs new file mode 100644 index 00000000..bfb96e78 --- /dev/null +++ b/programs/steward/src/state/large_bitmask.rs @@ -0,0 +1,104 @@ +/* + This file is largely copied over from bitmask.rs + This is because making a generic bitmask struct either didn't play well with + zero-copy, or it added too much overhead to a struct meant for performance. +*/ + +use anchor_lang::{prelude::Result, zero_copy}; +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::errors::StewardError; + +//We are allocating at this size to handle future growth of ValidatorHistory accounts, at 2800 in June 2024 +const LARGE_BITMASK_INDEXES: usize = 20_000; + +#[allow(clippy::integer_division)] +const LARGE_BITMASK: usize = (LARGE_BITMASK_INDEXES + 64 - 1) / 64; // ceil(LARGE_BITMASK_INDEXES / 64) + +/// Data structure used to efficiently pack a binary array, primarily used to store all validators. +/// Each validator has an index (its index in the spl_stake_pool::ValidatorList), corresponding to a bit in the bitmask. +/// When an operation is executed on a validator, the bit corresponding to that validator's index is set to 1. +/// When all bits are 1, the operation is complete. +#[derive(BorshSerialize, BorshDeserialize)] +#[zero_copy] +pub struct LargeBitMask { + pub values: [u64; LARGE_BITMASK], +} + +impl Default for LargeBitMask { + fn default() -> Self { + Self { + values: [0; LARGE_BITMASK], + } + } +} + +impl LargeBitMask { + #[allow(clippy::integer_division)] + pub fn set(&mut self, index: usize, value: bool) -> Result<()> { + if index >= LARGE_BITMASK_INDEXES { + return Err(StewardError::BitmaskOutOfBounds.into()); + } + let word = index / 64; + let bit = index % 64; + if value { + self.values[word] |= 1 << bit; + } else { + self.values[word] &= !(1 << bit); + } + Ok(()) + } + + #[allow(clippy::integer_division)] + pub fn get(&self, index: usize) -> Result { + if index >= LARGE_BITMASK_INDEXES { + return Err(StewardError::BitmaskOutOfBounds.into()); + } + let word = index / 64; + let bit = index % 64; + Ok((self.values[word] >> bit) & 1 == 1) + } + + /// Unsafe version of get, which does not check if the index is out of bounds. + #[inline] + #[allow(clippy::integer_division, clippy::arithmetic_side_effects)] + pub const fn get_unsafe(&self, index: usize) -> bool { + let word = index / 64; + let bit = index % 64; + (self.values[word] >> bit) & 1 == 1 + } + + pub fn reset(&mut self) { + self.values = [0; LARGE_BITMASK]; + } + + pub fn is_empty(&self) -> bool { + self.values.iter().all(|&x| x == 0) + } + + pub fn count(&self) -> usize { + self.values.iter().map(|x| x.count_ones() as usize).sum() + } + + #[allow(clippy::integer_division)] + pub fn is_complete(&self, num_validators: u64) -> Result { + let num_validators = num_validators as usize; + if num_validators > LARGE_BITMASK_INDEXES { + return Err(StewardError::BitmaskOutOfBounds.into()); + } + let full_words = num_validators / 64; + if !self.values[0..full_words].iter().all(|&x| x == u64::MAX) { + return Ok(false); + } + let remainder = num_validators % 64; + if remainder > 0 { + let mask: u64 = (1u64 << remainder) + .checked_sub(1) + .ok_or(StewardError::ArithmeticError)?; + if self.values[full_words] & mask != mask { + return Ok(false); + } + } + Ok(true) + } +} diff --git a/programs/steward/src/state/mod.rs b/programs/steward/src/state/mod.rs index 6dbaa41f..9877fcfe 100644 --- a/programs/steward/src/state/mod.rs +++ b/programs/steward/src/state/mod.rs @@ -1,9 +1,11 @@ pub mod accounts; pub mod bitmask; +pub mod large_bitmask; pub mod parameters; pub mod steward_state; pub use accounts::*; pub use bitmask::*; +pub use large_bitmask::*; pub use parameters::*; pub use steward_state::*; diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 3cf17131..0c091fc9 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -9,7 +9,9 @@ use crate::{ UnstakeState, }, errors::StewardError, - score::{instant_unstake_validator, validator_score}, + score::{ + instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, + }, utils::{epoch_progress, get_target_lamports, stake_lamports_at_validator_list_index, U8Bool}, Config, Parameters, }; @@ -27,11 +29,12 @@ fn invalid_state_error(_expected: String, _actual: String) -> Error { } #[event] +#[derive(Debug)] pub struct StateTransition { - epoch: u64, - slot: u64, - previous_state: String, - new_state: String, + pub epoch: u64, + pub slot: u64, + pub previous_state: String, + pub new_state: String, } pub fn maybe_transition_and_emit( @@ -39,19 +42,19 @@ pub fn maybe_transition_and_emit( clock: &Clock, params: &Parameters, epoch_schedule: &EpochSchedule, -) -> Result<()> { +) -> Result> { let initial_state = steward_state.state_tag.to_string(); steward_state.transition(clock, params, epoch_schedule)?; if initial_state != steward_state.state_tag.to_string() { - emit!(StateTransition { + return Ok(Some(StateTransition { epoch: clock.epoch, slot: clock.slot, previous_state: initial_state, new_state: steward_state.state_tag.to_string(), - }); + })); } - Ok(()) + Ok(None) } /// Tracks state of the stake pool. @@ -542,7 +545,7 @@ impl StewardState { cluster: &ClusterHistory, config: &Config, num_pool_validators: u64, - ) -> Result<()> { + ) -> Result> { if matches!(self.state_tag, StewardStateEnum::ComputeScores) { let current_epoch = clock.epoch; let current_slot = clock.slot; @@ -576,7 +579,7 @@ impl StewardState { // Skip scoring if already processed if self.progress.get(index)? { - return Ok(()); + return Ok(None); } // Skip scoring if marked for deletion @@ -602,7 +605,7 @@ impl StewardState { self.progress.set(index, true)?; - return Ok(()); + return Ok(None); } // Check that latest_update_slot is within the current epoch to guarantee previous epoch data is complete @@ -621,8 +624,7 @@ impl StewardState { return Err(StewardError::ClusterHistoryNotRecentEnough.into()); } - let score = validator_score(validator, index, cluster, config, current_epoch as u16)?; - emit!(score); + let score = validator_score(validator, cluster, config, current_epoch as u16)?; self.scores[index] = (score.score * 1_000_000_000.) as u32; self.yield_scores[index] = (score.yield_score * 1_000_000_000.) as u32; @@ -645,7 +647,7 @@ impl StewardState { )?; self.progress.set(index, true)?; - return Ok(()); + return Ok(Some(score)); } Err(invalid_state_error( "ComputeScores".to_string(), @@ -705,7 +707,7 @@ impl StewardState { index: usize, cluster: &ClusterHistory, config: &Config, - ) -> Result<()> { + ) -> Result> { if matches!(self.state_tag, StewardStateEnum::ComputeInstantUnstake) { if clock.epoch >= self.next_cycle_epoch { return Err(invalid_state_error( @@ -722,13 +724,13 @@ impl StewardState { // Skip if already processed if self.progress.get(index)? { - return Ok(()); + return Ok(None); } // Skip if marked for deletion if self.validators_to_remove.get(index)? { self.progress.set(index, true)?; - return Ok(()); + return Ok(None); } let first_slot = epoch_schedule.get_first_slot_in_epoch(clock.epoch); @@ -755,17 +757,16 @@ impl StewardState { let instant_unstake_result = instant_unstake_validator( validator, - index, cluster, config, first_slot, clock.epoch as u16, )?; - emit!(instant_unstake_result); + self.instant_unstake .set(index, instant_unstake_result.instant_unstake)?; self.progress.set(index, true)?; - return Ok(()); + return Ok(Some(instant_unstake_result)); } Err(invalid_state_error( "ComputeInstantUnstake".to_string(), @@ -816,14 +817,28 @@ impl StewardState { .checked_add(stake_rent) .ok_or(StewardError::ArithmeticError)?; - // Maximum increase amount is the total lamports in the reserve stake account minus 2 * stake_rent, which accounts for reserve rent + transient rent - // Saturating_sub because reserve stake may be less than 2 * stake_rent, but needs more than 2 * stake_rent to be able to delegate - let reserve_lamports = reserve_lamports.saturating_sub( - stake_rent - .checked_mul(2) - .ok_or(StewardError::ArithmeticError)?, + msg!("Reserve lamports before adjustment: {}", reserve_lamports); + msg!( + "Stake pool lamports before adjustment: {}", + stake_pool_lamports ); + // Maximum increase amount is the total lamports in the reserve stake account minus (num_validators + 1) * stake_rent, which covers rent for all validators plus the transient rent + let all_accounts_needed_reserve_for_rent = validator_list + .len() + .checked_add(1) + .ok_or(StewardError::ArithmeticError)?; + + let accounts_left_needed_reserve_for_rent = all_accounts_needed_reserve_for_rent + .checked_sub(self.progress.count() as u32) + .ok_or(StewardError::ArithmeticError)?; + + let reserve_minimum = stake_rent + .checked_mul(accounts_left_needed_reserve_for_rent as u64) + .ok_or(StewardError::ArithmeticError)?; + // Saturating_sub because reserve stake may be less than the reserve_minimum but needs more than the reserve_minimum to be able to delegate + let reserve_lamports = reserve_lamports.saturating_sub(reserve_minimum); + // Represents the amount of lamports that can be delegated to validators beyond the fixed costs of rent and minimum_delegation let stake_pool_lamports = stake_pool_lamports .checked_sub( @@ -909,6 +924,15 @@ impl StewardState { RebalanceType::None }; + msg!("Reserve lamports after adjustment: {}", reserve_lamports); + msg!( + "Stake pool lamports after adjustment: {}", + stake_pool_lamports + ); + msg!("Rebalance Type: {:?}", rebalance); + msg!("Current Lamports: {}", current_lamports); + msg!("Target Lamports: {}", target_lamports); + // Update internal state based on rebalance match rebalance { RebalanceType::Decrease(DecreaseComponents { @@ -917,13 +941,6 @@ impl StewardState { stake_deposit_unstake_lamports, total_unstake_lamports, }) => { - emit!(DecreaseComponents { - scoring_unstake_lamports, - instant_unstake_lamports, - stake_deposit_unstake_lamports, - total_unstake_lamports, - }); - self.validator_lamport_balances[index] = self.validator_lamport_balances[index] .saturating_sub(total_unstake_lamports); diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index 78d52735..7dcce9b2 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -15,9 +15,24 @@ pub fn get_stake_pool_address(account: &AccountLoader) -> Result Ok(config.stake_pool) } -pub fn get_config_authority(account: &AccountLoader) -> Result { +pub fn get_validator_list(account: &AccountLoader) -> Result { let config = account.load()?; - Ok(config.authority) + Ok(config.validator_list) +} + +pub fn get_config_admin(account: &AccountLoader) -> Result { + let config = account.load()?; + Ok(config.admin) +} + +pub fn get_config_blacklist_authority(account: &AccountLoader) -> Result { + let config = account.load()?; + Ok(config.blacklist_authority) +} + +pub fn get_config_parameter_authority(account: &AccountLoader) -> Result { + let config = account.load()?; + Ok(config.parameters_authority) } pub fn epoch_progress(clock: &Clock, epoch_schedule: &EpochSchedule) -> Result { @@ -86,7 +101,7 @@ pub fn get_validator_stake_info_at_index( Ok(validator_stake_info) } -pub fn check_validator_list_has_stake_status( +pub fn check_validator_list_has_stake_status_other_than( validator_list_account_info: &AccountInfo, flag: StakeStatus, ) -> Result { @@ -105,7 +120,7 @@ pub fn check_validator_list_has_stake_status( let stake_status = validator_list.data[stake_status_index]; - if stake_status == flag as u8 { + if stake_status != flag as u8 { return Ok(true); } } diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 1ca223b4..05b6f972 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -15,8 +15,8 @@ use jito_steward::{ bitmask::BitMask, constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT, STAKE_POOL_WITHDRAW_SEED}, utils::StakePool, - Config, Delegation, Parameters, Staker, StewardState, StewardStateAccount, StewardStateEnum, - UpdateParametersArgs, STATE_PADDING_0_SIZE, + Config, Delegation, LargeBitMask, Parameters, StewardState, StewardStateAccount, + StewardStateEnum, UpdateParametersArgs, STATE_PADDING_0_SIZE, }; use solana_program_test::*; use solana_sdk::{ @@ -65,7 +65,6 @@ impl Default for StakePoolMetadata { pub struct TestFixture { pub ctx: Rc>, pub stake_pool_meta: StakePoolMetadata, - pub staker: Pubkey, pub steward_config: Keypair, pub steward_state: Pubkey, pub cluster_history_account: Pubkey, @@ -104,11 +103,6 @@ impl TestFixture { let stake_pool_meta = StakePoolMetadata::default(); let steward_config = Keypair::new(); - let staker = Pubkey::find_program_address( - &[Staker::SEED, steward_config.pubkey().as_ref()], - &jito_steward::id(), - ) - .0; let steward_state = Pubkey::find_program_address( &[StewardStateAccount::SEED, steward_config.pubkey().as_ref()], &jito_steward::id(), @@ -140,7 +134,6 @@ impl TestFixture { Self { ctx, stake_pool_meta, - staker, steward_state, steward_config, validator_history_config, @@ -257,7 +250,7 @@ impl TestFixture { } } - pub async fn initialize_config(&self, parameters: Option) { + pub async fn initialize_steward(&self, parameters: Option) { // Default parameters from JIP let update_parameters_args = parameters.unwrap_or(UpdateParametersArgs { mev_commission_range: Some(0), // Set to pass validation, where epochs starts at 0 @@ -282,17 +275,16 @@ impl TestFixture { let instruction = Instruction { program_id: jito_steward::id(), - accounts: jito_steward::accounts::InitializeConfig { + accounts: jito_steward::accounts::InitializeSteward { config: self.steward_config.pubkey(), stake_pool: self.stake_pool_meta.stake_pool, - staker: self.staker, + state_account: self.steward_state, stake_pool_program: spl_stake_pool::id(), system_program: anchor_lang::solana_program::system_program::id(), - signer: self.keypair.pubkey(), + current_staker: self.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::InitializeConfig { - authority: self.keypair.pubkey(), + data: jito_steward::instruction::InitializeSteward { update_parameters_args, } .data(), @@ -307,23 +299,10 @@ impl TestFixture { self.submit_transaction_assert_success(transaction).await; } - pub async fn initialize_steward_state(&self) { - let instruction = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::InitializeState { - state_account: self.steward_state, - config: self.steward_config.pubkey(), - system_program: anchor_lang::solana_program::system_program::id(), - signer: self.keypair.pubkey(), - } - .to_account_metas(None), - data: jito_steward::instruction::InitializeState {}.data(), - }; - - let mut ixs = vec![instruction]; - + pub async fn realloc_steward_state(&self) { // Realloc validator history account let mut num_reallocs = (StewardStateAccount::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; + let mut ixs = vec![]; while num_reallocs > 0 { ixs.extend(vec![ @@ -837,11 +816,14 @@ impl Default for StateMachineFixtures { // Setup Config let config = Config { stake_pool: Pubkey::new_unique(), - authority: Pubkey::new_unique(), - blacklist: BitMask::default(), parameters, - _padding: [0; 1023], paused: false.into(), + validator_list: Pubkey::new_unique(), + admin: Pubkey::new_unique(), + parameters_authority: Pubkey::new_unique(), + blacklist_authority: Pubkey::new_unique(), + validator_history_blacklist: LargeBitMask::default(), + _padding: [0; 1023], }; // Setup Sysvars: Clock, EpochSchedule diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 83e0b464..901bf0b8 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -36,7 +36,6 @@ fn test_compute_score() { // Regular run let components = validator_score( &good_validator, - good_validator.index as usize, &cluster_history, &config, current_epoch as u16, @@ -64,14 +63,8 @@ fn test_compute_score() { let mut validator = good_validator; validator.history.last_mut().unwrap().mev_commission = 1001; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -92,14 +85,8 @@ fn test_compute_score() { let mut validator = good_validator; validator.history.arr[11].mev_commission = 1001; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -119,14 +106,8 @@ fn test_compute_score() { ); let mut validator = good_validator; validator.history.arr[9].mev_commission = 1001; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -148,17 +129,11 @@ fn test_compute_score() { // blacklist let validator = good_validator; config - .blacklist + .validator_history_blacklist .set(validator.index as usize, true) .unwrap(); - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -176,19 +151,13 @@ fn test_compute_score() { epoch: current_epoch as u16 } ); - config.blacklist.reset(); + config.validator_history_blacklist.reset(); // superminority score let mut validator = good_validator; validator.history.last_mut().unwrap().is_superminority = 1; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -212,14 +181,8 @@ fn test_compute_score() { for i in 0..19 { validator.history.arr_mut()[i].is_superminority = 1; } - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -244,14 +207,8 @@ fn test_compute_score() { validator.history.arr_mut()[i].mev_commission = ValidatorHistoryEntry::default().mev_commission; } - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -273,14 +230,8 @@ fn test_compute_score() { // commission let mut validator = good_validator; validator.history.last_mut().unwrap().commission = 11; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -310,14 +261,8 @@ fn test_compute_score() { // commission above regular threshold, below historical threshold, outside of regular threshold window validator.history.arr[0].commission = 14; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -337,14 +282,8 @@ fn test_compute_score() { ); validator.history.arr[0].commission = 16; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -371,14 +310,8 @@ fn test_compute_score() { validator.history.arr_mut()[i].epoch_credits = 880; cluster_history.history.arr_mut()[i].total_blocks = 1000; } - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -400,14 +333,8 @@ fn test_compute_score() { // delinquency let mut validator = good_validator; validator.history.arr[10].epoch_credits = 0; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -434,14 +361,8 @@ fn test_compute_score() { cluster_history.history.arr[10].total_blocks = ClusterHistoryEntry::default().total_blocks; cluster_history.history.arr[11].total_blocks = ClusterHistoryEntry::default().total_blocks; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -466,14 +387,8 @@ fn test_compute_score() { assert_eq!(current_epoch, 20); validator.history.arr[current_epoch as usize].epoch_credits = 0; cluster_history.history.arr[current_epoch as usize].total_blocks = 0; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -505,27 +420,15 @@ fn test_compute_score() { validator.history.arr[current_epoch as usize - 2].is_superminority = ValidatorHistoryEntry::default().is_superminority; validator.history.arr[current_epoch as usize - 3].is_superminority = 1; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert!(components.superminority_score == 0.0); // Test error: superminority should exist if epoch credits exist let mut validator = good_validator; validator.history.arr[current_epoch as usize].is_superminority = ValidatorHistoryEntry::default().is_superminority; - let res = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ); + let res = validator_score(&validator, &cluster_history, &config, current_epoch as u16); assert!(res == Err(StewardError::StakeHistoryNotRecentEnough.into())); } @@ -557,7 +460,6 @@ fn test_instant_unstake() { let res = instant_unstake_validator( &good_validator, - good_validator.index as usize, &cluster_history, &config, start_slot, @@ -580,17 +482,17 @@ fn test_instant_unstake() { // Is blacklisted config - .blacklist + .validator_history_blacklist .set(good_validator.index as usize, true) .unwrap(); let res = instant_unstake_validator( &good_validator, - good_validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res.is_ok()); assert!( res.unwrap() @@ -604,17 +506,17 @@ fn test_instant_unstake() { epoch: current_epoch } ); - config.blacklist.reset(); + config.validator_history_blacklist.reset(); // Delinquency threshold + Commission let res = instant_unstake_validator( &bad_validator, - bad_validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res.is_ok()); assert!( res.unwrap() @@ -635,12 +537,12 @@ fn test_instant_unstake() { ClusterHistoryEntry::default().total_blocks; let res = instant_unstake_validator( &bad_validator, - bad_validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res == Err(StewardError::ClusterHistoryNotRecentEnough.into())); let cluster_history = default_fixture.cluster_history; @@ -650,13 +552,26 @@ fn test_instant_unstake() { let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); - assert!(res == Err(StewardError::VoteHistoryNotRecentEnough.into())); + println!("NEED Error: {:?}", res); + + assert!(res.is_ok()); + assert!( + res.unwrap() + == InstantUnstakeComponents { + instant_unstake: true, + delinquency_check: true, + commission_check: false, + mev_commission_check: false, + is_blacklisted: false, + vote_account: validator.vote_account, + epoch: current_epoch + } + ); let mut validator = validators[0]; validator @@ -667,12 +582,12 @@ fn test_instant_unstake() { ValidatorHistoryEntry::default().vote_account_last_update_slot; let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res == Err(StewardError::VoteHistoryNotRecentEnough.into())); // Not sure how commission would be unset with epoch credits set but test anyway @@ -680,7 +595,6 @@ fn test_instant_unstake() { validator.history.last_mut().unwrap().commission = ValidatorHistoryEntry::default().commission; let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, @@ -705,7 +619,6 @@ fn test_instant_unstake() { ValidatorHistoryEntry::default().mev_commission; let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, @@ -730,7 +643,6 @@ fn test_instant_unstake() { cluster_history.history.last_mut().unwrap().total_blocks = 0; let res = instant_unstake_validator( &good_validator, - good_validator.index as usize, &cluster_history, &config, start_slot, diff --git a/tests/tests/steward/test_integration.rs b/tests/tests/steward/test_integration.rs index e8dcbc00..dcc0acc3 100644 --- a/tests/tests/steward/test_integration.rs +++ b/tests/tests/steward/test_integration.rs @@ -36,8 +36,8 @@ async fn test_compute_delegations() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let clock: Clock = fixture.get_sysvar().await; @@ -101,7 +101,6 @@ async fn test_compute_delegations() { accounts: jito_steward::accounts::ComputeDelegations { config: fixture.steward_config.pubkey(), state_account: fixture.steward_state, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeDelegations {}.data(), @@ -146,7 +145,6 @@ async fn test_compute_delegations() { accounts: jito_steward::accounts::ComputeDelegations { config: fixture.steward_config.pubkey(), state_account: fixture.steward_state, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeDelegations {}.data(), @@ -173,8 +171,8 @@ async fn test_compute_scores() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let epoch_credits: Vec<(u64, u64, u64)> = vec![(0, 1, 0), (1, 2, 1), (2, 3, 2), (3, 4, 3), (4, 5, 4)]; @@ -311,7 +309,6 @@ async fn test_compute_scores() { validator_history: validator_history_account, validator_list: fixture.stake_pool_meta.validator_list, cluster_history: cluster_history_account, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeScore { @@ -469,7 +466,7 @@ async fn test_compute_instant_unstake() { let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; fixture - .initialize_config(Some(UpdateParametersArgs { + .initialize_steward(Some(UpdateParametersArgs { mev_commission_range: Some(0), // Set to pass validation, where epochs starts at 0 epoch_credits_range: Some(0), // Set to pass validation, where epochs starts at 0 commission_range: Some(0), // Set to pass validation, where epochs starts at 0 @@ -490,7 +487,7 @@ async fn test_compute_instant_unstake() { minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 })) .await; - fixture.initialize_steward_state().await; + fixture.realloc_steward_state().await; let epoch_credits = vec![(0, 1, 0), (1, 2, 1), (2, 3, 2), (3, 4, 3), (4, 5, 4)]; let vote_account = Pubkey::new_unique(); @@ -620,7 +617,6 @@ async fn test_compute_instant_unstake() { validator_history: validator_history_account, validator_list: fixture.stake_pool_meta.validator_list, cluster_history: cluster_history_account, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeInstantUnstake { @@ -703,8 +699,8 @@ async fn test_idle() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let clock: Clock = fixture.get_sysvar().await; let epoch_schedule: EpochSchedule = fixture.get_sysvar().await; @@ -739,7 +735,6 @@ async fn test_idle() { accounts: jito_steward::accounts::Idle { config: fixture.steward_config.pubkey(), state_account: fixture.steward_state, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Idle {}.data(), @@ -839,8 +834,8 @@ async fn test_rebalance_increase() { .advance_num_epochs(epoch_schedule.first_normal_epoch - clock.epoch, 10) .await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) @@ -969,7 +964,6 @@ async fn test_rebalance_increase() { config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -981,7 +975,6 @@ async fn test_rebalance_increase() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -996,7 +989,6 @@ async fn test_rebalance_increase() { stake_pool: fixture.stake_pool_meta.stake_pool, reserve_stake: fixture.stake_pool_meta.reserve, stake_pool_program: spl_stake_pool::id(), - staker: fixture.staker, withdraw_authority, vote_account, stake_account: stake_account_address, @@ -1007,7 +999,6 @@ async fn test_rebalance_increase() { stake_program: stake::program::id(), stake_config: stake::config::ID, stake_history: solana_program::sysvar::stake_history::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { @@ -1059,7 +1050,13 @@ async fn test_rebalance_increase() { pool_minimum_delegation ); - let expected_transient_stake = reserve_before_rebalance.lamports - 2 * stake_rent; + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + let validators_that_need_rent = steward_state_account.state.num_pool_validators + 1 + - (steward_state_account.state.progress.count() as u64 - 1); + let expected_transient_stake = + reserve_before_rebalance.lamports - (stake_rent * validators_that_need_rent); assert_eq!( transient_stake_account.stake().unwrap().delegation.stake, expected_transient_stake @@ -1078,8 +1075,8 @@ async fn test_rebalance_decrease() { .advance_num_epochs(epoch_schedule.first_normal_epoch - clock.epoch, 10) .await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) @@ -1203,7 +1200,6 @@ async fn test_rebalance_decrease() { config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -1215,7 +1211,6 @@ async fn test_rebalance_decrease() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -1307,7 +1302,6 @@ async fn test_rebalance_decrease() { stake_pool: fixture.stake_pool_meta.stake_pool, reserve_stake: fixture.stake_pool_meta.reserve, stake_pool_program: spl_stake_pool::id(), - staker: fixture.staker, withdraw_authority, vote_account, stake_account: stake_account_address, @@ -1318,7 +1312,6 @@ async fn test_rebalance_decrease() { stake_program: stake::program::id(), stake_config: stake::config::ID, stake_history: solana_program::sysvar::stake_history::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { @@ -1382,8 +1375,8 @@ async fn test_rebalance_other_cases() { .advance_num_epochs(epoch_schedule.first_normal_epoch - clock.epoch, 10) .await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) @@ -1445,7 +1438,6 @@ async fn test_rebalance_other_cases() { config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -1457,7 +1449,6 @@ async fn test_rebalance_other_cases() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -1485,7 +1476,6 @@ async fn test_rebalance_other_cases() { stake_pool: fixture.stake_pool_meta.stake_pool, reserve_stake: fixture.stake_pool_meta.reserve, stake_pool_program: spl_stake_pool::id(), - staker: fixture.staker, withdraw_authority, vote_account, stake_account: stake_account_address, @@ -1496,7 +1486,6 @@ async fn test_rebalance_other_cases() { stake_program: stake::program::id(), stake_config: stake::config::ID, stake_history: solana_program::sysvar::stake_history::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { diff --git a/tests/tests/steward/test_parameters.rs b/tests/tests/steward/test_parameters.rs index 591dce04..6665ab35 100644 --- a/tests/tests/steward/test_parameters.rs +++ b/tests/tests/steward/test_parameters.rs @@ -174,8 +174,8 @@ async fn _set_parameter(fixture: &TestFixture, update_parameters: &UpdateParamet async fn test_update_parameters() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _set_parameter( &fixture, diff --git a/tests/tests/steward/test_spl_passthrough.rs b/tests/tests/steward/test_spl_passthrough.rs index b0a7b080..1fc9e51f 100644 --- a/tests/tests/steward/test_spl_passthrough.rs +++ b/tests/tests/steward/test_spl_passthrough.rs @@ -6,8 +6,9 @@ use anchor_lang::{ }; use jito_steward::{ constants::MAX_VALIDATORS, + derive_steward_state_address, utils::{StakePool, ValidatorList}, - Config, Delegation, Staker, StewardStateAccount, StewardStateEnum, + Config, Delegation, StewardStateAccount, StewardStateEnum, }; use rand::prelude::SliceRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -179,11 +180,10 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { let instruction = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AddValidatorToPool { - steward_state: fixture.steward_state, + state_account: fixture.steward_state, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -195,7 +195,7 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AddValidatorToPool { @@ -242,11 +242,11 @@ async fn _set_and_check_preferred_validator( program_id: jito_steward::id(), accounts: jito_steward::accounts::SetPreferredValidator { 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, - staker: fixture.staker, validator_list: fixture.stake_pool_meta.validator_list, - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::SetPreferredValidator { @@ -325,11 +325,10 @@ async fn _increase_and_check_stake( program_id: jito_steward::id(), accounts: jito_steward::accounts::IncreaseValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -342,7 +341,7 @@ async fn _increase_and_check_stake( stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::IncreaseValidatorStake { @@ -418,11 +417,10 @@ async fn _increase_and_check_additional_stake( program_id: jito_steward::id(), accounts: jito_steward::accounts::IncreaseAdditionalValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -434,7 +432,7 @@ async fn _increase_and_check_additional_stake( stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), ephemeral_stake_account, } .to_account_metas(None), @@ -470,16 +468,16 @@ async fn _increase_and_check_additional_stake( ); } -pub async fn _set_staker(fixture: &TestFixture, staker: Pubkey, new_staker: Pubkey) { +pub async fn _set_staker(fixture: &TestFixture, new_staker: Pubkey) { let instruction = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::SetStaker { config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker, + state_account: fixture.steward_state, new_staker, - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::SetStaker {}.data(), @@ -509,8 +507,8 @@ async fn test_add_validator_to_pool() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; { // Test add 1 validator @@ -532,8 +530,8 @@ async fn test_remove_validator_from_pool() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Setup the steward state // _setup_test_steward_state(&fixture, MAX_VALIDATORS, 1_000_000_000).await; @@ -562,10 +560,9 @@ async fn test_remove_validator_from_pool() { program_id: jito_steward::id(), accounts: jito_steward::accounts::RemoveValidatorFromPool { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, stake_account: stake_account_address, @@ -573,7 +570,7 @@ async fn test_remove_validator_from_pool() { clock: sysvar::clock::id(), system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::RemoveValidatorFromPool { @@ -627,8 +624,8 @@ async fn test_set_preferred_validator() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -685,8 +682,8 @@ async fn test_increase_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -707,8 +704,8 @@ async fn test_decrease_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -742,11 +739,10 @@ async fn test_decrease_validator_stake() { program_id: jito_steward::id(), accounts: jito_steward::accounts::DecreaseValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -758,7 +754,7 @@ async fn test_decrease_validator_stake() { stake_history: sysvar::stake_history::id(), system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::DecreaseValidatorStake { @@ -787,8 +783,8 @@ async fn test_increase_additional_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -810,8 +806,8 @@ async fn test_decrease_additional_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -858,11 +854,10 @@ async fn test_decrease_additional_validator_stake() { program_id: jito_steward::id(), accounts: jito_steward::accounts::DecreaseAdditionalValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -873,7 +868,7 @@ async fn test_decrease_additional_validator_stake() { stake_history: sysvar::stake_history::id(), system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), ephemeral_stake_account, } .to_account_metas(None), @@ -905,8 +900,8 @@ async fn test_set_staker() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let new_staker = Keypair::new(); @@ -921,15 +916,12 @@ async fn test_set_staker() { StakePool::try_deserialize_unchecked(&mut stake_pool_account_raw.data.as_slice()) .expect("Failed to deserialize stake pool account"); - let (staker, _) = Pubkey::find_program_address( - &[Staker::SEED, fixture.steward_config.pubkey().as_ref()], - &jito_steward::id(), - ); + let (steward_state, _) = derive_steward_state_address(&fixture.steward_config.pubkey()); // Assert accounts are set up correctly - assert!(stake_pool_account.staker.eq(&staker)); - assert!(fixture.staker.eq(&staker)); - assert!(config_account.authority.eq(&fixture.keypair.pubkey())); + assert!(stake_pool_account.staker.eq(&steward_state)); + assert!(fixture.steward_state.eq(&steward_state)); + assert!(config_account.admin.eq(&fixture.keypair.pubkey())); assert!(config_account .stake_pool .eq(&fixture.stake_pool_meta.stake_pool)); @@ -937,12 +929,12 @@ async fn test_set_staker() { { // Test 1: Set staker to same staker - _set_staker(&fixture, fixture.staker, fixture.staker).await; + _set_staker(&fixture, fixture.steward_state).await; } { // Test 2: Set staker to different staker - _set_staker(&fixture, fixture.staker, new_staker.pubkey()).await; + _set_staker(&fixture, new_staker.pubkey()).await; } drop(fixture); diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index da660681..7bab19ff 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -151,7 +151,7 @@ fn test_compute_scores() { // Test blacklist validator config - .blacklist + .validator_history_blacklist .set(validators[0].index as usize, true) .unwrap(); let res = state.compute_score( @@ -446,7 +446,7 @@ fn test_compute_instant_unstake_success() { state.progress.reset(); state.instant_unstake.reset(); config - .blacklist + .validator_history_blacklist .set(validators[0].index as usize, true) .unwrap(); diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 6f759cb0..4126bd3e 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -3,7 +3,9 @@ use anchor_lang::{ solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}, InstructionData, ToAccountMetas, }; -use jito_steward::{utils::ValidatorList, Config, StewardStateAccount}; +use jito_steward::{ + instructions::AuthorityType, utils::ValidatorList, Config, StewardStateAccount, +}; use solana_program_test::*; use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; use tests::steward_fixtures::{ @@ -46,7 +48,6 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -58,7 +59,6 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -108,8 +108,8 @@ async fn test_auto_add_validator_to_pool() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _auto_add_validator_to_pool(&fixture, &Pubkey::new_unique()).await; @@ -121,8 +121,8 @@ async fn test_auto_remove() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let vote_account = Pubkey::new_unique(); @@ -135,7 +135,10 @@ async fn test_auto_remove() { fixture.stake_accounts_for_validator(vote_account).await; // Add vote account + println!("DID STEWARD STATE"); + _auto_add_validator_to_pool(&fixture, &vote_account).await; + println!("ADDED VALIDATOR"); let auto_remove_validator_ix = Instruction { program_id: jito_steward::id(), @@ -145,7 +148,6 @@ async fn test_auto_remove() { state_account: fixture.steward_state, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -158,7 +160,6 @@ async fn test_auto_remove() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoRemoveValidatorFromPool { @@ -207,7 +208,7 @@ async fn test_pause() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; + fixture.initialize_steward(None).await; let ix = Instruction { program_id: jito_steward::id(), @@ -266,7 +267,7 @@ async fn test_blacklist() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; + fixture.initialize_steward(None).await; let ix = Instruction { program_id: jito_steward::id(), @@ -275,7 +276,10 @@ async fn test_blacklist() { authority: fixture.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::AddValidatorToBlacklist { index: 0 }.data(), + data: jito_steward::instruction::AddValidatorToBlacklist { + validator_history_blacklist: 0, + } + .data(), }; let tx = Transaction::new_signed_with_payer( @@ -290,7 +294,7 @@ async fn test_blacklist() { let config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - assert!(config.blacklist.get(0).unwrap()); + assert!(config.validator_history_blacklist.get(0).unwrap()); let ix = Instruction { program_id: jito_steward::id(), @@ -299,7 +303,10 @@ async fn test_blacklist() { authority: fixture.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::RemoveValidatorFromBlacklist { index: 0 }.data(), + data: jito_steward::instruction::RemoveValidatorFromBlacklist { + validator_history_blacklist: 0, + } + .data(), }; let tx = Transaction::new_signed_with_payer( @@ -313,7 +320,7 @@ async fn test_blacklist() { let config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - assert!(!config.blacklist.get(0).unwrap()); + assert!(!config.validator_history_blacklist.get(0).unwrap()); drop(fixture); } @@ -323,7 +330,7 @@ async fn test_set_new_authority() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; + fixture.initialize_steward(None).await; // Regular test let new_authority = Keypair::new(); @@ -337,10 +344,13 @@ async fn test_set_new_authority() { accounts: jito_steward::accounts::SetNewAuthority { config: fixture.steward_config.pubkey(), new_authority: new_authority.pubkey(), - authority: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::SetNewAuthority {}.data(), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetAdmin, + } + .data(), }; let tx = Transaction::new_signed_with_payer( &[ix], @@ -354,7 +364,58 @@ async fn test_set_new_authority() { let config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - assert!(config.authority == new_authority.pubkey()); + assert!(config.admin == new_authority.pubkey()); + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::SetNewAuthority { + config: fixture.steward_config.pubkey(), + new_authority: new_authority.pubkey(), + admin: new_authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetBlacklistAuthority, + } + .data(), + }; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&new_authority.pubkey()), + &[&new_authority], + ctx.borrow().last_blockhash, + ); + + fixture.submit_transaction_assert_success(tx).await; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::SetNewAuthority { + config: fixture.steward_config.pubkey(), + new_authority: new_authority.pubkey(), + admin: new_authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetParametersAuthority, + } + .data(), + }; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&new_authority.pubkey()), + &[&new_authority], + ctx.borrow().last_blockhash, + ); + + fixture.submit_transaction_assert_success(tx).await; + + let config: Config = fixture + .load_and_deserialize(&fixture.steward_config.pubkey()) + .await; + assert!(config.admin == new_authority.pubkey()); + assert!(config.blacklist_authority == new_authority.pubkey()); + assert!(config.parameters_authority == new_authority.pubkey()); // Try to transfer back with original authority let ix = Instruction { @@ -362,21 +423,29 @@ async fn test_set_new_authority() { accounts: jito_steward::accounts::SetNewAuthority { config: fixture.steward_config.pubkey(), new_authority: fixture.keypair.pubkey(), - authority: fixture.keypair.pubkey(), + admin: new_authority.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::SetNewAuthority {}.data(), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetAdmin, + } + .data(), }; let tx = Transaction::new_signed_with_payer( &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], + Some(&new_authority.pubkey()), + &[&new_authority], ctx.borrow().last_blockhash, ); - fixture - .submit_transaction_assert_error(tx, "Unauthorized") + fixture.submit_transaction_assert_success(tx).await; + + let config: Config = fixture + .load_and_deserialize(&fixture.steward_config.pubkey()) .await; + assert!(config.admin == fixture.keypair.pubkey()); + assert!(config.blacklist_authority == new_authority.pubkey()); + assert!(config.parameters_authority == new_authority.pubkey()); drop(fixture); } diff --git a/utils/steward-cli/Cargo.toml b/utils/steward-cli/Cargo.toml index 4a9e9e70..8134dbb6 100644 --- a/utils/steward-cli/Cargo.toml +++ b/utils/steward-cli/Cargo.toml @@ -6,9 +6,13 @@ description = "CLI to manage the steward program" [dependencies] anchor-lang = "0.30.0" +anyhow = "1.0.86" clap = { version = "4.3.0", features = ["derive", "env"] } +dotenv = "0.15.0" futures = "0.3.21" futures-util = "0.3.21" +jito-steward = { features = ["no-entrypoint"], path = "../../programs/steward" } +keeper-core = { path = "../../keepers/keeper-core" } log = "0.4.18" solana-account-decoder = "1.18" solana-clap-utils = "1.18" @@ -16,11 +20,7 @@ solana-client = "1.18" solana-metrics = "1.18" solana-program = "1.18" solana-sdk = "1.18" -thiserror = "1.0.37" spl-stake-pool = { features = ["no-entrypoint"], version = "1.0.0" } +thiserror = "1.0.37" tokio = { version = "1.36.0", features = ["full"] } validator-history = { features = ["no-entrypoint"], path = "../../programs/validator-history" } -jito-steward = { features = ["no-entrypoint"], path = "../../programs/steward" } -keeper-core = { path = "../../keepers/keeper-core" } -dotenv = "0.15.0" -anyhow = "1.0.86" diff --git a/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs index e0b7206c..9bcea96d 100644 --- a/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs +++ b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs @@ -56,7 +56,6 @@ pub async fn command_auto_add_validator_from_pool( config: args.permissionless_parameters.steward_config, stake_pool_program: spl_stake_pool::id(), stake_pool: steward_accounts.stake_pool_address, - staker: steward_accounts.staker_address, reserve_stake: steward_accounts.stake_pool_account.reserve_stake, withdraw_authority: steward_accounts.stake_pool_withdraw_authority, validator_list: steward_accounts.validator_list_address, @@ -68,7 +67,6 @@ pub async fn command_auto_add_validator_from_pool( stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), diff --git a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs index 9fa2f442..d6cd47be 100644 --- a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs +++ b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs @@ -68,7 +68,6 @@ pub async fn command_auto_remove_validator_from_pool( state_account: steward_accounts.state_address, stake_pool_program: spl_stake_pool::id(), stake_pool: steward_accounts.stake_pool_address, - staker: steward_accounts.staker_address, reserve_stake: steward_accounts.stake_pool_account.reserve_stake, withdraw_authority: steward_accounts.stake_pool_withdraw_authority, validator_list: steward_accounts.validator_list_address, @@ -81,7 +80,6 @@ pub async fn command_auto_remove_validator_from_pool( stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoRemoveValidatorFromPool { diff --git a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs index a444ec42..e7740d6b 100644 --- a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs +++ b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs @@ -106,12 +106,11 @@ pub async fn command_remove_bad_validators( Instruction { program_id, accounts: jito_steward::accounts::RemoveValidatorFromPool { - signer: arc_payer.pubkey(), + admin: arc_payer.pubkey(), config: steward_config, - steward_state: steward_accounts.state_address, + state_account: steward_accounts.state_address, stake_pool_program: spl_stake_pool::id(), stake_pool: steward_accounts.stake_pool_address, - staker: steward_accounts.stake_pool_account.staker, withdraw_authority: steward_accounts.stake_pool_withdraw_authority, validator_list: steward_accounts.validator_list_address, stake_account: stake_address, diff --git a/utils/steward-cli/src/commands/cranks/compute_delegations.rs b/utils/steward-cli/src/commands/cranks/compute_delegations.rs index d26183cd..e88073e2 100644 --- a/utils/steward-cli/src/commands/cranks/compute_delegations.rs +++ b/utils/steward-cli/src/commands/cranks/compute_delegations.rs @@ -47,7 +47,6 @@ pub async fn command_crank_compute_delegations( accounts: jito_steward::accounts::ComputeDelegations { config: steward_config, state_account: state_address, - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeDelegations {}.data(), diff --git a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs index c4f5f7eb..98921476 100644 --- a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs +++ b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs @@ -7,7 +7,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; use validator_history::id as validator_history_id; -use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, signer::Signer}; +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file}; use crate::{ commands::command_args::CrankComputeInstantUnstake, @@ -80,7 +80,6 @@ pub async fn command_crank_compute_instant_unstake( validator_history: *history_account, validator_list: steward_accounts.validator_list_address, cluster_history, - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeInstantUnstake { diff --git a/utils/steward-cli/src/commands/cranks/compute_score.rs b/utils/steward-cli/src/commands/cranks/compute_score.rs index 92d888a8..8d2fc086 100644 --- a/utils/steward-cli/src/commands/cranks/compute_score.rs +++ b/utils/steward-cli/src/commands/cranks/compute_score.rs @@ -7,7 +7,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; use validator_history::id as validator_history_id; -use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, signer::Signer}; +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file}; use crate::{ commands::command_args::CrankComputeScore, @@ -86,7 +86,6 @@ pub async fn command_crank_compute_score( validator_history: *history_account, validator_list: steward_accounts.validator_list_address, cluster_history, - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeScore { diff --git a/utils/steward-cli/src/commands/cranks/idle.rs b/utils/steward-cli/src/commands/cranks/idle.rs index 353cd5f2..bc6d6c52 100644 --- a/utils/steward-cli/src/commands/cranks/idle.rs +++ b/utils/steward-cli/src/commands/cranks/idle.rs @@ -44,7 +44,6 @@ pub async fn command_crank_idle( accounts: jito_steward::accounts::Idle { config: steward_config, state_account: state_address, - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Idle {}.data(), diff --git a/utils/steward-cli/src/commands/cranks/rebalance.rs b/utils/steward-cli/src/commands/cranks/rebalance.rs index 62b94f4b..308260ee 100644 --- a/utils/steward-cli/src/commands/cranks/rebalance.rs +++ b/utils/steward-cli/src/commands/cranks/rebalance.rs @@ -8,9 +8,7 @@ use solana_program::instruction::Instruction; use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; use validator_history::id as validator_history_id; -use solana_sdk::{ - pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, -}; +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, stake, system_program}; use crate::{ commands::command_args::CrankRebalance, @@ -97,7 +95,6 @@ pub async fn command_crank_rebalance( validator_history: *history_account, stake_pool_program: spl_stake_pool::id(), stake_pool: steward_accounts.stake_pool_address, - staker: steward_accounts.staker_address, withdraw_authority: steward_accounts.stake_pool_withdraw_authority, validator_list: steward_accounts.validator_list_address, reserve_stake: steward_accounts.stake_pool_account.reserve_stake, @@ -110,7 +107,6 @@ pub async fn command_crank_rebalance( clock: solana_sdk::sysvar::clock::id(), stake_history: solana_sdk::sysvar::stake_history::id(), stake_config: stake::config::ID, - signer: payer.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { diff --git a/utils/steward-cli/src/commands/info/view_config.rs b/utils/steward-cli/src/commands/info/view_config.rs index 75805309..f1fb389c 100644 --- a/utils/steward-cli/src/commands/info/view_config.rs +++ b/utils/steward-cli/src/commands/info/view_config.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use jito_steward::{Config, Staker}; +use jito_steward::Config; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; @@ -20,38 +20,34 @@ pub async fn command_view_config( let steward_config_account = get_steward_config_account(client, &steward_config).await?; let steward_state = get_steward_state_address(&program_id, &steward_config); - let (steward_staker, _) = - Pubkey::find_program_address(&[Staker::SEED, steward_config.as_ref()], &program_id); // let mut output = String::new(); // Initialize the string directly - _print_default_config( - &steward_config, - &steward_state, - &steward_staker, - &steward_config_account, - ); + _print_default_config(&steward_config, &steward_state, &steward_config_account); Ok(()) } -fn _print_default_config( - steward_config: &Pubkey, - steward_state: &Pubkey, - steward_staker: &Pubkey, - config_account: &Config, -) { +fn _print_default_config(steward_config: &Pubkey, steward_state: &Pubkey, config_account: &Config) { let mut formatted_string = String::new(); formatted_string += "------- Config -------\n"; formatted_string += "📚 Accounts 📚\n"; formatted_string += &format!("Config: {}\n", steward_config); - formatted_string += &format!("Authority: {}\n", config_account.authority); - formatted_string += &format!("Staker: {}\n", steward_staker); + formatted_string += &format!("Admin: {}\n", config_account.admin); + formatted_string += &format!("Blacklist Auth: {}\n", config_account.blacklist_authority); + formatted_string += &format!( + "Parameter Auth: {}\n", + config_account.parameters_authority + ); + formatted_string += &format!("Staker (State): {}\n", steward_state); formatted_string += &format!("State: {}\n", steward_state); formatted_string += &format!("Stake Pool: {}\n", config_account.stake_pool); formatted_string += "\n↺ State ↺\n"; formatted_string += &format!("Is Paused: {:?}\n", config_account.paused); - formatted_string += &format!("Blacklisted: {:?}\n", config_account.blacklist.count()); + formatted_string += &format!( + "Blacklisted: {:?}\n", + config_account.validator_history_blacklist.count() + ); formatted_string += "\n⚙️ Parameters ⚙️\n"; formatted_string += &format!( "Commission Range: {:?}\n", diff --git a/utils/steward-cli/src/commands/info/view_state.rs b/utils/steward-cli/src/commands/info/view_state.rs index a96d8073..af0d9b78 100644 --- a/utils/steward-cli/src/commands/info/view_state.rs +++ b/utils/steward-cli/src/commands/info/view_state.rs @@ -81,10 +81,6 @@ fn _print_verbose_state(steward_state_accounts: &UsefulStewardAccounts) { u64::from(validator.active_stake_lamports) ); formatted_string += &format!("Index: {:?}\n", index); - formatted_string += &format!( - "Is Blacklisted: {:?}\n", - steward_state_accounts.config_account.blacklist.get(index) - ); formatted_string += &format!( "Is Instant Unstake: {:?}\n", steward_state_accounts diff --git a/utils/steward-cli/src/commands/init/init_state.rs b/utils/steward-cli/src/commands/init/init_state.rs index 76290bdf..f347a815 100644 --- a/utils/steward-cli/src/commands/init/init_state.rs +++ b/utils/steward-cli/src/commands/init/init_state.rs @@ -21,7 +21,6 @@ use crate::{ }, }; -const MAX_REALLOCS: usize = (StewardStateAccount::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; const REALLOCS_PER_TX: usize = 10; pub async fn command_init_state( @@ -44,49 +43,25 @@ pub async fn command_init_state( let validator_list = stake_pool_account.validator_list; - let mut reallocs_left_to_run = MAX_REALLOCS; - let mut should_create = true; - - match client.get_account(&steward_state).await { - Ok(steward_state_account_raw) => { - if steward_state_account_raw.data.len() == StewardStateAccount::SIZE { - match StewardStateAccount::try_deserialize( - &mut steward_state_account_raw.data.as_slice(), - ) { - Ok(steward_state_account) => { - if steward_state_account.is_initialized.into() { - println!("State account already exists"); - return Ok(()); - } - } - Err(_) => { /* Account is not initialized, continue */ } - }; - } - - // if it already exists, we don't need to create it - should_create = false; - - let data_length = steward_state_account_raw.data.len(); - let whats_left = StewardStateAccount::SIZE - data_length.min(StewardStateAccount::SIZE); + let steward_state_account_raw = client.get_account(&steward_state).await?; - reallocs_left_to_run = - (whats_left.max(MAX_ALLOC_BYTES) - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; - } - Err(_) => { /* Account does not exist, continue */ } + if steward_state_account_raw.data.len() == StewardStateAccount::SIZE { + match StewardStateAccount::try_deserialize(&mut steward_state_account_raw.data.as_slice()) { + Ok(steward_state_account) => { + if steward_state_account.is_initialized.into() { + println!("State account already exists"); + return Ok(()); + } + } + Err(_) => { /* Account is not initialized, continue */ } + }; } - if should_create { - let signature = _create_state( - client, - &program_id, - &authority, - &steward_state, - &steward_config, - ) - .await?; + let data_length = steward_state_account_raw.data.len(); + let whats_left = StewardStateAccount::SIZE - data_length.min(StewardStateAccount::SIZE); - println!("Created Steward State: {}", signature); - } + let mut reallocs_left_to_run = + (whats_left.max(MAX_ALLOC_BYTES) - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; let reallocs_to_run = reallocs_left_to_run; let mut reallocs_ran = 0; @@ -128,41 +103,6 @@ pub async fn command_init_state( Ok(()) } -async fn _create_state( - client: &RpcClient, - program_id: &Pubkey, - authority: &Keypair, - steward_state: &Pubkey, - steward_config: &Pubkey, -) -> Result { - let init_ix = Instruction { - program_id: *program_id, - accounts: jito_steward::accounts::InitializeState { - state_account: *steward_state, - config: *steward_config, - system_program: anchor_lang::solana_program::system_program::id(), - signer: authority.pubkey(), - } - .to_account_metas(None), - data: jito_steward::instruction::InitializeState {}.data(), - }; - - let blockhash = client.get_latest_blockhash().await?; - - let transaction = Transaction::new_signed_with_payer( - &[init_ix], - Some(&authority.pubkey()), - &[&authority], - blockhash, - ); - - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?; - - Ok(signature) -} - #[allow(clippy::too_many_arguments)] async fn _realloc_x_times( client: &RpcClient, diff --git a/utils/steward-cli/src/commands/init/init_config.rs b/utils/steward-cli/src/commands/init/init_steward.rs similarity index 83% rename from utils/steward-cli/src/commands/init/init_config.rs rename to utils/steward-cli/src/commands/init/init_steward.rs index 9aa80084..44e64bfb 100644 --- a/utils/steward-cli/src/commands/init/init_config.rs +++ b/utils/steward-cli/src/commands/init/init_steward.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anchor_lang::{InstructionData, ToAccountMetas}; use anyhow::Result; -use jito_steward::UpdateParametersArgs; +use jito_steward::{derive_steward_state_address, UpdateParametersArgs}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; @@ -13,10 +13,7 @@ use solana_sdk::{ transaction::Transaction, }; -use crate::{ - commands::command_args::InitConfig, - utils::{accounts::get_steward_staker_address, transactions::configure_instruction}, -}; +use crate::{commands::command_args::InitConfig, utils::transactions::configure_instruction}; pub async fn command_init_config( args: InitConfig, @@ -44,7 +41,7 @@ pub async fn command_init_config( } }; - let steward_staker = get_steward_staker_address(&program_id, &steward_config.pubkey()); + let (state_account, _) = derive_steward_state_address(&steward_config.pubkey()); let update_parameters_args: UpdateParametersArgs = args.config_parameters.into(); @@ -61,17 +58,16 @@ pub async fn command_init_config( let init_ix = Instruction { program_id, - accounts: jito_steward::accounts::InitializeConfig { + accounts: jito_steward::accounts::InitializeSteward { config: steward_config.pubkey(), stake_pool: args.stake_pool, - staker: steward_staker, + state_account, stake_pool_program: spl_stake_pool::id(), system_program: anchor_lang::solana_program::system_program::id(), - signer: staker_keypair.pubkey(), + current_staker: staker_keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::InitializeConfig { - authority: authority.pubkey(), + data: jito_steward::instruction::InitializeSteward { update_parameters_args, } .data(), diff --git a/utils/steward-cli/src/commands/init/mod.rs b/utils/steward-cli/src/commands/init/mod.rs index 07eea724..d1692dc5 100644 --- a/utils/steward-cli/src/commands/init/mod.rs +++ b/utils/steward-cli/src/commands/init/mod.rs @@ -1,2 +1,2 @@ -pub mod init_config; pub mod init_state; +pub mod init_steward; diff --git a/utils/steward-cli/src/main.rs b/utils/steward-cli/src/main.rs index 46a27416..79e941a3 100644 --- a/utils/steward-cli/src/main.rs +++ b/utils/steward-cli/src/main.rs @@ -20,7 +20,7 @@ use commands::{ view_next_index_to_remove::command_view_next_index_to_remove, view_state::command_view_state, }, - init::{init_config::command_init_config, init_state::command_init_state}, + init::{init_state::command_init_state, init_steward::command_init_config}, }; use dotenv::dotenv; use solana_client::nonblocking::rpc_client::RpcClient; diff --git a/utils/steward-cli/src/utils/accounts.rs b/utils/steward-cli/src/utils/accounts.rs index ea3c01ea..a3cc8395 100644 --- a/utils/steward-cli/src/utils/accounts.rs +++ b/utils/steward-cli/src/utils/accounts.rs @@ -2,7 +2,7 @@ use anchor_lang::AccountDeserialize; use anyhow::Result; use jito_steward::{ utils::{StakePool, ValidatorList}, - Config, Staker, StewardStateAccount, + Config, StewardStateAccount, }; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; @@ -11,8 +11,6 @@ use validator_history::{ClusterHistory, ValidatorHistory}; pub struct UsefulStewardAccounts { pub config_account: Config, - pub staker_account: Staker, - pub staker_address: Pubkey, pub state_account: StewardStateAccount, pub state_address: Pubkey, pub stake_pool_account: StakePool, @@ -32,8 +30,7 @@ pub async fn get_all_steward_accounts( get_steward_state_account(client, program_id, steward_config).await?; let stake_pool_address = config_account.stake_pool; let stake_pool_account = get_stake_pool_account(client, &stake_pool_address).await?; - let (staker_account, staker_address) = - get_steward_staker_account(client, program_id, steward_config).await?; + let stake_pool_withdraw_authority = get_withdraw_authority_address(&stake_pool_address); let validator_list_address = stake_pool_account.validator_list; let validator_list_account = @@ -43,8 +40,6 @@ pub async fn get_all_steward_accounts( config_account, state_account, state_address, - staker_account, - staker_address, stake_pool_account, stake_pool_address, stake_pool_withdraw_authority, @@ -102,28 +97,6 @@ pub fn get_withdraw_authority_address(stake_pool_address: &Pubkey) -> Pubkey { withdraw_authority } -pub fn get_steward_staker_address(program_id: &Pubkey, steward_config: &Pubkey) -> Pubkey { - let (steward_staker, _) = - Pubkey::find_program_address(&[Staker::SEED, steward_config.as_ref()], program_id); - - steward_staker -} - -pub async fn get_steward_staker_account( - client: &RpcClient, - program_id: &Pubkey, - steward_config: &Pubkey, -) -> Result<(Staker, Pubkey)> { - let steward_staker = get_steward_staker_address(program_id, steward_config); - - let staker_raw_account = client.get_account(&steward_staker).await?; - - Ok(( - Staker::try_deserialize(&mut staker_raw_account.data.as_slice())?, - steward_staker, - )) -} - pub async fn get_validator_list_account( client: &RpcClient, validator_list: &Pubkey, From 104f1b7462e52088c06e14c972ea1589358a18da Mon Sep 17 00:00:00 2001 From: Evan B Date: Mon, 1 Jul 2024 17:17:16 -0400 Subject: [PATCH 08/18] More events emitted (#54) Puts all events except for Scoring / instant unstake response types in a single file --- programs/steward/idl/steward.json | 113 ++++++++++++++++++ programs/steward/src/delegation.rs | 12 +- programs/steward/src/events.rs | 93 ++++++++++++++ .../auto_add_validator_to_pool.rs | 15 ++- .../auto_remove_validator_from_pool.rs | 8 ++ .../src/instructions/epoch_maintenance.rs | 10 ++ .../steward/src/instructions/rebalance.rs | 50 +------- programs/steward/src/lib.rs | 1 + programs/steward/src/state/steward_state.rs | 13 +- tests/tests/steward/test_algorithms.rs | 4 +- 10 files changed, 244 insertions(+), 75 deletions(-) create mode 100644 programs/steward/src/events.rs diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 57e49a14..c385b4c4 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -1407,6 +1407,32 @@ } ], "events": [ + { + "name": "AutoAddValidatorEvent", + "discriminator": [ + 123, + 65, + 239, + 15, + 82, + 216, + 206, + 28 + ] + }, + { + "name": "AutoRemoveValidatorEvent", + "discriminator": [ + 211, + 46, + 52, + 163, + 17, + 38, + 197, + 186 + ] + }, { "name": "DecreaseComponents", "discriminator": [ @@ -1420,6 +1446,19 @@ 8 ] }, + { + "name": "EpochMaintenanceEvent", + "discriminator": [ + 255, + 149, + 70, + 161, + 199, + 176, + 9, + 42 + ] + }, { "name": "InstantUnstakeComponents", "discriminator": [ @@ -1661,6 +1700,46 @@ ] } }, + { + "name": "AutoAddValidatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_list_index", + "type": "u64" + }, + { + "name": "vote_account", + "type": "pubkey" + } + ] + } + }, + { + "name": "AutoRemoveValidatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_list_index", + "type": "u64" + }, + { + "name": "vote_account", + "type": "pubkey" + }, + { + "name": "vote_account_closed", + "type": "bool" + }, + { + "name": "stake_account_deactivated", + "type": "bool" + } + ] + } + }, { "name": "BitMask", "docs": [ @@ -2037,6 +2116,40 @@ ] } }, + { + "name": "EpochMaintenanceEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_index_to_remove", + "type": { + "option": "u64" + } + }, + { + "name": "validator_list_length", + "type": "u64" + }, + { + "name": "num_pool_validators", + "type": "u64" + }, + { + "name": "validators_to_remove", + "type": "u64" + }, + { + "name": "validators_to_add", + "type": "u64" + }, + { + "name": "maintenance_complete", + "type": "bool" + } + ] + } + }, { "name": "InstantUnstakeComponents", "type": { diff --git a/programs/steward/src/delegation.rs b/programs/steward/src/delegation.rs index 14da68d6..5c4afdaa 100644 --- a/programs/steward/src/delegation.rs +++ b/programs/steward/src/delegation.rs @@ -1,8 +1,7 @@ -use anchor_lang::idl::*; use anchor_lang::prelude::*; -use borsh::BorshSerialize; use spl_stake_pool::big_vec::BigVec; +use crate::events::DecreaseComponents; use crate::{ errors::StewardError, utils::{get_target_lamports, stake_lamports_at_validator_list_index}, @@ -16,15 +15,6 @@ pub enum RebalanceType { None, } -#[event] -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct DecreaseComponents { - pub scoring_unstake_lamports: u64, - pub instant_unstake_lamports: u64, - pub stake_deposit_unstake_lamports: u64, - pub total_unstake_lamports: u64, -} - /// Given a target validator, determines how much stake to remove on this validator given the constraints of unstaking caps. /// Validators with lower yield_scores are prioritized for unstaking. We simulate unstaking movements on each validator, starting /// from the lowest yield_score validator, until we reach the target validator. If the target validator is reached and there is still diff --git a/programs/steward/src/events.rs b/programs/steward/src/events.rs new file mode 100644 index 00000000..c1f57228 --- /dev/null +++ b/programs/steward/src/events.rs @@ -0,0 +1,93 @@ +use anchor_lang::idl::{ + types::{IdlEnumVariant, IdlTypeDef, IdlTypeDefTy}, + IdlBuild, +}; +use anchor_lang::prelude::{event, AnchorDeserialize, AnchorSerialize}; +use anchor_lang::solana_program::pubkey::Pubkey; +use borsh::{BorshDeserialize, BorshSerialize}; + +#[event] +pub struct AutoRemoveValidatorEvent { + pub validator_list_index: u64, + pub vote_account: Pubkey, + pub vote_account_closed: bool, + pub stake_account_deactivated: bool, +} + +#[event] +pub struct AutoAddValidatorEvent { + pub validator_list_index: u64, + pub vote_account: Pubkey, +} + +#[event] +pub struct EpochMaintenanceEvent { + pub validator_index_to_remove: Option, + pub validator_list_length: u64, + pub num_pool_validators: u64, + pub validators_to_remove: u64, + pub validators_to_add: u64, + pub maintenance_complete: bool, +} + +#[event] +#[derive(Debug)] +pub struct StateTransition { + pub epoch: u64, + pub slot: u64, + pub previous_state: String, + pub new_state: String, +} + +#[event] +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct DecreaseComponents { + pub scoring_unstake_lamports: u64, + pub instant_unstake_lamports: u64, + pub stake_deposit_unstake_lamports: u64, + pub total_unstake_lamports: u64, +} + +#[event] +pub struct RebalanceEvent { + pub vote_account: Pubkey, + pub epoch: u16, + pub rebalance_type_tag: RebalanceTypeTag, + pub increase_lamports: u64, + pub decrease_components: DecreaseComponents, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum RebalanceTypeTag { + None, + Increase, + Decrease, +} + +impl IdlBuild for RebalanceTypeTag { + fn create_type() -> Option { + Some(IdlTypeDef { + name: "RebalanceTypeTag".to_string(), + ty: IdlTypeDefTy::Enum { + variants: vec![ + IdlEnumVariant { + name: "None".to_string(), + fields: None, + }, + IdlEnumVariant { + name: "Increase".to_string(), + fields: None, + }, + IdlEnumVariant { + name: "Decrease".to_string(), + fields: None, + }, + ], + }, + docs: Default::default(), + generics: Default::default(), + serialization: Default::default(), + repr: Default::default(), + }) + } +} diff --git a/programs/steward/src/instructions/auto_add_validator_to_pool.rs b/programs/steward/src/instructions/auto_add_validator_to_pool.rs index 49a57626..b9b2ff76 100644 --- a/programs/steward/src/instructions/auto_add_validator_to_pool.rs +++ b/programs/steward/src/instructions/auto_add_validator_to_pool.rs @@ -1,5 +1,6 @@ use crate::constants::{MAX_VALIDATORS, STAKE_POOL_WITHDRAW_SEED}; use crate::errors::StewardError; +use crate::events::AutoAddValidatorEvent; use crate::state::{Config, StewardStateAccount}; use crate::utils::{deserialize_stake_pool, get_stake_pool_address}; use anchor_lang::prelude::*; @@ -111,13 +112,14 @@ pub fn handler(ctx: Context) -> Result<()> { StewardError::EpochMaintenanceNotComplete ); - { + let validator_list_len = { let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; - if validator_list.len().checked_add(1).unwrap() > MAX_VALIDATORS as u32 { - return Err(StewardError::MaxValidatorsReached.into()); - } + validator_list.len() + }; + if validator_list_len.checked_add(1).unwrap() > MAX_VALIDATORS as u32 { + return Err(StewardError::MaxValidatorsReached.into()); } let start_epoch = @@ -150,6 +152,11 @@ pub fn handler(ctx: Context) -> Result<()> { // Have to drop the state account before calling the CPI drop(state_account); + emit!(AutoAddValidatorEvent { + vote_account: ctx.accounts.vote_account.key(), + validator_list_index: validator_list_len as u64 + }); + invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), 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 e18422e4..70a205e1 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -2,6 +2,7 @@ use std::num::NonZeroU32; use crate::constants::STAKE_POOL_WITHDRAW_SEED; use crate::errors::StewardError; +use crate::events::AutoRemoveValidatorEvent; use crate::state::Config; use crate::utils::{ deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, @@ -167,6 +168,13 @@ pub fn handler(ctx: Context, validator_list_index: usize) - state_account .state .mark_validator_for_removal(validator_list_index)?; + + emit!(AutoRemoveValidatorEvent { + vote_account: ctx.accounts.vote_account.key(), + validator_list_index: validator_list_index as u64, + stake_account_deactivated, + vote_account_closed, + }); } invoke_signed( diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index ff538c94..95c9a40a 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -1,5 +1,6 @@ use crate::{ errors::StewardError, + events::EpochMaintenanceEvent, utils::{ check_validator_list_has_stake_status_other_than, deserialize_stake_pool, get_stake_pool_address, get_validator_list_length, @@ -94,6 +95,15 @@ pub fn handler( state_account.state.checked_validators_removed_from_list = false.into(); state_account.state.rebalance_completed = false.into(); } + + emit!(EpochMaintenanceEvent { + validator_index_to_remove: validator_index_to_remove.map(|x| x as u64), + validator_list_length: get_validator_list_length(&ctx.accounts.validator_list)? as u64, + num_pool_validators: state_account.state.num_pool_validators, + validators_to_remove: state_account.state.validators_to_remove.count() as u64, + validators_to_add: state_account.state.validators_added as u64, + maintenance_complete: okay_to_update, + }); } Ok(()) diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index ab338ad7..3bf09d4d 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -1,7 +1,6 @@ use std::num::NonZeroU32; use anchor_lang::{ - idl::types::*, idl::*, prelude::*, solana_program::{ @@ -10,7 +9,7 @@ use anchor_lang::{ system_program, sysvar, vote, }, }; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; use spl_pod::solana_program::stake::state::StakeStateV2; use spl_stake_pool::{ find_stake_program_address, find_transient_stake_program_address, minimum_delegation, @@ -20,8 +19,9 @@ use validator_history::ValidatorHistory; use crate::{ constants::STAKE_POOL_WITHDRAW_SEED, - delegation::{DecreaseComponents, RebalanceType}, + delegation::RebalanceType, errors::StewardError, + events::{DecreaseComponents, RebalanceEvent, RebalanceTypeTag}, maybe_transition_and_emit, utils::{deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index}, Config, StewardStateAccount, @@ -287,15 +287,6 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( Ok(()) } -#[event] -pub struct RebalanceEvent { - pub vote_account: Pubkey, - pub epoch: u16, - pub rebalance_type_tag: RebalanceTypeTag, - pub increase_lamports: u64, - pub decrease_components: DecreaseComponents, -} - fn rebalance_to_event( vote_account: Pubkey, epoch: u16, @@ -325,38 +316,3 @@ fn rebalance_to_event( }, } } - -#[derive(BorshSerialize, BorshDeserialize, Debug)] -pub enum RebalanceTypeTag { - None, - Increase, - Decrease, -} - -impl IdlBuild for RebalanceTypeTag { - fn create_type() -> Option { - Some(IdlTypeDef { - name: "RebalanceTypeTag".to_string(), - ty: IdlTypeDefTy::Enum { - variants: vec![ - IdlEnumVariant { - name: "None".to_string(), - fields: None, - }, - IdlEnumVariant { - name: "Increase".to_string(), - fields: None, - }, - IdlEnumVariant { - name: "Decrease".to_string(), - fields: None, - }, - ], - }, - docs: Default::default(), - generics: Default::default(), - serialization: Default::default(), - repr: Default::default(), - }) - } -} diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 138daf11..74b8f938 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -9,6 +9,7 @@ mod allocator; pub mod constants; pub mod delegation; pub mod errors; +pub mod events; pub mod instructions; pub mod score; pub mod state; diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 0c091fc9..41a81424 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -5,10 +5,10 @@ use crate::{ bitmask::BitMask, constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, delegation::{ - decrease_stake_calculation, increase_stake_calculation, DecreaseComponents, RebalanceType, - UnstakeState, + decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState, }, errors::StewardError, + events::{DecreaseComponents, StateTransition}, score::{ instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, }, @@ -28,15 +28,6 @@ fn invalid_state_error(_expected: String, _actual: String) -> Error { StewardError::InvalidState.into() } -#[event] -#[derive(Debug)] -pub struct StateTransition { - pub epoch: u64, - pub slot: u64, - pub previous_state: String, - pub new_state: String, -} - pub fn maybe_transition_and_emit( steward_state: &mut StewardState, clock: &Clock, diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 901bf0b8..02bd60aa 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -3,10 +3,10 @@ use anchor_lang::AnchorSerialize; use jito_steward::{ constants::SORTED_INDEX_DEFAULT, delegation::{ - decrease_stake_calculation, increase_stake_calculation, DecreaseComponents, RebalanceType, - UnstakeState, + decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState, }, errors::StewardError, + events::DecreaseComponents, insert_sorted_index, score::{ instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, From be2fa23e3ccccde764d8e7a0522488763d36a077 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:25:51 -0600 Subject: [PATCH 09/18] REFACTOR: Added status flags (#55) - Added status flags for better tracking of the state machine - Fixed issue on new epoch not being able to reset state to idle --------- Co-authored-by: Christian Krueger --- programs/steward/idl/steward.json | 40 ++----- .../src/instructions/epoch_maintenance.rs | 34 ++++-- .../steward/src/instructions/realloc_state.rs | 3 +- .../src/instructions/reset_steward_state.rs | 3 +- programs/steward/src/state/steward_state.rs | 100 ++++++++++++------ tests/src/steward_fixtures.rs | 4 +- tests/tests/steward/test_state_transitions.rs | 14 +-- .../src/commands/info/view_state.rs | 6 +- 8 files changed, 113 insertions(+), 91 deletions(-) diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index c385b4c4..de1fb130 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -2741,44 +2741,18 @@ "type": "u64" }, { - "name": "validators_added", - "docs": [ - "Number of validators added to the pool in the current cycle" - ], - "type": "u16" - }, - { - "name": "compute_delegations_completed", - "docs": [ - "Tracks whether delegation computation has been completed" - ], - "type": { - "defined": { - "name": "U8Bool" - } - } - }, - { - "name": "rebalance_completed", + "name": "status_flags", "docs": [ - "Tracks whether unstake and delegate steps have completed" + "Flags to track state transitions and operations" ], - "type": { - "defined": { - "name": "U8Bool" - } - } + "type": "u32" }, { - "name": "checked_validators_removed_from_list", + "name": "validators_added", "docs": [ - "So we only have to check the validator list once for `ReadyToRemove`" + "Number of validators added to the pool in the current cycle" ], - "type": { - "defined": { - "name": "U8Bool" - } - } + "type": "u16" }, { "name": "_padding0", @@ -2788,7 +2762,7 @@ "type": { "array": [ "u8", - 40003 + 40002 ] } } diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 95c9a40a..8c75fea0 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -5,7 +5,8 @@ use crate::{ check_validator_list_has_stake_status_other_than, deserialize_stake_pool, get_stake_pool_address, get_validator_list_length, }, - Config, StewardStateAccount, + Config, StewardStateAccount, CHECKED_VALIDATORS_REMOVED_FROM_LIST, COMPUTE_INSTANT_UNSTAKES, + EPOCH_MAINTENANCE, POST_LOOP_IDLE, PRE_LOOP_IDLE, REBALANCE, RESET_TO_IDLE, }; use anchor_lang::prelude::*; use spl_stake_pool::state::StakeStatus; @@ -49,7 +50,14 @@ pub fn handler( StewardError::StakePoolNotUpdated ); - if (!state_account.state.checked_validators_removed_from_list).into() { + // Keep this unset until we have completed all maintenance tasks + state_account.state.unset_flag(EPOCH_MAINTENANCE); + + // We only need to check this once per maintenance cycle + if !state_account + .state + .has_flag(CHECKED_VALIDATORS_REMOVED_FROM_LIST) + { // Ensure there are no validators in the list that have not been removed, that should be require!( !check_validator_list_has_stake_status_other_than( @@ -59,7 +67,9 @@ pub fn handler( StewardError::ValidatorsHaveNotBeenRemoved ); - state_account.state.checked_validators_removed_from_list = true.into(); + state_account + .state + .set_flag(CHECKED_VALIDATORS_REMOVED_FROM_LIST); } { @@ -88,12 +98,22 @@ pub fn handler( let okay_to_update = state_account.state.validators_to_remove.is_empty() && state_account .state - .checked_validators_removed_from_list - .into(); + .has_flag(CHECKED_VALIDATORS_REMOVED_FROM_LIST); + if okay_to_update { state_account.state.current_epoch = clock.epoch; - state_account.state.checked_validators_removed_from_list = false.into(); - state_account.state.rebalance_completed = false.into(); + + // We keep Compute Scores and Compute Delegations to be unset on next epoch cycle + state_account.state.unset_flag( + CHECKED_VALIDATORS_REMOVED_FROM_LIST + | PRE_LOOP_IDLE + | COMPUTE_INSTANT_UNSTAKES + | REBALANCE + | POST_LOOP_IDLE, + ); + state_account + .state + .set_flag(RESET_TO_IDLE | EPOCH_MAINTENANCE); } emit!(EpochMaintenanceEvent { diff --git a/programs/steward/src/instructions/realloc_state.rs b/programs/steward/src/instructions/realloc_state.rs index 1cc13bd8..b2e6ef8d 100644 --- a/programs/steward/src/instructions/realloc_state.rs +++ b/programs/steward/src/instructions/realloc_state.rs @@ -85,12 +85,11 @@ pub fn handler(ctx: Context) -> Result<()> { .checked_add(config.parameters.num_epochs_between_scoring) .ok_or(StewardError::ArithmeticError)?; state_account.state.delegations = [Delegation::default(); MAX_VALIDATORS]; - state_account.state.rebalance_completed = false.into(); state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; state_account.state.validators_to_remove = BitMask::default(); state_account.state.validators_added = 0; - state_account.state.checked_validators_removed_from_list = false.into(); + state_account.state.clear_flags(); state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; } diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs index 2ed4d167..b647f4c7 100644 --- a/programs/steward/src/instructions/reset_steward_state.rs +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -58,12 +58,11 @@ pub fn handler(ctx: Context) -> Result<()> { .checked_add(config.parameters.num_epochs_between_scoring) .ok_or(StewardError::ArithmeticError)?; state_account.state.delegations = [Delegation::default(); MAX_VALIDATORS]; - state_account.state.rebalance_completed = false.into(); state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; state_account.state.validators_to_remove = BitMask::default(); state_account.state.validators_added = 0; - state_account.state.checked_validators_removed_from_list = false.into(); + state_account.state.clear_flags(); state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; Ok(()) } diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 41a81424..8e782992 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -12,7 +12,7 @@ use crate::{ score::{ instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, }, - utils::{epoch_progress, get_target_lamports, stake_lamports_at_validator_list_index, U8Bool}, + utils::{epoch_progress, get_target_lamports, stake_lamports_at_validator_list_index}, Config, Parameters, }; use anchor_lang::idl::types::*; @@ -110,24 +110,18 @@ pub struct StewardState { /// Total lamports that have been due to stake deposits this cycle pub stake_deposit_unstake_total: u64, + /// Flags to track state transitions and operations + pub status_flags: u32, + /// Number of validators added to the pool in the current cycle pub validators_added: u16, - /// Tracks whether delegation computation has been completed - pub compute_delegations_completed: U8Bool, - - /// Tracks whether unstake and delegate steps have completed - pub rebalance_completed: U8Bool, - - /// So we only have to check the validator list once for `ReadyToRemove` - pub checked_validators_removed_from_list: U8Bool, - /// Future state and #[repr(C)] alignment pub _padding0: [u8; STATE_PADDING_0_SIZE], // TODO ADD MORE PADDING } -pub const STATE_PADDING_0_SIZE: usize = MAX_VALIDATORS * 8 + 3; +pub const STATE_PADDING_0_SIZE: usize = MAX_VALIDATORS * 8 + 2; #[derive(Clone, Copy)] #[repr(u64)] @@ -236,7 +230,45 @@ impl IdlBuild for StewardStateEnum { } } +// BITS 0-7 COMPLETED PROGRESS FLAGS +// Used to mark the completion of a particular state +pub const COMPUTE_SCORE: u32 = 1 << 0; +pub const COMPUTE_DELEGATIONS: u32 = 1 << 1; +pub const EPOCH_MAINTENANCE: u32 = 1 << 2; +pub const PRE_LOOP_IDLE: u32 = 1 << 3; +pub const COMPUTE_INSTANT_UNSTAKES: u32 = 1 << 4; +pub const REBALANCE: u32 = 1 << 5; +pub const POST_LOOP_IDLE: u32 = 1 << 6; +// BITS 8-15 RESERVED FOR FUTURE USE +// BITS 16-23 OPERATIONAL FLAGS +/// In epoch maintenance, we only need to check the validator pool +/// once for any validators that still need to be removed +/// when there are no validators to remove from the pool, the operation continues +/// and this condition is not checked again +pub const CHECKED_VALIDATORS_REMOVED_FROM_LIST: u32 = 1 << 16; +/// In epoch maintenance, when a new epoch is detected, we need a flag to tell the +/// state transition layer that it needs to be reset to the IDLE state +/// this flag is set in in epoch_maintenance and unset in the IDLE state transition +pub const RESET_TO_IDLE: u32 = 1 << 17; +// BITS 24-31 RESERVED FOR FUTURE USE + impl StewardState { + pub fn set_flag(&mut self, flag: u32) { + self.status_flags |= flag; + } + + pub fn clear_flags(&mut self) { + self.status_flags = 0; + } + + pub fn unset_flag(&mut self, flag: u32) { + self.status_flags &= !flag; + } + + pub fn has_flag(&self, flag: u32) -> bool { + self.status_flags & flag != 0 + } + /// Top level transition method. Tries to transition to a new state based on current state and epoch conditions pub fn transition( &mut self, @@ -287,7 +319,6 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, @@ -297,6 +328,7 @@ impl StewardState { self.state_tag = StewardStateEnum::ComputeDelegations; self.progress = BitMask::default(); self.delegations = [Delegation::default(); MAX_VALIDATORS]; + self.set_flag(COMPUTE_SCORE); } Ok(()) } @@ -309,15 +341,13 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if self.compute_delegations_completed.into() { + } else if self.has_flag(COMPUTE_DELEGATIONS) { self.state_tag = StewardStateEnum::Idle; - self.rebalance_completed = false.into(); } Ok(()) } @@ -331,22 +361,28 @@ impl StewardState { epoch_progress: f64, min_epoch_progress_for_instant_unstake: f64, ) -> Result<()> { + let completed_loop = self.has_flag(REBALANCE); + if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if (!self.rebalance_completed).into() - && epoch_progress >= min_epoch_progress_for_instant_unstake - { - //NOTE: rebalance_completed is cleared on epoch change in `epoch_maintenance` + } else if !completed_loop { + self.unset_flag(RESET_TO_IDLE); - self.state_tag = StewardStateEnum::ComputeInstantUnstake; - self.instant_unstake = BitMask::default(); - self.progress = BitMask::default(); + self.set_flag(PRE_LOOP_IDLE); + + if epoch_progress >= min_epoch_progress_for_instant_unstake { + self.state_tag = StewardStateEnum::ComputeInstantUnstake; + self.instant_unstake = BitMask::default(); + self.progress = BitMask::default(); + } + } else if completed_loop { + self.set_flag(POST_LOOP_IDLE) } + Ok(()) } @@ -358,19 +394,20 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if current_epoch > self.current_epoch { + } else if self.has_flag(RESET_TO_IDLE) { self.state_tag = StewardStateEnum::Idle; self.instant_unstake = BitMask::default(); self.progress = BitMask::default(); + // NOTE: RESET_TO_IDLE is cleared in the Idle transition } else if self.progress.is_complete(self.num_pool_validators)? { self.state_tag = StewardStateEnum::Rebalance; self.progress = BitMask::default(); + self.set_flag(COMPUTE_INSTANT_UNSTAKES); } Ok(()) } @@ -383,19 +420,18 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if current_epoch > self.current_epoch { + } else if self.has_flag(RESET_TO_IDLE) { self.state_tag = StewardStateEnum::Idle; self.progress = BitMask::default(); - self.rebalance_completed = false.into(); + // NOTE: RESET_TO_IDLE is cleared in the Idle transition } else if self.progress.is_complete(self.num_pool_validators)? { self.state_tag = StewardStateEnum::Idle; - self.rebalance_completed = true.into(); + self.set_flag(REBALANCE); } Ok(()) } @@ -420,8 +456,8 @@ impl StewardState { self.stake_deposit_unstake_total = 0; self.delegations = [Delegation::default(); MAX_VALIDATORS]; self.instant_unstake = BitMask::default(); - self.compute_delegations_completed = false.into(); - self.rebalance_completed = false.into(); + self.clear_flags(); + Ok(()) } @@ -675,7 +711,7 @@ impl StewardState { }; } - self.compute_delegations_completed = true.into(); + self.set_flag(COMPUTE_DELEGATIONS); return Ok(()); } diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 05b6f972..395466e3 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -934,10 +934,8 @@ impl Default for StateMachineFixtures { stake_deposit_unstake_total: 0, delegations: [Delegation::default(); MAX_VALIDATORS], instant_unstake: BitMask::default(), - compute_delegations_completed: false.into(), - rebalance_completed: false.into(), + status_flags: 0, validators_added: 0, - checked_validators_removed_from_list: false.into(), validators_to_remove: BitMask::default(), _padding0: [0; STATE_PADDING_0_SIZE], }; diff --git a/tests/tests/steward/test_state_transitions.rs b/tests/tests/steward/test_state_transitions.rs index 75e3eea8..01f4ab7b 100644 --- a/tests/tests/steward/test_state_transitions.rs +++ b/tests/tests/steward/test_state_transitions.rs @@ -3,7 +3,9 @@ These tests cover all possible state transitions when calling the `transition` method on the `StewardState` struct. */ -use jito_steward::{constants::MAX_VALIDATORS, Delegation, StewardStateEnum}; +use jito_steward::{ + constants::MAX_VALIDATORS, Delegation, StewardStateEnum, REBALANCE, RESET_TO_IDLE, +}; use tests::steward_fixtures::StateMachineFixtures; #[test] @@ -214,7 +216,7 @@ pub fn test_idle_noop() { // Case 2: still after instant_unstake_epoch_progress but after rebalance is completed clock.slot = epoch_schedule.get_last_slot_in_epoch(clock.epoch); - state.rebalance_completed = true.into(); + state.set_flag(REBALANCE); let res = state.transition(clock, parameters, epoch_schedule); assert!(res.is_ok()); assert!(matches!(state.state_tag, StewardStateEnum::Idle)); @@ -274,15 +276,13 @@ pub fn test_compute_instant_unstake_to_rebalance() { pub fn test_compute_instant_unstake_to_idle() { let mut fixtures = Box::::default(); - let current_epoch = fixtures.clock.epoch; let clock = &mut fixtures.clock; let epoch_schedule = &fixtures.epoch_schedule; let parameters = &fixtures.config.parameters; let state = &mut fixtures.state; state.state_tag = StewardStateEnum::ComputeInstantUnstake; - clock.epoch = current_epoch + 1; - clock.slot = epoch_schedule.get_last_slot_in_epoch(clock.epoch); + state.set_flag(RESET_TO_IDLE); let res = state.transition(clock, parameters, epoch_schedule); assert!(res.is_ok()); @@ -350,8 +350,8 @@ pub fn test_rebalance_to_idle() { // Test didn't finish rebalance case state.state_tag = StewardStateEnum::Rebalance; state.progress.reset(); - clock.epoch += 1; - clock.slot = epoch_schedule.get_last_slot_in_epoch(clock.epoch); + state.set_flag(RESET_TO_IDLE); + let res = state.transition(clock, parameters, epoch_schedule); assert!(res.is_ok()); assert!(matches!(state.state_tag, StewardStateEnum::Idle)); diff --git a/utils/steward-cli/src/commands/info/view_state.rs b/utils/steward-cli/src/commands/info/view_state.rs index af0d9b78..291ff3a0 100644 --- a/utils/steward-cli/src/commands/info/view_state.rs +++ b/utils/steward-cli/src/commands/info/view_state.rs @@ -165,11 +165,7 @@ fn _print_default_state( "Stake Deposit Unstake Total: {}\n", state.stake_deposit_unstake_total ); - formatted_string += &format!( - "Compute Delegations Completed: {:?}\n", - state.compute_delegations_completed - ); - formatted_string += &format!("Rebalance Completed: {:?}\n", state.rebalance_completed); + formatted_string += &format!("Padding0 Length: {}\n", state._padding0.len()); formatted_string += "---------------------"; From 0425db0396f433dc2c26fd7a5d2c29b094950ae7 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Tue, 23 Jul 2024 07:32:19 -0600 Subject: [PATCH 10/18] FEATURE: Mark delinquent validators for instant removal (#57) To cover the edge case when some validators can be removed on the same epoch, we treat them differently. If there are any validators that are marked for instant removal - they have to be removed before anything else can happen. This could be further cleaned by running epoch maintenance over all validators. But for now, it should cover us. --------- Co-authored-by: Christian Krueger --- .github/workflows/build.yaml | 2 +- programs/steward/idl/steward.json | 85 +++++++++++++- programs/steward/src/errors.rs | 8 ++ programs/steward/src/events.rs | 1 + .../auto_add_validator_to_pool.rs | 15 ++- .../auto_remove_validator_from_pool.rs | 111 ++++++++++++------ .../src/instructions/compute_delegations.rs | 29 +++-- .../instructions/compute_instant_unstake.rs | 35 ++++-- .../steward/src/instructions/compute_score.rs | 26 ++-- .../src/instructions/epoch_maintenance.rs | 46 +++----- programs/steward/src/instructions/idle.rs | 31 +++-- .../instructions/instant_remove_validator.rs | 81 +++++++++++++ programs/steward/src/instructions/mod.rs | 2 + .../steward/src/instructions/realloc_state.rs | 1 + .../steward/src/instructions/rebalance.rs | 34 ++++-- .../src/instructions/reset_steward_state.rs | 1 + programs/steward/src/lib.rs | 8 ++ programs/steward/src/state/steward_state.rs | 51 ++++++-- tests/src/steward_fixtures.rs | 5 + tests/tests/steward/test_spl_passthrough.rs | 17 +-- tests/tests/steward/test_steward.rs | 39 +++++- 21 files changed, 475 insertions(+), 153 deletions(-) create mode 100644 programs/steward/src/instructions/instant_remove_validator.rs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f61fdef7..4e118e11 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,7 +23,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: cargo-audit - - run: cargo audit --ignore RUSTSEC-2022-0093 --ignore RUSTSEC-2023-0065 --ignore RUSTSEC-2024-0336 --ignore RUSTSEC-2024-0344 + - run: cargo audit --ignore RUSTSEC-2022-0093 --ignore RUSTSEC-2023-0065 --ignore RUSTSEC-2024-0336 --ignore RUSTSEC-2024-0344 --ignore RUSTSEC-2024-0357 lint: name: lint diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index de1fb130..9378d755 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -881,6 +881,43 @@ } ] }, + { + "name": "instant_remove_validator", + "docs": [ + "Housekeeping, run at the start of any new epoch before any other instructions" + ], + "discriminator": [ + 119, + 127, + 216, + 135, + 24, + 63, + 229, + 242 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "state_account", + "writable": true + }, + { + "name": "validator_list" + }, + { + "name": "stake_pool" + } + ], + "args": [ + { + "name": "validator_index_to_remove", + "type": "u64" + } + ] + }, { "name": "pause_steward", "discriminator": [ @@ -1624,36 +1661,56 @@ }, { "code": 6022, + "name": "ValidatorMarkedActive", + "msg": "Validator was marked active when it should be deactivating" + }, + { + "code": 6023, "name": "MaxValidatorsReached", "msg": "Max validators reached" }, { - "code": 6023, + "code": 6024, "name": "ValidatorHistoryMismatch", "msg": "Validator history account does not match vote account" }, { - "code": 6024, + "code": 6025, "name": "EpochMaintenanceNotComplete", "msg": "Epoch Maintenance must be called before continuing" }, { - "code": 6025, + "code": 6026, "name": "StakePoolNotUpdated", "msg": "The stake pool must be updated before continuing" }, { - "code": 6026, + "code": 6027, + "name": "EpochMaintenanceAlreadyComplete", + "msg": "Epoch Maintenance has already been completed" + }, + { + "code": 6028, + "name": "ValidatorsNeedToBeRemoved", + "msg": "Validators are marked for immediate removal" + }, + { + "code": 6029, + "name": "NoValidatorsNeedToBeRemoved", + "msg": "No validators are marked for immediate removal" + }, + { + "code": 6030, "name": "ValidatorNotMarkedForRemoval", "msg": "Validator not marked for removal" }, { - "code": 6027, + "code": 6031, "name": "ValidatorsHaveNotBeenRemoved", "msg": "Validators have not been removed" }, { - "code": 6028, + "code": 6032, "name": "ListStateMismatch", "msg": "Validator List count does not match state machine" } @@ -1736,6 +1793,10 @@ { "name": "stake_account_deactivated", "type": "bool" + }, + { + "name": "marked_for_immediate_removal", + "type": "bool" } ] } @@ -2690,6 +2751,18 @@ } } }, + { + "name": "validators_for_immediate_removal", + "docs": [ + "Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool", + "This happens when a validator is able to be removed within the same epoch as it was marked" + ], + "type": { + "defined": { + "name": "BitMask" + } + } + }, { "name": "start_computing_scores_slot", "docs": [ diff --git a/programs/steward/src/errors.rs b/programs/steward/src/errors.rs index de36d4ad..ea80d06f 100644 --- a/programs/steward/src/errors.rs +++ b/programs/steward/src/errors.rs @@ -48,6 +48,8 @@ pub enum StewardError { ArithmeticError, #[msg("Validator not eligible for removal. Must be delinquent or have closed vote account")] ValidatorNotRemovable, + #[msg("Validator was marked active when it should be deactivating")] + ValidatorMarkedActive, #[msg("Max validators reached")] MaxValidatorsReached, #[msg("Validator history account does not match vote account")] @@ -56,6 +58,12 @@ pub enum StewardError { EpochMaintenanceNotComplete, #[msg("The stake pool must be updated before continuing")] StakePoolNotUpdated, + #[msg("Epoch Maintenance has already been completed")] + EpochMaintenanceAlreadyComplete, + #[msg("Validators are marked for immediate removal")] + ValidatorsNeedToBeRemoved, + #[msg("No validators are marked for immediate removal")] + NoValidatorsNeedToBeRemoved, #[msg("Validator not marked for removal")] ValidatorNotMarkedForRemoval, #[msg("Validators have not been removed")] diff --git a/programs/steward/src/events.rs b/programs/steward/src/events.rs index c1f57228..8de1d7ae 100644 --- a/programs/steward/src/events.rs +++ b/programs/steward/src/events.rs @@ -12,6 +12,7 @@ pub struct AutoRemoveValidatorEvent { pub vote_account: Pubkey, pub vote_account_closed: bool, pub stake_account_deactivated: bool, + pub marked_for_immediate_removal: bool, } #[event] diff --git a/programs/steward/src/instructions/auto_add_validator_to_pool.rs b/programs/steward/src/instructions/auto_add_validator_to_pool.rs index b9b2ff76..91d7382a 100644 --- a/programs/steward/src/instructions/auto_add_validator_to_pool.rs +++ b/programs/steward/src/instructions/auto_add_validator_to_pool.rs @@ -107,10 +107,17 @@ pub fn handler(ctx: Context) -> Result<()> { let epoch = Clock::get()?.epoch; // Should not be able to add a validator if update is not complete - require!( - epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); + { + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + } let validator_list_len = { let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; 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 70a205e1..1b0ee4bb 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -12,6 +12,7 @@ use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; use anchor_lang::{prelude::*, system_program}; use spl_pod::solana_program::borsh1::try_from_slice_unchecked; use spl_pod::solana_program::stake::state::StakeStateV2; +use spl_stake_pool::state::StakeStatus; use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; use validator_history::state::ValidatorHistory; @@ -124,8 +125,11 @@ pub struct AutoRemoveValidator<'info> { */ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { + let stake_account_deactivated; + let vote_account_closed; + { - let mut state_account = ctx.accounts.state_account.load_mut()?; + let state_account = ctx.accounts.state_account.load()?; let validator_list = &ctx.accounts.validator_list; let epoch = Clock::get()?.epoch; @@ -143,7 +147,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) - ); // Checks state for deactivate delinquent status, preventing pool from merging stake with activating - let stake_account_deactivated = { + stake_account_deactivated = { let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); let stake_state: StakeStateV2 = try_from_slice_unchecked::(stake_account_data)?; @@ -158,57 +162,86 @@ pub fn handler(ctx: Context, validator_list_index: usize) - }; // Check if vote account closed - let vote_account_closed = ctx.accounts.vote_account.owner == &system_program::ID; + vote_account_closed = ctx.accounts.vote_account.owner == &system_program::ID; require!( stake_account_deactivated || vote_account_closed, StewardError::ValidatorNotRemovable ); + } + + { + invoke_signed( + &spl_stake_pool::instruction::remove_validator_from_pool( + &ctx.accounts.stake_pool_program.key(), + &ctx.accounts.stake_pool.key(), + &ctx.accounts.state_account.key(), + &ctx.accounts.withdraw_authority.key(), + &ctx.accounts.validator_list.key(), + &ctx.accounts.stake_account.key(), + &ctx.accounts.transient_stake_account.key(), + ), + &[ + ctx.accounts.stake_pool.to_account_info(), + ctx.accounts.state_account.to_account_info(), + ctx.accounts.reserve_stake.to_owned(), + ctx.accounts.withdraw_authority.to_owned(), + ctx.accounts.validator_list.to_account_info(), + ctx.accounts.stake_account.to_account_info(), + ctx.accounts.transient_stake_account.to_account_info(), + ctx.accounts.vote_account.to_account_info(), + ctx.accounts.rent.to_account_info(), + ctx.accounts.clock.to_account_info(), + ctx.accounts.stake_history.to_account_info(), + ctx.accounts.stake_config.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.stake_program.to_account_info(), + ], + &[&[ + StewardStateAccount::SEED, + &ctx.accounts.config.key().to_bytes(), + &[ctx.bumps.state_account], + ]], + )?; + } + + { + // Read the state account again + let mut state_account = ctx.accounts.state_account.load_mut()?; + let validator_list = &ctx.accounts.validator_list; + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; - state_account - .state - .mark_validator_for_removal(validator_list_index)?; + let stake_status = StakeStatus::try_from(validator_stake_info.status)?; + let marked_for_immediate_removal: bool; + + match stake_status { + StakeStatus::Active => { + // Should never happen + return Err(StewardError::ValidatorMarkedActive.into()); + } + StakeStatus::DeactivatingValidator | StakeStatus::ReadyForRemoval => { + marked_for_immediate_removal = true; + state_account + .state + .mark_validator_for_immediate_removal(validator_list_index)?; + } + StakeStatus::DeactivatingAll | StakeStatus::DeactivatingTransient => { + marked_for_immediate_removal = false; + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } + } emit!(AutoRemoveValidatorEvent { vote_account: ctx.accounts.vote_account.key(), validator_list_index: validator_list_index as u64, stake_account_deactivated, vote_account_closed, + marked_for_immediate_removal, }); } - invoke_signed( - &spl_stake_pool::instruction::remove_validator_from_pool( - &ctx.accounts.stake_pool_program.key(), - &ctx.accounts.stake_pool.key(), - &ctx.accounts.state_account.key(), - &ctx.accounts.withdraw_authority.key(), - &ctx.accounts.validator_list.key(), - &ctx.accounts.stake_account.key(), - &ctx.accounts.transient_stake_account.key(), - ), - &[ - ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.state_account.to_account_info(), - ctx.accounts.reserve_stake.to_owned(), - ctx.accounts.withdraw_authority.to_owned(), - ctx.accounts.validator_list.to_account_info(), - ctx.accounts.stake_account.to_account_info(), - ctx.accounts.transient_stake_account.to_account_info(), - ctx.accounts.vote_account.to_account_info(), - ctx.accounts.rent.to_account_info(), - ctx.accounts.clock.to_account_info(), - ctx.accounts.stake_history.to_account_info(), - ctx.accounts.stake_config.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.stake_program.to_account_info(), - ], - &[&[ - StewardStateAccount::SEED, - &ctx.accounts.config.key().to_bytes(), - &[ctx.bumps.state_account], - ]], - )?; - Ok(()) } diff --git a/programs/steward/src/instructions/compute_delegations.rs b/programs/steward/src/instructions/compute_delegations.rs index 005fe540..27729ead 100644 --- a/programs/steward/src/instructions/compute_delegations.rs +++ b/programs/steward/src/instructions/compute_delegations.rs @@ -1,5 +1,5 @@ use crate::errors::StewardError; -use crate::{maybe_transition_and_emit, Config, StewardStateAccount}; +use crate::{maybe_transition_and_emit, Config, StewardStateAccount, StewardStateEnum}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -25,13 +25,28 @@ pub fn handler(ctx: Context) -> Result<()> { let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); + { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); + require!( + matches!( + state_account.state.state_tag, + StewardStateEnum::ComputeDelegations + ), + StewardError::InvalidState + ); + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); } state_account diff --git a/programs/steward/src/instructions/compute_instant_unstake.rs b/programs/steward/src/instructions/compute_instant_unstake.rs index bfb50f44..576d0606 100644 --- a/programs/steward/src/instructions/compute_instant_unstake.rs +++ b/programs/steward/src/instructions/compute_instant_unstake.rs @@ -2,7 +2,7 @@ use crate::{ errors::StewardError, maybe_transition_and_emit, utils::{get_validator_list, get_validator_stake_info_at_index}, - Config, StewardStateAccount, + Config, StewardStateAccount, StewardStateEnum, }; use anchor_lang::prelude::*; use validator_history::{ClusterHistory, ValidatorHistory}; @@ -41,6 +41,30 @@ pub fn handler(ctx: Context, validator_list_index: usize) let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + matches!( + state_account.state.state_tag, + StewardStateEnum::ComputeInstantUnstake + ), + StewardError::InvalidState + ); + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + } + let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; require!( @@ -48,15 +72,6 @@ pub fn handler(ctx: Context, validator_list_index: usize) StewardError::ValidatorNotInList ); - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - if let Some(instant_unstake) = state_account.state.compute_instant_unstake( &clock, &epoch_schedule, diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index 585420ae..04bad5f9 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -42,6 +42,22 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul let clock: Clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + } + let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; require!( @@ -51,15 +67,6 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul let num_pool_validators = get_validator_list_length(validator_list)?; - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - // May need to force an extra transition here in case cranking got stuck in any previous state // and it's now the start of a new scoring cycle if !matches!( @@ -73,6 +80,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul &epoch_schedule, )?; } + require!( matches!( state_account.state.state_tag, diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 8c75fea0..4cd72e2e 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -3,7 +3,7 @@ use crate::{ events::EpochMaintenanceEvent, utils::{ check_validator_list_has_stake_status_other_than, deserialize_stake_pool, - get_stake_pool_address, get_validator_list_length, + get_stake_pool_address, get_validator_list, get_validator_list_length, }, Config, StewardStateAccount, CHECKED_VALIDATORS_REMOVED_FROM_LIST, COMPUTE_INSTANT_UNSTAKES, EPOCH_MAINTENANCE, POST_LOOP_IDLE, PRE_LOOP_IDLE, REBALANCE, RESET_TO_IDLE, @@ -23,7 +23,7 @@ pub struct EpochMaintenance<'info> { pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: Correct account guaranteed if address is correct - #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] + #[account(address = get_validator_list(&config)?)] pub validator_list: AccountInfo<'info>, /// CHECK: Correct account guaranteed if address is correct @@ -50,35 +50,31 @@ pub fn handler( StewardError::StakePoolNotUpdated ); - // Keep this unset until we have completed all maintenance tasks - state_account.state.unset_flag(EPOCH_MAINTENANCE); + require!( + state_account.state.current_epoch < clock.epoch, + StewardError::EpochMaintenanceAlreadyComplete + ); - // We only need to check this once per maintenance cycle - if !state_account - .state - .has_flag(CHECKED_VALIDATORS_REMOVED_FROM_LIST) - { - // Ensure there are no validators in the list that have not been removed, that should be - require!( - !check_validator_list_has_stake_status_other_than( - &ctx.accounts.validator_list, - StakeStatus::Active - )?, - StewardError::ValidatorsHaveNotBeenRemoved - ); + // Ensure there are no validators in the list that have not been removed, that should be + require!( + !check_validator_list_has_stake_status_other_than( + &ctx.accounts.validator_list, + StakeStatus::Active + )?, + StewardError::ValidatorsHaveNotBeenRemoved + ); - state_account - .state - .set_flag(CHECKED_VALIDATORS_REMOVED_FROM_LIST); - } + state_account.state.unset_flag(EPOCH_MAINTENANCE); { // Routine - Remove marked validators // We still want these checks to run even if we don't specify a validator to remove let validators_in_list = get_validator_list_length(&ctx.accounts.validator_list)?; - let validators_to_remove = state_account.state.validators_to_remove.count(); + let validators_to_remove = state_account.state.validators_to_remove.count() + + state_account.state.validators_for_immediate_removal.count(); // Ensure we have a 1-1 mapping between the number of validators in the list and the number of validators in the state + // If we don't have this mapping, everything needs to be removed require!( state_account.state.num_pool_validators as usize + state_account.state.validators_added as usize @@ -95,10 +91,7 @@ pub fn handler( { // Routine - Update state - let okay_to_update = state_account.state.validators_to_remove.is_empty() - && state_account - .state - .has_flag(CHECKED_VALIDATORS_REMOVED_FROM_LIST); + let okay_to_update = state_account.state.validators_to_remove.is_empty(); if okay_to_update { state_account.state.current_epoch = clock.epoch; @@ -115,7 +108,6 @@ pub fn handler( .state .set_flag(RESET_TO_IDLE | EPOCH_MAINTENANCE); } - emit!(EpochMaintenanceEvent { validator_index_to_remove: validator_index_to_remove.map(|x| x as u64), validator_list_length: get_validator_list_length(&ctx.accounts.validator_list)? as u64, diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index 73173e5c..be16761c 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -25,18 +25,25 @@ pub fn handler(ctx: Context) -> Result<()> { let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - require!( - matches!(state_account.state.state_tag, StewardStateEnum::Idle), - StewardError::InvalidState - ); - - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); + { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + matches!(state_account.state.state_tag, StewardStateEnum::Idle), + StewardError::InvalidState + ); + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); } maybe_transition_and_emit( diff --git a/programs/steward/src/instructions/instant_remove_validator.rs b/programs/steward/src/instructions/instant_remove_validator.rs new file mode 100644 index 00000000..63d76057 --- /dev/null +++ b/programs/steward/src/instructions/instant_remove_validator.rs @@ -0,0 +1,81 @@ +use crate::{ + errors::StewardError, + utils::{ + check_validator_list_has_stake_status_other_than, deserialize_stake_pool, + get_stake_pool_address, get_validator_list, get_validator_list_length, + }, + Config, StewardStateAccount, +}; +use anchor_lang::prelude::*; +use spl_stake_pool::state::StakeStatus; + +#[derive(Accounts)] +pub struct InstantRemoveValidator<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + /// CHECK: Correct account guaranteed if address is correct + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, + + /// CHECK: Correct account guaranteed if address is correct + #[account( + address = get_stake_pool_address(&config)? + )] + pub stake_pool: AccountInfo<'info>, +} + +/// Removes validators from the pool that have been marked for immediate removal +pub fn handler( + ctx: Context, + validator_index_to_remove: usize, +) -> Result<()> { + let stake_pool = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + let mut state_account = ctx.accounts.state_account.load_mut()?; + + let clock = Clock::get()?; + let validators_to_remove = state_account.state.validators_for_immediate_removal.count(); + let validators_in_list = get_validator_list_length(&ctx.accounts.validator_list)?; + + require!( + clock.epoch == stake_pool.last_update_epoch, + StewardError::StakePoolNotUpdated + ); + + require!( + state_account + .state + .validators_for_immediate_removal + .get(validator_index_to_remove)?, + StewardError::ValidatorNotInList + ); + + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + - validators_to_remove + == validators_in_list, + StewardError::ListStateMismatch + ); + + // Ensure there are no validators in the list that have not been removed, that should be + require!( + !check_validator_list_has_stake_status_other_than( + &ctx.accounts.validator_list, + StakeStatus::Active + )?, + StewardError::ValidatorsHaveNotBeenRemoved + ); + + state_account + .state + .remove_validator(validator_index_to_remove)?; + + Ok(()) +} diff --git a/programs/steward/src/instructions/mod.rs b/programs/steward/src/instructions/mod.rs index fdef8aa5..27337283 100644 --- a/programs/steward/src/instructions/mod.rs +++ b/programs/steward/src/instructions/mod.rs @@ -9,6 +9,7 @@ pub mod compute_score; pub mod epoch_maintenance; pub mod idle; pub mod initialize_steward; +pub mod instant_remove_validator; pub mod pause_steward; pub mod realloc_state; pub mod rebalance; @@ -29,6 +30,7 @@ pub use compute_score::*; pub use epoch_maintenance::*; pub use idle::*; pub use initialize_steward::*; +pub use instant_remove_validator::*; pub use pause_steward::*; pub use realloc_state::*; pub use rebalance::*; diff --git a/programs/steward/src/instructions/realloc_state.rs b/programs/steward/src/instructions/realloc_state.rs index b2e6ef8d..0d5057e8 100644 --- a/programs/steward/src/instructions/realloc_state.rs +++ b/programs/steward/src/instructions/realloc_state.rs @@ -88,6 +88,7 @@ pub fn handler(ctx: Context) -> Result<()> { state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; state_account.state.validators_to_remove = BitMask::default(); + state_account.state.validators_for_immediate_removal = BitMask::default(); state_account.state.validators_added = 0; state_account.state.clear_flags(); state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index 3bf09d4d..9aba351a 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -24,7 +24,7 @@ use crate::{ events::{DecreaseComponents, RebalanceEvent, RebalanceTypeTag}, maybe_transition_and_emit, utils::{deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index}, - Config, StewardStateAccount, + Config, StewardStateAccount, StewardStateEnum, }; #[derive(Accounts)] @@ -111,7 +111,7 @@ pub struct Rebalance<'info> { pub transient_stake_account: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(owner = vote::program::ID)] + #[account(constraint = (vote_account.owner == &vote::program::ID || vote_account.owner == &system_program::ID))] pub vote_account: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -146,6 +146,27 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + matches!(state_account.state.state_tag, StewardStateEnum::Rebalance), + StewardError::InvalidState + ); + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + } + let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; require!( @@ -154,15 +175,6 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( ); let transient_seed = u64::from(validator_stake_info.transient_seed_suffix); - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - let minimum_delegation = minimum_delegation(get_minimum_delegation()?); let stake_rent = Rent::get()?.minimum_balance(StakeStateV2::size_of()); diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs index b647f4c7..0588b014 100644 --- a/programs/steward/src/instructions/reset_steward_state.rs +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -61,6 +61,7 @@ pub fn handler(ctx: Context) -> Result<()> { state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; state_account.state.validators_to_remove = BitMask::default(); + state_account.state.validators_for_immediate_removal = BitMask::default(); state_account.state.validators_added = 0; state_account.state.clear_flags(); state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 74b8f938..574fd923 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -94,6 +94,14 @@ pub mod steward { instructions::epoch_maintenance::handler(ctx, validator_index_to_remove.map(|x| x as usize)) } + /// Housekeeping, run at the start of any new epoch before any other instructions + pub fn instant_remove_validator( + ctx: Context, + validator_index_to_remove: u64, + ) -> Result<()> { + instructions::instant_remove_validator::handler(ctx, validator_index_to_remove as usize) + } + /// Computes score for a the validator at `validator_list_index` for the current cycle. pub fn compute_score(ctx: Context, validator_list_index: u64) -> Result<()> { instructions::compute_score::handler(ctx, validator_list_index as usize) diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 8e782992..b0c070b3 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -87,6 +87,10 @@ pub struct StewardState { /// This is cleaned up in the next epoch pub validators_to_remove: BitMask, + /// Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool + /// This happens when a validator is able to be removed within the same epoch as it was marked + pub validators_for_immediate_removal: BitMask, + ////// Cycle metadata fields ////// /// Slot of the first ComputeScores instruction in the current cycle pub start_computing_scores_slot: u64, @@ -463,15 +467,27 @@ impl StewardState { /// Update internal state when a validator is removed from the pool 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)?; + require!( - self.validators_to_remove.get(index)?, + marked_for_regular_removal || marked_for_immediate_removal, StewardError::ValidatorNotMarkedForRemoval ); - self.num_pool_validators = self - .num_pool_validators - .checked_sub(1) - .ok_or(StewardError::ArithmeticError)?; + // If the validator was marked for removal in the current cycle, decrement validators_added + if index >= self.num_pool_validators as usize { + self.validators_added = self + .validators_added + .checked_sub(1) + .ok_or(StewardError::ArithmeticError)?; + } else { + self.num_pool_validators = self + .num_pool_validators + .checked_sub(1) + .ok_or(StewardError::ArithmeticError)?; + } + let num_pool_validators = self.num_pool_validators as usize; // Shift all validator state to the left @@ -486,6 +502,8 @@ impl StewardState { self.progress.set(i, self.progress.get(next_i)?)?; self.validators_to_remove .set(i, self.validators_to_remove.get(next_i)?)?; + self.validators_for_immediate_removal + .set(i, self.validators_for_immediate_removal.get(next_i)?)?; } // Update score indices @@ -533,9 +551,14 @@ impl StewardState { self.sorted_yield_score_indices[num_pool_validators] = SORTED_INDEX_DEFAULT; self.delegations[num_pool_validators] = Delegation::default(); self.instant_unstake.set(num_pool_validators, false)?; - self.validators_to_remove.set(num_pool_validators, false)?; self.progress.set(num_pool_validators, false)?; + if marked_for_regular_removal { + self.validators_to_remove.set(index, false)?; + } else { + self.validators_for_immediate_removal.set(index, false)?; + } + Ok(()) } @@ -546,6 +569,10 @@ impl StewardState { self.validators_to_remove.set(index, true) } + pub fn mark_validator_for_immediate_removal(&mut self, index: usize) -> Result<()> { + self.validators_for_immediate_removal.set(index, true) + } + /// Called when adding a validator to the pool so that we can ensure a 1-1 mapping between /// the validator list and the steward state pub fn increment_validator_to_add(&mut self) -> Result<()> { @@ -610,7 +637,9 @@ impl StewardState { } // Skip scoring if marked for deletion - if self.validators_to_remove.get(index)? { + if self.validators_to_remove.get(index)? + || self.validators_for_immediate_removal.get(index)? + { self.scores[index] = 0_u32; self.yield_scores[index] = 0_u32; @@ -755,7 +784,9 @@ impl StewardState { } // Skip if marked for deletion - if self.validators_to_remove.get(index)? { + if self.validators_to_remove.get(index)? + || self.validators_for_immediate_removal.get(index)? + { self.progress.set(index, true)?; return Ok(None); } @@ -835,7 +866,9 @@ impl StewardState { } // Skip if marked for deletion - if self.validators_to_remove.get(index)? { + if self.validators_to_remove.get(index)? + || self.validators_for_immediate_removal.get(index)? + { self.progress.set(index, true)?; return Ok(RebalanceType::None); } diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index 395466e3..bc376121 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -526,6 +526,10 @@ impl TestFixture { }; if let Err(e) = process_transaction_result { + if !e.to_string().contains(error_message) { + panic!("Error: {}\n\nDoes not match {}", e, error_message); + } + assert!(e.to_string().contains(error_message)); } else { panic!("Error: Transaction succeeded. Expected {}", error_message); @@ -937,6 +941,7 @@ impl Default for StateMachineFixtures { status_flags: 0, validators_added: 0, validators_to_remove: BitMask::default(), + validators_for_immediate_removal: BitMask::default(), _padding0: [0; STATE_PADDING_0_SIZE], }; diff --git a/tests/tests/steward/test_spl_passthrough.rs b/tests/tests/steward/test_spl_passthrough.rs index 1fc9e51f..141bb451 100644 --- a/tests/tests/steward/test_spl_passthrough.rs +++ b/tests/tests/steward/test_spl_passthrough.rs @@ -161,21 +161,6 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { fixture.simulate_stake_pool_update().await; - let epoch_maintenance_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(), - }; - // Add Validator let instruction = Instruction { program_id: jito_steward::id(), @@ -207,7 +192,7 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { let latest_blockhash = _get_latest_blockhash(fixture).await; let transaction = Transaction::new_signed_with_payer( - &[epoch_maintenance_ix, instruction], + &[instruction], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], latest_blockhash, diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 4126bd3e..60a941fa 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -135,10 +135,8 @@ async fn test_auto_remove() { fixture.stake_accounts_for_validator(vote_account).await; // Add vote account - println!("DID STEWARD STATE"); _auto_add_validator_to_pool(&fixture, &vote_account).await; - println!("ADDED VALIDATOR"); let auto_remove_validator_ix = Instruction { program_id: jito_steward::id(), @@ -200,6 +198,43 @@ async fn test_auto_remove() { fixture.submit_transaction_assert_success(tx).await; + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + assert!( + steward_state_account + .state + .validators_for_immediate_removal + .count() + == 1 + ); + + let instant_remove_validator_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: 0, + } + .data(), + }; + + let tx = Transaction::new_signed_with_payer( + &[instant_remove_validator_ix.clone()], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture.ctx.borrow().last_blockhash, + ); + + fixture + .submit_transaction_assert_error(tx, "ValidatorsHaveNotBeenRemoved") + .await; + drop(fixture); } From a7dccaee22c1855e1bef4aa18bd8a9512b506ad9 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:00:31 -0600 Subject: [PATCH 11/18] FEATURE: Adding re-usable crank_checks function (#58) The new `crank_checks` function does all of the checks needed across each crank. Now with the new addition of looking for mismatched indices. --------- Co-authored-by: Christian Krueger Co-authored-by: Evan Batsell --- programs/steward/idl/steward.json | 6 ++ .../src/instructions/compute_delegations.rs | 37 +++------ .../instructions/compute_instant_unstake.rs | 32 ++------ .../steward/src/instructions/compute_score.rs | 25 ++---- .../src/instructions/epoch_maintenance.rs | 14 ++-- programs/steward/src/instructions/idle.rs | 35 ++++----- .../steward/src/instructions/rebalance.rs | 32 +++----- programs/steward/src/state/steward_state.rs | 9 +-- programs/steward/src/utils.rs | 42 +++++++++- tests/src/steward_fixtures.rs | 40 +++++++++- tests/tests/steward/test_integration.rs | 77 +++++++++++++++---- .../commands/cranks/compute_delegations.rs | 12 +-- utils/steward-cli/src/commands/cranks/idle.rs | 12 +-- 13 files changed, 217 insertions(+), 156 deletions(-) diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 9378d755..91ef2790 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -330,6 +330,9 @@ { "name": "state_account", "writable": true + }, + { + "name": "validator_list" } ], "args": [] @@ -649,6 +652,9 @@ { "name": "state_account", "writable": true + }, + { + "name": "validator_list" } ], "args": [] diff --git a/programs/steward/src/instructions/compute_delegations.rs b/programs/steward/src/instructions/compute_delegations.rs index 27729ead..ca48a3fe 100644 --- a/programs/steward/src/instructions/compute_delegations.rs +++ b/programs/steward/src/instructions/compute_delegations.rs @@ -1,4 +1,4 @@ -use crate::errors::StewardError; +use crate::utils::{get_validator_list, state_checks}; use crate::{maybe_transition_and_emit, Config, StewardStateAccount, StewardStateEnum}; use anchor_lang::prelude::*; @@ -12,6 +12,10 @@ pub struct ComputeDelegations<'info> { bump )] pub state_account: AccountLoader<'info, StewardStateAccount>, + + /// CHECK: Account owner checked, account type checked in get_validator_stake_info_at_index + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, } /* @@ -21,33 +25,16 @@ It computes a share of the pool for each validator. pub fn handler(ctx: Context) -> Result<()> { let config = ctx.accounts.config.load()?; let mut state_account = ctx.accounts.state_account.load_mut()?; - let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - { - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - - require!( - matches!( - state_account.state.state_tag, - StewardStateEnum::ComputeDelegations - ), - StewardError::InvalidState - ); - - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - require!( - state_account.state.validators_for_immediate_removal.count() == 0, - StewardError::ValidatorsNeedToBeRemoved - ); - } + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::ComputeDelegations), + )?; state_account .state diff --git a/programs/steward/src/instructions/compute_instant_unstake.rs b/programs/steward/src/instructions/compute_instant_unstake.rs index 576d0606..96ba4d29 100644 --- a/programs/steward/src/instructions/compute_instant_unstake.rs +++ b/programs/steward/src/instructions/compute_instant_unstake.rs @@ -1,7 +1,7 @@ use crate::{ errors::StewardError, maybe_transition_and_emit, - utils::{get_validator_list, get_validator_stake_info_at_index}, + utils::{get_validator_list, get_validator_stake_info_at_index, state_checks}, Config, StewardStateAccount, StewardStateEnum, }; use anchor_lang::prelude::*; @@ -41,29 +41,13 @@ pub fn handler(ctx: Context, validator_list_index: usize) let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - { - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - - require!( - matches!( - state_account.state.state_tag, - StewardStateEnum::ComputeInstantUnstake - ), - StewardError::InvalidState - ); - - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - require!( - state_account.state.validators_for_immediate_removal.count() == 0, - StewardError::ValidatorsNeedToBeRemoved - ); - } + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::ComputeInstantUnstake), + )?; let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index 04bad5f9..c7c1f70f 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -3,7 +3,10 @@ use anchor_lang::prelude::*; use crate::{ errors::StewardError, maybe_transition_and_emit, - utils::{get_validator_list, get_validator_list_length, get_validator_stake_info_at_index}, + utils::{ + get_validator_list, get_validator_list_length, get_validator_stake_info_at_index, + state_checks, + }, Config, StewardStateAccount, StewardStateEnum, }; use validator_history::{ClusterHistory, ValidatorHistory}; @@ -42,21 +45,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul let clock: Clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - { - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - require!( - state_account.state.validators_for_immediate_removal.count() == 0, - StewardError::ValidatorsNeedToBeRemoved - ); - } + state_checks(&clock, &config, &state_account, validator_list, None)?; let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; @@ -65,8 +54,6 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul StewardError::ValidatorNotInList ); - let num_pool_validators = get_validator_list_length(validator_list)?; - // May need to force an extra transition here in case cranking got stuck in any previous state // and it's now the start of a new scoring cycle if !matches!( @@ -89,6 +76,8 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul StewardError::InvalidState ); + let num_pool_validators = get_validator_list_length(validator_list)?; + if let Some(score) = state_account.state.compute_score( &clock, &epoch_schedule, diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index 4cd72e2e..c4fab4f9 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -5,8 +5,8 @@ use crate::{ check_validator_list_has_stake_status_other_than, deserialize_stake_pool, get_stake_pool_address, get_validator_list, get_validator_list_length, }, - Config, StewardStateAccount, CHECKED_VALIDATORS_REMOVED_FROM_LIST, COMPUTE_INSTANT_UNSTAKES, - EPOCH_MAINTENANCE, POST_LOOP_IDLE, PRE_LOOP_IDLE, REBALANCE, RESET_TO_IDLE, + Config, StewardStateAccount, COMPUTE_INSTANT_UNSTAKES, EPOCH_MAINTENANCE, POST_LOOP_IDLE, + PRE_LOOP_IDLE, REBALANCE, RESET_TO_IDLE, }; use anchor_lang::prelude::*; use spl_stake_pool::state::StakeStatus; @@ -97,13 +97,9 @@ pub fn handler( state_account.state.current_epoch = clock.epoch; // We keep Compute Scores and Compute Delegations to be unset on next epoch cycle - state_account.state.unset_flag( - CHECKED_VALIDATORS_REMOVED_FROM_LIST - | PRE_LOOP_IDLE - | COMPUTE_INSTANT_UNSTAKES - | REBALANCE - | POST_LOOP_IDLE, - ); + state_account + .state + .unset_flag(PRE_LOOP_IDLE | COMPUTE_INSTANT_UNSTAKES | REBALANCE | POST_LOOP_IDLE); state_account .state .set_flag(RESET_TO_IDLE | EPOCH_MAINTENANCE); diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index be16761c..1239dc81 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -1,7 +1,9 @@ use anchor_lang::prelude::*; use crate::{ - errors::StewardError, maybe_transition_and_emit, Config, StewardStateAccount, StewardStateEnum, + maybe_transition_and_emit, + utils::{get_validator_list, state_checks}, + Config, StewardStateAccount, StewardStateEnum, }; #[derive(Accounts)] @@ -14,6 +16,10 @@ pub struct Idle<'info> { bump )] pub state_account: AccountLoader<'info, StewardStateAccount>, + + /// CHECK: account type checked in state_checks and address set in config + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, } /* @@ -25,26 +31,13 @@ pub fn handler(ctx: Context) -> Result<()> { let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - { - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - - require!( - matches!(state_account.state.state_tag, StewardStateEnum::Idle), - StewardError::InvalidState - ); - - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - require!( - state_account.state.validators_for_immediate_removal.count() == 0, - StewardError::ValidatorsNeedToBeRemoved - ); - } + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::Idle), + )?; maybe_transition_and_emit( &mut state_account.state, diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index 9aba351a..1be2901d 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -23,7 +23,10 @@ use crate::{ errors::StewardError, events::{DecreaseComponents, RebalanceEvent, RebalanceTypeTag}, maybe_transition_and_emit, - utils::{deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index}, + utils::{ + deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, + state_checks, + }, Config, StewardStateAccount, StewardStateEnum, }; @@ -146,26 +149,13 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - { - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - - require!( - matches!(state_account.state.state_tag, StewardStateEnum::Rebalance), - StewardError::InvalidState - ); - - require!( - clock.epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - require!( - state_account.state.validators_for_immediate_removal.count() == 0, - StewardError::ValidatorsNeedToBeRemoved - ); - } + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::Rebalance), + )?; let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index b0c070b3..d52731fb 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -127,7 +127,7 @@ pub struct StewardState { pub const STATE_PADDING_0_SIZE: usize = MAX_VALIDATORS * 8 + 2; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] #[repr(u64)] pub enum StewardStateEnum { /// Start state @@ -245,15 +245,10 @@ pub const REBALANCE: u32 = 1 << 5; pub const POST_LOOP_IDLE: u32 = 1 << 6; // BITS 8-15 RESERVED FOR FUTURE USE // BITS 16-23 OPERATIONAL FLAGS -/// In epoch maintenance, we only need to check the validator pool -/// once for any validators that still need to be removed -/// when there are no validators to remove from the pool, the operation continues -/// and this condition is not checked again -pub const CHECKED_VALIDATORS_REMOVED_FROM_LIST: u32 = 1 << 16; /// In epoch maintenance, when a new epoch is detected, we need a flag to tell the /// state transition layer that it needs to be reset to the IDLE state /// this flag is set in in epoch_maintenance and unset in the IDLE state transition -pub const RESET_TO_IDLE: u32 = 1 << 17; +pub const RESET_TO_IDLE: u32 = 1 << 16; // BITS 24-31 RESERVED FOR FUTURE USE impl StewardState { diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index 7dcce9b2..1f5d01c8 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -8,7 +8,47 @@ use spl_stake_pool::{ state::{StakeStatus, ValidatorListHeader, ValidatorStakeInfo}, }; -use crate::{errors::StewardError, Config, Delegation}; +use crate::{errors::StewardError, Config, Delegation, StewardStateAccount, StewardStateEnum}; + +pub fn state_checks( + clock: &Clock, + config: &Config, + state_account: &StewardStateAccount, + validator_list_account_info: &AccountInfo, + expected_state: Option, +) -> Result<()> { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + if let Some(expected_state) = expected_state { + require!( + state_account.state.state_tag == expected_state, + StewardError::InvalidState + ); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + + // Ensure we have a 1-1 mapping between the number of validators + let validators_in_list = get_validator_list_length(validator_list_account_info)?; + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + == validators_in_list, + StewardError::ListStateMismatch + ); + + Ok(()) +} pub fn get_stake_pool_address(account: &AccountLoader) -> Result { let config = account.load()?; diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index bc376121..bea989ea 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -15,6 +15,7 @@ use jito_steward::{ bitmask::BitMask, constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT, STAKE_POOL_WITHDRAW_SEED}, utils::StakePool, + utils::ValidatorList, Config, Delegation, LargeBitMask, Parameters, StewardState, StewardStateAccount, StewardStateEnum, UpdateParametersArgs, STATE_PADDING_0_SIZE, }; @@ -25,8 +26,8 @@ use solana_sdk::{ stake::state::StakeStateV2, transaction::Transaction, }; use spl_stake_pool::{ - find_stake_program_address, find_transient_stake_program_address, - state::{Fee, StakeStatus, ValidatorList, ValidatorStakeInfo}, + find_stake_program_address, find_transient_stake_program_address, minimum_delegation, + state::{Fee, StakeStatus, ValidatorList as SPLValidatorList, ValidatorStakeInfo}, }; use validator_history::{ self, constants::MAX_ALLOC_BYTES, CircBuf, CircBufCluster, ClusterHistory, ClusterHistoryEntry, @@ -364,6 +365,39 @@ impl TestFixture { self.submit_transaction_assert_success(transaction).await; } + pub async fn initialize_validator_list(&self, num_validators: usize) { + let stake_program_minimum = self.fetch_minimum_delegation().await; + let pool_minimum_delegation = minimum_delegation(stake_program_minimum); + let stake_rent = self.fetch_stake_rent().await; + let minimum_active_stake_with_rent = pool_minimum_delegation + stake_rent; + + let validator_list_account_info = + self.get_account(&self.stake_pool_meta.validator_list).await; + + let validator_list: ValidatorList = self + .load_and_deserialize(&self.stake_pool_meta.validator_list) + .await; + + let mut spl_validator_list = validator_list.as_ref().clone(); + + for _ in 0..num_validators { + spl_validator_list.validators.push(ValidatorStakeInfo { + active_stake_lamports: minimum_active_stake_with_rent.into(), + vote_account_address: Pubkey::new_unique(), + ..ValidatorStakeInfo::default() + }); + } + + self.ctx.borrow_mut().set_account( + &self.stake_pool_meta.validator_list, + &serialized_validator_list_account( + spl_validator_list.clone(), + Some(validator_list_account_info.data.len()), + ) + .into(), + ); + } + // Turn this into a fixture creator pub async fn initialize_cluster_history_account(&self) -> ClusterHistory { todo!() @@ -622,7 +656,7 @@ pub fn closed_vote_account() -> Account { // TODO write a function to serialize any account with T: AnchorSerialize pub fn serialized_validator_list_account( - validator_list: ValidatorList, + validator_list: SPLValidatorList, account_size: Option, ) -> Account { // Passes in size because zeros at the end will be truncated during serialization diff --git a/tests/tests/steward/test_integration.rs b/tests/tests/steward/test_integration.rs index dcc0acc3..f43bcbdf 100644 --- a/tests/tests/steward/test_integration.rs +++ b/tests/tests/steward/test_integration.rs @@ -96,11 +96,14 @@ async fn test_compute_delegations() { &serialized_config(steward_config).into(), ); + fixture.initialize_validator_list(MAX_VALIDATORS).await; + let compute_delegations_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(), @@ -114,6 +117,7 @@ async fn test_compute_delegations() { &[&fixture.keypair], ctx.borrow().last_blockhash, ); + fixture.submit_transaction_assert_success(tx).await; let steward_state_account: StewardStateAccount = @@ -145,6 +149,7 @@ async fn test_compute_delegations() { 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(), @@ -332,8 +337,6 @@ async fn test_compute_scores() { fixture.submit_transaction_assert_success(tx).await; - println!("Okay!"); - let mut steward_state_account: StewardStateAccount = fixture.load_and_deserialize(&fixture.steward_state).await; @@ -577,10 +580,13 @@ async fn test_compute_instant_unstake() { account_type: AccountType::ValidatorList, max_validators: MAX_VALIDATORS as u32, }, - validators: vec![ValidatorStakeInfo { - vote_account_address: vote_account, - ..ValidatorStakeInfo::default() - }], + validators: vec![ + ValidatorStakeInfo { + vote_account_address: vote_account, + ..ValidatorStakeInfo::default() + }, + ValidatorStakeInfo::default(), + ], }; fixture.ctx.borrow_mut().set_account( @@ -632,6 +638,20 @@ async fn test_compute_instant_unstake() { ctx.borrow().last_blockhash, ); + let test_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + + println!("{:?}", validator_list.validators.len()); + println!( + "{:?}", + test_state_account.state.num_pool_validators + + test_state_account.state.validators_added as u64 + ); + fixture.submit_transaction_assert_success(tx).await; steward_state_account = fixture.load_and_deserialize(&fixture.steward_state).await; assert!(matches!( @@ -720,6 +740,8 @@ async fn test_idle() { steward_state_account.state.current_epoch = epoch_schedule.first_normal_epoch; steward_state_account.state.num_pool_validators = MAX_VALIDATORS as u64; + fixture.initialize_validator_list(MAX_VALIDATORS).await; + ctx.borrow_mut().set_account( &fixture.steward_state, &serialized_steward_state_account(steward_state_account).into(), @@ -735,6 +757,7 @@ async fn test_idle() { 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(), @@ -944,15 +967,6 @@ async fn test_rebalance_increase() { &serialized_stake_pool_account(stake_pool_spl, std::mem::size_of::()).into(), ); - let mut steward_state_account: StewardStateAccount = - fixture.load_and_deserialize(&fixture.steward_state).await; - - steward_state_account.state.num_pool_validators += 1; - ctx.borrow_mut().set_account( - &fixture.steward_state, - &serialized_steward_state_account(steward_state_account).into(), - ); - let (stake_account_address, transient_stake_account_address, withdraw_authority) = fixture.stake_accounts_for_validator(vote_account).await; @@ -1020,6 +1034,17 @@ async fn test_rebalance_increase() { fixture.submit_transaction_assert_success(tx).await; + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + // Force validator into the active set, don't wait for next cycle + steward_state_account.state.num_pool_validators += 1; + steward_state_account.state.validators_added -= 1; + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + let reserve_before_rebalance = fixture.get_account(&fixture.stake_pool_meta.reserve).await; let tx = Transaction::new_signed_with_payer( @@ -1228,6 +1253,17 @@ async fn test_rebalance_decrease() { ); fixture.submit_transaction_assert_success(tx).await; + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + // Force validator into the active set, don't wait for next cycle + // steward_state_account.state.num_pool_validators += 1; + steward_state_account.state.validators_added -= 1; + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + // Simulating stake deposit let stake_account_data = fixture.get_account(&stake_account_address).await; @@ -1466,6 +1502,17 @@ async fn test_rebalance_other_cases() { ); fixture.submit_transaction_assert_success(tx).await; + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + // Force validator into the active set, don't wait for next cycle + steward_state_account.state.num_pool_validators += 1; + steward_state_account.state.validators_added -= 1; + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + let rebalance_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::Rebalance { diff --git a/utils/steward-cli/src/commands/cranks/compute_delegations.rs b/utils/steward-cli/src/commands/cranks/compute_delegations.rs index e88073e2..ab2fa739 100644 --- a/utils/steward-cli/src/commands/cranks/compute_delegations.rs +++ b/utils/steward-cli/src/commands/cranks/compute_delegations.rs @@ -12,7 +12,7 @@ use solana_sdk::{ use crate::{ commands::command_args::CrankComputeDelegations, - utils::{accounts::get_steward_state_account, transactions::configure_instruction}, + utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, }; pub async fn command_crank_compute_delegations( @@ -28,15 +28,14 @@ pub async fn command_crank_compute_delegations( let steward_config = args.steward_config; - let (state_account, state_address) = - get_steward_state_account(client, &program_id, &steward_config).await?; + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; - match state_account.state.state_tag { + match steward_accounts.state_account.state.state_tag { StewardStateEnum::ComputeDelegations => { /* Continue */ } _ => { println!( "State account is not in Compute Delegation state: {}", - state_account.state.state_tag + steward_accounts.state_account.state.state_tag ); return Ok(()); } @@ -46,7 +45,8 @@ pub async fn command_crank_compute_delegations( program_id, accounts: jito_steward::accounts::ComputeDelegations { config: steward_config, - state_account: state_address, + state_account: steward_accounts.state_address, + validator_list: steward_accounts.validator_list_address, } .to_account_metas(None), data: jito_steward::instruction::ComputeDelegations {}.data(), diff --git a/utils/steward-cli/src/commands/cranks/idle.rs b/utils/steward-cli/src/commands/cranks/idle.rs index bc6d6c52..6289667b 100644 --- a/utils/steward-cli/src/commands/cranks/idle.rs +++ b/utils/steward-cli/src/commands/cranks/idle.rs @@ -10,7 +10,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, }; -use crate::{commands::command_args::CrankIdle, utils::accounts::get_steward_state_account}; +use crate::{commands::command_args::CrankIdle, utils::accounts::get_all_steward_accounts}; pub async fn command_crank_idle( args: CrankIdle, @@ -25,15 +25,14 @@ pub async fn command_crank_idle( let steward_config = args.steward_config; - let (state_account, state_address) = - get_steward_state_account(client, &program_id, &steward_config).await?; + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; - match state_account.state.state_tag { + match steward_accounts.state_account.state.state_tag { StewardStateEnum::Idle => { /* Continue */ } _ => { println!( "State account is not in Idle state: {}", - state_account.state.state_tag + steward_accounts.state_account.state.state_tag ); return Ok(()); } @@ -43,7 +42,8 @@ pub async fn command_crank_idle( program_id, accounts: jito_steward::accounts::Idle { config: steward_config, - state_account: state_address, + state_account: steward_accounts.state_address, + validator_list: steward_accounts.validator_list_address, } .to_account_metas(None), data: jito_steward::instruction::Idle {}.data(), From f4ea93ae1bbda1b8ccb3d0d1ef85a5eb252c3265 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:24:25 -0600 Subject: [PATCH 12/18] TWEAKS: Last cleanup (#62) Co-authored-by: Christian Krueger --- programs/steward/idl/steward.json | 171 ++++++++++-------- programs/steward/src/constants.rs | 9 +- programs/steward/src/delegation.rs | 3 +- programs/steward/src/errors.rs | 20 +- programs/steward/src/events.rs | 9 +- .../auto_add_validator_to_pool.rs | 126 ++++++------- .../auto_remove_validator_from_pool.rs | 53 +++--- .../src/instructions/compute_delegations.rs | 4 +- .../instructions/compute_instant_unstake.rs | 5 +- .../steward/src/instructions/compute_score.rs | 7 +- .../src/instructions/epoch_maintenance.rs | 8 +- programs/steward/src/instructions/idle.rs | 4 +- .../instructions/instant_remove_validator.rs | 27 ++- .../steward/src/instructions/realloc_state.rs | 2 +- .../steward/src/instructions/rebalance.rs | 107 ++++++----- .../src/instructions/spl_passthrough.rs | 35 +++- programs/steward/src/lib.rs | 48 +++-- programs/steward/src/score.rs | 7 +- programs/steward/src/state/parameters.rs | 4 +- programs/steward/src/state/steward_state.rs | 44 +---- programs/steward/src/utils.rs | 91 ++++++++-- run_tests.sh | 4 +- tests/src/steward_fixtures.rs | 28 +-- tests/tests/steward/test_algorithms.rs | 2 +- tests/tests/steward/test_integration.rs | 61 +++++-- tests/tests/steward/test_parameters.rs | 3 +- tests/tests/steward/test_steward.rs | 18 +- 27 files changed, 522 insertions(+), 378 deletions(-) diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 91ef2790..754c2d03 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -42,6 +42,9 @@ }, { "name": "add_validator_to_pool", + "docs": [ + "Passthrough spl-stake-pool: Add a validator to the pool" + ], "discriminator": [ 181, 6, @@ -144,9 +147,6 @@ { "name": "validator_history_account" }, - { - "name": "stake_pool_program" - }, { "name": "stake_pool", "writable": true @@ -170,22 +170,25 @@ "name": "vote_account" }, { - "name": "rent" + "name": "stake_history" }, { - "name": "clock" + "name": "stake_config" }, { - "name": "stake_history" + "name": "stake_program" }, { - "name": "stake_config" + "name": "stake_pool_program" }, { "name": "system_program" }, { - "name": "stake_program" + "name": "rent" + }, + { + "name": "clock" } ], "args": [] @@ -207,18 +210,15 @@ ], "accounts": [ { - "name": "validator_history_account" + "name": "config" }, { - "name": "config" + "name": "validator_history_account" }, { "name": "state_account", "writable": true }, - { - "name": "stake_pool_program" - }, { "name": "stake_pool", "writable": true @@ -249,22 +249,25 @@ "name": "vote_account" }, { - "name": "rent" + "name": "stake_history" }, { - "name": "clock" + "name": "stake_config" }, { - "name": "stake_history" + "name": "stake_program" }, { - "name": "stake_config" + "name": "stake_pool_program" }, { "name": "system_program" }, { - "name": "stake_program" + "name": "rent" + }, + { + "name": "clock" } ], "args": [ @@ -419,6 +422,9 @@ }, { "name": "decrease_additional_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Decrease additional validator stake" + ], "discriminator": [ 90, 22, @@ -508,6 +514,9 @@ }, { "name": "decrease_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Decrease validator stake" + ], "discriminator": [ 145, 203, @@ -661,6 +670,9 @@ }, { "name": "increase_additional_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Increase additional validator stake" + ], "discriminator": [ 93, 136, @@ -752,6 +764,9 @@ }, { "name": "increase_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Increase validator stake" + ], "discriminator": [ 5, 121, @@ -890,7 +905,7 @@ { "name": "instant_remove_validator", "docs": [ - "Housekeeping, run at the start of any new epoch before any other instructions" + "When a validator is marked for immediate removal, it needs to be removed before normal functions can continue" ], "discriminator": [ 119, @@ -926,6 +941,9 @@ }, { "name": "pause_steward", + "docs": [ + "Pauses the steward, preventing any further state transitions" + ], "discriminator": [ 214, 85, @@ -1105,6 +1123,9 @@ }, { "name": "remove_validator_from_pool", + "docs": [ + "Passthrough spl-stake-pool: Remove a validator from the pool" + ], "discriminator": [ 161, 32, @@ -1206,6 +1227,9 @@ }, { "name": "resume_steward", + "docs": [ + "Resumes the steward, allowing state transitions to continue" + ], "discriminator": [ 25, 71, @@ -1268,6 +1292,9 @@ }, { "name": "set_preferred_validator", + "docs": [ + "Passthrough spl-stake-pool: Set the preferred validator" + ], "discriminator": [ 114, 42, @@ -1321,6 +1348,9 @@ }, { "name": "set_staker", + "docs": [ + "Passthrough spl-stake-pool: Set the staker for the pool" + ], "discriminator": [ 149, 203, @@ -1573,152 +1603,138 @@ }, { "code": 6003, - "name": "AddValidatorsNotComplete", - "msg": "Add validators step must be completed before any other steps can be taken" - }, - { - "code": 6004, - "name": "EpochNotOver", - "msg": "Cannot reset state before epoch is over" - }, - { - "code": 6005, "name": "Unauthorized", "msg": "Unauthorized to perform this action" }, { - "code": 6006, + "code": 6004, "name": "BitmaskOutOfBounds", "msg": "Bitmask index out of bounds" }, { - "code": 6007, - "name": "StateNotReset", - "msg": "Epoch state not reset" - }, - { - "code": 6008, - "name": "ValidatorOutOfRange", - "msg": "Validator History created after epoch start, out of range" + "code": 6005, + "name": "InvalidState", + "msg": "Invalid state" }, { - "code": 6009, - "name": "InvalidState" + "code": 6006, + "name": "StakeStateIsNotStake", + "msg": "Stake state is not Stake" }, { - "code": 6010, + "code": 6007, "name": "ValidatorBelowStakeMinimum", "msg": "Validator not eligible to be added to the pool. Must meet stake minimum" }, { - "code": 6011, + "code": 6008, "name": "ValidatorBelowLivenessMinimum", "msg": "Validator not eligible to be added to the pool. Must meet recent voting minimum" }, { - "code": 6012, + "code": 6009, "name": "VoteHistoryNotRecentEnough", "msg": "Validator History vote data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6013, + "code": 6010, "name": "StakeHistoryNotRecentEnough", "msg": "Validator History stake data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6014, + "code": 6011, "name": "ClusterHistoryNotRecentEnough", "msg": "Cluster History data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6015, + "code": 6012, "name": "StateMachinePaused", "msg": "Steward State Machine is paused. No state machine actions can be taken" }, { - "code": 6016, + "code": 6013, "name": "InvalidParameterValue", "msg": "Config parameter is out of range or otherwise invalid" }, { - "code": 6017, + "code": 6014, "name": "InstantUnstakeNotReady", "msg": "Instant unstake cannot be performed yet." }, { - "code": 6018, + "code": 6015, "name": "ValidatorIndexOutOfBounds", "msg": "Validator index out of bounds of state machine" }, { - "code": 6019, + "code": 6016, "name": "ValidatorListTypeMismatch", "msg": "ValidatorList account type mismatch" }, { - "code": 6020, + "code": 6017, "name": "ArithmeticError", "msg": "An operation caused an overflow/underflow" }, { - "code": 6021, + "code": 6018, "name": "ValidatorNotRemovable", "msg": "Validator not eligible for removal. Must be delinquent or have closed vote account" }, { - "code": 6022, + "code": 6019, "name": "ValidatorMarkedActive", "msg": "Validator was marked active when it should be deactivating" }, { - "code": 6023, + "code": 6020, "name": "MaxValidatorsReached", "msg": "Max validators reached" }, { - "code": 6024, - "name": "ValidatorHistoryMismatch", - "msg": "Validator history account does not match vote account" - }, - { - "code": 6025, + "code": 6021, "name": "EpochMaintenanceNotComplete", "msg": "Epoch Maintenance must be called before continuing" }, { - "code": 6026, + "code": 6022, "name": "StakePoolNotUpdated", "msg": "The stake pool must be updated before continuing" }, { - "code": 6027, + "code": 6023, "name": "EpochMaintenanceAlreadyComplete", "msg": "Epoch Maintenance has already been completed" }, { - "code": 6028, + "code": 6024, "name": "ValidatorsNeedToBeRemoved", "msg": "Validators are marked for immediate removal" }, { - "code": 6029, - "name": "NoValidatorsNeedToBeRemoved", - "msg": "No validators are marked for immediate removal" - }, - { - "code": 6030, + "code": 6025, "name": "ValidatorNotMarkedForRemoval", "msg": "Validator not marked for removal" }, { - "code": 6031, + "code": 6026, "name": "ValidatorsHaveNotBeenRemoved", "msg": "Validators have not been removed" }, { - "code": 6032, + "code": 6027, "name": "ListStateMismatch", "msg": "Validator List count does not match state machine" + }, + { + "code": 6028, + "name": "VoteAccountDoesNotMatch", + "msg": "Vote account does not match" + }, + { + "code": 6029, + "name": "ValidatorNeedsToBeMarkedForRemoval", + "msg": "Validator needs to be marked for removal" } ], "types": [ @@ -2253,7 +2269,7 @@ { "name": "is_blacklisted", "docs": [ - "Checks if validator was added to blacklist blacklisted" + "Checks if validator was added to blacklist" ], "type": "bool" }, @@ -2361,7 +2377,7 @@ "type": "u8" }, { - "name": "padding0", + "name": "_padding_0", "docs": [ "Required so that the struct is 8-byte aligned", "https://doc.rust-lang.org/reference/type-layout.html#reprc-structs" @@ -2439,6 +2455,15 @@ "Minimum epochs voting required to be in the pool ValidatorList and eligible for delegation" ], "type": "u64" + }, + { + "name": "_padding_1", + "type": { + "array": [ + "u64", + 32 + ] + } } ] } diff --git a/programs/steward/src/constants.rs b/programs/steward/src/constants.rs index 1bacb9c5..e79bf67b 100644 --- a/programs/steward/src/constants.rs +++ b/programs/steward/src/constants.rs @@ -1,8 +1,11 @@ -pub const MAX_ALLOC_BYTES: usize = 10240; +pub const MAX_ALLOC_BYTES: usize = 10_240; +pub const VEC_SIZE_BYTES: usize = 4; +pub const U64_SIZE: usize = 8; +pub const STAKE_STATUS_OFFSET: usize = 40; pub const STAKE_POOL_WITHDRAW_SEED: &[u8] = b"withdraw"; pub const STAKE_POOL_TRANSIENT_SEED: &[u8] = b"transient"; -pub const MAX_VALIDATORS: usize = 5000; -pub const BASIS_POINTS_MAX: u16 = 10000; +pub const MAX_VALIDATORS: usize = 5_000; +pub const BASIS_POINTS_MAX: u16 = 10_000; pub const COMMISSION_MAX: u8 = 100; pub const SORTED_INDEX_DEFAULT: u16 = u16::MAX; // Need at least 1% of slots remaining (4320 slots) to execute steps in state machine diff --git a/programs/steward/src/delegation.rs b/programs/steward/src/delegation.rs index 5c4afdaa..e5786d4f 100644 --- a/programs/steward/src/delegation.rs +++ b/programs/steward/src/delegation.rs @@ -160,7 +160,8 @@ pub fn increase_stake_calculation( } return Err(StewardError::ValidatorIndexOutOfBounds.into()); } - Err(StewardError::InvalidState.into()) + + Err(StewardError::ValidatorIndexOutOfBounds.into()) } #[derive(Default)] diff --git a/programs/steward/src/errors.rs b/programs/steward/src/errors.rs index ea80d06f..d1c0fe69 100644 --- a/programs/steward/src/errors.rs +++ b/programs/steward/src/errors.rs @@ -8,20 +8,14 @@ pub enum StewardError { ScoringNotComplete, #[msg("Validator does not exist at the ValidatorList index provided")] ValidatorNotInList, - #[msg("Add validators step must be completed before any other steps can be taken")] - AddValidatorsNotComplete, - #[msg("Cannot reset state before epoch is over")] - EpochNotOver, #[msg("Unauthorized to perform this action")] Unauthorized, #[msg("Bitmask index out of bounds")] BitmaskOutOfBounds, - #[msg("Epoch state not reset")] - StateNotReset, - #[msg("Validator History created after epoch start, out of range")] - ValidatorOutOfRange, - // Use invalid_state_error method to ensure expected and actual are logged + #[msg("Invalid state")] InvalidState, + #[msg("Stake state is not Stake")] + StakeStateIsNotStake, #[msg("Validator not eligible to be added to the pool. Must meet stake minimum")] ValidatorBelowStakeMinimum, #[msg("Validator not eligible to be added to the pool. Must meet recent voting minimum")] @@ -52,8 +46,6 @@ pub enum StewardError { ValidatorMarkedActive, #[msg("Max validators reached")] MaxValidatorsReached, - #[msg("Validator history account does not match vote account")] - ValidatorHistoryMismatch, #[msg("Epoch Maintenance must be called before continuing")] EpochMaintenanceNotComplete, #[msg("The stake pool must be updated before continuing")] @@ -62,12 +54,14 @@ pub enum StewardError { EpochMaintenanceAlreadyComplete, #[msg("Validators are marked for immediate removal")] ValidatorsNeedToBeRemoved, - #[msg("No validators are marked for immediate removal")] - NoValidatorsNeedToBeRemoved, #[msg("Validator not marked for removal")] ValidatorNotMarkedForRemoval, #[msg("Validators have not been removed")] ValidatorsHaveNotBeenRemoved, #[msg("Validator List count does not match state machine")] ListStateMismatch, + #[msg("Vote account does not match")] + VoteAccountDoesNotMatch, + #[msg("Validator needs to be marked for removal")] + ValidatorNeedsToBeMarkedForRemoval, } diff --git a/programs/steward/src/events.rs b/programs/steward/src/events.rs index 8de1d7ae..d85683d7 100644 --- a/programs/steward/src/events.rs +++ b/programs/steward/src/events.rs @@ -7,6 +7,8 @@ use anchor_lang::solana_program::pubkey::Pubkey; use borsh::{BorshDeserialize, BorshSerialize}; #[event] +#[derive(Debug, Clone)] + pub struct AutoRemoveValidatorEvent { pub validator_list_index: u64, pub vote_account: Pubkey, @@ -16,12 +18,14 @@ pub struct AutoRemoveValidatorEvent { } #[event] +#[derive(Debug, Clone)] pub struct AutoAddValidatorEvent { pub validator_list_index: u64, pub vote_account: Pubkey, } #[event] +#[derive(Debug, Clone)] pub struct EpochMaintenanceEvent { pub validator_index_to_remove: Option, pub validator_list_length: u64, @@ -32,7 +36,7 @@ pub struct EpochMaintenanceEvent { } #[event] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct StateTransition { pub epoch: u64, pub slot: u64, @@ -50,6 +54,7 @@ pub struct DecreaseComponents { } #[event] +#[derive(Debug, Clone)] pub struct RebalanceEvent { pub vote_account: Pubkey, pub epoch: u16, @@ -58,7 +63,7 @@ pub struct RebalanceEvent { pub decrease_components: DecreaseComponents, } -#[derive(BorshSerialize, BorshDeserialize, Debug)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub enum RebalanceTypeTag { None, Increase, diff --git a/programs/steward/src/instructions/auto_add_validator_to_pool.rs b/programs/steward/src/instructions/auto_add_validator_to_pool.rs index 91d7382a..6fab6031 100644 --- a/programs/steward/src/instructions/auto_add_validator_to_pool.rs +++ b/programs/steward/src/instructions/auto_add_validator_to_pool.rs @@ -2,12 +2,14 @@ use crate::constants::{MAX_VALIDATORS, STAKE_POOL_WITHDRAW_SEED}; use crate::errors::StewardError; use crate::events::AutoAddValidatorEvent; use crate::state::{Config, StewardStateAccount}; -use crate::utils::{deserialize_stake_pool, get_stake_pool_address}; +use crate::utils::{ + add_validator_check, deserialize_stake_pool, get_stake_pool_address, get_validator_list_length, +}; use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; use spl_stake_pool::find_stake_program_address; -use spl_stake_pool::state::ValidatorListHeader; use validator_history::state::ValidatorHistory; +use validator_history::ValidatorHistoryEntry; #[derive(Accounts)] pub struct AutoAddValidator<'info> { @@ -28,12 +30,6 @@ pub struct AutoAddValidator<'info> { )] pub validator_history_account: AccountLoader<'info, ValidatorHistory>, - /// CHECK: CPI address - #[account( - address = spl_stake_pool::ID - )] - pub stake_pool_program: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account( mut, @@ -76,10 +72,6 @@ pub struct AutoAddValidator<'info> { #[account(owner = vote::program::ID)] pub vote_account: AccountInfo<'info>, - pub rent: Sysvar<'info, Rent>, - - pub clock: Sysvar<'info, Clock>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = sysvar::stake_history::ID)] pub stake_history: AccountInfo<'info>, @@ -88,11 +80,21 @@ pub struct AutoAddValidator<'info> { #[account(address = stake::config::ID)] pub stake_config: AccountInfo<'info>, - pub system_program: Program<'info, System>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, + + /// CHECK: CPI address + #[account( + address = spl_stake_pool::ID + )] + pub stake_pool_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, + + pub rent: Sysvar<'info, Rent>, + + pub clock: Sysvar<'info, Clock>, } /* @@ -101,68 +103,52 @@ all the validators we want to be eligible for delegation, as well as to accept s Performs some eligibility checks in order to not fill up the validator list with offline or malicious validators. */ pub fn handler(ctx: Context) -> Result<()> { - let mut state_account = ctx.accounts.steward_state.load_mut()?; - let config = ctx.accounts.config.load()?; - let validator_history = ctx.accounts.validator_history_account.load()?; - let epoch = Clock::get()?.epoch; - - // Should not be able to add a validator if update is not complete { - require!( - epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - - require!( - state_account.state.validators_for_immediate_removal.count() == 0, - StewardError::ValidatorsNeedToBeRemoved - ); - } - - let validator_list_len = { - let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; - - validator_list.len() - }; - if validator_list_len.checked_add(1).unwrap() > MAX_VALIDATORS as u32 { - return Err(StewardError::MaxValidatorsReached.into()); - } + let mut state_account = ctx.accounts.steward_state.load_mut()?; + let config = ctx.accounts.config.load()?; + let validator_history = ctx.accounts.validator_history_account.load()?; + let validator_list = &ctx.accounts.validator_list; + let clock = Clock::get()?; + let epoch = clock.epoch; + + add_validator_check(&clock, &config, &state_account, validator_list)?; + + let validator_list_len = get_validator_list_length(&ctx.accounts.validator_list)?; + if validator_list_len.checked_add(1).unwrap() > MAX_VALIDATORS { + return Err(StewardError::MaxValidatorsReached.into()); + } - let start_epoch = - epoch.saturating_sub(config.parameters.minimum_voting_epochs.saturating_sub(1)); - if let Some(entry) = validator_history.history.last() { - // Steward requires that validators have been active for last minimum_voting_epochs epochs - if validator_history - .history - .epoch_credits_range(start_epoch as u16, epoch as u16) - .iter() - .any(|entry| entry.is_none()) - { + let start_epoch = + epoch.saturating_sub(config.parameters.minimum_voting_epochs.saturating_sub(1)); + if let Some(entry) = validator_history.history.last() { + // Steward requires that validators have been active for last minimum_voting_epochs epochs + if validator_history + .history + .epoch_credits_range(start_epoch as u16, epoch as u16) + .iter() + .any(|entry| entry.is_none()) + { + return Err(StewardError::ValidatorBelowLivenessMinimum.into()); + } + if entry.activated_stake_lamports + == ValidatorHistoryEntry::default().activated_stake_lamports + { + return Err(StewardError::StakeHistoryNotRecentEnough.into()); + } + if entry.activated_stake_lamports < config.parameters.minimum_stake_lamports { + return Err(StewardError::ValidatorBelowStakeMinimum.into()); + } + } else { return Err(StewardError::ValidatorBelowLivenessMinimum.into()); } - if entry.activated_stake_lamports < config.parameters.minimum_stake_lamports { - msg!( - "Validator {} below minimum. Required: {} Actual: {}", - validator_history.vote_account, - config.parameters.minimum_stake_lamports, - entry.activated_stake_lamports - ); - return Err(StewardError::ValidatorBelowStakeMinimum.into()); - } - } else { - return Err(StewardError::ValidatorBelowLivenessMinimum.into()); - } - state_account.state.increment_validator_to_add()?; + state_account.state.increment_validator_to_add()?; - // Have to drop the state account before calling the CPI - drop(state_account); - - emit!(AutoAddValidatorEvent { - vote_account: ctx.accounts.vote_account.key(), - validator_list_index: validator_list_len as u64 - }); + emit!(AutoAddValidatorEvent { + vote_account: ctx.accounts.vote_account.key(), + validator_list_index: validator_list_len as u64 + }); + } invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( 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 1b0ee4bb..ee62daa8 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -6,10 +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, }; use crate::StewardStateAccount; +use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; -use anchor_lang::{prelude::*, system_program}; use spl_pod::solana_program::borsh1::try_from_slice_unchecked; use spl_pod::solana_program::stake::state::StakeStateV2; use spl_stake_pool::state::StakeStatus; @@ -19,6 +20,8 @@ use validator_history::state::ValidatorHistory; #[derive(Accounts)] #[instruction(validator_list_index: u64)] pub struct AutoRemoveValidator<'info> { + pub config: AccountLoader<'info, Config>, + #[account( seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], seeds::program = validator_history::ID, @@ -26,8 +29,6 @@ pub struct AutoRemoveValidator<'info> { )] pub validator_history_account: AccountLoader<'info, ValidatorHistory>, - pub config: AccountLoader<'info, Config>, - #[account( mut, seeds = [StewardStateAccount::SEED, config.key().as_ref()], @@ -35,12 +36,6 @@ pub struct AutoRemoveValidator<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, - /// CHECK: CPI address - #[account( - address = spl_stake_pool::ID - )] - pub stake_pool_program: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account( mut, @@ -98,14 +93,9 @@ pub struct AutoRemoveValidator<'info> { )] pub transient_stake_account: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool - #[account(constraint = (vote_account.owner == &vote::program::ID || vote_account.owner == &system_program::ID))] + /// CHECK: Owner check done in handler pub vote_account: AccountInfo<'info>, - pub rent: Sysvar<'info, Rent>, - - pub clock: Sysvar<'info, Clock>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = sysvar::stake_history::ID)] pub stake_history: AccountInfo<'info>, @@ -114,24 +104,35 @@ pub struct AutoRemoveValidator<'info> { #[account(address = stake::config::ID)] pub stake_config: AccountInfo<'info>, - pub system_program: Program<'info, System>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, -} -/* + /// CHECK: CPI address + #[account( + address = spl_stake_pool::ID + )] + pub stake_pool_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, + + pub rent: Sysvar<'info, Rent>, + + pub clock: Sysvar<'info, Clock>, +} -*/ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { let stake_account_deactivated; let vote_account_closed; { + let config = ctx.accounts.config.load()?; let state_account = ctx.accounts.state_account.load()?; let validator_list = &ctx.accounts.validator_list; - let epoch = Clock::get()?.epoch; + let clock = Clock::get()?; + let epoch = clock.epoch; + + remove_validator_check(&clock, &config, &state_account, validator_list)?; let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; @@ -140,12 +141,6 @@ pub fn handler(ctx: Context, validator_list_index: usize) - StewardError::ValidatorNotInList ); - // Should not be able to remove a validator if update is not complete - require!( - epoch == state_account.state.current_epoch, - StewardError::EpochMaintenanceNotComplete - ); - // Checks state for deactivate delinquent status, preventing pool from merging stake with activating stake_account_deactivated = { let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); @@ -156,13 +151,13 @@ pub fn handler(ctx: Context, validator_list_index: usize) - StakeStateV2::Stake(_meta, stake, _stake_flags) => { stake.delegation.deactivation_epoch } - _ => return Err(StewardError::InvalidState.into()), // TODO fix + _ => return Err(StewardError::StakeStateIsNotStake.into()), }; deactivation_epoch < epoch }; // Check if vote account closed - vote_account_closed = ctx.accounts.vote_account.owner == &system_program::ID; + vote_account_closed = *ctx.accounts.vote_account.owner != vote::program::ID; require!( stake_account_deactivated || vote_account_closed, diff --git a/programs/steward/src/instructions/compute_delegations.rs b/programs/steward/src/instructions/compute_delegations.rs index ca48a3fe..694f1ca0 100644 --- a/programs/steward/src/instructions/compute_delegations.rs +++ b/programs/steward/src/instructions/compute_delegations.rs @@ -1,5 +1,5 @@ use crate::utils::{get_validator_list, state_checks}; -use crate::{maybe_transition_and_emit, Config, StewardStateAccount, StewardStateEnum}; +use crate::{maybe_transition, Config, StewardStateAccount, StewardStateEnum}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -40,7 +40,7 @@ pub fn handler(ctx: Context) -> Result<()> { .state .compute_delegations(clock.epoch, &config)?; - if let Some(event) = maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, diff --git a/programs/steward/src/instructions/compute_instant_unstake.rs b/programs/steward/src/instructions/compute_instant_unstake.rs index 96ba4d29..367f57f4 100644 --- a/programs/steward/src/instructions/compute_instant_unstake.rs +++ b/programs/steward/src/instructions/compute_instant_unstake.rs @@ -1,6 +1,6 @@ use crate::{ errors::StewardError, - maybe_transition_and_emit, + maybe_transition, utils::{get_validator_list, get_validator_stake_info_at_index, state_checks}, Config, StewardStateAccount, StewardStateEnum, }; @@ -18,6 +18,7 @@ pub struct ComputeInstantUnstake<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, + /// CHECK: We check it is the correct vote account in the handler pub validator_history: AccountLoader<'info, ValidatorHistory>, #[account(address = get_validator_list(&config)?)] @@ -67,7 +68,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) emit!(instant_unstake); } - if let Some(event) = maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index c7c1f70f..a2ef5b3d 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::{ errors::StewardError, - maybe_transition_and_emit, + maybe_transition, utils::{ get_validator_list, get_validator_list_length, get_validator_stake_info_at_index, state_checks, @@ -45,6 +45,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul let clock: Clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + // We don't check the state here because we force it below state_checks(&clock, &config, &state_account, validator_list, None)?; let validator_stake_info = @@ -60,7 +61,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul state_account.state.state_tag, StewardStateEnum::ComputeScores ) { - maybe_transition_and_emit( + maybe_transition( &mut state_account.state, &clock, &config.parameters, @@ -90,7 +91,7 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul emit!(score); } - if let Some(event) = maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs index c4fab4f9..2b3cafe7 100644 --- a/programs/steward/src/instructions/epoch_maintenance.rs +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -59,7 +59,7 @@ pub fn handler( require!( !check_validator_list_has_stake_status_other_than( &ctx.accounts.validator_list, - StakeStatus::Active + &[StakeStatus::Active] )?, StewardError::ValidatorsHaveNotBeenRemoved ); @@ -91,7 +91,11 @@ pub fn handler( { // Routine - Update state - let okay_to_update = state_account.state.validators_to_remove.is_empty(); + let okay_to_update = state_account.state.validators_to_remove.is_empty() + && state_account + .state + .validators_for_immediate_removal + .is_empty(); if okay_to_update { state_account.state.current_epoch = clock.epoch; diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index 1239dc81..0dc5265f 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use crate::{ - maybe_transition_and_emit, + maybe_transition, utils::{get_validator_list, state_checks}, Config, StewardStateAccount, StewardStateEnum, }; @@ -39,7 +39,7 @@ pub fn handler(ctx: Context) -> Result<()> { Some(StewardStateEnum::Idle), )?; - maybe_transition_and_emit( + maybe_transition( &mut state_account.state, &clock, &config.parameters, diff --git a/programs/steward/src/instructions/instant_remove_validator.rs b/programs/steward/src/instructions/instant_remove_validator.rs index 63d76057..099e87aa 100644 --- a/programs/steward/src/instructions/instant_remove_validator.rs +++ b/programs/steward/src/instructions/instant_remove_validator.rs @@ -43,6 +43,11 @@ pub fn handler( let validators_to_remove = state_account.state.validators_for_immediate_removal.count(); let validators_in_list = get_validator_list_length(&ctx.accounts.validator_list)?; + require!( + state_account.state.current_epoch == clock.epoch, + StewardError::EpochMaintenanceNotComplete + ); + require!( clock.epoch == stake_pool.last_update_epoch, StewardError::StakePoolNotUpdated @@ -56,23 +61,27 @@ pub fn handler( StewardError::ValidatorNotInList ); - require!( - state_account.state.num_pool_validators as usize - + state_account.state.validators_added as usize - - validators_to_remove - == validators_in_list, - StewardError::ListStateMismatch - ); - // Ensure there are no validators in the list that have not been removed, that should be require!( !check_validator_list_has_stake_status_other_than( &ctx.accounts.validator_list, - StakeStatus::Active + &[ + StakeStatus::Active, + StakeStatus::DeactivatingAll, + StakeStatus::DeactivatingTransient + ] )?, StewardError::ValidatorsHaveNotBeenRemoved ); + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + - validators_to_remove + == validators_in_list, + StewardError::ListStateMismatch + ); + state_account .state .remove_validator(validator_index_to_remove)?; diff --git a/programs/steward/src/instructions/realloc_state.rs b/programs/steward/src/instructions/realloc_state.rs index 0d5057e8..ef9adef3 100644 --- a/programs/steward/src/instructions/realloc_state.rs +++ b/programs/steward/src/instructions/realloc_state.rs @@ -79,7 +79,7 @@ pub fn handler(ctx: Context) -> Result<()> { state_account.state.yield_scores = [0; MAX_VALIDATORS]; state_account.state.sorted_yield_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; state_account.state.progress = BitMask::default(); - state_account.state.current_epoch = clock.epoch; + state_account.state.current_epoch = 0; // will be set by epoch_maintenance state_account.state.next_cycle_epoch = clock .epoch .checked_add(config.parameters.num_epochs_between_scoring) diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index 1be2901d..76d14ca1 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -22,7 +22,7 @@ use crate::{ delegation::RebalanceType, errors::StewardError, events::{DecreaseComponents, RebalanceEvent, RebalanceTypeTag}, - maybe_transition_and_emit, + maybe_transition, utils::{ deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, state_checks, @@ -113,8 +113,7 @@ pub struct Rebalance<'info> { )] pub transient_stake_account: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool - #[account(constraint = (vote_account.owner == &vote::program::ID || vote_account.owner == &system_program::ID))] + /// CHECK: We check the owning program in the handler pub vote_account: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -142,55 +141,69 @@ pub struct Rebalance<'info> { } pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { - let config = ctx.accounts.config.load()?; - let mut state_account = ctx.accounts.state_account.load_mut()?; let validator_history = ctx.accounts.validator_history.load()?; let validator_list = &ctx.accounts.validator_list; let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + let config = ctx.accounts.config.load()?; - state_checks( - &clock, - &config, - &state_account, - &ctx.accounts.validator_list, - Some(StewardStateEnum::Rebalance), - )?; - - let validator_stake_info = - get_validator_stake_info_at_index(validator_list, validator_list_index)?; - require!( - validator_stake_info.vote_account_address == validator_history.vote_account, - StewardError::ValidatorNotInList - ); - let transient_seed = u64::from(validator_stake_info.transient_seed_suffix); - - let minimum_delegation = minimum_delegation(get_minimum_delegation()?); - let stake_rent = Rent::get()?.minimum_balance(StakeStateV2::size_of()); - - let result = { - let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; - - let stake_pool_lamports_with_fixed_cost = - deserialize_stake_pool(&ctx.accounts.stake_pool)?.total_lamports; - let reserve_lamports_with_rent = ctx.accounts.reserve_stake.lamports(); - - state_account.state.rebalance( - clock.epoch, - validator_list_index, - &validator_list, - stake_pool_lamports_with_fixed_cost, - reserve_lamports_with_rent, - minimum_delegation, - stake_rent, - &config.parameters, - )? - }; + let rebalance_type: RebalanceType; + let transient_seed: u64; - drop(state_account); + { + let mut state_account = ctx.accounts.state_account.load_mut()?; + + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::Rebalance), + )?; + + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + require!( + validator_stake_info.vote_account_address == validator_history.vote_account, + StewardError::ValidatorNotInList + ); + + if ctx.accounts.vote_account.owner != &vote::program::ID + && !state_account + .state + .validators_to_remove + .get(validator_list_index)? + { + return Err(StewardError::ValidatorNeedsToBeMarkedForRemoval.into()); + } + + transient_seed = u64::from(validator_stake_info.transient_seed_suffix); + + let minimum_delegation = minimum_delegation(get_minimum_delegation()?); + let stake_rent = Rent::get()?.minimum_balance(StakeStateV2::size_of()); + + rebalance_type = { + let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; + let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + + let stake_pool_lamports_with_fixed_cost = + deserialize_stake_pool(&ctx.accounts.stake_pool)?.total_lamports; + let reserve_lamports_with_rent = ctx.accounts.reserve_stake.lamports(); + + state_account.state.rebalance( + clock.epoch, + validator_list_index, + &validator_list, + stake_pool_lamports_with_fixed_cost, + reserve_lamports_with_rent, + minimum_delegation, + stake_rent, + &config.parameters, + )? + }; + } - match result.clone() { + match rebalance_type.clone() { RebalanceType::Decrease(decrease_components) => { invoke_signed( &spl_stake_pool::instruction::decrease_validator_stake_with_reserve( @@ -273,10 +286,10 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Result<( emit!(rebalance_to_event( ctx.accounts.vote_account.key(), clock.epoch as u16, - result + rebalance_type )); - if let Some(event) = maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, diff --git a/programs/steward/src/instructions/spl_passthrough.rs b/programs/steward/src/instructions/spl_passthrough.rs index 5f985c74..b9b2bfaa 100644 --- a/programs/steward/src/instructions/spl_passthrough.rs +++ b/programs/steward/src/instructions/spl_passthrough.rs @@ -16,7 +16,7 @@ use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; use spl_stake_pool::find_stake_program_address; use spl_stake_pool::instruction::PreferredValidatorType; -use spl_stake_pool::state::ValidatorListHeader; +use spl_stake_pool::state::{StakeStatus, ValidatorListHeader}; use std::num::NonZeroU32; use validator_history::ValidatorHistory; @@ -181,7 +181,7 @@ pub fn remove_validator_from_pool_handler( validator_list_index: usize, ) -> Result<()> { { - let mut state_account = ctx.accounts.state_account.load_mut()?; + 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 @@ -207,10 +207,6 @@ pub fn remove_validator_from_pool_handler( return Err(StewardError::ValidatorNotInList.into()); } } - - state_account - .state - .mark_validator_for_removal(validator_list_index)?; } invoke_signed( @@ -239,6 +235,33 @@ pub fn remove_validator_from_pool_handler( &[ctx.bumps.state_account], ]], )?; + + { + // Read the state account again + let mut state_account = ctx.accounts.state_account.load_mut()?; + let validator_list = &ctx.accounts.validator_list; + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + + let stake_status = StakeStatus::try_from(validator_stake_info.status)?; + + match stake_status { + StakeStatus::Active => { + // Should never happen + return Err(StewardError::ValidatorMarkedActive.into()); + } + StakeStatus::DeactivatingValidator | StakeStatus::ReadyForRemoval => { + state_account + .state + .mark_validator_for_immediate_removal(validator_list_index)?; + } + StakeStatus::DeactivatingAll | StakeStatus::DeactivatingTransient => { + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } + } + } Ok(()) } diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 574fd923..7c600806 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -30,15 +30,25 @@ To initialize a Steward-managed pool: 3) `realloc_state` - increases the size of the State account to StewardStateAccount::SIZE, and initializes values once at that size Each cycle, the following steps are performed by a permissionless cranker: -1) compute_score (once per validator) +x) epoch_maintenance ( once per epoch ) +1) compute_score ( once per validator ) 2) compute_delegations 3) idle -4) compute_instant_unstake (once per validator) -5) rebalance (once per validator) +4) compute_instant_unstake ( once per validator ) +5) rebalance ( once per validator ) For the remaining epochs in a cycle, the state will repeat idle->compute_instant_unstake->rebalance. After `num_epochs_between_scoring` epochs, the state can transition back to ComputeScores. +To manage the validators in the pool, there are the following permissionless instructions: +- `auto_add_validator_to_pool` +- `auto_remove_validator_from_pool` +- `instant_remove_validator` - called when a validator can be removed within the same epoch it was marked for removal + +There are three authorities within the program: +- `admin` - can update authority, pause, resume, and reset state +- `parameters_authority` - can update parameters +- `blacklist_authority` - can add and remove validators from the blacklist If manual intervention is required, the following spl-stake-pool instructions are available, and can be executed by the config.authority: - `add_validator_to_pool` @@ -48,7 +58,6 @@ If manual intervention is required, the following spl-stake-pool instructions ar - `decrease_validator_stake` - `increase_additional_validator_stake` - `decrease_additional_validator_stake` -- `redelegate` - `set_staker` */ #[program] @@ -86,6 +95,14 @@ pub mod steward { instructions::auto_remove_validator_from_pool::handler(ctx, validator_list_index as usize) } + /// When a validator is marked for immediate removal, it needs to be removed before normal functions can continue + pub fn instant_remove_validator( + ctx: Context, + validator_index_to_remove: u64, + ) -> Result<()> { + instructions::instant_remove_validator::handler(ctx, validator_index_to_remove as usize) + } + /// Housekeeping, run at the start of any new epoch before any other instructions pub fn epoch_maintenance( ctx: Context, @@ -94,14 +111,6 @@ pub mod steward { instructions::epoch_maintenance::handler(ctx, validator_index_to_remove.map(|x| x as usize)) } - /// Housekeeping, run at the start of any new epoch before any other instructions - pub fn instant_remove_validator( - ctx: Context, - validator_index_to_remove: u64, - ) -> Result<()> { - instructions::instant_remove_validator::handler(ctx, validator_index_to_remove as usize) - } - /// Computes score for a the validator at `validator_list_index` for the current cycle. pub fn compute_score(ctx: Context, validator_list_index: u64) -> Result<()> { instructions::compute_score::handler(ctx, validator_list_index as usize) @@ -143,10 +152,12 @@ pub mod steward { instructions::set_new_authority::handler(ctx, authority_type) } + /// Pauses the steward, preventing any further state transitions pub fn pause_steward(ctx: Context) -> Result<()> { instructions::pause_steward::handler(ctx) } + /// Resumes the steward, allowing state transitions to continue pub fn resume_steward(ctx: Context) -> Result<()> { instructions::resume_steward::handler(ctx) } @@ -175,8 +186,6 @@ pub mod steward { instructions::update_parameters::handler(ctx, &update_parameters_args) } - /* TEMPORARY ADMIN INSTRUCTIONS for testing */ - /// Resets steward state account to its initial state. pub fn reset_steward_state(ctx: Context) -> Result<()> { instructions::reset_steward_state::handler(ctx) @@ -189,12 +198,15 @@ pub mod steward { instructions::close_steward_accounts::handler(ctx) } - /* Passthrough instructions to spl-stake-pool, where the signer is Staker. Must be invoked by `config.authority` */ + /* Passthrough instructions */ + /* passthrough to spl-stake-pool, where the signer is Staker. Must be invoked by `config.authority` */ + /// Passthrough spl-stake-pool: Set the staker for the pool pub fn set_staker(ctx: Context) -> Result<()> { instructions::spl_passthrough::set_staker_handler(ctx) } + /// Passthrough spl-stake-pool: Add a validator to the pool pub fn add_validator_to_pool( ctx: Context, validator_seed: Option, @@ -202,6 +214,7 @@ pub mod steward { instructions::spl_passthrough::add_validator_to_pool_handler(ctx, validator_seed) } + /// Passthrough spl-stake-pool: Remove a validator from the pool pub fn remove_validator_from_pool( ctx: Context, validator_list_index: u64, @@ -212,6 +225,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Set the preferred validator pub fn set_preferred_validator( ctx: Context, validator_type: PreferredValidatorType, @@ -224,6 +238,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Increase validator stake pub fn increase_validator_stake( ctx: Context, lamports: u64, @@ -236,6 +251,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Decrease validator stake pub fn decrease_validator_stake( ctx: Context, lamports: u64, @@ -248,6 +264,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Increase additional validator stake pub fn increase_additional_validator_stake( ctx: Context, lamports: u64, @@ -262,6 +279,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Decrease additional validator stake pub fn decrease_additional_validator_stake( ctx: Context, lamports: u64, diff --git a/programs/steward/src/score.rs b/programs/steward/src/score.rs index d581e68f..a82176de 100644 --- a/programs/steward/src/score.rs +++ b/programs/steward/src/score.rs @@ -164,7 +164,7 @@ pub fn validator_score( /* If epoch credits exist, we expect the validator to have a superminority flag set. If not, scoring fails and we wait for the stake oracle to call UpdateStakeHistory. - If epoch credits is not set, we iterate through last 10 epochs to find the latest superminority flag. + If epoch credits is not set, we iterate through last `commission_range` epochs to find the latest superminority flag. If no entry is found, we assume the validator is not a superminority validator. */ let is_superminority = if validator.history.epoch_credits_latest().is_some() { @@ -196,8 +196,7 @@ pub fn validator_score( /////// Blacklist /////// let blacklisted_score = if config .validator_history_blacklist - .get(validator.index as usize) - .unwrap_or(false) + .get(validator.index as usize)? { 0.0 } else { @@ -248,7 +247,7 @@ pub struct InstantUnstakeComponents { /// Checks if validator has increased MEV commission > mev_commission_bps_threshold pub mev_commission_check: bool, - /// Checks if validator was added to blacklist blacklisted + /// Checks if validator was added to blacklist pub is_blacklisted: bool, pub vote_account: Pubkey, diff --git a/programs/steward/src/state/parameters.rs b/programs/steward/src/state/parameters.rs index 1f287358..0c74d515 100644 --- a/programs/steward/src/state/parameters.rs +++ b/programs/steward/src/state/parameters.rs @@ -174,7 +174,7 @@ pub struct Parameters { /// Required so that the struct is 8-byte aligned /// https://doc.rust-lang.org/reference/type-layout.html#reprc-structs - pub padding0: [u8; 6], + pub _padding_0: [u8; 6], /////// Delegation parameters /////// /// Number of validators to delegate to @@ -207,6 +207,8 @@ pub struct Parameters { /// Minimum epochs voting required to be in the pool ValidatorList and eligible for delegation pub minimum_voting_epochs: u64, + + pub _padding_1: [u64; 32], } impl Parameters { diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index d52731fb..358018c2 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -22,13 +22,7 @@ use bytemuck::{Pod, Zeroable}; use spl_stake_pool::big_vec::BigVec; use validator_history::{ClusterHistory, ValidatorHistory}; -// Tests will fail here - comment out msg! to pass -fn invalid_state_error(_expected: String, _actual: String) -> Error { - // msg!("Invalid state. Expected {}, Actual {}", expected, actual); - StewardError::InvalidState.into() -} - -pub fn maybe_transition_and_emit( +pub fn maybe_transition( steward_state: &mut StewardState, clock: &Clock, params: &Parameters, @@ -700,10 +694,8 @@ impl StewardState { self.progress.set(index, true)?; return Ok(Some(score)); } - Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )) + + Err(StewardError::InvalidState.into()) } /// Given list of scores, finds top `num_delegation_validators` and assigns an equal share @@ -713,10 +705,7 @@ impl StewardState { pub fn compute_delegations(&mut self, current_epoch: u64, config: &Config) -> Result<()> { if matches!(self.state_tag, StewardStateEnum::ComputeDelegations) { if current_epoch >= self.next_cycle_epoch { - return Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )); + return Err(StewardError::InvalidState.into()); } let validators_to_delegate = select_validators_to_delegate( @@ -739,10 +728,7 @@ impl StewardState { return Ok(()); } - Err(invalid_state_error( - "ComputeDelegations".to_string(), - self.state_tag.to_string(), - )) + Err(StewardError::InvalidState.into()) } /// One instruction per validator. @@ -761,10 +747,7 @@ impl StewardState { ) -> Result> { if matches!(self.state_tag, StewardStateEnum::ComputeInstantUnstake) { if clock.epoch >= self.next_cycle_epoch { - return Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )); + return Err(StewardError::InvalidState.into()); } if epoch_progress(clock, epoch_schedule)? @@ -821,10 +804,7 @@ impl StewardState { self.progress.set(index, true)?; return Ok(Some(instant_unstake_result)); } - Err(invalid_state_error( - "ComputeInstantUnstake".to_string(), - self.state_tag.to_string(), - )) + Err(StewardError::InvalidState.into()) } /// One instruction per validator. @@ -849,10 +829,7 @@ impl StewardState { ) -> Result { if matches!(self.state_tag, StewardStateEnum::Rebalance) { if current_epoch >= self.next_cycle_epoch { - return Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )); + return Err(StewardError::InvalidState.into()); } // Skip if already processed @@ -1048,10 +1025,7 @@ impl StewardState { self.progress.set(index, true)?; return Ok(rebalance); } - Err(invalid_state_error( - "Rebalance".to_string(), - self.state_tag.to_string(), - )) + Err(StewardError::InvalidState.into()) } } diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index 1f5d01c8..a991caa7 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -8,8 +8,14 @@ use spl_stake_pool::{ state::{StakeStatus, ValidatorListHeader, ValidatorStakeInfo}, }; -use crate::{errors::StewardError, Config, Delegation, StewardStateAccount, StewardStateEnum}; +use crate::{ + constants::{STAKE_STATUS_OFFSET, U64_SIZE, VEC_SIZE_BYTES}, + errors::StewardError, + Config, Delegation, StewardStateAccount, StewardStateEnum, +}; +/// Checks called before any cranking state function. Note that expected_state is optional - +/// this is due to ComputeScores handling it's own state check. pub fn state_checks( clock: &Clock, config: &Config, @@ -50,6 +56,65 @@ pub fn state_checks( Ok(()) } +pub fn remove_validator_check( + clock: &Clock, + config: &Config, + state_account: &StewardStateAccount, + validator_list_account_info: &AccountInfo, +) -> Result<()> { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + // Ensure we have a 1-1 mapping between the number of validators + let validators_in_list = get_validator_list_length(validator_list_account_info)?; + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + == validators_in_list, + StewardError::ListStateMismatch + ); + + Ok(()) +} + +pub fn add_validator_check( + clock: &Clock, + config: &Config, + state_account: &StewardStateAccount, + validator_list_account_info: &AccountInfo, +) -> Result<()> { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + + // Ensure we have a 1-1 mapping between the number of validators + let validators_in_list = get_validator_list_length(validator_list_account_info)?; + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + == validators_in_list, + StewardError::ListStateMismatch + ); + + Ok(()) +} + pub fn get_stake_pool_address(account: &AccountLoader) -> Result { let config = account.load()?; Ok(config.stake_pool) @@ -95,8 +160,6 @@ pub fn get_target_lamports(delegation: &Delegation, stake_pool_lamports: u64) -> .ok_or_else(|| StewardError::ArithmeticError.into()) } -const VEC_SIZE_BYTES: usize = 4; - /// Utility to efficiently extract stake lamports and transient stake from a validator list. /// Frankenstein of spl_stake_pool::big_vec::BigVec::deserialize_slice /// and spl_stake_pool::state::ValidatorStakeInfo::active_lamports_greater_than @@ -108,11 +171,11 @@ pub fn stake_lamports_at_validator_list_index( let active_start_index = VEC_SIZE_BYTES.saturating_add(index.saturating_mul(ValidatorStakeInfo::LEN)); let active_end_index = active_start_index - .checked_add(8) + .checked_add(U64_SIZE) .ok_or(StewardError::ArithmeticError)?; let transient_start_index = active_end_index; let transient_end_index = transient_start_index - .checked_add(8) + .checked_add(U64_SIZE) .ok_or(StewardError::ArithmeticError)?; let slice = &validator_list.data[active_start_index..active_end_index]; let active_stake_lamport_pod = pod_from_bytes::(slice).unwrap(); @@ -143,7 +206,7 @@ pub fn get_validator_stake_info_at_index( pub fn check_validator_list_has_stake_status_other_than( validator_list_account_info: &AccountInfo, - flag: StakeStatus, + flags: &[StakeStatus], ) -> 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)?; @@ -155,12 +218,19 @@ pub fn check_validator_list_has_stake_status_other_than( for index in 0..validator_list.len() as usize { let stake_status_index = VEC_SIZE_BYTES .saturating_add(index.saturating_mul(ValidatorStakeInfo::LEN)) - .checked_add(40) + .checked_add(STAKE_STATUS_OFFSET) .ok_or(StewardError::ArithmeticError)?; let stake_status = validator_list.data[stake_status_index]; - if stake_status != flag as u8 { + let mut has_flag = false; + for flag in flags.iter() { + if stake_status == *flag as u8 { + has_flag = true; + } + } + + if !has_flag { return Ok(true); } } @@ -276,6 +346,8 @@ impl IdlBuild for PreferredValidatorType { } } +// Below are nice to haves for deserializing accounts but not strictly necessary for on-chain logic +// A good amount of this is copied from anchor #[derive(Clone)] pub struct StakePool(spl_stake_pool::state::StakePool); @@ -321,9 +393,6 @@ impl Deref for StakePool { } } -// #[cfg(feature = "idl-build")] -// impl anchor_lang::IdlBuild for StakePool {} - #[derive(Clone)] pub struct ValidatorList(spl_stake_pool::state::ValidatorList); diff --git a/run_tests.sh b/run_tests.sh index 4333e474..4a54f8e0 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -5,12 +5,12 @@ cargo build-sbf --manifest-path programs/steward/Cargo.toml; cargo build-sbf --manifest-path programs/validator-history/Cargo.toml; # Run all tests except the specified one -SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=5000000 cargo test -- --skip steward::test_state_methods +SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=8000000 cargo test -- --skip steward::test_state_methods # Check if the previous command succeeded if [ $? -eq 0 ]; then # Run the specific test in isolation - SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=5000000 cargo test --package tests --test mod steward::test_state_methods + SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=8000000 cargo test --package tests --test mod steward::test_state_methods else echo "Some tests failed, skipping the isolated test run." fi \ No newline at end of file diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index bea989ea..65d53e61 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -300,6 +300,18 @@ impl TestFixture { self.submit_transaction_assert_success(transaction).await; } + pub async fn get_latest_blockhash(&self) -> Hash { + let blockhash = { + let mut banks_client = self.ctx.borrow_mut().banks_client.clone(); + banks_client + .get_new_latest_blockhash(&Hash::default()) + .await + .unwrap() + }; + + blockhash + } + pub async fn realloc_steward_state(&self) { // Realloc validator history account let mut num_reallocs = (StewardStateAccount::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; @@ -418,6 +430,7 @@ impl TestFixture { validator_history.history.push(ValidatorHistoryEntry { epoch: i, epoch_credits: 400000, + activated_stake_lamports: 100_000_000_000_000, ..ValidatorHistoryEntry::default() }); } @@ -569,18 +582,6 @@ impl TestFixture { panic!("Error: Transaction succeeded. Expected {}", error_message); } } - - pub async fn get_latest_blockhash(&self) -> Hash { - let blockhash = { - let mut banks_client = self.ctx.borrow_mut().banks_client.clone(); - banks_client - .get_new_latest_blockhash(&Hash::default()) - .await - .unwrap() - }; - - blockhash - } } pub fn validator_history_config_account(bump: u8, num_validators: u32) -> Account { @@ -838,7 +839,7 @@ impl Default for StateMachineFixtures { instant_unstake_delinquency_threshold_ratio: 0.1, commission_threshold: 10, historical_commission_threshold: 10, - padding0: [0; 6], + _padding_0: [0; 6], num_delegation_validators: 3, scoring_unstake_cap_bps: 1000, instant_unstake_cap_bps: 1000, @@ -849,6 +850,7 @@ impl Default for StateMachineFixtures { num_epochs_between_scoring: 10, minimum_stake_lamports: 1, minimum_voting_epochs: 1, + _padding_1: [0; 32], }; // Setup Config diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 02bd60aa..94a1545e 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -842,7 +842,7 @@ fn test_increase_stake_calculation() { 0, ); assert!(match result { - Err(e) => e == StewardError::InvalidState.into(), + Err(e) => e == StewardError::ValidatorIndexOutOfBounds.into(), _ => false, }); diff --git a/tests/tests/steward/test_integration.rs b/tests/tests/steward/test_integration.rs index f43bcbdf..469f114d 100644 --- a/tests/tests/steward/test_integration.rs +++ b/tests/tests/steward/test_integration.rs @@ -34,7 +34,6 @@ use validator_history::{ #[tokio::test] async fn test_compute_delegations() { let fixture = TestFixture::new().await; - let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; fixture.initialize_steward(None).await; fixture.realloc_steward_state().await; @@ -115,7 +114,7 @@ async fn test_compute_delegations() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -162,7 +161,7 @@ async fn test_compute_delegations() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture .submit_transaction_assert_error(tx, "StateMachinePaused") @@ -174,7 +173,6 @@ async fn test_compute_delegations() { #[tokio::test] async fn test_compute_scores() { let fixture = TestFixture::new().await; - let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; fixture.initialize_steward(None).await; fixture.realloc_steward_state().await; @@ -201,6 +199,7 @@ async fn test_compute_scores() { validator_history.history.push(ValidatorHistoryEntry { epoch: i, epoch_credits: 1000, + activated_stake_lamports: 100_000_000_000_000, commission: 0, mev_commission: 0, is_superminority: 0, @@ -332,7 +331,7 @@ async fn test_compute_scores() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -466,7 +465,6 @@ async fn test_compute_scores() { #[tokio::test] async fn test_compute_instant_unstake() { let fixture = TestFixture::new().await; - let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; fixture .initialize_steward(Some(UpdateParametersArgs { @@ -518,6 +516,7 @@ async fn test_compute_instant_unstake() { validator_history.history.push(ValidatorHistoryEntry { epoch: clock.epoch as u16, epoch_credits: 1000, + activated_stake_lamports: 100_000_000_000_000, commission: 100, // This is the condition causing instant unstake mev_commission: 0, is_superminority: 0, @@ -635,7 +634,7 @@ async fn test_compute_instant_unstake() { &[compute_instant_unstake_ix.clone()], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); let test_state_account: StewardStateAccount = @@ -766,7 +765,7 @@ async fn test_idle() { &[idle_ix.clone()], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1029,7 +1028,7 @@ async fn test_rebalance_increase() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1055,7 +1054,7 @@ async fn test_rebalance_increase() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1249,7 +1248,7 @@ async fn test_rebalance_decrease() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1364,7 +1363,7 @@ async fn test_rebalance_decrease() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1417,7 +1416,6 @@ async fn test_rebalance_other_cases() { let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - steward_config.set_paused(true); steward_config.parameters.minimum_voting_epochs = 1; let vote_account = Pubkey::new_unique(); @@ -1449,6 +1447,28 @@ async fn test_rebalance_other_cases() { }); } + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let clock: Clock = fixture.get_sysvar().await; + + steward_state_account.state.current_epoch = clock.epoch; + steward_state_account.state.num_pool_validators = MAX_VALIDATORS as u64 - 1; + steward_state_account.state.state_tag = StewardStateEnum::Rebalance; + + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + + ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account( + spl_validator_list.clone(), + Some(validator_list_account_info.data.len()), + ) + .into(), + ); + ctx.borrow_mut().set_account( &fixture.stake_pool_meta.validator_list, &serialized_validator_list_account( @@ -1498,7 +1518,7 @@ async fn test_rebalance_other_cases() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1544,8 +1564,19 @@ async fn test_rebalance_other_cases() { &[rebalance_ix.clone()], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, + ); + + let mut steward_config: Config = fixture + .load_and_deserialize(&fixture.steward_config.pubkey()) + .await; + steward_config.set_paused(true); + + ctx.borrow_mut().set_account( + &fixture.steward_config.pubkey(), + &serialized_config(steward_config).into(), ); + fixture .submit_transaction_assert_error(tx, "StateMachinePaused") .await; diff --git a/tests/tests/steward/test_parameters.rs b/tests/tests/steward/test_parameters.rs index 6665ab35..36c7a08e 100644 --- a/tests/tests/steward/test_parameters.rs +++ b/tests/tests/steward/test_parameters.rs @@ -242,7 +242,8 @@ fn _test_parameter( num_epochs_between_scoring: 10, minimum_stake_lamports: 5_000_000_000_000, minimum_voting_epochs: 5, - padding0: [0; 6], + _padding_0: [0; 6], + _padding_1: [0; 32], }); // First Valid Epoch diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 60a941fa..73c30e81 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -9,8 +9,8 @@ use jito_steward::{ use solana_program_test::*; use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; use tests::steward_fixtures::{ - closed_vote_account, new_vote_account, serialized_steward_state_account, - serialized_validator_history_account, system_account, validator_history_default, TestFixture, + closed_vote_account, new_vote_account, serialized_validator_history_account, system_account, + validator_history_default, TestFixture, }; use validator_history::{ValidatorHistory, ValidatorHistoryEntry}; @@ -78,6 +78,7 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke for i in 0..20 { validator_history.history.push(ValidatorHistoryEntry { epoch: i, + activated_stake_lamports: 100_000_000_000_000, epoch_credits: 400000, vote_account_last_update_slot: 100, ..ValidatorHistoryEntry::default() @@ -166,19 +167,6 @@ async fn test_auto_remove() { .data(), }; - let mut steward_state_account: StewardStateAccount = - fixture.load_and_deserialize(&fixture.steward_state).await; - - // Fake add vote account to state - steward_state_account.state.num_pool_validators = 1; - steward_state_account.state.sorted_score_indices[0] = 0; - steward_state_account.state.sorted_yield_score_indices[0] = 0; - - fixture.ctx.borrow_mut().set_account( - &fixture.steward_state, - &serialized_steward_state_account(steward_state_account).into(), - ); - let tx = Transaction::new_signed_with_payer( &[auto_remove_validator_ix], Some(&fixture.keypair.pubkey()), From f83602dda1fa211ecf908eaf597d0ceae479d84d Mon Sep 17 00:00:00 2001 From: Evan B Date: Sun, 28 Jul 2024 22:19:46 -0400 Subject: [PATCH 13/18] Missing emits and final program address (#65) --- Anchor.toml | 2 +- Cargo.lock | 8 ++++---- programs/steward/Cargo.toml | 2 ++ programs/steward/idl/steward.json | 2 +- programs/steward/src/instructions/compute_score.rs | 6 ++++-- programs/steward/src/instructions/idle.rs | 6 ++++-- programs/steward/src/lib.rs | 2 +- programs/validator-history/Cargo.toml | 2 ++ 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index 970e14b9..c99a48ce 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -3,7 +3,7 @@ resolution = false skip-lint = false [programs.localnet] validator_history = "HistoryJTGbKQD2mRgLZ3XhqHnN811Qpez8X9kCcGHoa" -steward = "sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP" +steward = "Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8" [workspace] members = [ diff --git a/Cargo.lock b/Cargo.lock index 09dcba52..1e4b97bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6445,9 +6445,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -6466,9 +6466,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", diff --git a/programs/steward/Cargo.toml b/programs/steward/Cargo.toml index 874bfcfe..6032572c 100644 --- a/programs/steward/Cargo.toml +++ b/programs/steward/Cargo.toml @@ -14,6 +14,8 @@ name = "jito_steward" no-entrypoint = [] no-idl = [] no-log-ix-name = [] +mainnet-beta = [] +testnet = [] cpi = ["no-entrypoint"] default = ["custom-heap"] custom-heap = [] diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 754c2d03..4333de0b 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -1,5 +1,5 @@ { - "address": "sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP", + "address": "Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8", "metadata": { "name": "steward", "version": "0.1.0", diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index a2ef5b3d..580029e9 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -61,12 +61,14 @@ pub fn handler(ctx: Context, validator_list_index: usize) -> Resul state_account.state.state_tag, StewardStateEnum::ComputeScores ) { - maybe_transition( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } } require!( diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index 0dc5265f..4afc6585 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -39,12 +39,14 @@ pub fn handler(ctx: Context) -> Result<()> { Some(StewardStateEnum::Idle), )?; - maybe_transition( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 7c600806..67b9ae69 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -17,7 +17,7 @@ pub mod utils; pub use state::*; -declare_id!("sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP"); +declare_id!("Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8"); /* This program manages the selection of validators and delegation of stake for a SPL Stake Pool. diff --git a/programs/validator-history/Cargo.toml b/programs/validator-history/Cargo.toml index 6ed95c8c..377f3cbb 100644 --- a/programs/validator-history/Cargo.toml +++ b/programs/validator-history/Cargo.toml @@ -17,6 +17,8 @@ name = "validator_history" no-entrypoint = [] no-idl = [] no-log-ix-name = [] +mainnet-beta = [] +testnet = [] cpi = ["no-entrypoint"] default = ["custom-heap"] custom-heap = [] From ad711cea7246c25e417a785d40219b4f847b75ca Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:20:04 -0600 Subject: [PATCH 14/18] PATCH: Carry over epoch maintenance flag (#64) Co-authored-by: Christian Krueger --- programs/steward/src/state/steward_state.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 358018c2..cc09a0d7 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -449,7 +449,12 @@ impl StewardState { self.stake_deposit_unstake_total = 0; self.delegations = [Delegation::default(); MAX_VALIDATORS]; self.instant_unstake = BitMask::default(); + + let has_epoch_maintenance = self.has_flag(EPOCH_MAINTENANCE); self.clear_flags(); + if has_epoch_maintenance { + self.set_flag(EPOCH_MAINTENANCE); + } Ok(()) } From 2021ba5abaf18c7a7a8a0b42366a93d99586dcb9 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Sun, 28 Jul 2024 22:58:16 -0400 Subject: [PATCH 15/18] Fix build and steward verified build --- .github/workflows/build.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4e118e11..9468c272 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -76,7 +76,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: anchor-cli - version: "0.30.0" + version: "0.30.1" - name: install solana toolsuite run: sh -c "$(curl -sSfL https://release.solana.com/v1.18.11/install)" - name: add to path @@ -90,6 +90,8 @@ jobs: # run verified build - run: solana-verify build --library-name validator_history + - run: solana-verify build --library-name jito_steward -- --features mainnet-beta + # upload the IDL and verified build - name: Upload validator_history.so uses: actions/upload-artifact@v4 From 30ae14b2cc8aa01f9a763ece8ff5537d5bf8a02d Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Mon, 29 Jul 2024 17:18:05 -0400 Subject: [PATCH 16/18] Fix indices and checks for deactivating validators --- .../auto_remove_validator_from_pool.rs | 55 +++++++++++++++++-- programs/steward/src/state/steward_state.rs | 8 +-- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs index ee62daa8..84e01606 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -10,7 +10,7 @@ use crate::utils::{ }; use crate::StewardStateAccount; use anchor_lang::prelude::*; -use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; +use anchor_lang::solana_program::{clock::Epoch, 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; @@ -124,13 +124,13 @@ pub struct AutoRemoveValidator<'info> { pub fn handler(ctx: Context, validator_list_index: usize) -> Result<()> { let stake_account_deactivated; let vote_account_closed; + let clock = Clock::get()?; + let epoch = clock.epoch; { let config = ctx.accounts.config.load()?; let state_account = ctx.accounts.state_account.load()?; let validator_list = &ctx.accounts.validator_list; - let clock = Clock::get()?; - let epoch = clock.epoch; remove_validator_check(&clock, &config, &state_account, validator_list)?; @@ -210,12 +210,39 @@ pub fn handler(ctx: Context, validator_list_index: usize) - let stake_status = StakeStatus::try_from(validator_stake_info.status)?; let marked_for_immediate_removal: bool; + 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)?; + marked_for_immediate_removal = true; + } else { + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + marked_for_immediate_removal = false; + } + } + StakeStatus::ReadyForRemoval => { marked_for_immediate_removal = true; state_account .state @@ -240,3 +267,23 @@ 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/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index cc09a0d7..bead7169 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -77,14 +77,14 @@ pub struct StewardState { /// Tracks progress of states that require one instruction per validator pub progress: BitMask, - /// Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool - /// This is cleaned up in the next epoch - pub validators_to_remove: BitMask, - /// Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool /// This happens when a validator is able to be removed within the same epoch as it was marked pub validators_for_immediate_removal: BitMask, + /// Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool + /// This is cleaned up in the next epoch + pub validators_to_remove: BitMask, + ////// Cycle metadata fields ////// /// Slot of the first ComputeScores instruction in the current cycle pub start_computing_scores_slot: u64, From 173476e7834a36d28a9c7d5afdff87cf67ebdb5c Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:11:17 -0600 Subject: [PATCH 17/18] HOTFIX: Last IDL added (#66) Co-authored-by: Christian Krueger --- programs/steward/idl/steward.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 4333de0b..8fce5c22 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -2771,10 +2771,10 @@ } }, { - "name": "validators_to_remove", + "name": "validators_for_immediate_removal", "docs": [ - "Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool", - "This is cleaned up in the next epoch" + "Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool", + "This happens when a validator is able to be removed within the same epoch as it was marked" ], "type": { "defined": { @@ -2783,10 +2783,10 @@ } }, { - "name": "validators_for_immediate_removal", + "name": "validators_to_remove", "docs": [ - "Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool", - "This happens when a validator is able to be removed within the same epoch as it was marked" + "Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool", + "This is cleaned up in the next epoch" ], "type": { "defined": { From 1428eeae4333997568c73cdd77d43c4e7c692401 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 31 Jul 2024 10:54:07 -0400 Subject: [PATCH 18/18] Fix clearing marked for removal values --- programs/steward/src/state/steward_state.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index bead7169..9ab234ac 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -546,12 +546,9 @@ impl StewardState { self.delegations[num_pool_validators] = Delegation::default(); self.instant_unstake.set(num_pool_validators, false)?; self.progress.set(num_pool_validators, false)?; - - if marked_for_regular_removal { - self.validators_to_remove.set(index, false)?; - } else { - self.validators_for_immediate_removal.set(index, false)?; - } + self.validators_to_remove.set(num_pool_validators, false)?; + self.validators_for_immediate_removal + .set(num_pool_validators, false)?; Ok(()) }