use crate::num_rational::Rational32;
use crate::shard_layout::ShardLayout;
use crate::types::validator_stake::ValidatorStake;
use crate::types::{
AccountId, Balance, BlockChunkValidatorStats, BlockHeightDelta, EpochHeight, NumSeats,
NumShards, ProtocolVersion, ShardId, ValidatorKickoutReason,
};
use borsh::{BorshDeserialize, BorshSerialize};
use near_primitives_core::hash::CryptoHash;
use near_primitives_core::version::PROTOCOL_VERSION;
use near_schema_checker_lib::ProtocolSchema;
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::ops::Bound;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub const AGGREGATOR_KEY: &[u8] = b"AGGREGATOR";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DynamicReshardingConfig {
pub memory_usage_threshold: u64,
pub min_child_memory_usage: u64,
pub max_number_of_shards: NumShards,
pub min_epochs_between_resharding: EpochHeight,
pub force_split_shards: Vec<ShardId>,
pub block_split_shards: Vec<ShardId>,
}
impl Default for DynamicReshardingConfig {
fn default() -> Self {
Self {
memory_usage_threshold: 999_999_999_999_999,
min_child_memory_usage: 999_999_999_999_999,
max_number_of_shards: 999_999_999_999_999,
min_epochs_between_resharding: 999_999_999_999_999,
force_split_shards: vec![],
block_split_shards: vec![],
}
}
}
#[derive(Clone, Eq, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ShardLayoutConfig {
Static { shard_layout: ShardLayout },
Dynamic { dynamic_resharding_config: DynamicReshardingConfig },
}
impl Default for ShardLayoutConfig {
fn default() -> Self {
ShardLayoutConfig::Static { shard_layout: ShardLayout::single_shard() }
}
}
impl ShardLayoutConfig {
pub fn static_shard_layout(&self) -> Option<&ShardLayout> {
match self {
ShardLayoutConfig::Static { shard_layout } => Some(shard_layout),
ShardLayoutConfig::Dynamic { .. } => None,
}
}
pub fn dynamic_resharding_config(&self) -> Option<&DynamicReshardingConfig> {
match self {
ShardLayoutConfig::Static { .. } => None,
ShardLayoutConfig::Dynamic { dynamic_resharding_config } => {
Some(dynamic_resharding_config)
}
}
}
}
#[derive(
Clone, Eq, Debug, PartialEq, serde::Serialize, serde::Deserialize, derive_builder::Builder,
)]
pub struct EpochConfig {
pub epoch_length: BlockHeightDelta,
pub num_block_producer_seats: NumSeats,
pub num_block_producer_seats_per_shard: Vec<NumSeats>,
pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
pub block_producer_kickout_threshold: u8,
pub chunk_producer_kickout_threshold: u8,
pub chunk_validator_only_kickout_threshold: u8,
pub target_validator_mandates_per_shard: NumSeats,
pub validator_max_kickout_stake_perc: u8,
pub online_min_threshold: Rational32,
pub online_max_threshold: Rational32,
pub fishermen_threshold: Balance,
pub minimum_stake_divisor: u64,
pub protocol_upgrade_stake_threshold: Rational32,
#[serde(flatten)]
#[builder(setter(custom))]
pub shard_layout_config: ShardLayoutConfig,
pub num_chunk_producer_seats: NumSeats,
pub num_chunk_validator_seats: NumSeats,
pub num_chunk_only_producer_seats: NumSeats,
pub minimum_validators_per_shard: NumSeats,
pub minimum_stake_ratio: Rational32,
pub chunk_producer_assignment_changes_limit: NumSeats,
pub shuffle_shard_assignment_for_chunk_producers: bool,
pub max_inflation_rate: Rational32,
}
impl EpochConfig {
pub fn num_validators(&self) -> NumSeats {
self.num_block_producer_seats
.max(self.num_chunk_producer_seats)
.max(self.num_chunk_validator_seats)
}
pub fn static_shard_layout(&self) -> Option<ShardLayout> {
self.shard_layout_config.static_shard_layout().cloned()
}
pub fn dynamic_resharding_config(&self) -> Option<&DynamicReshardingConfig> {
self.shard_layout_config.dynamic_resharding_config()
}
pub fn with_shard_layout(mut self, shard_layout: ShardLayout) -> Self {
self.shard_layout_config = ShardLayoutConfig::Static { shard_layout };
self
}
}
impl EpochConfigBuilder {
pub fn shard_layout(&mut self, shard_layout: ShardLayout) -> &mut Self {
self.shard_layout_config = Some(ShardLayoutConfig::Static { shard_layout });
self
}
pub fn dynamic_resharding_config(
&mut self,
dynamic_resharding_config: DynamicReshardingConfig,
) -> &mut Self {
self.shard_layout_config = Some(ShardLayoutConfig::Dynamic { dynamic_resharding_config });
self
}
pub fn shard_layout_config(&mut self, config: ShardLayoutConfig) -> &mut Self {
self.shard_layout_config = Some(config);
self
}
}
impl EpochConfig {
pub fn genesis_test(
num_block_producer_seats: NumSeats,
shard_layout: ShardLayout,
epoch_length: BlockHeightDelta,
block_producer_kickout_threshold: u8,
chunk_producer_kickout_threshold: u8,
chunk_validator_only_kickout_threshold: u8,
protocol_upgrade_stake_threshold: Rational32,
fishermen_threshold: Balance,
) -> Self {
Self {
epoch_length,
num_block_producer_seats,
num_block_producer_seats_per_shard: vec![
num_block_producer_seats;
shard_layout.shard_ids().count()
],
avg_hidden_validator_seats_per_shard: vec![],
target_validator_mandates_per_shard: 68,
validator_max_kickout_stake_perc: 100,
online_min_threshold: Rational32::new(90, 100),
online_max_threshold: Rational32::new(99, 100),
minimum_stake_divisor: 10,
protocol_upgrade_stake_threshold,
block_producer_kickout_threshold,
chunk_producer_kickout_threshold,
chunk_validator_only_kickout_threshold,
fishermen_threshold,
shard_layout_config: ShardLayoutConfig::Static { shard_layout },
num_chunk_producer_seats: 100,
num_chunk_validator_seats: 300,
num_chunk_only_producer_seats: 300,
minimum_validators_per_shard: 1,
minimum_stake_ratio: Rational32::new(160i32, 1_000_000i32),
chunk_producer_assignment_changes_limit: 5,
shuffle_shard_assignment_for_chunk_producers: false,
max_inflation_rate: Rational32::new(1, 40),
}
}
pub fn minimal() -> EpochConfigBuilder {
let mut builder = EpochConfigBuilder::default();
builder
.epoch_length(0)
.num_block_producer_seats(0)
.num_block_producer_seats_per_shard(vec![])
.avg_hidden_validator_seats_per_shard(vec![])
.block_producer_kickout_threshold(0)
.chunk_producer_kickout_threshold(0)
.chunk_validator_only_kickout_threshold(0)
.target_validator_mandates_per_shard(0)
.validator_max_kickout_stake_perc(0)
.online_min_threshold(0.into())
.online_max_threshold(0.into())
.fishermen_threshold(Balance::ZERO)
.minimum_stake_divisor(0)
.protocol_upgrade_stake_threshold(0.into())
.shard_layout(ShardLayout::single_shard())
.num_chunk_producer_seats(100)
.num_chunk_validator_seats(300)
.num_chunk_only_producer_seats(300)
.minimum_validators_per_shard(1)
.minimum_stake_ratio(Rational32::new(160i32, 1_000_000i32))
.chunk_producer_assignment_changes_limit(5)
.shuffle_shard_assignment_for_chunk_producers(false)
.max_inflation_rate(Rational32::new(1, 40));
builder
}
pub fn mock(epoch_length: BlockHeightDelta, shard_layout: ShardLayout) -> EpochConfigBuilder {
let mut builder = EpochConfigBuilder::default();
builder
.epoch_length(epoch_length)
.num_block_producer_seats(2)
.num_block_producer_seats_per_shard(vec![1, 1])
.avg_hidden_validator_seats_per_shard(vec![1, 1])
.block_producer_kickout_threshold(0)
.chunk_producer_kickout_threshold(0)
.chunk_validator_only_kickout_threshold(0)
.target_validator_mandates_per_shard(1)
.validator_max_kickout_stake_perc(0)
.online_min_threshold(Rational32::new(1i32, 4i32))
.online_max_threshold(Rational32::new(3i32, 4i32))
.fishermen_threshold(Balance::from_yoctonear(1))
.minimum_stake_divisor(1)
.protocol_upgrade_stake_threshold(Rational32::new(3i32, 4i32))
.shard_layout(shard_layout)
.num_chunk_producer_seats(100)
.num_chunk_validator_seats(300)
.num_chunk_only_producer_seats(300)
.minimum_validators_per_shard(1)
.minimum_stake_ratio(Rational32::new(160i32, 1_000_000i32))
.chunk_producer_assignment_changes_limit(5)
.shuffle_shard_assignment_for_chunk_producers(false)
.max_inflation_rate(Rational32::new(1, 40));
builder
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ShardConfig {
pub num_block_producer_seats_per_shard: Vec<NumSeats>,
pub avg_hidden_validator_seats_per_shard: Vec<NumSeats>,
pub shard_layout: ShardLayout,
}
impl ShardConfig {
pub fn new(epoch_config: EpochConfig, shard_layout: ShardLayout) -> Self {
Self {
num_block_producer_seats_per_shard: epoch_config.num_block_producer_seats_per_shard,
avg_hidden_validator_seats_per_shard: epoch_config.avg_hidden_validator_seats_per_shard,
shard_layout,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct AllEpochConfigTestOverrides {
pub block_producer_kickout_threshold: Option<u8>,
pub chunk_producer_kickout_threshold: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct AllEpochConfig {
config_store: EpochConfigStore,
chain_id: String,
epoch_length: BlockHeightDelta,
genesis_protocol_version: ProtocolVersion,
}
impl AllEpochConfig {
pub fn from_epoch_config_store(
chain_id: &str,
epoch_length: BlockHeightDelta,
config_store: EpochConfigStore,
genesis_protocol_version: ProtocolVersion,
) -> Self {
Self {
config_store,
chain_id: chain_id.to_string(),
epoch_length,
genesis_protocol_version,
}
}
pub fn for_protocol_version(&self, protocol_version: ProtocolVersion) -> EpochConfig {
let mut config = self.config_store.get_config(protocol_version).as_ref().clone();
config.epoch_length = self.epoch_length;
config
}
pub fn chain_id(&self) -> &str {
&self.chain_id
}
pub fn genesis_protocol_version(&self) -> ProtocolVersion {
self.genesis_protocol_version
}
}
#[derive(BorshSerialize, BorshDeserialize, ProtocolSchema)]
pub struct EpochSummary {
pub prev_epoch_last_block_hash: CryptoHash,
pub all_proposals: Vec<ValidatorStake>,
pub validator_kickout: HashMap<AccountId, ValidatorKickoutReason>,
pub validator_block_chunk_stats: HashMap<AccountId, BlockChunkValidatorStats>,
pub next_next_epoch_version: ProtocolVersion,
}
macro_rules! include_config {
($chain:expr, $version:expr, $file:expr) => {
(
$chain,
$version,
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/res/epoch_configs/",
$chain,
"/",
$file
)),
)
};
}
static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[
include_config!("mainnet", 29, "29.json"),
include_config!("mainnet", 48, "48.json"),
include_config!("mainnet", 56, "56.json"),
include_config!("mainnet", 64, "64.json"),
include_config!("mainnet", 65, "65.json"),
include_config!("mainnet", 69, "69.json"),
include_config!("mainnet", 70, "70.json"),
include_config!("mainnet", 71, "71.json"),
include_config!("mainnet", 72, "72.json"),
include_config!("mainnet", 75, "75.json"),
include_config!("mainnet", 76, "76.json"),
include_config!("mainnet", 78, "78.json"),
include_config!("mainnet", 80, "80.json"),
include_config!("mainnet", 81, "81.json"),
include_config!("mainnet", 143, "143.json"),
include_config!("testnet", 29, "29.json"),
include_config!("testnet", 48, "48.json"),
include_config!("testnet", 56, "56.json"),
include_config!("testnet", 64, "64.json"),
include_config!("testnet", 65, "65.json"),
include_config!("testnet", 69, "69.json"),
include_config!("testnet", 70, "70.json"),
include_config!("testnet", 71, "71.json"),
include_config!("testnet", 72, "72.json"),
include_config!("testnet", 75, "75.json"),
include_config!("testnet", 76, "76.json"),
include_config!("testnet", 78, "78.json"),
include_config!("testnet", 80, "80.json"),
include_config!("testnet", 81, "81.json"),
include_config!("testnet", 143, "143.json"),
];
#[derive(Debug, Clone)]
pub struct EpochConfigStore {
store: BTreeMap<ProtocolVersion, Arc<EpochConfig>>,
}
impl EpochConfigStore {
pub fn for_chain_id(chain_id: &str, config_dir: Option<PathBuf>) -> Option<Self> {
let mut store = Self::load_default_epoch_configs(chain_id);
if !store.is_empty() {
return Some(Self { store });
}
if let Some(config_dir) = config_dir {
store = Self::load_epoch_config_from_file_system(config_dir.to_str().unwrap());
}
if store.is_empty() { None } else { Some(Self { store }) }
}
fn load_default_epoch_configs(chain_id: &str) -> BTreeMap<ProtocolVersion, Arc<EpochConfig>> {
let mut store = BTreeMap::new();
for (chain, version, content) in CONFIGS {
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 {} and version {}: {:#}",
chain_id, version, e
)
});
store.insert(*version, Arc::new(config));
}
}
store
}
fn load_epoch_config_from_file_system(
directory: &str,
) -> BTreeMap<ProtocolVersion, Arc<EpochConfig>> {
fn get_epoch_config(
dir_entry: fs::DirEntry,
) -> Option<(ProtocolVersion, Arc<EpochConfig>)> {
let path = dir_entry.path();
if !(path.extension()? == "json") {
return None;
}
let file_name = path.file_stem()?.to_str()?.to_string();
let protocol_version = file_name.parse().expect("Invalid protocol version");
if protocol_version > PROTOCOL_VERSION {
return None;
}
let contents = fs::read_to_string(&path).ok()?;
let epoch_config = serde_json::from_str(&contents).unwrap_or_else(|_| {
panic!("Failed to parse epoch config for version {}", protocol_version)
});
Some((protocol_version, epoch_config))
}
fs::read_dir(directory)
.expect("Failed opening epoch config directory")
.filter_map(Result::ok)
.filter_map(get_epoch_config)
.collect()
}
pub fn test(store: BTreeMap<ProtocolVersion, Arc<EpochConfig>>) -> Self {
Self { store }
}
pub fn test_single_version(
protocol_version: ProtocolVersion,
epoch_config: EpochConfig,
) -> Self {
Self::test(BTreeMap::from([(protocol_version, Arc::new(epoch_config))]))
}
pub fn get_config(&self, protocol_version: ProtocolVersion) -> &Arc<EpochConfig> {
self.store
.range((Bound::Unbounded, Bound::Included(protocol_version)))
.next_back()
.unwrap_or_else(|| {
panic!("Failed to find EpochConfig for protocol version {}", protocol_version)
})
.1
}
pub fn iter(&self) -> impl Iterator<Item = (&ProtocolVersion, &Arc<EpochConfig>)> {
self.store.iter()
}
fn dump_epoch_config(directory: &Path, version: &ProtocolVersion, config: &Arc<EpochConfig>) {
let content = serde_json::to_string_pretty(config.as_ref()).unwrap();
let path = PathBuf::from(directory).join(format!("{}.json", version));
fs::write(path, content).unwrap();
}
pub fn dump_epoch_configs_between(
&self,
first_version: Option<&ProtocolVersion>,
last_version: Option<&ProtocolVersion>,
directory: impl AsRef<Path>,
) {
self.store
.iter()
.filter(|(version, _)| {
first_version.is_none_or(|first_version| *version >= first_version)
})
.filter(|(version, _)| last_version.is_none_or(|last_version| *version <= last_version))
.for_each(|(version, config)| {
Self::dump_epoch_config(directory.as_ref(), version, config);
});
if let Some(first_version) = first_version {
if !self.store.contains_key(&first_version) {
let config = self.get_config(*first_version);
Self::dump_epoch_config(directory.as_ref(), first_version, config);
}
}
}
}
#[cfg(test)]
mod tests {
use super::EpochConfigStore;
use crate::epoch_manager::EpochConfig;
use near_primitives_core::types::ProtocolVersion;
use near_primitives_core::version::PROTOCOL_VERSION;
use num_rational::Rational32;
use std::fs;
use std::path::Path;
#[test]
fn test_dump_epoch_configs_mainnet() {
let tmp_dir = tempfile::tempdir().unwrap();
EpochConfigStore::for_chain_id("mainnet", None).unwrap().dump_epoch_configs_between(
Some(&55),
Some(&68),
tmp_dir.path().to_str().unwrap(),
);
let dumped_files = fs::read_dir(tmp_dir.path()).unwrap();
let dumped_files: Vec<_> =
dumped_files.map(|entry| entry.unwrap().file_name().into_string().unwrap()).collect();
assert!(dumped_files.contains(&String::from("55.json")));
assert!(dumped_files.contains(&String::from("64.json")));
assert!(dumped_files.contains(&String::from("65.json")));
let contents_55 = fs::read_to_string(tmp_dir.path().join("55.json")).unwrap();
let epoch_config_55: EpochConfig = serde_json::from_str(&contents_55).unwrap();
let epoch_config_48 = parse_config_file("mainnet", 48).unwrap();
assert_eq!(epoch_config_55, epoch_config_48);
}
#[test]
fn test_dump_and_load_epoch_configs_mainnet() {
let tmp_dir = tempfile::tempdir().unwrap();
let epoch_configs = EpochConfigStore::for_chain_id("mainnet", None).unwrap();
epoch_configs.dump_epoch_configs_between(
Some(&55),
Some(&68),
tmp_dir.path().to_str().unwrap(),
);
let loaded_epoch_configs = EpochConfigStore::test(
EpochConfigStore::load_epoch_config_from_file_system(tmp_dir.path().to_str().unwrap()),
);
EpochConfigStore::dump_epoch_config(
tmp_dir.path(),
&(PROTOCOL_VERSION + 1),
&epoch_configs.get_config(PROTOCOL_VERSION),
);
let loaded_after_insert_epoch_configs = EpochConfigStore::test(
EpochConfigStore::load_epoch_config_from_file_system(tmp_dir.path().to_str().unwrap()),
);
assert_eq!(loaded_epoch_configs.store, loaded_after_insert_epoch_configs.store);
EpochConfigStore::dump_epoch_config(
tmp_dir.path(),
&(PROTOCOL_VERSION - 22),
&epoch_configs.get_config(PROTOCOL_VERSION),
);
let loaded_after_insert_epoch_configs = EpochConfigStore::test(
EpochConfigStore::load_epoch_config_from_file_system(tmp_dir.path().to_str().unwrap()),
);
assert_ne!(loaded_epoch_configs.store, loaded_after_insert_epoch_configs.store);
}
#[test]
fn test_protocol_upgrade_80() {
for chain_id in ["mainnet", "testnet"] {
let epoch_configs = EpochConfigStore::for_chain_id(chain_id, None).unwrap();
let epoch_config = epoch_configs.get_config(80);
assert_eq!(epoch_config.num_chunk_validator_seats, 500);
}
}
#[test]
fn test_protocol_upgrade_81() {
for chain_id in ["mainnet", "testnet"] {
let epoch_configs = EpochConfigStore::for_chain_id(chain_id, None).unwrap();
let epoch_config = epoch_configs.get_config(81);
assert_eq!(epoch_config.max_inflation_rate, Rational32::new(1, 40));
}
}
fn parse_config_file(chain_id: &str, protocol_version: ProtocolVersion) -> Option<EpochConfig> {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("res/epoch_configs")
.join(chain_id)
.join(format!("{}.json", protocol_version));
if path.exists() {
let content = fs::read_to_string(path).unwrap();
let config: EpochConfig = serde_json::from_str(&content).unwrap();
Some(config)
} else {
None
}
}
}