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#[derive(Clone, Eq, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
23pub struct EpochConfig {
24 pub epoch_length: BlockHeightDelta,
26 pub num_block_producer_seats: NumSeats,
28 pub num_block_producer_seats_per_shard: Vec<NumSeats>,
30 pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
32 pub block_producer_kickout_threshold: u8,
34 pub chunk_producer_kickout_threshold: u8,
36 pub chunk_validator_only_kickout_threshold: u8,
38 pub target_validator_mandates_per_shard: NumSeats,
40 pub validator_max_kickout_stake_perc: u8,
42 pub online_min_threshold: Rational32,
44 pub online_max_threshold: Rational32,
46 pub fishermen_threshold: Balance,
48 pub minimum_stake_divisor: u64,
50 pub protocol_upgrade_stake_threshold: Rational32,
52 pub shard_layout: ShardLayout,
54 pub num_chunk_producer_seats: NumSeats,
58 pub num_chunk_validator_seats: NumSeats,
60 pub num_chunk_only_producer_seats: NumSeats,
64 pub minimum_validators_per_shard: NumSeats,
66 pub minimum_stake_ratio: Rational32,
68 pub chunk_producer_assignment_changes_limit: NumSeats,
73 pub shuffle_shard_assignment_for_chunk_producers: bool,
75 pub max_inflation_rate: Rational32,
76}
77
78impl EpochConfig {
79 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 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 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#[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#[derive(Debug, Clone)]
221pub struct AllEpochConfig {
222 config_store: EpochConfigStore,
225 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 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 pub all_proposals: Vec<ValidatorStake>,
269 pub validator_kickout: HashMap<AccountId, ValidatorKickoutReason>,
271 pub validator_block_chunk_stats: HashMap<AccountId, BlockChunkValidatorStats>,
273 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
294static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[
296 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 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#[derive(Debug, Clone)]
332pub struct EpochConfigStore {
333 store: BTreeMap<ProtocolVersion, Arc<EpochConfig>>,
334}
335
336impl EpochConfigStore {
337 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 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 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 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 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 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 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 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 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 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 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}