diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index d956cfc..7db6423 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -1620,6 +1620,19 @@ 77 ] }, + { + "name": "InstantUnstakeComponentsV2", + "discriminator": [ + 138, + 62, + 181, + 14, + 9, + 48, + 109, + 47 + ] + }, { "name": "RebalanceEvent", "discriminator": [ @@ -1646,6 +1659,19 @@ 251 ] }, + { + "name": "ScoreComponentsV2", + "discriminator": [ + 140, + 182, + 108, + 49, + 246, + 38, + 247, + 181 + ] + }, { "name": "StateTransition", "discriminator": [ @@ -2315,6 +2341,47 @@ }, { "name": "InstantUnstakeComponents", + "docs": [ + "Deprecated: This struct is no longer emitted but is kept to allow parsing of old events.", + "Because the event discriminator is based on struct name, it's important to rename the struct if", + "fields are changed." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "instant_unstake", + "type": "bool" + }, + { + "name": "delinquency_check", + "type": "bool" + }, + { + "name": "commission_check", + "type": "bool" + }, + { + "name": "mev_commission_check", + "type": "bool" + }, + { + "name": "is_blacklisted", + "type": "bool" + }, + { + "name": "vote_account", + "type": "pubkey" + }, + { + "name": "epoch", + "type": "u16" + } + ] + } + }, + { + "name": "InstantUnstakeComponentsV2", "type": { "kind": "struct", "fields": [ @@ -2360,6 +2427,67 @@ { "name": "epoch", "type": "u16" + }, + { + "name": "details", + "docs": [ + "Details about why a given check was calculated" + ], + "type": { + "defined": { + "name": "InstantUnstakeDetails" + } + } + } + ] + } + }, + { + "name": "InstantUnstakeDetails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch_credits_latest", + "docs": [ + "Latest epoch credits" + ], + "type": "u64" + }, + { + "name": "vote_account_last_update_slot", + "docs": [ + "Latest vote account update slot" + ], + "type": "u64" + }, + { + "name": "total_blocks_latest", + "docs": [ + "Latest total blocks" + ], + "type": "u32" + }, + { + "name": "cluster_history_slot_index", + "docs": [ + "Cluster history slot index" + ], + "type": "u64" + }, + { + "name": "commission", + "docs": [ + "Commission value" + ], + "type": "u8" + }, + { + "name": "mev_commission", + "docs": [ + "MEV commission value" + ], + "type": "u16" } ] } @@ -2617,6 +2745,67 @@ }, { "name": "ScoreComponents", + "docs": [ + "Deprecated: This struct is no longer emitted but is kept to allow parsing of old events.", + "Because the event discriminator is based on struct name, it's important to rename the struct if", + "fields are changed." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "score", + "type": "f64" + }, + { + "name": "yield_score", + "type": "f64" + }, + { + "name": "mev_commission_score", + "type": "f64" + }, + { + "name": "blacklisted_score", + "type": "f64" + }, + { + "name": "superminority_score", + "type": "f64" + }, + { + "name": "delinquency_score", + "type": "f64" + }, + { + "name": "running_jito_score", + "type": "f64" + }, + { + "name": "commission_score", + "type": "f64" + }, + { + "name": "historical_commission_score", + "type": "f64" + }, + { + "name": "vote_credits_ratio", + "type": "f64" + }, + { + "name": "vote_account", + "type": "pubkey" + }, + { + "name": "epoch", + "type": "u16" + } + ] + } + }, + { + "name": "ScoreComponentsV2", "type": { "kind": "struct", "fields": [ @@ -2698,6 +2887,88 @@ { "name": "epoch", "type": "u16" + }, + { + "name": "details", + "docs": [ + "Details about why a given score was calculated" + ], + "type": { + "defined": { + "name": "ScoreDetails" + } + } + } + ] + } + }, + { + "name": "ScoreDetails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_mev_commission", + "docs": [ + "Max MEV commission observed" + ], + "type": "u16" + }, + { + "name": "max_mev_commission_epoch", + "docs": [ + "Epoch of max MEV commission" + ], + "type": "u16" + }, + { + "name": "superminority_epoch", + "docs": [ + "Epoch when superminority was detected" + ], + "type": "u16" + }, + { + "name": "delinquency_ratio", + "docs": [ + "Ratio that failed delinquency check" + ], + "type": "f64" + }, + { + "name": "delinquency_epoch", + "docs": [ + "Epoch when delinquency was detected" + ], + "type": "u16" + }, + { + "name": "max_commission", + "docs": [ + "Max commission observed" + ], + "type": "u8" + }, + { + "name": "max_commission_epoch", + "docs": [ + "Epoch of max commission" + ], + "type": "u16" + }, + { + "name": "max_historical_commission", + "docs": [ + "Max historical commission observed" + ], + "type": "u8" + }, + { + "name": "max_historical_commission_epoch", + "docs": [ + "Epoch of max historical commission" + ], + "type": "u16" } ] } diff --git a/programs/steward/src/constants.rs b/programs/steward/src/constants.rs index 05b91b7..2b2b391 100644 --- a/programs/steward/src/constants.rs +++ b/programs/steward/src/constants.rs @@ -9,6 +9,7 @@ pub const BASIS_POINTS_MAX: u16 = 10_000; pub const COMMISSION_MAX: u8 = 100; pub const SORTED_INDEX_DEFAULT: u16 = u16::MAX; pub const LAMPORT_BALANCE_DEFAULT: u64 = u64::MAX; +pub const EPOCH_DEFAULT: u16 = u16::MAX; // Need at least 1% of slots remaining (4320 slots) to execute steps in state machine pub const EPOCH_PROGRESS_MAX: f64 = 0.99; // Cannot go more than 100 epochs without scoring diff --git a/programs/steward/src/events.rs b/programs/steward/src/events.rs index d85683d..df5af67 100644 --- a/programs/steward/src/events.rs +++ b/programs/steward/src/events.rs @@ -97,3 +97,38 @@ impl IdlBuild for RebalanceTypeTag { }) } } + +/// Deprecated: This struct is no longer emitted but is kept to allow parsing of old events. +/// Because the event discriminator is based on struct name, it's important to rename the struct if +/// fields are changed. +#[event] +#[derive(Debug, PartialEq)] +pub struct ScoreComponents { + pub score: f64, + pub yield_score: f64, + pub mev_commission_score: f64, + pub blacklisted_score: f64, + pub superminority_score: f64, + pub delinquency_score: f64, + pub running_jito_score: f64, + pub commission_score: f64, + pub historical_commission_score: f64, + pub vote_credits_ratio: f64, + pub vote_account: Pubkey, + pub epoch: u16, +} + +/// Deprecated: This struct is no longer emitted but is kept to allow parsing of old events. +/// Because the event discriminator is based on struct name, it's important to rename the struct if +/// fields are changed. +#[event] +#[derive(Debug, PartialEq, Eq)] +pub struct InstantUnstakeComponents { + pub instant_unstake: bool, + pub delinquency_check: bool, + pub commission_check: bool, + pub mev_commission_check: bool, + pub is_blacklisted: bool, + pub vote_account: Pubkey, + pub epoch: u16, +} diff --git a/programs/steward/src/score.rs b/programs/steward/src/score.rs index a82176d..aec2f94 100644 --- a/programs/steward/src/score.rs +++ b/programs/steward/src/score.rs @@ -5,14 +5,16 @@ use anchor_lang::{ use validator_history::{ClusterHistory, ValidatorHistory}; use crate::{ - constants::{BASIS_POINTS_MAX, COMMISSION_MAX, VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH}, + constants::{ + BASIS_POINTS_MAX, COMMISSION_MAX, EPOCH_DEFAULT, VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH, + }, errors::StewardError::{self, ArithmeticError}, Config, }; #[event] #[derive(Debug, PartialEq)] -pub struct ScoreComponents { +pub struct ScoreComponentsV2 { /// Product of all scoring components pub score: f64, @@ -47,6 +49,39 @@ pub struct ScoreComponents { pub vote_account: Pubkey, pub epoch: u16, + + /// Details about why a given score was calculated + pub details: ScoreDetails, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, PartialEq)] +pub struct ScoreDetails { + /// Max MEV commission observed + pub max_mev_commission: u16, + + /// Epoch of max MEV commission + pub max_mev_commission_epoch: u16, + + /// Epoch when superminority was detected + pub superminority_epoch: u16, + + /// Ratio that failed delinquency check + pub delinquency_ratio: f64, + + /// Epoch when delinquency was detected + pub delinquency_epoch: u16, + + /// Max commission observed + pub max_commission: u8, + + /// Epoch of max commission + pub max_commission_epoch: u16, + + /// Max historical commission observed + pub max_historical_commission: u8, + + /// Epoch of max historical commission + pub max_historical_commission_epoch: u16, } pub fn validator_score( @@ -54,187 +89,322 @@ pub fn validator_score( cluster: &ClusterHistory, config: &Config, current_epoch: u16, -) -> Result { +) -> Result { let params = &config.parameters; - /////// MEV Commission /////// + /////// Shared windows /////// let mev_commission_window = validator.history.mev_commission_range( current_epoch .checked_sub(params.mev_commission_range) .ok_or(ArithmeticError)?, current_epoch, ); - let max_mev_commission = mev_commission_window - .iter() - .filter_map(|&i| i) - .max() - .unwrap_or(BASIS_POINTS_MAX); - let mev_commission_score: f64 = if max_mev_commission <= params.mev_commission_bps_threshold { - 1.0 - } else { - 0.0 - }; - - /////// Running Jito /////// - let running_jito = mev_commission_window.iter().any(|i| i.is_some()); - let running_jito_score: f64 = if running_jito { 1.0 } else { 0.0 }; - - /////// Vote Credits Ratio, Delinquency /////// - - // Epoch credits should not include current epoch because it is in progress and data would be incomplete let epoch_credits_start = current_epoch .checked_sub(params.epoch_credits_range) .ok_or(ArithmeticError)?; + // Epoch credits should not include current epoch because it is in progress and data would be incomplete let epoch_credits_end = current_epoch.checked_sub(1).ok_or(ArithmeticError)?; let epoch_credits_window = validator .history .epoch_credits_range(epoch_credits_start, epoch_credits_end); - let average_vote_credits = epoch_credits_window.iter().filter_map(|&i| i).sum::() as f64 - / epoch_credits_window.len() as f64; - let total_blocks_window = cluster .history .total_blocks_range(epoch_credits_start, epoch_credits_end); + let commission_window = validator.history.commission_range( + current_epoch + .checked_sub(params.commission_range) + .ok_or(ArithmeticError)?, + current_epoch, + ); + + /////// Component calculations /////// + let (mev_commission_score, max_mev_commission, max_mev_commission_epoch, running_jito_score) = + calculate_mev_commission( + &mev_commission_window, + current_epoch, + params.mev_commission_bps_threshold, + )?; + + let (vote_credits_ratio, delinquency_score, delinquency_ratio, delinquency_epoch) = + calculate_epoch_credits( + &epoch_credits_window, + &total_blocks_window, + epoch_credits_start, + params.scoring_delinquency_threshold_ratio, + )?; + + let (commission_score, max_commission, max_commission_epoch) = calculate_commission( + &commission_window, + current_epoch, + params.commission_threshold, + )?; + + let (historical_commission_score, max_historical_commission, max_historical_commission_epoch) = + calculate_historical_commission( + validator, + current_epoch, + params.historical_commission_threshold, + )?; + + let (superminority_score, superminority_epoch) = + calculate_superminority(validator, current_epoch, params.commission_range)?; + + let blacklisted_score = calculate_blacklist(config, validator.index)?; + + /////// Formula /////// + + let yield_score = vote_credits_ratio * (1. - max_commission as f64 / COMMISSION_MAX as f64); + + let score = mev_commission_score + * commission_score + * historical_commission_score + * blacklisted_score + * superminority_score + * delinquency_score + * running_jito_score + * yield_score; + + Ok(ScoreComponentsV2 { + score, + yield_score, + mev_commission_score, + blacklisted_score, + superminority_score, + delinquency_score, + running_jito_score, + commission_score, + historical_commission_score, + vote_credits_ratio, + vote_account: validator.vote_account, + epoch: current_epoch, + details: ScoreDetails { + max_mev_commission, + max_mev_commission_epoch, + superminority_epoch, + delinquency_ratio, + delinquency_epoch, + max_commission, + max_commission_epoch, + max_historical_commission, + max_historical_commission_epoch, + }, + }) +} + +/// Finds max MEV commission in the last `mev_commission_range` epochs and determines if it is above a threshold. +/// Also determines if validator has had a MEV commission in the last 10 epochs to ensure they are running jito-solana +fn calculate_mev_commission( + mev_commission_window: &[Option], + current_epoch: u16, + mev_commission_bps_threshold: u16, +) -> Result<(f64, u16, u16, f64)> { + let (max_mev_commission, max_mev_commission_epoch) = mev_commission_window + .iter() + .rev() + .enumerate() + .filter_map(|(i, &commission)| commission.map(|c| (c, current_epoch.checked_sub(i as u16)))) + .max_by_key(|&(commission, _)| commission) + .unwrap_or((BASIS_POINTS_MAX, Some(current_epoch))); + + let max_mev_commission_epoch = max_mev_commission_epoch.ok_or(StewardError::ArithmeticError)?; + + let mev_commission_score = if max_mev_commission <= mev_commission_bps_threshold { + 1.0 + } else { + 0.0 + }; + + /////// Running Jito /////// + let running_jito_score = if mev_commission_window.iter().any(|i| i.is_some()) { + 1.0 + } else { + 0.0 + }; + + Ok(( + mev_commission_score, + max_mev_commission, + max_mev_commission_epoch, + running_jito_score, + )) +} + +/// Calculates the vote credits ratio and delinquency score for the validator +fn calculate_epoch_credits( + epoch_credits_window: &[Option], + total_blocks_window: &[Option], + epoch_credits_start: u16, + scoring_delinquency_threshold_ratio: f64, +) -> Result<(f64, f64, f64, u16)> { + let average_vote_credits = epoch_credits_window.iter().filter_map(|&i| i).sum::() as f64 + / epoch_credits_window.len() as f64; + // Get average of total blocks in window, ignoring values where upload was missed let average_blocks = total_blocks_window.iter().filter_map(|&i| i).sum::() as f64 / total_blocks_window.iter().filter(|i| i.is_some()).count() as f64; - // Delinquency heuristic - let excessive_delinquency_threshold = epoch_credits_window + // Delinquency heuristic - not actual delinquency + let mut delinquency_score = 1.0; + let mut delinquency_ratio = 1.0; + let mut delinquency_epoch = EPOCH_DEFAULT; + + for (i, (maybe_credits, maybe_blocks)) in epoch_credits_window .iter() .zip(total_blocks_window.iter()) - .any(|(maybe_credits, maybe_blocks)| { + .enumerate() + { + if let Some(blocks) = maybe_blocks { // If vote credits are None, then validator was not active because we retroactively fill credits for last 64 epochs. // If total blocks are None, then keeper missed an upload and validator should not be punished. - maybe_blocks.map_or(false, |total_blocks| { - (maybe_credits.unwrap_or(0) as f64 / total_blocks as f64) - < params.scoring_delinquency_threshold_ratio - }) - }); + let credits = maybe_credits.unwrap_or(0); + let ratio = credits as f64 / *blocks as f64; + if ratio < scoring_delinquency_threshold_ratio { + delinquency_score = 0.0; + delinquency_ratio = ratio; + delinquency_epoch = epoch_credits_start + .checked_add(i as u16) + .ok_or(StewardError::ArithmeticError)?; + break; + } + } + } - let delinquency_score: f64 = if !excessive_delinquency_threshold { - 1.0 - } else { - 0.0 - }; + Ok(( + average_vote_credits / average_blocks, + delinquency_score, + delinquency_ratio, + delinquency_epoch, + )) +} +/// Finds max commission in the last `commission_range` epochs +fn calculate_commission( + commission_window: &[Option], + current_epoch: u16, + commission_threshold: u8, +) -> Result<(f64, u8, u16)> { /////// Commission /////// - - let commission_window = validator.history.commission_range( - current_epoch - .checked_sub(params.commission_range) - .ok_or(ArithmeticError)?, - current_epoch, - ); - let commission_u8 = commission_window + let (max_commission, max_commission_epoch) = commission_window .iter() - .filter_map(|&i| i) - .max() - .unwrap_or(0); + .rev() + .enumerate() + .filter_map(|(i, &commission)| commission.map(|c| (c, current_epoch.checked_sub(i as u16)))) + .max_by_key(|&(commission, _)| commission) + .unwrap_or((0, Some(current_epoch))); - let commission_score = if commission_u8 <= params.commission_threshold { + let max_commission_epoch = max_commission_epoch.ok_or(StewardError::ArithmeticError)?; + + let commission_score = if max_commission <= commission_threshold { 1.0 } else { 0.0 }; - let commission = commission_u8 as f64 / COMMISSION_MAX as f64; - /////// Historical Commission /////// + Ok((commission_score, max_commission, max_commission_epoch)) +} - let historical_commission_max = validator +/// Checks if validator has commission above a threshold in any epoch in their history +fn calculate_historical_commission( + validator: &ValidatorHistory, + current_epoch: u16, + historical_commission_threshold: u8, +) -> Result<(f64, u8, u16)> { + let (max_historical_commission, max_historical_commission_epoch) = validator .history .commission_range(VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH as u16, current_epoch) .iter() - .filter_map(|&i| i) - .max() - .unwrap_or(0); + .rev() + .enumerate() + .filter_map(|(i, &commission)| commission.map(|c| (c, current_epoch.checked_sub(i as u16)))) + .max_by_key(|&(commission, _)| commission) + .unwrap_or((0, Some(VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH as u16))); + + let max_historical_commission_epoch = + max_historical_commission_epoch.ok_or(StewardError::ArithmeticError)?; - let historical_commission_score: f64 = - if historical_commission_max <= params.historical_commission_threshold { + let historical_commission_score = + if max_historical_commission <= historical_commission_threshold { 1.0 } else { 0.0 }; - /////// Superminority /////// + Ok(( + historical_commission_score, + max_historical_commission, + max_historical_commission_epoch, + )) +} + +/// Checks if validator is in the top 1/3 of validators by stake for the current epoch +fn calculate_superminority( + validator: &ValidatorHistory, + current_epoch: u16, + commission_range: u16, +) -> Result<(f64, u16)> { /* 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 `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() { + if validator.history.epoch_credits_latest().is_some() { if let Some(superminority) = validator.history.superminority_latest() { - superminority == 1 + if superminority == 1 { + Ok((0.0, current_epoch)) + } else { + Ok((1.0, EPOCH_DEFAULT)) + } } else { - return Err(StewardError::StakeHistoryNotRecentEnough.into()); + Err(StewardError::StakeHistoryNotRecentEnough.into()) } } else { let superminority_window = validator.history.superminority_range( current_epoch - .checked_sub(params.commission_range) + .checked_sub(commission_range) .ok_or(ArithmeticError)?, current_epoch, ); - let status = superminority_window + let (status, epoch) = superminority_window .iter() + .enumerate() .rev() - .filter_map(|&i| i) + .filter_map(|(i, &superminority)| { + superminority.map(|s| (s, current_epoch.checked_sub(i as u16))) + }) .next() - .unwrap_or(0) - == 1; - status - }; + .unwrap_or((0, Some(current_epoch))); + + let epoch = epoch.ok_or(StewardError::ArithmeticError)?; - let superminority_score = if !is_superminority { 1.0 } else { 0.0 }; + if status == 1 { + Ok((0.0, epoch)) + } else { + Ok((1.0, EPOCH_DEFAULT)) + } + } +} - /////// Blacklist /////// - let blacklisted_score = if config +/// Checks if validator is blacklisted using the validator history index in the config's blacklist +fn calculate_blacklist(config: &Config, validator_index: u32) -> Result { + if config .validator_history_blacklist - .get(validator.index as usize)? + .get(validator_index as usize)? { - 0.0 + Ok(0.0) } else { - 1.0 - }; - - /////// Formula /////// - - let yield_score = (average_vote_credits / average_blocks) * (1. - commission); - - let score = mev_commission_score - * commission_score - * historical_commission_score - * blacklisted_score - * superminority_score - * delinquency_score - * running_jito_score - * yield_score; - - Ok(ScoreComponents { - score, - yield_score, - mev_commission_score, - blacklisted_score, - superminority_score, - delinquency_score, - running_jito_score, - commission_score, - historical_commission_score, - vote_credits_ratio: average_vote_credits / average_blocks, - vote_account: validator.vote_account, - epoch: current_epoch, - }) + Ok(1.0) + } } #[event] #[derive(Debug, PartialEq, Eq)] -pub struct InstantUnstakeComponents { +pub struct InstantUnstakeComponentsV2 { /// Aggregate of all checks pub instant_unstake: bool, @@ -253,6 +423,30 @@ pub struct InstantUnstakeComponents { pub vote_account: Pubkey, pub epoch: u16, + + /// Details about why a given check was calculated + pub details: InstantUnstakeDetails, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq)] +pub struct InstantUnstakeDetails { + /// Latest epoch credits + pub epoch_credits_latest: u64, + + /// Latest vote account update slot + pub vote_account_last_update_slot: u64, + + /// Latest total blocks + pub total_blocks_latest: u32, + + /// Cluster history slot index + pub cluster_history_slot_index: u64, + + /// Commission value + pub commission: u8, + + /// MEV commission value + pub mev_commission: u16, } /// Method to calculate if a validator should be unstaked instantly this epoch. @@ -263,44 +457,97 @@ pub fn instant_unstake_validator( config: &Config, epoch_start_slot: u64, current_epoch: u16, -) -> Result { +) -> Result { let params = &config.parameters; - /////// Delinquency /////// - // Compare validator vote rate against cluster block production rate this epoch + /////// Shared calculations /////// let cluster_history_slot_index = cluster .cluster_history_last_update_slot .checked_sub(epoch_start_slot) .ok_or(StewardError::ArithmeticError)?; - let blocks_produced_rate = cluster + let total_blocks_latest = cluster .history .total_blocks_latest() - .ok_or(StewardError::ClusterHistoryNotRecentEnough)? as f64 - / cluster_history_slot_index as f64; + .ok_or(StewardError::ClusterHistoryNotRecentEnough)?; - let vote_account_latest_slot = validator + let vote_account_last_update_slot = validator .history .vote_account_last_update_slot_latest() .ok_or(StewardError::VoteHistoryNotRecentEnough)?; - let validator_history_slot_index = vote_account_latest_slot + let validator_history_slot_index = vote_account_last_update_slot .checked_sub(epoch_start_slot) .ok_or(StewardError::ArithmeticError)?; - let vote_credits_rate = validator.history.epoch_credits_latest().unwrap_or(0) as f64 - / validator_history_slot_index as f64; + let epoch_credits_latest = validator.history.epoch_credits_latest().unwrap_or(0); + + /////// Component calculations /////// + let delinquency_check = calculate_instant_unstake_delinquency( + total_blocks_latest, + cluster_history_slot_index, + epoch_credits_latest, + validator_history_slot_index, + params.instant_unstake_delinquency_threshold_ratio, + ); + + let (mev_commission_check, mev_commission_bps) = calculate_instant_unstake_mev_commission( + validator, + current_epoch, + params.mev_commission_bps_threshold, + ); + + let (commission_check, commission) = + calculate_instant_unstake_commission(validator, params.commission_threshold); + + let is_blacklisted = calculate_instant_unstake_blacklist(config, validator.index)?; + + let instant_unstake = + delinquency_check || commission_check || mev_commission_check || is_blacklisted; - let delinquency_check = if blocks_produced_rate > 0. { - (vote_credits_rate / blocks_produced_rate) - < params.instant_unstake_delinquency_threshold_ratio + Ok(InstantUnstakeComponentsV2 { + instant_unstake, + delinquency_check, + commission_check, + mev_commission_check, + is_blacklisted, + vote_account: validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: epoch_credits_latest as u64, + vote_account_last_update_slot, + total_blocks_latest, + cluster_history_slot_index, + commission, + mev_commission: mev_commission_bps, + }, + }) +} + +/// Calculates if the validator should be unstaked due to delinquency +fn calculate_instant_unstake_delinquency( + total_blocks_latest: u32, + cluster_history_slot_index: u64, + epoch_credits_latest: u32, + validator_history_slot_index: u64, + instant_unstake_delinquency_threshold_ratio: f64, +) -> bool { + let blocks_produced_rate = total_blocks_latest as f64 / cluster_history_slot_index as f64; + let vote_credits_rate = epoch_credits_latest as f64 / validator_history_slot_index as f64; + + if blocks_produced_rate > 0. { + (vote_credits_rate / blocks_produced_rate) < instant_unstake_delinquency_threshold_ratio } else { false - }; + } +} - /////// MEV Commission /////// - // If MEV commission isn't set, we won't unstake because there may be issues setting tip distribution acct. - // Checks previous and current in case this validator happens to have its first slot late in the epoch +/// Calculates if the validator should be unstaked due to MEV commission +fn calculate_instant_unstake_mev_commission( + validator: &ValidatorHistory, + current_epoch: u16, + mev_commission_bps_threshold: u16, +) -> (bool, u16) { let previous_epoch = current_epoch.saturating_sub(1); let mev_commission_previous_current = validator .history @@ -310,31 +557,26 @@ pub fn instant_unstake_validator( .filter_map(|&i| i) .max() .unwrap_or(0); - let mev_commission_check = mev_commission_bps > params.mev_commission_bps_threshold; - - /////// Commission /////// + let mev_commission_check = mev_commission_bps > mev_commission_bps_threshold; + (mev_commission_check, mev_commission_bps) +} +/// Calculates if the validator should be unstaked due to commission +fn calculate_instant_unstake_commission( + validator: &ValidatorHistory, + commission_threshold: u8, +) -> (bool, u8) { let commission = validator .history .commission_latest() .unwrap_or(COMMISSION_MAX); + let commission_check = commission > commission_threshold; + (commission_check, commission) +} - let commission_check = commission > params.commission_threshold; - - /////// Blacklist /////// - let is_blacklisted = config +/// Checks if the validator is blacklisted +fn calculate_instant_unstake_blacklist(config: &Config, validator_index: u32) -> Result { + config .validator_history_blacklist - .get(validator.index as usize)?; - - let instant_unstake = - delinquency_check || commission_check || mev_commission_check || is_blacklisted; - Ok(InstantUnstakeComponents { - instant_unstake, - delinquency_check, - commission_check, - mev_commission_check, - is_blacklisted, - vote_account: validator.vote_account, - epoch: current_epoch, - }) + .get(validator_index as usize) } diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 62b9e59..6d76a21 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -10,7 +10,7 @@ use crate::{ errors::StewardError, events::{DecreaseComponents, StateTransition}, score::{ - instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, + instant_unstake_validator, validator_score, InstantUnstakeComponentsV2, ScoreComponentsV2, }, utils::{epoch_progress, get_target_lamports, stake_lamports_at_validator_list_index}, Config, Parameters, @@ -597,7 +597,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; @@ -753,7 +753,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(StewardError::InvalidState.into()); diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 08a0eb3..2b07210 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -1,7 +1,7 @@ // Unit tests for scoring, instant unstake, and delegation methods use anchor_lang::AnchorSerialize; use jito_steward::{ - constants::SORTED_INDEX_DEFAULT, + constants::{EPOCH_DEFAULT, SORTED_INDEX_DEFAULT}, delegation::{ decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState, }, @@ -9,7 +9,8 @@ use jito_steward::{ events::DecreaseComponents, insert_sorted_index, score::{ - instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, + instant_unstake_validator, validator_score, InstantUnstakeComponentsV2, + InstantUnstakeDetails, ScoreComponentsV2, ScoreDetails, }, select_validators_to_delegate, Delegation, }; @@ -43,7 +44,7 @@ fn test_compute_score() { .unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 1.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -55,7 +56,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: good_validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -67,7 +79,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 1.0, mev_commission_score: 0.0, @@ -79,7 +91,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 1001, + max_mev_commission_epoch: current_epoch as u16, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -89,7 +112,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 1.0, mev_commission_score: 0.0, @@ -101,16 +124,28 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 1001, + max_mev_commission_epoch: 11, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); + // Test high mev commission outside of range let mut validator = good_validator; validator.history.arr[9].mev_commission = 1001; let components = validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 1.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -122,7 +157,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -136,7 +182,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -148,7 +194,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); config.validator_history_blacklist.reset(); @@ -160,7 +217,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -172,7 +229,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: current_epoch as u16, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -185,7 +253,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 1.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -197,7 +265,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -211,7 +290,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 1.0, mev_commission_score: 0.0, @@ -223,7 +302,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 10000, + max_mev_commission_epoch: 20, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 0, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -234,7 +324,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 0.89, mev_commission_score: 1.0, @@ -246,7 +336,18 @@ fn test_compute_score() { commission_score: 0.0, historical_commission_score: 0.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 11, + max_commission_epoch: current_epoch as u16, + max_historical_commission: 11, + max_historical_commission_epoch: current_epoch as u16, + } } ); @@ -265,7 +366,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 1.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -277,7 +378,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 10, + max_historical_commission: 14, + max_historical_commission_epoch: 0, + } } ); @@ -286,7 +398,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -298,7 +410,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 0.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 10, + max_historical_commission: 16, + max_historical_commission_epoch: 0, + } } ); @@ -314,7 +437,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.88, yield_score: 0.88, mev_commission_score: 1.0, @@ -326,7 +449,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 10, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -337,7 +471,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.0, yield_score: 0.95, mev_commission_score: 1.0, @@ -349,7 +483,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 0.0, + delinquency_epoch: 10, + max_commission: 0, + max_commission_epoch: 10, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -365,7 +510,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 0.9, yield_score: 0.9, mev_commission_score: 1.0, @@ -377,7 +522,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 10, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -391,7 +547,7 @@ fn test_compute_score() { validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, - ScoreComponents { + ScoreComponentsV2 { score: 1.0, yield_score: 1.0, mev_commission_score: 1.0, @@ -403,7 +559,18 @@ fn test_compute_score() { commission_score: 1.0, historical_commission_score: 1.0, vote_account: validator.vote_account, - epoch: current_epoch as u16 + epoch: current_epoch as u16, + details: ScoreDetails { + max_mev_commission: 0, + max_mev_commission_epoch: 10, + superminority_epoch: EPOCH_DEFAULT, + delinquency_ratio: 1.0, + delinquency_epoch: EPOCH_DEFAULT, + max_commission: 0, + max_commission_epoch: 10, + max_historical_commission: 0, + max_historical_commission_epoch: 0, + } } ); @@ -467,17 +634,25 @@ fn test_instant_unstake() { ); assert!(res.is_ok()); - assert!( - res.unwrap() - == InstantUnstakeComponents { - instant_unstake: false, - delinquency_check: false, - commission_check: false, - mev_commission_check: false, - is_blacklisted: false, - vote_account: good_validator.vote_account, - epoch: current_epoch + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: false, + delinquency_check: false, + commission_check: false, + mev_commission_check: false, + is_blacklisted: false, + vote_account: good_validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 1000, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 1000, + cluster_history_slot_index: 999, + commission: 0, + mev_commission: 0 } + } ); // Is blacklisted @@ -494,17 +669,25 @@ fn test_instant_unstake() { ); assert!(res.is_ok()); - assert!( - res.unwrap() - == InstantUnstakeComponents { - instant_unstake: true, - delinquency_check: false, - commission_check: false, - mev_commission_check: false, - is_blacklisted: true, - vote_account: good_validator.vote_account, - epoch: current_epoch + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: true, + delinquency_check: false, + commission_check: false, + mev_commission_check: false, + is_blacklisted: true, + vote_account: good_validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 1000, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 1000, + cluster_history_slot_index: 999, + commission: 0, + mev_commission: 0 } + } ); config.validator_history_blacklist.reset(); @@ -518,17 +701,25 @@ fn test_instant_unstake() { ); assert!(res.is_ok()); - assert!( - res.unwrap() - == InstantUnstakeComponents { - instant_unstake: true, - delinquency_check: true, - commission_check: true, - mev_commission_check: true, - is_blacklisted: false, - vote_account: bad_validator.vote_account, - epoch: current_epoch + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: true, + delinquency_check: true, + commission_check: true, + mev_commission_check: true, + is_blacklisted: false, + vote_account: bad_validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 200, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 1000, + cluster_history_slot_index: 999, + commission: 99, + mev_commission: 10000 } + } ); // Errors @@ -543,7 +734,7 @@ fn test_instant_unstake() { current_epoch, ); - assert!(res == Err(StewardError::ClusterHistoryNotRecentEnough.into())); + assert_eq!(res, Err(StewardError::ClusterHistoryNotRecentEnough.into())); let cluster_history = default_fixture.cluster_history; let mut validator = validators[0]; @@ -559,17 +750,25 @@ fn test_instant_unstake() { ); 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 + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: true, + delinquency_check: true, + commission_check: false, + mev_commission_check: false, + is_blacklisted: false, + vote_account: validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 0, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 1000, + cluster_history_slot_index: 999, + commission: 0, + mev_commission: 0 } + } ); let mut validator = validators[0]; @@ -587,7 +786,7 @@ fn test_instant_unstake() { current_epoch, ); - assert!(res == Err(StewardError::VoteHistoryNotRecentEnough.into())); + assert_eq!(res, Err(StewardError::VoteHistoryNotRecentEnough.into())); // Not sure how commission would be unset with epoch credits set but test anyway let mut validator = validators[0]; @@ -600,17 +799,25 @@ fn test_instant_unstake() { current_epoch, ); assert!(res.is_ok()); - assert!( - res.unwrap() - == InstantUnstakeComponents { - instant_unstake: true, - delinquency_check: false, - commission_check: true, - mev_commission_check: false, - is_blacklisted: false, - vote_account: validator.vote_account, - epoch: current_epoch + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: true, + delinquency_check: false, + commission_check: true, + mev_commission_check: false, + is_blacklisted: false, + vote_account: validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 1000, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 1000, + cluster_history_slot_index: 999, + commission: 100, + mev_commission: 0 } + } ); let mut validator = validators[0]; @@ -624,17 +831,25 @@ fn test_instant_unstake() { current_epoch, ); assert!(res.is_ok()); - assert!( - res.unwrap() - == InstantUnstakeComponents { - instant_unstake: false, - delinquency_check: false, - commission_check: false, - mev_commission_check: false, - is_blacklisted: false, - vote_account: validator.vote_account, - epoch: current_epoch + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: false, + delinquency_check: false, + commission_check: false, + mev_commission_check: false, + is_blacklisted: false, + vote_account: validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 1000, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 1000, + cluster_history_slot_index: 999, + commission: 0, + mev_commission: 0 } + } ); // Try to break it @@ -648,17 +863,25 @@ fn test_instant_unstake() { current_epoch, ); assert!(res.is_ok()); - assert!( - res.unwrap() - == InstantUnstakeComponents { - instant_unstake: false, - delinquency_check: false, - commission_check: false, - mev_commission_check: false, - is_blacklisted: false, - vote_account: good_validator.vote_account, - epoch: current_epoch + assert_eq!( + res.unwrap(), + InstantUnstakeComponentsV2 { + instant_unstake: false, + delinquency_check: false, + commission_check: false, + mev_commission_check: false, + is_blacklisted: false, + vote_account: good_validator.vote_account, + epoch: current_epoch, + details: InstantUnstakeDetails { + epoch_credits_latest: 1000, + vote_account_last_update_slot: start_slot + 999, + total_blocks_latest: 0, + cluster_history_slot_index: 999, + commission: 0, + mev_commission: 0 } + } ); }