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 += "---------------------";