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#[derive(Clone, Eq, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
24pub struct EpochConfig {
25 pub epoch_length: BlockHeightDelta,
27 pub num_block_producer_seats: NumSeats,
29 pub num_block_producer_seats_per_shard: Vec<NumSeats>,
31 pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
33 pub block_producer_kickout_threshold: u8,
35 pub chunk_producer_kickout_threshold: u8,
37 pub chunk_validator_only_kickout_threshold: u8,
39 pub target_validator_mandates_per_shard: NumSeats,
41 pub validator_max_kickout_stake_perc: u8,
43 pub online_min_threshold: Rational32,
45 pub online_max_threshold: Rational32,
47 #[serde(with = "dec_format")]
49 pub fishermen_threshold: Balance,
50 pub minimum_stake_divisor: u64,
52 pub protocol_upgrade_stake_threshold: Rational32,
54 pub shard_layout: ShardLayout,
56 pub num_chunk_producer_seats: NumSeats,
60 pub num_chunk_validator_seats: NumSeats,
62 pub num_chunk_only_producer_seats: NumSeats,
66 pub minimum_validators_per_shard: NumSeats,
68 pub minimum_stake_ratio: Rational32,
70 pub chunk_producer_assignment_changes_limit: NumSeats,
75 pub shuffle_shard_assignment_for_chunk_producers: bool,
77 pub max_inflation_rate: Rational32,
78}
79
80impl EpochConfig {
81 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 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 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#[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#[derive(Debug, Clone)]
223pub struct AllEpochConfig {
224 config_store: EpochConfigStore,
227 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 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 pub all_proposals: Vec<ValidatorStake>,
260 pub validator_kickout: HashMap<AccountId, ValidatorKickoutReason>,
262 pub validator_block_chunk_stats: HashMap<AccountId, BlockChunkValidatorStats>,
264 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
285static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[
287 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 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#[derive(Debug, Clone)]
323pub struct EpochConfigStore {
324 store: BTreeMap<ProtocolVersion, Arc<EpochConfig>>,
325}
326
327impl EpochConfigStore {
328 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 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 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 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 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 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 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 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 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 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 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}