Skip to content

Commit

Permalink
FEATURE: Mark delinquent validators for instant removal (#57)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
coachchucksol and Christian Krueger authored Jul 23, 2024
1 parent be2fa23 commit 0425db0
Show file tree
Hide file tree
Showing 21 changed files with 475 additions and 153 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 79 additions & 6 deletions programs/steward/idl/steward.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -1736,6 +1793,10 @@
{
"name": "stake_account_deactivated",
"type": "bool"
},
{
"name": "marked_for_immediate_removal",
"type": "bool"
}
]
}
Expand Down Expand Up @@ -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": [
Expand Down
8 changes: 8 additions & 0 deletions programs/steward/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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")]
Expand Down
1 change: 1 addition & 0 deletions programs/steward/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
15 changes: 11 additions & 4 deletions programs/steward/src/instructions/auto_add_validator_to_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,17 @@ pub fn handler(ctx: Context<AutoAddValidator>) -> 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()?;
Expand Down
111 changes: 72 additions & 39 deletions programs/steward/src/instructions/auto_remove_validator_from_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -124,8 +125,11 @@ pub struct AutoRemoveValidator<'info> {
*/
pub fn handler(ctx: Context<AutoRemoveValidator>, 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;

Expand All @@ -143,7 +147,7 @@ pub fn handler(ctx: Context<AutoRemoveValidator>, 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::<StakeStateV2>(stake_account_data)?;
Expand All @@ -158,57 +162,86 @@ pub fn handler(ctx: Context<AutoRemoveValidator>, 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(())
}
29 changes: 22 additions & 7 deletions programs/steward/src/instructions/compute_delegations.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -25,13 +25,28 @@ pub fn handler(ctx: Context<ComputeDelegations>) -> 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
Expand Down
Loading

0 comments on commit 0425db0

Please sign in to comment.