diff --git a/chain/epoch-manager/src/lib.rs b/chain/epoch-manager/src/lib.rs index c4383f01d73..df6f7e35bc0 100644 --- a/chain/epoch-manager/src/lib.rs +++ b/chain/epoch-manager/src/lib.rs @@ -28,6 +28,7 @@ use near_primitives::views::{ }; use near_store::{DBCol, Store, StoreUpdate, HEADER_HEAD_KEY}; use primitive_types::U256; +use reward_calculator::ValidatorOnlineThresholds; use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -507,7 +508,15 @@ impl EpochManager { { let mut sorted_validators = validator_block_chunk_stats .iter() - .map(|(account, stats)| (get_sortable_validator_online_ratio(stats), account)) + .map(|(account, stats)| { + ( + get_sortable_validator_online_ratio( + stats, + Some(chunk_validator_only_kickout_threshold), + ), + account, + ) + }) .collect::>(); sorted_validators.sort(); sorted_validators @@ -752,6 +761,20 @@ impl EpochManager { validator_block_chunk_stats.remove(account_id); } } + let epoch_config = self.get_epoch_config(block_info.epoch_id())?; + // If ChunkEndorsementsInBlockHeader feature is enabled, we use the chunk validator kickout threshold + // as the cutoff threshold for the endorsement ratio to remap the ratio to 0 or 1. + let online_thresholds = ValidatorOnlineThresholds { + online_min_threshold: epoch_config.online_min_threshold, + online_max_threshold: epoch_config.online_max_threshold, + endorsement_cutoff_threshold: if ProtocolFeature::ChunkEndorsementsInBlockHeader + .enabled(epoch_protocol_version) + { + Some(epoch_config.chunk_validator_only_kickout_threshold) + } else { + None + }, + }; self.reward_calculator.calculate_reward( validator_block_chunk_stats, &validator_stake, @@ -759,6 +782,7 @@ impl EpochManager { epoch_protocol_version, self.genesis_protocol_version, epoch_duration, + online_thresholds, ) }; let next_next_epoch_config = self.config.for_protocol_version(next_next_epoch_version); diff --git a/chain/epoch-manager/src/reward_calculator.rs b/chain/epoch-manager/src/reward_calculator.rs index 1e08f08b728..717fc1edf6b 100644 --- a/chain/epoch-manager/src/reward_calculator.rs +++ b/chain/epoch-manager/src/reward_calculator.rs @@ -13,6 +13,20 @@ use crate::validator_stats::get_validator_online_ratio; pub(crate) const NUM_NS_IN_SECOND: u64 = 1_000_000_000; pub const NUM_SECONDS_IN_A_YEAR: u64 = 24 * 60 * 60 * 365; +/// Contains online thresholds for validators. +#[derive(Clone, Debug)] +pub struct ValidatorOnlineThresholds { + /// Online minimum threshold below which validator doesn't receive reward. + pub online_min_threshold: Rational32, + /// Online maximum threshold above which validator gets full reward. + pub online_max_threshold: Rational32, + /// If set, contains a number between 0 and 100 (percentage), and endorsement ratio + /// below this threshold will be treated 0, and otherwise be treated 1, + /// before calculating the average uptime ratio of the validator. + /// If not set, endorsement ratio will be used as is. + pub endorsement_cutoff_threshold: Option, +} + #[derive(Clone, Debug)] pub struct RewardCalculator { pub max_inflation_rate: Rational32, @@ -20,8 +34,6 @@ pub struct RewardCalculator { pub epoch_length: u64, pub protocol_reward_rate: Rational32, pub protocol_treasury_account: AccountId, - pub online_min_threshold: Rational32, - pub online_max_threshold: Rational32, pub num_seconds_per_year: u64, } @@ -33,8 +45,6 @@ impl RewardCalculator { epoch_length: config.epoch_length, protocol_reward_rate: config.protocol_reward_rate, protocol_treasury_account: config.protocol_treasury_account.clone(), - online_max_threshold: config.online_max_threshold, - online_min_threshold: config.online_min_threshold, num_seconds_per_year: NUM_SECONDS_IN_A_YEAR, } } @@ -49,6 +59,7 @@ impl RewardCalculator { protocol_version: ProtocolVersion, genesis_protocol_version: ProtocolVersion, epoch_duration: u64, + online_thresholds: ValidatorOnlineThresholds, ) -> (HashMap, Balance) { let mut res = HashMap::new(); let num_validators = validator_block_chunk_stats.len(); @@ -90,7 +101,8 @@ impl RewardCalculator { let mut epoch_actual_reward = epoch_protocol_treasury; let total_stake: Balance = validator_stake.values().sum(); for (account_id, stats) in validator_block_chunk_stats { - let production_ratio = get_validator_online_ratio(&stats); + let production_ratio = + get_validator_online_ratio(&stats, online_thresholds.endorsement_cutoff_threshold); let average_produced_numer = production_ratio.numer(); let average_produced_denom = production_ratio.denom(); @@ -98,8 +110,10 @@ impl RewardCalculator { let expected_chunks = stats.chunk_stats.expected(); let expected_endorsements = stats.chunk_stats.endorsement_stats().expected; - let online_min_numer = U256::from(*self.online_min_threshold.numer() as u64); - let online_min_denom = U256::from(*self.online_min_threshold.denom() as u64); + let online_min_numer = + U256::from(*online_thresholds.online_min_threshold.numer() as u64); + let online_min_denom = + U256::from(*online_thresholds.online_min_threshold.denom() as u64); // If average of produced blocks below online min threshold, validator gets 0 reward. let chunk_only_producers_enabled = checked_feature!("stable", ChunkOnlyProducers, protocol_version); @@ -121,8 +135,10 @@ impl RewardCalculator { .get(&account_id) .unwrap_or_else(|| panic!("{} is not a validator", account_id)); // Online reward multiplier is min(1., (uptime - online_threshold_min) / (online_threshold_max - online_threshold_min). - let online_max_numer = U256::from(*self.online_max_threshold.numer() as u64); - let online_max_denom = U256::from(*self.online_max_threshold.denom() as u64); + let online_max_numer = + U256::from(*online_thresholds.online_max_threshold.numer() as u64); + let online_max_denom = + U256::from(*online_thresholds.online_max_threshold.denom() as u64); let online_numer = online_max_numer * online_min_denom - online_min_numer * online_max_denom; let mut uptime_numer = (average_produced_numer * online_min_denom @@ -161,8 +177,6 @@ mod tests { epoch_length, protocol_reward_rate: Ratio::new(0, 1), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(9, 10), - online_max_threshold: Ratio::new(1, 1), num_seconds_per_year: 1000000, }; let validator_block_chunk_stats = HashMap::from([ @@ -191,6 +205,11 @@ mod tests { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(9, 10), + online_max_threshold: Ratio::new(1, 1), + endorsement_cutoff_threshold: None, + }, ); assert_eq!( result.0, @@ -212,8 +231,6 @@ mod tests { epoch_length, protocol_reward_rate: Ratio::new(0, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(9, 10), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: 1000, }; let validator_block_chunk_stats = HashMap::from([ @@ -252,6 +269,11 @@ mod tests { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(9, 10), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: None, + }, ); // Total reward is 10_000_000. Divided by 3 equal stake validators - each gets 3_333_333. // test1 with 94.5% online gets 50% because of linear between (0.99-0.9) online. @@ -277,8 +299,6 @@ mod tests { epoch_length, protocol_reward_rate: Ratio::new(0, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(9, 10), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: 1000, }; let validator_block_chunk_stats = HashMap::from([ @@ -329,6 +349,11 @@ mod tests { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(9, 10), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: None, + }, ); // Total reward is 10_000_000. Divided by 4 equal stake validators - each gets 2_500_000. // test1 with 94.5% online gets 50% because of linear between (0.99-0.9) online. @@ -347,7 +372,6 @@ mod tests { } } - // Test rewards when some validators are only responsible for endorsements #[test] fn test_reward_stateless_validation() { let epoch_length = 1000; @@ -357,8 +381,6 @@ mod tests { epoch_length, protocol_reward_rate: Ratio::new(0, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(9, 10), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: 1000, }; let validator_block_chunk_stats = HashMap::from([ @@ -415,6 +437,11 @@ mod tests { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(9, 10), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: None, + }, ); // Total reward is 10_000_000. Divided by 4 equal stake validators - each gets 2_500_000. // test1 with 94.5% online gets 50% because of linear between (0.99-0.9) online. @@ -433,6 +460,95 @@ mod tests { } } + #[test] + fn test_reward_stateless_validation_with_endorsement_cutoff() { + let epoch_length = 1000; + let reward_calculator = RewardCalculator { + max_inflation_rate: Ratio::new(1, 100), + num_blocks_per_year: 1000, + epoch_length, + protocol_reward_rate: Ratio::new(0, 10), + protocol_treasury_account: "near".parse().unwrap(), + num_seconds_per_year: 1000, + }; + let validator_block_chunk_stats = HashMap::from([ + // Blocks, chunks, endorsements - endorsement ratio cutoff is exceeded + ( + "test1".parse().unwrap(), + BlockChunkValidatorStats { + block_stats: ValidatorStats { produced: 945, expected: 1000 }, + chunk_stats: ChunkStats { + production: ValidatorStats { produced: 944, expected: 1000 }, + endorsement: ValidatorStats { produced: 946, expected: 1000 }, + }, + }, + ), + // Blocks, chunks, endorsements - endorsement ratio cutoff is not exceeded + ( + "test2".parse().unwrap(), + BlockChunkValidatorStats { + block_stats: ValidatorStats { produced: 945, expected: 1000 }, + chunk_stats: ChunkStats { + production: ValidatorStats { produced: 944, expected: 1000 }, + endorsement: ValidatorStats { produced: 446, expected: 1000 }, + }, + }, + ), + // Endorsements only - endorsement ratio cutoff is exceeded + ( + "test3".parse().unwrap(), + BlockChunkValidatorStats { + block_stats: ValidatorStats { produced: 0, expected: 0 }, + chunk_stats: ChunkStats::new_with_endorsement(946, 1000), + }, + ), + // Endorsements only - endorsement ratio cutoff is not exceeded + ( + "test4".parse().unwrap(), + BlockChunkValidatorStats { + block_stats: ValidatorStats { produced: 0, expected: 0 }, + chunk_stats: ChunkStats::new_with_endorsement(446, 1000), + }, + ), + ]); + let validator_stake = HashMap::from([ + ("test1".parse().unwrap(), 500_000), + ("test2".parse().unwrap(), 500_000), + ("test3".parse().unwrap(), 500_000), + ("test4".parse().unwrap(), 500_000), + ]); + let total_supply = 1_000_000_000; + let result = reward_calculator.calculate_reward( + validator_block_chunk_stats, + &validator_stake, + total_supply, + PROTOCOL_VERSION, + PROTOCOL_VERSION, + epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(9, 10), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: Some(50), + }, + ); + // "test2" does not get reward since its uptime ratio goes below online_min_threshold, + // because its endorsement ratio is below the cutoff threshold. + // "test4" does not get reward since its endorsement ratio is below the cutoff threshold. + { + assert_eq!( + result.0, + HashMap::from([ + ("near".parse().unwrap(), 0), + ("test1".parse().unwrap(), 1_750_000u128), + ("test2".parse().unwrap(), 0), + ("test3".parse().unwrap(), 2_500_000u128), + ("test4".parse().unwrap(), 0) + ]) + ); + assert_eq!(result.1, 4_250_000u128); + } + } + /// Test that under an extreme setting (total supply 100b, epoch length half a day), /// reward calculation will not overflow. #[test] @@ -445,8 +561,6 @@ mod tests { epoch_length, protocol_reward_rate: Ratio::new(1, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(9, 10), - online_max_threshold: Ratio::new(1, 1), num_seconds_per_year: 60 * 60 * 24 * 365, }; let validator_block_chunk_stats = HashMap::from([( @@ -469,6 +583,11 @@ mod tests { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(9, 10), + online_max_threshold: Ratio::new(1, 1), + endorsement_cutoff_threshold: None, + }, ); } } diff --git a/chain/epoch-manager/src/shard_tracker.rs b/chain/epoch-manager/src/shard_tracker.rs index 8eea820eb5b..49cc1abf95c 100644 --- a/chain/epoch-manager/src/shard_tracker.rs +++ b/chain/epoch-manager/src/shard_tracker.rs @@ -255,8 +255,6 @@ mod tests { epoch_length: 1, protocol_reward_rate: Ratio::from_integer(0), protocol_treasury_account: "test".parse().unwrap(), - online_max_threshold: initial_epoch_config.online_max_threshold, - online_min_threshold: initial_epoch_config.online_min_threshold, num_seconds_per_year: 1000000, }; EpochManager::new( diff --git a/chain/epoch-manager/src/test_utils.rs b/chain/epoch-manager/src/test_utils.rs index 808a89ddb32..299280bc14b 100644 --- a/chain/epoch-manager/src/test_utils.rs +++ b/chain/epoch-manager/src/test_utils.rs @@ -190,8 +190,6 @@ pub fn default_reward_calculator() -> RewardCalculator { epoch_length: 1, protocol_reward_rate: Ratio::from_integer(0), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(90, 100), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: NUM_SECONDS_IN_A_YEAR, } } diff --git a/chain/epoch-manager/src/tests/mod.rs b/chain/epoch-manager/src/tests/mod.rs index 6e293debd8d..1bbe591e565 100644 --- a/chain/epoch-manager/src/tests/mod.rs +++ b/chain/epoch-manager/src/tests/mod.rs @@ -662,8 +662,6 @@ fn test_validator_reward_one_validator() { epoch_length, protocol_reward_rate: Ratio::new(1, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(90, 100), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: 50, }; let mut epoch_manager = @@ -715,6 +713,11 @@ fn test_validator_reward_one_validator() { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(90, 100), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: None, + }, ); let test2_reward = *validator_reward.get(AccountIdRef::new_or_panic("test2")).unwrap(); let protocol_reward = *validator_reward.get(AccountIdRef::new_or_panic("near")).unwrap(); @@ -745,8 +748,6 @@ fn test_validator_reward_weight_by_stake() { epoch_length, protocol_reward_rate: Ratio::new(1, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(90, 100), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: 50, }; let mut epoch_manager = @@ -798,6 +799,11 @@ fn test_validator_reward_weight_by_stake() { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(90, 100), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: None, + }, ); let test1_reward = *validator_reward.get(AccountIdRef::new_or_panic("test1")).unwrap(); let test2_reward = *validator_reward.get(AccountIdRef::new_or_panic("test2")).unwrap(); @@ -842,8 +848,6 @@ fn test_reward_multiple_shards() { epoch_length, protocol_reward_rate: Ratio::new(1, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(90, 100), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: 1_000_000, }; let num_shards = 2; @@ -913,6 +917,11 @@ fn test_reward_multiple_shards() { PROTOCOL_VERSION, PROTOCOL_VERSION, epoch_length * NUM_NS_IN_SECOND, + ValidatorOnlineThresholds { + online_min_threshold: Ratio::new(90, 100), + online_max_threshold: Ratio::new(99, 100), + endorsement_cutoff_threshold: None, + }, ); let test2_reward = *validator_reward.get(AccountIdRef::new_or_panic("test2")).unwrap(); let protocol_reward = *validator_reward.get(AccountIdRef::new_or_panic("near")).unwrap(); @@ -1160,8 +1169,6 @@ fn test_rewards_with_kickouts() { epoch_length, protocol_reward_rate: Ratio::new(1, 10), protocol_treasury_account: "near".parse().unwrap(), - online_min_threshold: Ratio::new(90, 100), - online_max_threshold: Ratio::new(99, 100), num_seconds_per_year: NUM_SECONDS_IN_A_YEAR, }; let mut em = setup_epoch_manager(validators, epoch_length, 1, 3, 10, 10, 0, reward_calculator); diff --git a/chain/epoch-manager/src/validator_stats.rs b/chain/epoch-manager/src/validator_stats.rs index 2fb918a7020..9f81c60e637 100644 --- a/chain/epoch-manager/src/validator_stats.rs +++ b/chain/epoch-manager/src/validator_stats.rs @@ -1,4 +1,4 @@ -use near_primitives::types::BlockChunkValidatorStats; +use near_primitives::types::{BlockChunkValidatorStats, ValidatorStats}; use num_bigint::BigUint; use num_rational::{BigRational, Ratio, Rational64}; use primitive_types::U256; @@ -7,10 +7,21 @@ use primitive_types::U256; /// This is an average of block produced / expected, chunk produced / expected, /// and chunk endorsed produced / expected. /// Note that it returns `Ratio` in raw form (not reduced). -pub(crate) fn get_validator_online_ratio(stats: &BlockChunkValidatorStats) -> Ratio { +/// +/// # Arguments +/// +/// * `stats` - stats for block and chunk production and chunk endorsement +/// * `endorsement_cutoff_threshold` - if set, a number between 0 and 100 (percentage) that +/// represents the minimum endorsement ratio below which the ratio is treated 0, and 1 otherwise +pub(crate) fn get_validator_online_ratio( + stats: &BlockChunkValidatorStats, + endorsement_cutoff_threshold: Option, +) -> Ratio { let expected_blocks = stats.block_stats.expected; let expected_chunks = stats.chunk_stats.expected(); - let expected_endorsements = stats.chunk_stats.endorsement_stats().expected; + + let (produced_endorsements, expected_endorsements) = + get_endorsement_ratio(stats.chunk_stats.endorsement_stats(), endorsement_cutoff_threshold); let (average_produced_numer, average_produced_denom) = match (expected_blocks, expected_chunks, expected_endorsements) { @@ -18,42 +29,49 @@ pub(crate) fn get_validator_online_ratio(stats: &BlockChunkValidatorStats) -> Ra (0, 0, 0) => (U256::from(0), U256::from(1)), // Validator was a stateless validator only (not expected to produce anything) (0, 0, expected_endorsements) => { - let endorsement_stats = stats.chunk_stats.endorsement_stats(); - (U256::from(endorsement_stats.produced), U256::from(expected_endorsements)) + (U256::from(produced_endorsements), U256::from(expected_endorsements)) } // Validator was a chunk-only producer (0, expected_chunks, 0) => { - (U256::from(stats.chunk_stats.produced()), U256::from(expected_chunks)) + let produced_chunks = stats.chunk_stats.produced(); + + (U256::from(produced_chunks), U256::from(expected_chunks)) } // Validator was only a block producer (expected_blocks, 0, 0) => { - (U256::from(stats.block_stats.produced), U256::from(expected_blocks)) + let produced_blocks = stats.block_stats.produced; + + (U256::from(produced_blocks), U256::from(expected_blocks)) } // Validator produced blocks and chunks, but not endorsements (expected_blocks, expected_chunks, 0) => { + let produced_blocks = stats.block_stats.produced; + let produced_chunks = stats.chunk_stats.produced(); + let numer = U256::from( - stats.block_stats.produced * expected_chunks - + stats.chunk_stats.produced() * expected_blocks, + produced_blocks * expected_chunks + produced_chunks * expected_blocks, ); let denom = U256::from(2 * expected_chunks * expected_blocks); (numer, denom) } // Validator produced chunks and endorsements, but not blocks (0, expected_chunks, expected_endorsements) => { - let endorsement_stats = stats.chunk_stats.endorsement_stats(); + let produced_chunks = stats.chunk_stats.produced(); + let numer = U256::from( - endorsement_stats.produced * expected_chunks - + stats.chunk_stats.produced() * expected_endorsements, + produced_endorsements * expected_chunks + + produced_chunks * expected_endorsements, ); let denom = U256::from(2 * expected_chunks * expected_endorsements); (numer, denom) } // Validator produced blocks and endorsements, but not chunks (expected_blocks, 0, expected_endorsements) => { - let endorsement_stats = stats.chunk_stats.endorsement_stats(); + let produced_blocks = stats.block_stats.produced; + let numer = U256::from( - endorsement_stats.produced * expected_blocks - + stats.block_stats.produced * expected_endorsements, + produced_endorsements * expected_blocks + + produced_blocks * expected_endorsements, ); let denom = U256::from(2 * expected_blocks * expected_endorsements); (numer, denom) @@ -62,7 +80,6 @@ pub(crate) fn get_validator_online_ratio(stats: &BlockChunkValidatorStats) -> Ra (expected_blocks, expected_chunks, expected_endorsements) => { let produced_blocks = stats.block_stats.produced; let produced_chunks = stats.chunk_stats.produced(); - let produced_endorsements = stats.chunk_stats.endorsement_stats().produced; let numer = U256::from( produced_blocks * expected_chunks * expected_endorsements @@ -89,8 +106,11 @@ pub(crate) fn get_validator_online_ratio(stats: &BlockChunkValidatorStats) -> Ra /// Instead of having a full-blown implementation of `U256`` for `num_integer::Integer` /// we wrap the value in a `BigInt` for now. /// TODO: Implement `num_integer::Integer` for `U256` and remove this function. -pub(crate) fn get_sortable_validator_online_ratio(stats: &BlockChunkValidatorStats) -> BigRational { - let ratio = get_validator_online_ratio(stats); +pub(crate) fn get_sortable_validator_online_ratio( + stats: &BlockChunkValidatorStats, + endorsement_cutoff_threshold: Option, +) -> BigRational { + let ratio = get_validator_online_ratio(stats, endorsement_cutoff_threshold); let mut bytes: [u8; size_of::()] = [0; size_of::()]; ratio.numer().to_little_endian(&mut bytes); let bignumer = BigUint::from_bytes_le(&bytes); @@ -115,3 +135,152 @@ pub(crate) fn get_sortable_validator_online_ratio_without_endorsements( / 2 } } +/// Applies the `cutoff_threshold` to the endorsement ratio encoded in the `stats`. +/// If `cutoff_threshold` is not provided, returns the same ratio from the `stats`. +/// If `cutoff_threshold` is provided, compares the endorsement ratio from the `stats` with the threshold. +/// If the ratio is below the threshold, it returns 0, otherwise it returns 1. +fn get_endorsement_ratio(stats: &ValidatorStats, cutoff_threshold: Option) -> (u64, u64) { + let (numer, denom) = if stats.expected == 0 { + debug_assert_eq!(stats.produced, 0); + (0, 0) + } else if let Some(threshold) = cutoff_threshold { + if stats.less_than(threshold) { + (0, 1) + } else { + (1, 1) + } + } else { + (stats.produced, stats.expected) + }; + (numer, denom) +} + +#[cfg(test)] +mod test { + use near_primitives::types::{BlockChunkValidatorStats, ChunkStats, ValidatorStats}; + use num_bigint::BigInt; + use num_rational::{Ratio, Rational32}; + use primitive_types::U256; + + use crate::validator_stats::{get_sortable_validator_online_ratio, get_validator_online_ratio}; + + const VALIDATOR_STATS: BlockChunkValidatorStats = BlockChunkValidatorStats { + block_stats: ValidatorStats { produced: 98, expected: 100 }, + chunk_stats: ChunkStats { + production: ValidatorStats { produced: 76, expected: 100 }, + endorsement: ValidatorStats { produced: 42, expected: 100 }, + }, + }; + + const VALIDATOR_STATS_NO_ENDORSEMENT: BlockChunkValidatorStats = BlockChunkValidatorStats { + block_stats: ValidatorStats { produced: 98, expected: 100 }, + chunk_stats: ChunkStats { + production: ValidatorStats { produced: 76, expected: 100 }, + endorsement: ValidatorStats { produced: 0, expected: 0 }, + }, + }; + + #[test] + fn test_average_uptime_ratio_without_endorsement_cutoff() { + let endorsement_cutoff = None; + let actual_ratio: Ratio = + get_validator_online_ratio(&VALIDATOR_STATS, endorsement_cutoff); + let expected_ratio: Ratio = + (Rational32::new(98, 100) + Rational32::new(76, 100) + Rational32::new(42, 100)) / 3; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_average_uptime_ratio_with_endorsement_cutoff_passed() { + let endorsement_cutoff = Some(30); + let actual_ratio: Ratio = + get_validator_online_ratio(&VALIDATOR_STATS, endorsement_cutoff); + let expected_ratio: Ratio = + (Rational32::new(98, 100) + Rational32::new(76, 100) + Rational32::from_integer(1)) / 3; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_average_uptime_ratio_with_endorsement_cutoff_not_passed() { + let endorsement_cutoff = Some(50); + let actual_ratio: Ratio = + get_validator_online_ratio(&VALIDATOR_STATS, endorsement_cutoff); + let expected_ratio: Ratio = + (Rational32::new(98, 100) + Rational32::new(76, 100) + Rational32::from_integer(0)) / 3; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_average_uptime_ratio_with_no_endorsement_expected() { + let endorsement_cutoff = Some(50); + let actual_ratio: Ratio = + get_validator_online_ratio(&VALIDATOR_STATS_NO_ENDORSEMENT, endorsement_cutoff); + let expected_ratio: Ratio = (Rational32::new(98, 100) + Rational32::new(76, 100)) / 2; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_sortable_average_uptime_ratio_without_endorsement_cutoff() { + let endorsement_cutoff = None; + let actual_ratio: Ratio = + get_sortable_validator_online_ratio(&VALIDATOR_STATS, endorsement_cutoff); + let expected_ratio: Ratio = + (Rational32::new(98, 100) + Rational32::new(76, 100) + Rational32::new(42, 100)) / 3; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_sortable_average_uptime_ratio_with_endorsement_cutoff_passed() { + let endorsement_cutoff = Some(30); + let actual_ratio: Ratio = + get_sortable_validator_online_ratio(&VALIDATOR_STATS, endorsement_cutoff); + let expected_ratio: Ratio = + (Rational32::new(98, 100) + Rational32::new(76, 100) + Rational32::from_integer(1)) / 3; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_sortable_average_uptime_ratio_with_endorsement_cutoff_not_passed() { + let endorsement_cutoff = Some(50); + let actual_ratio: Ratio = + get_sortable_validator_online_ratio(&VALIDATOR_STATS, endorsement_cutoff); + let expected_ratio: Ratio = + (Rational32::new(98, 100) + Rational32::new(76, 100) + Rational32::from_integer(0)) / 3; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } + + #[test] + fn test_sortable_average_uptime_ratio_with_no_endorsement_expected() { + let endorsement_cutoff = Some(50); + let actual_ratio: Ratio = get_sortable_validator_online_ratio( + &VALIDATOR_STATS_NO_ENDORSEMENT, + endorsement_cutoff, + ); + let expected_ratio: Ratio = (Rational32::new(98, 100) + Rational32::new(76, 100)) / 2; + assert_eq!( + actual_ratio.numer() * expected_ratio.denom(), + actual_ratio.denom() * expected_ratio.numer() + ); + } +} diff --git a/core/primitives/res/epoch_configs/mainnet/145.json b/core/primitives/res/epoch_configs/mainnet/145.json new file mode 100644 index 00000000000..5cd0fa2958b --- /dev/null +++ b/core/primitives/res/epoch_configs/mainnet/145.json @@ -0,0 +1,89 @@ +{ + "epoch_length": 43200, + "num_block_producer_seats": 100, + "num_block_producer_seats_per_shard": [ + 100, + 100, + 100, + 100, + 100, + 100 + ], + "avg_hidden_validator_seats_per_shard": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "block_producer_kickout_threshold": 80, + "chunk_producer_kickout_threshold": 80, + "chunk_validator_only_kickout_threshold": 70, + "target_validator_mandates_per_shard": 68, + "validator_max_kickout_stake_perc": 30, + "online_min_threshold": [ + 90, + 100 + ], + "online_max_threshold": [ + 99, + 100 + ], + "fishermen_threshold": 340282366920938463463374607431768211455, + "minimum_stake_divisor": 10, + "protocol_upgrade_stake_threshold": [ + 4, + 5 + ], + "shard_layout": { + "V1": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shards_split_map": [ + [ + 0 + ], + [ + 1 + ], + [ + 2, + 3 + ], + [ + 4 + ], + [ + 5 + ] + ], + "to_parent_shard_map": [ + 0, + 1, + 2, + 2, + 3, + 4 + ], + "version": 3 + } + }, + "validator_selection_config": { + "num_chunk_producer_seats": 100, + "num_chunk_validator_seats": 300, + "num_chunk_only_producer_seats": 0, + "minimum_validators_per_shard": 1, + "minimum_stake_ratio": [ + 1, + 62500 + ], + "chunk_producer_assignment_changes_limit": 5, + "shuffle_shard_assignment_for_chunk_producers": true + } +} \ No newline at end of file diff --git a/core/primitives/res/epoch_configs/mainnet/71.json b/core/primitives/res/epoch_configs/mainnet/71.json index 161382ed6ab..4c4880bd5e7 100644 --- a/core/primitives/res/epoch_configs/mainnet/71.json +++ b/core/primitives/res/epoch_configs/mainnet/71.json @@ -1,89 +1,89 @@ { - "epoch_length": 43200, - "num_block_producer_seats": 100, - "num_block_producer_seats_per_shard": [ - 100, - 100, - 100, - 100, - 100, - 100 - ], - "avg_hidden_validator_seats_per_shard": [ - 0, - 0, - 0, - 0, - 0, - 0 - ], - "block_producer_kickout_threshold": 80, - "chunk_producer_kickout_threshold": 80, - "chunk_validator_only_kickout_threshold": 80, - "target_validator_mandates_per_shard": 68, - "validator_max_kickout_stake_perc": 30, - "online_min_threshold": [ - 90, - 100 - ], - "online_max_threshold": [ - 99, - 100 - ], - "fishermen_threshold": 340282366920938463463374607431768211455, - "minimum_stake_divisor": 10, - "protocol_upgrade_stake_threshold": [ - 4, - 5 - ], - "shard_layout": { - "V1": { - "boundary_accounts": [ - "aurora", - "aurora-0", - "game.hot.tg", - "kkuuue2akv_1630967379.near", - "tge-lockup.sweat" + "epoch_length": 43200, + "num_block_producer_seats": 100, + "num_block_producer_seats_per_shard": [ + 100, + 100, + 100, + 100, + 100, + 100 + ], + "avg_hidden_validator_seats_per_shard": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "block_producer_kickout_threshold": 80, + "chunk_producer_kickout_threshold": 80, + "chunk_validator_only_kickout_threshold": 80, + "target_validator_mandates_per_shard": 68, + "validator_max_kickout_stake_perc": 30, + "online_min_threshold": [ + 90, + 100 + ], + "online_max_threshold": [ + 99, + 100 + ], + "fishermen_threshold": 340282366920938463463374607431768211455, + "minimum_stake_divisor": 10, + "protocol_upgrade_stake_threshold": [ + 4, + 5 + ], + "shard_layout": { + "V1": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shards_split_map": [ + [ + 0 ], - "shards_split_map": [ - [ - 0 - ], - [ - 1 - ], - [ - 2, - 3 - ], - [ - 4 - ], - [ - 5 - ] + [ + 1 ], - "to_parent_shard_map": [ - 0, - 1, - 2, + [ 2, - 3, + 3 + ], + [ 4 ], - "version": 3 - } - }, - "validator_selection_config": { - "num_chunk_producer_seats": 100, - "num_chunk_validator_seats": 300, - "num_chunk_only_producer_seats": 0, - "minimum_validators_per_shard": 1, - "minimum_stake_ratio": [ + [ + 5 + ] + ], + "to_parent_shard_map": [ + 0, 1, - 62500 + 2, + 2, + 3, + 4 ], - "chunk_producer_assignment_changes_limit": 5, - "shuffle_shard_assignment_for_chunk_producers": false + "version": 3 } - } \ No newline at end of file + }, + "validator_selection_config": { + "num_chunk_producer_seats": 100, + "num_chunk_validator_seats": 300, + "num_chunk_only_producer_seats": 0, + "minimum_validators_per_shard": 1, + "minimum_stake_ratio": [ + 1, + 62500 + ], + "chunk_producer_assignment_changes_limit": 5, + "shuffle_shard_assignment_for_chunk_producers": false + } +} \ No newline at end of file diff --git a/core/primitives/res/epoch_configs/mocknet/145.json b/core/primitives/res/epoch_configs/mocknet/145.json new file mode 100644 index 00000000000..c4256c522e5 --- /dev/null +++ b/core/primitives/res/epoch_configs/mocknet/145.json @@ -0,0 +1,89 @@ +{ + "epoch_length": 2000, + "num_block_producer_seats": 100, + "num_block_producer_seats_per_shard": [ + 100, + 100, + 100, + 100, + 100, + 100 + ], + "avg_hidden_validator_seats_per_shard": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "block_producer_kickout_threshold": 80, + "chunk_producer_kickout_threshold": 80, + "chunk_validator_only_kickout_threshold": 70, + "target_validator_mandates_per_shard": 68, + "validator_max_kickout_stake_perc": 30, + "online_min_threshold": [ + 90, + 100 + ], + "online_max_threshold": [ + 99, + 100 + ], + "fishermen_threshold": 340282366920938463463374607431768211455, + "minimum_stake_divisor": 10, + "protocol_upgrade_stake_threshold": [ + 4, + 5 + ], + "shard_layout": { + "V1": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shards_split_map": [ + [ + 0 + ], + [ + 1 + ], + [ + 2, + 3 + ], + [ + 4 + ], + [ + 5 + ] + ], + "to_parent_shard_map": [ + 0, + 1, + 2, + 2, + 3, + 4 + ], + "version": 3 + } + }, + "validator_selection_config": { + "num_chunk_producer_seats": 100, + "num_chunk_validator_seats": 300, + "num_chunk_only_producer_seats": 0, + "minimum_validators_per_shard": 1, + "minimum_stake_ratio": [ + 1, + 62500 + ], + "chunk_producer_assignment_changes_limit": 5, + "shuffle_shard_assignment_for_chunk_producers": true + } +} \ No newline at end of file diff --git a/core/primitives/res/epoch_configs/mocknet/71.json b/core/primitives/res/epoch_configs/mocknet/71.json index 45b792e8127..035ccf5a5d6 100644 --- a/core/primitives/res/epoch_configs/mocknet/71.json +++ b/core/primitives/res/epoch_configs/mocknet/71.json @@ -1,89 +1,89 @@ { - "epoch_length": 2000, - "num_block_producer_seats": 100, - "num_block_producer_seats_per_shard": [ - 100, - 100, - 100, - 100, - 100, - 100 - ], - "avg_hidden_validator_seats_per_shard": [ - 0, - 0, - 0, - 0, - 0, - 0 - ], - "block_producer_kickout_threshold": 80, - "chunk_producer_kickout_threshold": 80, - "chunk_validator_only_kickout_threshold": 80, - "target_validator_mandates_per_shard": 68, - "validator_max_kickout_stake_perc": 30, - "online_min_threshold": [ - 90, - 100 - ], - "online_max_threshold": [ - 99, - 100 - ], - "fishermen_threshold": 340282366920938463463374607431768211455, - "minimum_stake_divisor": 10, - "protocol_upgrade_stake_threshold": [ - 4, - 5 - ], - "shard_layout": { - "V1": { - "boundary_accounts": [ - "aurora", - "aurora-0", - "game.hot.tg", - "kkuuue2akv_1630967379.near", - "tge-lockup.sweat" + "epoch_length": 2000, + "num_block_producer_seats": 100, + "num_block_producer_seats_per_shard": [ + 100, + 100, + 100, + 100, + 100, + 100 + ], + "avg_hidden_validator_seats_per_shard": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "block_producer_kickout_threshold": 80, + "chunk_producer_kickout_threshold": 80, + "chunk_validator_only_kickout_threshold": 80, + "target_validator_mandates_per_shard": 68, + "validator_max_kickout_stake_perc": 30, + "online_min_threshold": [ + 90, + 100 + ], + "online_max_threshold": [ + 99, + 100 + ], + "fishermen_threshold": 340282366920938463463374607431768211455, + "minimum_stake_divisor": 10, + "protocol_upgrade_stake_threshold": [ + 4, + 5 + ], + "shard_layout": { + "V1": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shards_split_map": [ + [ + 0 ], - "shards_split_map": [ - [ - 0 - ], - [ - 1 - ], - [ - 2, - 3 - ], - [ - 4 - ], - [ - 5 - ] + [ + 1 ], - "to_parent_shard_map": [ - 0, - 1, - 2, + [ 2, - 3, + 3 + ], + [ 4 ], - "version": 3 - } - }, - "validator_selection_config": { - "num_chunk_producer_seats": 100, - "num_chunk_validator_seats": 300, - "num_chunk_only_producer_seats": 0, - "minimum_validators_per_shard": 1, - "minimum_stake_ratio": [ + [ + 5 + ] + ], + "to_parent_shard_map": [ + 0, 1, - 62500 + 2, + 2, + 3, + 4 ], - "chunk_producer_assignment_changes_limit": 5, - "shuffle_shard_assignment_for_chunk_producers": true + "version": 3 } - } \ No newline at end of file + }, + "validator_selection_config": { + "num_chunk_producer_seats": 100, + "num_chunk_validator_seats": 300, + "num_chunk_only_producer_seats": 0, + "minimum_validators_per_shard": 1, + "minimum_stake_ratio": [ + 1, + 62500 + ], + "chunk_producer_assignment_changes_limit": 5, + "shuffle_shard_assignment_for_chunk_producers": true + } +} \ No newline at end of file diff --git a/core/primitives/res/epoch_configs/testnet/145.json b/core/primitives/res/epoch_configs/testnet/145.json new file mode 100644 index 00000000000..ae6ca2a0066 --- /dev/null +++ b/core/primitives/res/epoch_configs/testnet/145.json @@ -0,0 +1,89 @@ +{ + "epoch_length": 43200, + "num_block_producer_seats": 20, + "num_block_producer_seats_per_shard": [ + 20, + 20, + 20, + 20, + 20, + 20 + ], + "avg_hidden_validator_seats_per_shard": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "block_producer_kickout_threshold": 80, + "chunk_producer_kickout_threshold": 80, + "chunk_validator_only_kickout_threshold": 70, + "target_validator_mandates_per_shard": 68, + "validator_max_kickout_stake_perc": 30, + "online_min_threshold": [ + 90, + 100 + ], + "online_max_threshold": [ + 99, + 100 + ], + "fishermen_threshold": 340282366920938463463374607431768211455, + "minimum_stake_divisor": 10, + "protocol_upgrade_stake_threshold": [ + 4, + 5 + ], + "shard_layout": { + "V1": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shards_split_map": [ + [ + 0 + ], + [ + 1 + ], + [ + 2, + 3 + ], + [ + 4 + ], + [ + 5 + ] + ], + "to_parent_shard_map": [ + 0, + 1, + 2, + 2, + 3, + 4 + ], + "version": 3 + } + }, + "validator_selection_config": { + "num_chunk_producer_seats": 20, + "num_chunk_validator_seats": 300, + "num_chunk_only_producer_seats": 0, + "minimum_validators_per_shard": 1, + "minimum_stake_ratio": [ + 1, + 62500 + ], + "chunk_producer_assignment_changes_limit": 5, + "shuffle_shard_assignment_for_chunk_producers": true + } +} \ No newline at end of file diff --git a/core/primitives/res/epoch_configs/testnet/71.json b/core/primitives/res/epoch_configs/testnet/71.json index fc2f46e0ee1..f89cd337f1c 100644 --- a/core/primitives/res/epoch_configs/testnet/71.json +++ b/core/primitives/res/epoch_configs/testnet/71.json @@ -1,89 +1,89 @@ { - "epoch_length": 43200, - "num_block_producer_seats": 20, - "num_block_producer_seats_per_shard": [ - 20, - 20, - 20, - 20, - 20, - 20 - ], - "avg_hidden_validator_seats_per_shard": [ - 0, - 0, - 0, - 0, - 0, - 0 - ], - "block_producer_kickout_threshold": 80, - "chunk_producer_kickout_threshold": 80, - "chunk_validator_only_kickout_threshold": 80, - "target_validator_mandates_per_shard": 68, - "validator_max_kickout_stake_perc": 30, - "online_min_threshold": [ - 90, - 100 - ], - "online_max_threshold": [ - 99, - 100 - ], - "fishermen_threshold": 340282366920938463463374607431768211455, - "minimum_stake_divisor": 10, - "protocol_upgrade_stake_threshold": [ - 4, - 5 - ], - "shard_layout": { - "V1": { - "boundary_accounts": [ - "aurora", - "aurora-0", - "game.hot.tg", - "kkuuue2akv_1630967379.near", - "tge-lockup.sweat" + "epoch_length": 43200, + "num_block_producer_seats": 20, + "num_block_producer_seats_per_shard": [ + 20, + 20, + 20, + 20, + 20, + 20 + ], + "avg_hidden_validator_seats_per_shard": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "block_producer_kickout_threshold": 80, + "chunk_producer_kickout_threshold": 80, + "chunk_validator_only_kickout_threshold": 80, + "target_validator_mandates_per_shard": 68, + "validator_max_kickout_stake_perc": 30, + "online_min_threshold": [ + 90, + 100 + ], + "online_max_threshold": [ + 99, + 100 + ], + "fishermen_threshold": 340282366920938463463374607431768211455, + "minimum_stake_divisor": 10, + "protocol_upgrade_stake_threshold": [ + 4, + 5 + ], + "shard_layout": { + "V1": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shards_split_map": [ + [ + 0 ], - "shards_split_map": [ - [ - 0 - ], - [ - 1 - ], - [ - 2, - 3 - ], - [ - 4 - ], - [ - 5 - ] + [ + 1 ], - "to_parent_shard_map": [ - 0, - 1, - 2, + [ 2, - 3, + 3 + ], + [ 4 ], - "version": 3 - } - }, - "validator_selection_config": { - "num_chunk_producer_seats": 20, - "num_chunk_validator_seats": 300, - "num_chunk_only_producer_seats": 0, - "minimum_validators_per_shard": 1, - "minimum_stake_ratio": [ + [ + 5 + ] + ], + "to_parent_shard_map": [ + 0, 1, - 62500 + 2, + 2, + 3, + 4 ], - "chunk_producer_assignment_changes_limit": 5, - "shuffle_shard_assignment_for_chunk_producers": false + "version": 3 } - } \ No newline at end of file + }, + "validator_selection_config": { + "num_chunk_producer_seats": 20, + "num_chunk_validator_seats": 300, + "num_chunk_only_producer_seats": 0, + "minimum_validators_per_shard": 1, + "minimum_stake_ratio": [ + 1, + 62500 + ], + "chunk_producer_assignment_changes_limit": 5, + "shuffle_shard_assignment_for_chunk_producers": false + } +} \ No newline at end of file diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index b797f203a63..281be439907 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -182,6 +182,8 @@ impl AllEpochConfig { Self::config_fix_min_stake_ratio(&mut config, protocol_version); + Self::config_chunk_endorsement_thresholds(&mut config, protocol_version); + Self::config_test_overrides(&mut config, &self.test_overrides); config @@ -305,6 +307,12 @@ impl AllEpochConfig { } } + fn config_chunk_endorsement_thresholds(config: &mut EpochConfig, protocol_version: u32) { + if ProtocolFeature::ChunkEndorsementsInBlockHeader.enabled(protocol_version) { + config.chunk_validator_only_kickout_threshold = 70; + } + } + fn config_test_overrides( config: &mut EpochConfig, test_overrides: &AllEpochConfigTestOverrides, @@ -392,6 +400,7 @@ static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[ include_config!("mainnet", 100, "100.json"), include_config!("mainnet", 101, "101.json"), include_config!("mainnet", 143, "143.json"), + include_config!("mainnet", 145, "145.json"), // Epoch configs for testnet (genesis protool version is 29). include_config!("testnet", 29, "29.json"), include_config!("testnet", 48, "48.json"), @@ -404,6 +413,7 @@ static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[ include_config!("testnet", 100, "100.json"), include_config!("testnet", 101, "101.json"), include_config!("testnet", 143, "143.json"), + include_config!("testnet", 145, "145.json"), // Epoch configs for mocknet (forknet) (genesis protool version is 29). // TODO(#11900): Check the forknet config and uncomment this. // include_config!("mocknet", 29, "29.json"), @@ -415,6 +425,7 @@ static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[ // include_config!("mocknet", 71, "71.json"), // include_config!("mocknet", 100, "100.json"), // include_config!("mocknet", 101, "101.json"), + // include_config!("mocknet", 145, "145.json"), ]; /// Store for `[EpochConfig]` per protocol version.` @@ -431,7 +442,10 @@ impl EpochConfigStore { for (chain, version, content) in CONFIGS.iter() { if *chain == chain_id { let config: EpochConfig = serde_json::from_str(*content).unwrap_or_else(|e| { - panic!("Failed to load epoch config files for chain {}: {:#}", chain_id, e) + panic!( + "Failed to load epoch config files for chain {} and version {}: {:#}", + chain_id, version, e + ) }); store.insert(*version, Arc::new(config)); } @@ -496,7 +510,7 @@ mod tests { } #[test] - fn test_epoch_config_store_ainnet() { + fn test_epoch_config_store_mainnet() { test_epoch_config_store("mainnet", 29); }