near_primitives/
epoch_manager.rs

1use crate::num_rational::Rational32;
2use crate::shard_layout::ShardLayout;
3use crate::types::validator_stake::ValidatorStake;
4use crate::types::{
5    AccountId, Balance, BlockChunkValidatorStats, BlockHeightDelta, NumSeats, ProtocolVersion,
6    ValidatorKickoutReason,
7};
8use borsh::{BorshDeserialize, BorshSerialize};
9use near_primitives_core::hash::CryptoHash;
10use near_primitives_core::serialize::dec_format;
11use near_primitives_core::version::PROTOCOL_VERSION;
12use near_schema_checker_lib::ProtocolSchema;
13use std::collections::{BTreeMap, HashMap};
14use std::fs;
15use std::ops::Bound;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19pub const AGGREGATOR_KEY: &[u8] = b"AGGREGATOR";
20
21/// Epoch config, determines validator assignment for given epoch.
22/// Can change from epoch to epoch depending on the sharding and other parameters, etc.
23#[derive(Clone, Eq, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
24pub struct EpochConfig {
25    /// Epoch length in block heights.
26    pub epoch_length: BlockHeightDelta,
27    /// Number of seats for block producers.
28    pub num_block_producer_seats: NumSeats,
29    /// Number of seats of block producers per each shard.
30    pub num_block_producer_seats_per_shard: Vec<NumSeats>,
31    /// Expected number of hidden validator seats per each shard.
32    pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
33    /// Threshold for kicking out block producers.
34    pub block_producer_kickout_threshold: u8,
35    /// Threshold for kicking out chunk producers.
36    pub chunk_producer_kickout_threshold: u8,
37    /// Threshold for kicking out nodes which are only chunk validators.
38    pub chunk_validator_only_kickout_threshold: u8,
39    /// Number of target chunk validator mandates for each shard.
40    pub target_validator_mandates_per_shard: NumSeats,
41    /// Max ratio of validators that we can kick out in an epoch
42    pub validator_max_kickout_stake_perc: u8,
43    /// Online minimum threshold below which validator doesn't receive reward.
44    pub online_min_threshold: Rational32,
45    /// Online maximum threshold above which validator gets full reward.
46    pub online_max_threshold: Rational32,
47    /// Stake threshold for becoming a fisherman.
48    #[serde(with = "dec_format")]
49    pub fishermen_threshold: Balance,
50    /// The minimum stake required for staking is last seat price divided by this number.
51    pub minimum_stake_divisor: u64,
52    /// Threshold of stake that needs to indicate that they ready for upgrade.
53    pub protocol_upgrade_stake_threshold: Rational32,
54    /// Shard layout of this epoch, may change from epoch to epoch
55    pub shard_layout: ShardLayout,
56    /// Additional configuration parameters for the new validator selection
57    /// algorithm. See <https://github.com/near/NEPs/pull/167> for details.
58    // #[default(100)]
59    pub num_chunk_producer_seats: NumSeats,
60    // #[default(300)]
61    pub num_chunk_validator_seats: NumSeats,
62    // TODO (#11267): deprecate after StatelessValidationV0 is in place.
63    // Use 300 for older protocol versions.
64    // #[default(300)]
65    pub num_chunk_only_producer_seats: NumSeats,
66    // #[default(1)]
67    pub minimum_validators_per_shard: NumSeats,
68    // #[default(Rational32::new(160, 1_000_000))]
69    pub minimum_stake_ratio: Rational32,
70    // #[default(5)]
71    /// Limits the number of shard changes in chunk producer assignments,
72    /// if algorithm is able to choose assignment with better balance of
73    /// number of chunk producers for shards.
74    pub chunk_producer_assignment_changes_limit: NumSeats,
75    // #[default(false)]
76    pub shuffle_shard_assignment_for_chunk_producers: bool,
77    pub max_inflation_rate: Rational32,
78}
79
80impl EpochConfig {
81    /// Total number of validator seats in the epoch since protocol version 69.
82    pub fn num_validators(&self) -> NumSeats {
83        self.num_block_producer_seats
84            .max(self.num_chunk_producer_seats)
85            .max(self.num_chunk_validator_seats)
86    }
87}
88
89impl EpochConfig {
90    // Create test-only epoch config.
91    // Not depends on genesis!
92    pub fn genesis_test(
93        num_block_producer_seats: NumSeats,
94        shard_layout: ShardLayout,
95        epoch_length: BlockHeightDelta,
96        block_producer_kickout_threshold: u8,
97        chunk_producer_kickout_threshold: u8,
98        chunk_validator_only_kickout_threshold: u8,
99        protocol_upgrade_stake_threshold: Rational32,
100        fishermen_threshold: Balance,
101    ) -> Self {
102        Self {
103            epoch_length,
104            num_block_producer_seats,
105            num_block_producer_seats_per_shard: vec![
106                num_block_producer_seats;
107                shard_layout.shard_ids().count()
108            ],
109            avg_hidden_validator_seats_per_shard: vec![],
110            target_validator_mandates_per_shard: 68,
111            validator_max_kickout_stake_perc: 100,
112            online_min_threshold: Rational32::new(90, 100),
113            online_max_threshold: Rational32::new(99, 100),
114            minimum_stake_divisor: 10,
115            protocol_upgrade_stake_threshold,
116            block_producer_kickout_threshold,
117            chunk_producer_kickout_threshold,
118            chunk_validator_only_kickout_threshold,
119            fishermen_threshold,
120            shard_layout,
121            num_chunk_producer_seats: 100,
122            num_chunk_validator_seats: 300,
123            num_chunk_only_producer_seats: 300,
124            minimum_validators_per_shard: 1,
125            minimum_stake_ratio: Rational32::new(160i32, 1_000_000i32),
126            chunk_producer_assignment_changes_limit: 5,
127            shuffle_shard_assignment_for_chunk_producers: false,
128            max_inflation_rate: Rational32::new(1, 40),
129        }
130    }
131
132    /// Minimal config for testing.
133    pub fn minimal() -> Self {
134        Self {
135            epoch_length: 0,
136            num_block_producer_seats: 0,
137            num_block_producer_seats_per_shard: vec![],
138            avg_hidden_validator_seats_per_shard: vec![],
139            block_producer_kickout_threshold: 0,
140            chunk_producer_kickout_threshold: 0,
141            chunk_validator_only_kickout_threshold: 0,
142            target_validator_mandates_per_shard: 0,
143            validator_max_kickout_stake_perc: 0,
144            online_min_threshold: 0.into(),
145            online_max_threshold: 0.into(),
146            fishermen_threshold: 0,
147            minimum_stake_divisor: 0,
148            protocol_upgrade_stake_threshold: 0.into(),
149            shard_layout: ShardLayout::single_shard(),
150            num_chunk_producer_seats: 100,
151            num_chunk_validator_seats: 300,
152            num_chunk_only_producer_seats: 300,
153            minimum_validators_per_shard: 1,
154            minimum_stake_ratio: Rational32::new(160i32, 1_000_000i32),
155            chunk_producer_assignment_changes_limit: 5,
156            shuffle_shard_assignment_for_chunk_producers: false,
157            max_inflation_rate: Rational32::new(1, 40),
158        }
159    }
160
161    pub fn mock(epoch_length: BlockHeightDelta, shard_layout: ShardLayout) -> Self {
162        Self {
163            epoch_length,
164            num_block_producer_seats: 2,
165            num_block_producer_seats_per_shard: vec![1, 1],
166            avg_hidden_validator_seats_per_shard: vec![1, 1],
167            block_producer_kickout_threshold: 0,
168            chunk_producer_kickout_threshold: 0,
169            chunk_validator_only_kickout_threshold: 0,
170            target_validator_mandates_per_shard: 1,
171            validator_max_kickout_stake_perc: 0,
172            online_min_threshold: Rational32::new(1i32, 4i32),
173            online_max_threshold: Rational32::new(3i32, 4i32),
174            fishermen_threshold: 1,
175            minimum_stake_divisor: 1,
176            protocol_upgrade_stake_threshold: Rational32::new(3i32, 4i32),
177            shard_layout,
178            num_chunk_producer_seats: 100,
179            num_chunk_validator_seats: 300,
180            num_chunk_only_producer_seats: 300,
181            minimum_validators_per_shard: 1,
182            minimum_stake_ratio: Rational32::new(160i32, 1_000_000i32),
183            chunk_producer_assignment_changes_limit: 5,
184            shuffle_shard_assignment_for_chunk_producers: false,
185            max_inflation_rate: Rational32::new(1, 40),
186        }
187    }
188}
189
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
191pub struct ShardConfig {
192    pub num_block_producer_seats_per_shard: Vec<NumSeats>,
193    pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
194    pub shard_layout: ShardLayout,
195}
196
197impl ShardConfig {
198    pub fn new(epoch_config: EpochConfig) -> Self {
199        Self {
200            num_block_producer_seats_per_shard: epoch_config
201                .num_block_producer_seats_per_shard
202                .clone(),
203            avg_hidden_validator_seats_per_shard: epoch_config
204                .avg_hidden_validator_seats_per_shard
205                .clone(),
206            shard_layout: epoch_config.shard_layout,
207        }
208    }
209}
210
211/// Testing overrides to apply to the EpochConfig returned by the `for_protocol_version`.
212/// All fields should be optional and the default should be a no-op.
213#[derive(Clone, Debug, Default)]
214pub struct AllEpochConfigTestOverrides {
215    pub block_producer_kickout_threshold: Option<u8>,
216    pub chunk_producer_kickout_threshold: Option<u8>,
217}
218
219/// AllEpochConfig manages protocol configs that might be changing throughout epochs (hence EpochConfig).
220/// The main function in AllEpochConfig is ::for_protocol_version which takes a protocol version
221/// and returns the EpochConfig that should be used for this protocol version.
222#[derive(Debug, Clone)]
223pub struct AllEpochConfig {
224    /// Store for EpochConfigs, provides configs per protocol version.
225    /// Initialized only for production, ie. when `use_protocol_version` is true.
226    config_store: EpochConfigStore,
227    /// Chain Id. Some parameters are specific to certain chains.
228    chain_id: String,
229    epoch_length: BlockHeightDelta,
230}
231
232impl AllEpochConfig {
233    pub fn from_epoch_config_store(
234        chain_id: &str,
235        epoch_length: BlockHeightDelta,
236        config_store: EpochConfigStore,
237    ) -> Self {
238        Self { config_store, chain_id: chain_id.to_string(), epoch_length }
239    }
240
241    pub fn for_protocol_version(&self, protocol_version: ProtocolVersion) -> EpochConfig {
242        let mut config = self.config_store.get_config(protocol_version).as_ref().clone();
243        // TODO(#11265): epoch length is overridden in many tests so we
244        // need to support it here. Consider removing `epoch_length` from
245        // EpochConfig.
246        config.epoch_length = self.epoch_length;
247        config
248    }
249
250    pub fn chain_id(&self) -> &str {
251        &self.chain_id
252    }
253}
254
255#[derive(BorshSerialize, BorshDeserialize, ProtocolSchema)]
256pub struct EpochSummary {
257    pub prev_epoch_last_block_hash: CryptoHash,
258    /// Proposals from the epoch, only the latest one per account
259    pub all_proposals: Vec<ValidatorStake>,
260    /// Kickout set, includes slashed
261    pub validator_kickout: HashMap<AccountId, ValidatorKickoutReason>,
262    /// Only for validators who met the threshold and didn't get slashed
263    pub validator_block_chunk_stats: HashMap<AccountId, BlockChunkValidatorStats>,
264    /// Protocol version for next next epoch, as summary of epoch T defines
265    /// epoch T+2.
266    pub next_next_epoch_version: ProtocolVersion,
267}
268
269macro_rules! include_config {
270    ($chain:expr, $version:expr, $file:expr) => {
271        (
272            $chain,
273            $version,
274            include_str!(concat!(
275                env!("CARGO_MANIFEST_DIR"),
276                "/res/epoch_configs/",
277                $chain,
278                "/",
279                $file
280            )),
281        )
282    };
283}
284
285/// List of (chain_id, version, JSON content) tuples used to initialize the EpochConfigStore.
286static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[
287    // Epoch configs for mainnet (genesis protocol version is 29).
288    include_config!("mainnet", 29, "29.json"),
289    include_config!("mainnet", 48, "48.json"),
290    include_config!("mainnet", 56, "56.json"),
291    include_config!("mainnet", 64, "64.json"),
292    include_config!("mainnet", 65, "65.json"),
293    include_config!("mainnet", 69, "69.json"),
294    include_config!("mainnet", 70, "70.json"),
295    include_config!("mainnet", 71, "71.json"),
296    include_config!("mainnet", 72, "72.json"),
297    include_config!("mainnet", 75, "75.json"),
298    include_config!("mainnet", 76, "76.json"),
299    include_config!("mainnet", 78, "78.json"),
300    include_config!("mainnet", 80, "80.json"),
301    include_config!("mainnet", 81, "81.json"),
302    include_config!("mainnet", 143, "143.json"),
303    // Epoch configs for testnet (genesis protocol version is 29).
304    include_config!("testnet", 29, "29.json"),
305    include_config!("testnet", 48, "48.json"),
306    include_config!("testnet", 56, "56.json"),
307    include_config!("testnet", 64, "64.json"),
308    include_config!("testnet", 65, "65.json"),
309    include_config!("testnet", 69, "69.json"),
310    include_config!("testnet", 70, "70.json"),
311    include_config!("testnet", 71, "71.json"),
312    include_config!("testnet", 72, "72.json"),
313    include_config!("testnet", 75, "75.json"),
314    include_config!("testnet", 76, "76.json"),
315    include_config!("testnet", 78, "78.json"),
316    include_config!("testnet", 80, "80.json"),
317    include_config!("testnet", 81, "81.json"),
318    include_config!("testnet", 143, "143.json"),
319];
320
321/// Store for `[EpochConfig]` per protocol version.`
322#[derive(Debug, Clone)]
323pub struct EpochConfigStore {
324    store: BTreeMap<ProtocolVersion, Arc<EpochConfig>>,
325}
326
327impl EpochConfigStore {
328    /// Creates a config store to contain the EpochConfigs for the given chain parsed from the JSON files.
329    /// If no configs are found for the given chain, try to load the configs from the file system.
330    /// If there are no configs found, return None.
331    pub fn for_chain_id(chain_id: &str, config_dir: Option<PathBuf>) -> Option<Self> {
332        let mut store = Self::load_default_epoch_configs(chain_id);
333
334        if !store.is_empty() {
335            return Some(Self { store });
336        }
337        if let Some(config_dir) = config_dir {
338            store = Self::load_epoch_config_from_file_system(config_dir.to_str().unwrap());
339        }
340
341        if store.is_empty() { None } else { Some(Self { store }) }
342    }
343
344    /// Loads the default epoch configs for the given chain from the CONFIGS array.
345    fn load_default_epoch_configs(chain_id: &str) -> BTreeMap<ProtocolVersion, Arc<EpochConfig>> {
346        let mut store = BTreeMap::new();
347        for (chain, version, content) in CONFIGS {
348            if *chain == chain_id {
349                let config: EpochConfig = serde_json::from_str(*content).unwrap_or_else(|e| {
350                    panic!(
351                        "Failed to load epoch config files for chain {} and version {}: {:#}",
352                        chain_id, version, e
353                    )
354                });
355                store.insert(*version, Arc::new(config));
356            }
357        }
358        store
359    }
360
361    /// Reads the json files from the epoch config directory.
362    fn load_epoch_config_from_file_system(
363        directory: &str,
364    ) -> BTreeMap<ProtocolVersion, Arc<EpochConfig>> {
365        fn get_epoch_config(
366            dir_entry: fs::DirEntry,
367        ) -> Option<(ProtocolVersion, Arc<EpochConfig>)> {
368            let path = dir_entry.path();
369            if !(path.extension()? == "json") {
370                return None;
371            }
372            let file_name = path.file_stem()?.to_str()?.to_string();
373            let protocol_version = file_name.parse().expect("Invalid protocol version");
374            if protocol_version > PROTOCOL_VERSION {
375                return None;
376            }
377            let contents = fs::read_to_string(&path).ok()?;
378            let epoch_config = serde_json::from_str(&contents).unwrap_or_else(|_| {
379                panic!("Failed to parse epoch config for version {}", protocol_version)
380            });
381            Some((protocol_version, epoch_config))
382        }
383
384        fs::read_dir(directory)
385            .expect("Failed opening epoch config directory")
386            .filter_map(Result::ok)
387            .filter_map(get_epoch_config)
388            .collect()
389    }
390
391    pub fn test(store: BTreeMap<ProtocolVersion, Arc<EpochConfig>>) -> Self {
392        Self { store }
393    }
394
395    pub fn test_single_version(
396        protocol_version: ProtocolVersion,
397        epoch_config: EpochConfig,
398    ) -> Self {
399        Self::test(BTreeMap::from([(protocol_version, Arc::new(epoch_config))]))
400    }
401
402    /// Returns the EpochConfig for the given protocol version.
403    /// This panics if no config is found for the given version, thus the initialization via `for_chain_id` should
404    /// only be performed for chains with some configs stored in files.
405    pub fn get_config(&self, protocol_version: ProtocolVersion) -> &Arc<EpochConfig> {
406        self.store
407            .range((Bound::Unbounded, Bound::Included(protocol_version)))
408            .next_back()
409            .unwrap_or_else(|| {
410                panic!("Failed to find EpochConfig for protocol version {}", protocol_version)
411            })
412            .1
413    }
414
415    fn dump_epoch_config(directory: &Path, version: &ProtocolVersion, config: &Arc<EpochConfig>) {
416        let content = serde_json::to_string_pretty(config.as_ref()).unwrap();
417        let path = PathBuf::from(directory).join(format!("{}.json", version));
418        fs::write(path, content).unwrap();
419    }
420
421    /// Dumps all the configs between the beginning and end protocol versions to the given directory.
422    /// If the beginning version doesn't exist, the closest config to it will be dumped.
423    pub fn dump_epoch_configs_between(
424        &self,
425        first_version: Option<&ProtocolVersion>,
426        last_version: Option<&ProtocolVersion>,
427        directory: impl AsRef<Path>,
428    ) {
429        // Dump all the configs between the beginning and end versions, inclusive.
430        self.store
431            .iter()
432            .filter(|(version, _)| {
433                first_version.is_none_or(|first_version| *version >= first_version)
434            })
435            .filter(|(version, _)| last_version.is_none_or(|last_version| *version <= last_version))
436            .for_each(|(version, config)| {
437                Self::dump_epoch_config(directory.as_ref(), version, config);
438            });
439
440        // Dump the closest config to the beginning version if it doesn't exist.
441        if let Some(first_version) = first_version {
442            if !self.store.contains_key(&first_version) {
443                let config = self.get_config(*first_version);
444                Self::dump_epoch_config(directory.as_ref(), first_version, config);
445            }
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::EpochConfigStore;
453    use crate::epoch_manager::EpochConfig;
454    use near_primitives_core::types::ProtocolVersion;
455    use near_primitives_core::version::PROTOCOL_VERSION;
456    use num_rational::Rational32;
457    use std::fs;
458    use std::path::Path;
459
460    #[test]
461    fn test_dump_epoch_configs_mainnet() {
462        let tmp_dir = tempfile::tempdir().unwrap();
463        EpochConfigStore::for_chain_id("mainnet", None).unwrap().dump_epoch_configs_between(
464            Some(&55),
465            Some(&68),
466            tmp_dir.path().to_str().unwrap(),
467        );
468
469        // Check if tmp dir contains the dumped files. 55, 64, 65.
470        let dumped_files = fs::read_dir(tmp_dir.path()).unwrap();
471        let dumped_files: Vec<_> =
472            dumped_files.map(|entry| entry.unwrap().file_name().into_string().unwrap()).collect();
473
474        assert!(dumped_files.contains(&String::from("55.json")));
475        assert!(dumped_files.contains(&String::from("64.json")));
476        assert!(dumped_files.contains(&String::from("65.json")));
477
478        // Check if 55.json is equal to 48.json from res/epoch_configs/mainnet.
479        let contents_55 = fs::read_to_string(tmp_dir.path().join("55.json")).unwrap();
480        let epoch_config_55: EpochConfig = serde_json::from_str(&contents_55).unwrap();
481        let epoch_config_48 = parse_config_file("mainnet", 48).unwrap();
482        assert_eq!(epoch_config_55, epoch_config_48);
483    }
484
485    #[test]
486    fn test_dump_and_load_epoch_configs_mainnet() {
487        let tmp_dir = tempfile::tempdir().unwrap();
488        let epoch_configs = EpochConfigStore::for_chain_id("mainnet", None).unwrap();
489        epoch_configs.dump_epoch_configs_between(
490            Some(&55),
491            Some(&68),
492            tmp_dir.path().to_str().unwrap(),
493        );
494
495        let loaded_epoch_configs = EpochConfigStore::test(
496            EpochConfigStore::load_epoch_config_from_file_system(tmp_dir.path().to_str().unwrap()),
497        );
498
499        // Insert a config for an newer protocol version. It should be ignored.
500        EpochConfigStore::dump_epoch_config(
501            tmp_dir.path(),
502            &(PROTOCOL_VERSION + 1),
503            &epoch_configs.get_config(PROTOCOL_VERSION),
504        );
505
506        let loaded_after_insert_epoch_configs = EpochConfigStore::test(
507            EpochConfigStore::load_epoch_config_from_file_system(tmp_dir.path().to_str().unwrap()),
508        );
509        assert_eq!(loaded_epoch_configs.store, loaded_after_insert_epoch_configs.store);
510
511        // Insert a config for an older protocol version. It should be loaded.
512        EpochConfigStore::dump_epoch_config(
513            tmp_dir.path(),
514            &(PROTOCOL_VERSION - 22),
515            &epoch_configs.get_config(PROTOCOL_VERSION),
516        );
517
518        let loaded_after_insert_epoch_configs = EpochConfigStore::test(
519            EpochConfigStore::load_epoch_config_from_file_system(tmp_dir.path().to_str().unwrap()),
520        );
521        assert_ne!(loaded_epoch_configs.store, loaded_after_insert_epoch_configs.store);
522    }
523
524    #[test]
525    fn test_protocol_upgrade_80() {
526        for chain_id in ["mainnet", "testnet"] {
527            let epoch_configs = EpochConfigStore::for_chain_id(chain_id, None).unwrap();
528            let epoch_config = epoch_configs.get_config(80);
529            assert_eq!(epoch_config.num_chunk_validator_seats, 500);
530        }
531    }
532
533    #[test]
534    fn test_protocol_upgrade_81() {
535        for chain_id in ["mainnet", "testnet"] {
536            let epoch_configs = EpochConfigStore::for_chain_id(chain_id, None).unwrap();
537            let epoch_config = epoch_configs.get_config(81);
538            assert_eq!(epoch_config.max_inflation_rate, Rational32::new(1, 40));
539        }
540    }
541
542    fn parse_config_file(chain_id: &str, protocol_version: ProtocolVersion) -> Option<EpochConfig> {
543        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
544            .join("res/epoch_configs")
545            .join(chain_id)
546            .join(format!("{}.json", protocol_version));
547        if path.exists() {
548            let content = fs::read_to_string(path).unwrap();
549            let config: EpochConfig = serde_json::from_str(&content).unwrap();
550            Some(config)
551        } else {
552            None
553        }
554    }
555}