diff --git a/programs/steward/src/score.rs b/programs/steward/src/score.rs index aec2f949..c8ca986c 100644 --- a/programs/steward/src/score.rs +++ b/programs/steward/src/score.rs @@ -197,7 +197,7 @@ pub fn validator_score( /// 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( +pub fn calculate_mev_commission( mev_commission_window: &[Option], current_epoch: u16, mev_commission_bps_threshold: u16, @@ -234,18 +234,27 @@ fn calculate_mev_commission( } /// Calculates the vote credits ratio and delinquency score for the validator -fn calculate_epoch_credits( +pub 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)> { + if epoch_credits_window.is_empty() || total_blocks_window.is_empty() { + return Err(StewardError::ArithmeticError.into()); + } + let average_vote_credits = epoch_credits_window.iter().filter_map(|&i| i).sum::() as f64 / epoch_credits_window.len() as f64; + let nonzero_blocks = total_blocks_window.iter().filter(|i| i.is_some()).count(); + if nonzero_blocks == 0 { + return Err(StewardError::ArithmeticError.into()); + } + // 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; + let average_blocks = + total_blocks_window.iter().filter_map(|&i| i).sum::() as f64 / nonzero_blocks as f64; // Delinquency heuristic - not actual delinquency let mut delinquency_score = 1.0; @@ -259,7 +268,7 @@ fn calculate_epoch_credits( { 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. + // If total blocks are None, then keepers missed an upload and validator should not be punished. let credits = maybe_credits.unwrap_or(0); let ratio = credits as f64 / *blocks as f64; if ratio < scoring_delinquency_threshold_ratio { @@ -282,7 +291,7 @@ fn calculate_epoch_credits( } /// Finds max commission in the last `commission_range` epochs -fn calculate_commission( +pub fn calculate_commission( commission_window: &[Option], current_epoch: u16, commission_threshold: u8, @@ -308,11 +317,15 @@ fn calculate_commission( } /// Checks if validator has commission above a threshold in any epoch in their history -fn calculate_historical_commission( +pub fn calculate_historical_commission( validator: &ValidatorHistory, current_epoch: u16, historical_commission_threshold: u8, ) -> Result<(f64, u8, u16)> { + if validator.history.is_empty() { + return Err(StewardError::ArithmeticError.into()); + } + let (max_historical_commission, max_historical_commission_epoch) = validator .history .commission_range(VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH as u16, current_epoch) @@ -341,7 +354,7 @@ fn calculate_historical_commission( } /// Checks if validator is in the top 1/3 of validators by stake for the current epoch -fn calculate_superminority( +pub fn calculate_superminority( validator: &ValidatorHistory, current_epoch: u16, commission_range: u16, @@ -372,8 +385,8 @@ fn calculate_superminority( let (status, epoch) = superminority_window .iter() - .enumerate() .rev() + .enumerate() .filter_map(|(i, &superminority)| { superminority.map(|s| (s, current_epoch.checked_sub(i as u16))) }) @@ -391,7 +404,7 @@ fn calculate_superminority( } /// Checks if validator is blacklisted using the validator history index in the config's blacklist -fn calculate_blacklist(config: &Config, validator_index: u32) -> Result { +pub fn calculate_blacklist(config: &Config, validator_index: u32) -> Result { if config .validator_history_blacklist .get(validator_index as usize)? @@ -489,7 +502,7 @@ pub fn instant_unstake_validator( 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, @@ -525,25 +538,32 @@ pub fn instant_unstake_validator( } /// Calculates if the validator should be unstaked due to delinquency -fn calculate_instant_unstake_delinquency( +pub 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 { +) -> Result { + if cluster_history_slot_index == 0 || validator_history_slot_index == 0 { + return Err(StewardError::ArithmeticError.into()); + } + 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 + Ok( + (vote_credits_rate / blocks_produced_rate) + < instant_unstake_delinquency_threshold_ratio, + ) } else { - false + Ok(false) } } /// Calculates if the validator should be unstaked due to MEV commission -fn calculate_instant_unstake_mev_commission( +pub fn calculate_instant_unstake_mev_commission( validator: &ValidatorHistory, current_epoch: u16, mev_commission_bps_threshold: u16, @@ -562,7 +582,7 @@ fn calculate_instant_unstake_mev_commission( } /// Calculates if the validator should be unstaked due to commission -fn calculate_instant_unstake_commission( +pub fn calculate_instant_unstake_commission( validator: &ValidatorHistory, commission_threshold: u8, ) -> (bool, u8) { @@ -575,7 +595,7 @@ fn calculate_instant_unstake_commission( } /// Checks if the validator is blacklisted -fn calculate_instant_unstake_blacklist(config: &Config, validator_index: u32) -> Result { +pub fn calculate_instant_unstake_blacklist(config: &Config, validator_index: u32) -> Result { config .validator_history_blacklist .get(validator_index as usize) diff --git a/programs/validator-history/src/state.rs b/programs/validator-history/src/state.rs index 4a5627b4..6a52f05c 100644 --- a/programs/validator-history/src/state.rs +++ b/programs/validator-history/src/state.rs @@ -112,7 +112,6 @@ pub struct CircBuf { pub arr: [ValidatorHistoryEntry; MAX_ITEMS], } -#[cfg(test)] impl Default for CircBuf { fn default() -> Self { Self { @@ -789,7 +788,6 @@ pub struct CircBufCluster { pub arr: [ClusterHistoryEntry; MAX_ITEMS], } -#[cfg(test)] impl Default for CircBufCluster { fn default() -> Self { Self { diff --git a/tests/tests/steward/mod.rs b/tests/tests/steward/mod.rs index 85b64831..d79627a5 100644 --- a/tests/tests/steward/mod.rs +++ b/tests/tests/steward/mod.rs @@ -1,6 +1,7 @@ mod test_algorithms; mod test_integration; mod test_parameters; +mod test_scoring; mod test_spl_passthrough; mod test_state_methods; mod test_state_transitions; diff --git a/tests/tests/steward/test_scoring.rs b/tests/tests/steward/test_scoring.rs new file mode 100644 index 00000000..43af59b1 --- /dev/null +++ b/tests/tests/steward/test_scoring.rs @@ -0,0 +1,601 @@ +use jito_steward::{score::*, Config, LargeBitMask, Parameters}; +use solana_sdk::pubkey::Pubkey; +use validator_history::{CircBuf, ValidatorHistory}; + +// Fixtures + +fn create_config( + mev_commission_bps_threshold: u16, + commission_threshold: u8, + historical_commission_threshold: u8, +) -> Config { + Config { + parameters: Parameters { + mev_commission_bps_threshold, + commission_threshold, + historical_commission_threshold, + mev_commission_range: 10, + epoch_credits_range: 10, + commission_range: 10, + scoring_delinquency_threshold_ratio: 0.9, + instant_unstake_delinquency_threshold_ratio: 0.8, + ..Default::default() + }, + stake_pool: Pubkey::new_unique(), + validator_list: Pubkey::new_unique(), + admin: Pubkey::new_unique(), + parameters_authority: Pubkey::new_unique(), + blacklist_authority: Pubkey::new_unique(), + validator_history_blacklist: LargeBitMask::default(), + paused: false.into(), + _padding: [0; 1023], + } +} + +fn create_validator_history( + mev_commissions: &[u16], + commissions: &[u8], + epoch_credits: &[u32], + superminority: &[u8], +) -> ValidatorHistory { + let mut history = CircBuf::default(); + for (i, (((&mev, &comm), &credits), &super_min)) in mev_commissions + .iter() + .zip(commissions) + .zip(epoch_credits) + .zip(superminority) + .enumerate() + { + history.push(validator_history::ValidatorHistoryEntry { + epoch: i as u16, + mev_commission: mev, + commission: comm, + epoch_credits: credits, + is_superminority: super_min, + ..Default::default() + }); + } + ValidatorHistory { + history, + struct_version: 0, + vote_account: Pubkey::new_unique(), + index: 0, + bump: 0, + _padding0: [0; 7], + last_ip_timestamp: 0, + last_version_timestamp: 0, + _padding1: [0; 232], + } +} + +mod test_calculate_mev_commission { + use jito_steward::score::calculate_mev_commission; + + #[test] + fn test_normal() { + let mev_commissions = [100, 200, 300, 400, 500]; + let window = mev_commissions.iter().map(|&c| Some(c)).collect::>(); + let current_epoch = 4; + let threshold = 300; + + let (score, max_commission, max_epoch, running_jito) = + calculate_mev_commission(&window, current_epoch, threshold).unwrap(); + + assert_eq!(score, 0.0); + assert_eq!(max_commission, 500); + assert_eq!(max_epoch, 4); + assert_eq!(running_jito, 1.0); + + // All commissions below threshold, and epoch is first instance of max commission + let window = [Some(100), Some(200), Some(250), Some(250)]; + let (score, max_commission, max_epoch, running_jito) = + calculate_mev_commission(&window, 3, 300).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(max_commission, 250); + assert_eq!(max_epoch, 2); + assert_eq!(running_jito, 1.0); + + // Window with Nones + let window = [None, None, None]; + let (score, max_commission, max_epoch, running_jito) = + calculate_mev_commission(&window, 2, 300).unwrap(); + assert_eq!(score, 0.0); + assert_eq!(max_commission, 10000); + assert_eq!(max_epoch, 2); + assert_eq!(running_jito, 0.0); + } + + #[test] + fn test_edge_cases() { + // Empty window + let window: [Option; 0] = []; + let (score, max_commission, max_epoch, running_jito) = + calculate_mev_commission(&window, 0, 300).unwrap(); + assert_eq!(score, 0.0); + assert_eq!(max_commission, 10000); + assert_eq!(max_epoch, 0); + assert_eq!(running_jito, 0.0); + + // Test Arithmetic error + let window = [Some(0), Some(0), Some(0)]; + let result = calculate_mev_commission(&window, 0, 300); + assert!(result.is_err()); + } +} + +mod test_calculate_epoch_credits { + use jito_steward::constants::EPOCH_DEFAULT; + + use super::*; + + #[test] + fn test_normal() { + let epoch_credits = [Some(800), Some(800), Some(800)]; + let total_blocks = [Some(1000), Some(1000), Some(1000)]; + let epoch_start = 0; + let threshold = 0.9; + + let (ratio, delinquency_score, delinquency_ratio, delinquency_epoch) = + calculate_epoch_credits(&epoch_credits, &total_blocks, epoch_start, threshold).unwrap(); + + assert_eq!(ratio, 0.8); + assert_eq!(delinquency_score, 0.0); + assert_eq!(delinquency_ratio, 0.8); + assert_eq!(delinquency_epoch, 0); + } + + #[test] + fn test_edge_cases() { + // Delinquency detected + let epoch_credits = [Some(700), Some(800), Some(850)]; + let total_blocks = [Some(1000), Some(1000), Some(1000)]; + let (_ratio, delinquency_score, delinquency_ratio, delinquency_epoch) = + calculate_epoch_credits(&epoch_credits, &total_blocks, 0, 0.9).unwrap(); + assert_eq!(delinquency_score, 0.0); + assert_eq!(delinquency_ratio, 0.7); + assert_eq!(delinquency_epoch, 0); + + // Missing data + let epoch_credits = [None, Some(800), Some(900)]; + let total_blocks = [Some(1000), None, Some(1000)]; + let (ratio, delinquency_score, delinquency_ratio, delinquency_epoch) = + calculate_epoch_credits(&epoch_credits, &total_blocks, 0, 0.9).unwrap(); + assert_eq!(ratio, 1700. / 3000.); + assert_eq!(delinquency_score, 0.0); + assert_eq!(delinquency_ratio, 0.0); + assert_eq!(delinquency_epoch, 0); + + // No delinquent epochs + let epoch_credits = [Some(800), Some(900), Some(1000)]; + let total_blocks = [Some(1000), Some(1000), Some(1000)]; + let (ratio, delinquency_score, delinquency_ratio, delinquency_epoch) = + calculate_epoch_credits(&epoch_credits, &total_blocks, 0, 0.7).unwrap(); + assert_eq!(ratio, 0.9); + assert_eq!(delinquency_score, 1.0); + assert_eq!(delinquency_ratio, 1.0); + assert_eq!(delinquency_epoch, EPOCH_DEFAULT); + + // Empty windows + let epoch_credits: [Option; 0] = []; + let total_blocks: [Option; 0] = []; + let result = calculate_epoch_credits(&epoch_credits, &total_blocks, 0, 0.9); + assert!(result.is_err()); + + // Test Arithmetic error + let epoch_credits = [Some(1), Some(0)]; + let total_blocks = [Some(1), Some(1)]; + let result = calculate_epoch_credits(&epoch_credits, &total_blocks, u16::MAX, 0.9); + assert!(result.is_err()); + + // Test all blocks none error + let epoch_credits = [Some(1), Some(1)]; + let total_blocks = [None, None]; + let result = calculate_epoch_credits(&epoch_credits, &total_blocks, u16::MAX, 0.9); + assert!(result.is_err()); + } +} + +mod test_calculate_commission { + use super::*; + + #[test] + fn test_normal() { + let commission_window = [Some(5), Some(7), Some(6)]; + let current_epoch = 2; + let threshold = 8; + + let (score, max_commission, max_epoch) = + calculate_commission(&commission_window, current_epoch, threshold).unwrap(); + + assert_eq!(score, 1.0); + assert_eq!(max_commission, 7); + assert_eq!(max_epoch, 1); + + // Commission above threshold + let commission_window = [Some(5), Some(10), Some(6)]; + let (score, max_commission, max_epoch) = + calculate_commission(&commission_window, 2, 8).unwrap(); + assert_eq!(score, 0.0); + assert_eq!(max_commission, 10); + assert_eq!(max_epoch, 1); + } + + #[test] + fn test_edge_cases() { + // Empty window + let commission_window: [Option; 0] = []; + let (score, max_commission, max_epoch) = + calculate_commission(&commission_window, 0, 8).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(max_commission, 0); + assert_eq!(max_epoch, 0); + + // Window with None values + let commission_window = [None, Some(5), None]; + let (score, max_commission, max_epoch) = + calculate_commission(&commission_window, 2, 8).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(max_commission, 5); + assert_eq!(max_epoch, 1); + + // Test Arithmetic error + let commission_window = [Some(0), Some(0), Some(0)]; + let result = calculate_commission(&commission_window, 0, 8); + assert!(result.is_err()); + } +} + +mod test_calculate_historical_commission { + use jito_steward::constants::VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH; + + use super::*; + + #[test] + fn test_normal() { + let validator = create_validator_history( + &[100; 10], + &[5, 6, 7, 8, 7, 6, 5, 4, 3, 2], + &[1000; 10], + &[0; 10], + ); + let current_epoch = 9; + let threshold = 8; + + let (score, max_commission, max_epoch) = + calculate_historical_commission(&validator, current_epoch, threshold).unwrap(); + + assert_eq!(score, 1.0); + assert_eq!(max_commission, 8); + assert_eq!(max_epoch, 3); + + // Commission above threshold + let validator = create_validator_history( + &[100; 10], + &[5, 6, 7, 9, 7, 6, 5, 4, 3, 2], + &[1000; 10], + &[0; 10], + ); + let (score, max_commission, max_epoch) = + calculate_historical_commission(&validator, 9, 8).unwrap(); + assert_eq!(score, 0.0); + assert_eq!(max_commission, 9); + assert_eq!(max_epoch, 3); + } + + #[test] + fn test_edge_cases() { + // Empty history + let validator = create_validator_history(&[], &[], &[], &[]); + let result = calculate_historical_commission(&validator, 0, 8); + assert!(result.is_err()); + + // History with None values + let mut validator = create_validator_history( + &[100; 10], + &[5, 6, 7, 8, 7, 6, 5, 4, 3, 2], + &[1000; 10], + &[0; 10], + ); + validator + .history + .push(validator_history::ValidatorHistoryEntry::default()); + let (score, max_commission, max_epoch) = + calculate_historical_commission(&validator, 10, 8).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(max_commission, 8); + assert_eq!(max_epoch, 3); + + // Test all commissions none + let validator = create_validator_history(&[100; 10], &[u8::MAX; 10], &[1000; 10], &[0; 10]); + let (score, max_comission, max_epoch) = + calculate_historical_commission(&validator, 1, 8).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(max_comission, 0); + assert_eq!(max_epoch, VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH as u16); + } +} + +mod test_calculate_superminority { + use jito_steward::constants::EPOCH_DEFAULT; + + use super::*; + + #[test] + fn test_normal() { + // Superminority detected + let validator = create_validator_history( + &[100; 10], + &[5; 10], + &[1000; 10], + &[0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ); + let (score, epoch) = calculate_superminority(&validator, 9, 10).unwrap(); + assert_eq!(score, 0.0); + assert_eq!(epoch, 9); + + let validator = create_validator_history( + &[100; 10], + &[5; 10], + &[1000; 10], + &[0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + ); + let current_epoch = 9; + let commission_range = 10; + + let (score, epoch) = + calculate_superminority(&validator, current_epoch, commission_range).unwrap(); + + assert_eq!(score, 1.0); + assert_eq!(epoch, EPOCH_DEFAULT); + + // Superminority with missed uploads after epoch 3 + let validator = create_validator_history( + &[100; 6], + &[5; 6], + &[u32::MAX; 6], + &[0, 0, 0, 1, u8::MAX, u8::MAX], + ); + let current_epoch = 5; + let commission_range = 4; + let (score, epoch) = + calculate_superminority(&validator, current_epoch, commission_range).unwrap(); + assert_eq!(score, 0.0); + assert_eq!(epoch, 3); + + // Superminority with missed uploads after epoch 3 + let validator = create_validator_history( + &[100; 6], + &[5; 6], + &[u32::MAX; 6], + &[0, 0, 0, 0, u8::MAX, u8::MAX], + ); + let current_epoch = 5; + let commission_range = 4; + let (score, epoch) = + calculate_superminority(&validator, current_epoch, commission_range).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(epoch, EPOCH_DEFAULT); + } + + #[test] + fn test_edge_cases() { + // Empty history + let validator = create_validator_history(&[], &[], &[], &[]); + let result = calculate_superminority(&validator, 0, 10); + assert!(result.is_err()); + + // Arithmetic error + let validator = create_validator_history(&[100; 10], &[5; 10], &[u32::MAX; 10], &[0; 10]); + let result = calculate_superminority(&validator, 0, 1); + assert!(result.is_err()); + + // History with None values + let mut validator = create_validator_history(&[100; 10], &[5; 10], &[1000; 10], &[0; 10]); + validator + .history + .push(validator_history::ValidatorHistoryEntry::default()); + let (score, epoch) = calculate_superminority(&validator, 10, 10).unwrap(); + assert_eq!(score, 1.0); + assert_eq!(epoch, EPOCH_DEFAULT); + } +} + +mod test_calculate_blacklist { + use super::*; + + #[test] + fn test_normal() { + let mut config = create_config(300, 8, 10); + config.validator_history_blacklist.set(5, true).unwrap(); + + let score = calculate_blacklist(&config, 5).unwrap(); + assert_eq!(score, 0.0); + + let score = calculate_blacklist(&config, 6).unwrap(); + assert_eq!(score, 1.0); + } + + #[test] + fn test_edge_cases() { + let config = create_config(300, 8, 10); + + // Index out of bounds + let result = calculate_blacklist(&config, u32::MAX); + assert!(result.is_err()); + } +} + +mod test_calculate_instant_unstake_delinquency { + use super::*; + + #[test] + fn test_normal() { + let total_blocks_latest = 1000; + let cluster_history_slot_index = 1000; + let epoch_credits_latest = 900; + let validator_history_slot_index = 1000; + let threshold = 0.8; + + let result = calculate_instant_unstake_delinquency( + total_blocks_latest, + cluster_history_slot_index, + epoch_credits_latest, + validator_history_slot_index, + threshold, + ) + .unwrap(); + + assert!(!result); + + // Delinquency detected + let result = calculate_instant_unstake_delinquency(1000, 1000, 700, 1000, 0.8).unwrap(); + assert!(result); + } + + #[test] + fn test_edge_cases() { + // Zero blocks produced + let result = calculate_instant_unstake_delinquency(0, 1000, 900, 1000, 0.8).unwrap(); + assert!(!result); + + // Zero slots + let result = calculate_instant_unstake_delinquency(1000, 0, 900, 1000, 0.8); + assert!(result.is_err()); + } +} + +mod test_calculate_instant_unstake_mev_commission { + use super::*; + + #[test] + fn test_normal() { + let validator = + create_validator_history(&[100, 200, 300, 400, 500], &[5; 5], &[1000; 5], &[0; 5]); + let current_epoch = 4; + let threshold = 300; + + let (check, commission) = + calculate_instant_unstake_mev_commission(&validator, current_epoch, threshold); + + assert!(check); + assert_eq!(commission, 500); + } + + #[test] + fn test_edge_cases() { + // MEV commission below threshold + let validator = + create_validator_history(&[100, 200, 300, 200, 100], &[5; 5], &[1000; 5], &[0; 5]); + let current_epoch = 4; + let threshold = 300; + + let (check, commission) = + calculate_instant_unstake_mev_commission(&validator, current_epoch, threshold); + + assert!(!check); + assert_eq!(commission, 200); + + // No MEV commission data + let validator = create_validator_history(&[u16::MAX], &[5], &[1000], &[0]); + let (check, commission) = + calculate_instant_unstake_mev_commission(&validator, 0, threshold); + + assert!(!check); + assert_eq!(commission, 0); + + // Only one epoch of data + let validator = create_validator_history( + &[u16::MAX, 400], + &[u8::MAX, 5], + &[u32::MAX, 1000], + &[u8::MAX, 0], + ); + let (check, commission) = + calculate_instant_unstake_mev_commission(&validator, 1, threshold); + + assert!(check); + assert_eq!(commission, 400); + + // Threshold at exactly the highest commission + let validator = + create_validator_history(&[100, 200, 300, 400, 500], &[5; 5], &[1000; 5], &[0; 5]); + let threshold = 500; + + let (check, commission) = + calculate_instant_unstake_mev_commission(&validator, 4, threshold); + + assert!(!check); + assert_eq!(commission, 500); + } +} + +mod test_calculate_instant_unstake_commission { + use jito_steward::constants::COMMISSION_MAX; + + use super::*; + + #[test] + fn test_normal() { + let validator = create_validator_history(&[5; 5], &[1, 2, 3, 4, 5], &[1000; 5], &[0; 5]); + let threshold = 4; + + let (check, commission) = calculate_instant_unstake_commission(&validator, threshold); + + assert!(check); + assert_eq!(commission, 5); + } + + #[test] + fn test_edge_cases() { + // Commission at threshold + let validator = create_validator_history(&[5; 5], &[1, 2, 3, 4, 5], &[1000; 5], &[0; 5]); + let threshold = 5; + + let (check, commission) = calculate_instant_unstake_commission(&validator, threshold); + + assert!(!check); + assert_eq!(commission, 5); + + // No commission data + let validator = create_validator_history(&[5; 5], &[u8::MAX; 5], &[1000; 5], &[0; 5]); + let threshold = 5; + + let (check, commission) = calculate_instant_unstake_commission(&validator, threshold); + + assert!(check); + assert_eq!(commission, COMMISSION_MAX); + + // Only one epoch of data + let validator = create_validator_history( + &[u16::MAX, 5], + &[u8::MAX, 3], + &[u32::MAX, 1000], + &[u8::MAX, 1], + ); + let threshold = 5; + + let (check, commission) = calculate_instant_unstake_commission(&validator, threshold); + + assert!(!check); + assert_eq!(commission, 3); + } +} + +mod test_calculate_instant_unstake_blacklist { + + use super::*; + + #[test] + fn test_normal() { + let mut config = create_config(300, 8, 10); + config.validator_history_blacklist.set(5, true).unwrap(); + + let result = calculate_instant_unstake_blacklist(&config, 5).unwrap(); + assert!(result); + + let result = calculate_instant_unstake_blacklist(&config, 6).unwrap(); + assert!(!result); + } + + /* single line fn, no edge cases */ +}