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