From 7befdbd4c90b767d99e7ec22ea5d28a5862cc129 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Wed, 15 May 2024 17:29:10 +0300 Subject: [PATCH] feat: consider all proposals for chunk validators (#11252) Solve #11202 and continue simplifying proposals processing logic. I want to make epoch info generation as straightforward as possible, by moving code for old protocol versions to local submodule so it won't be distracting. Now it should be more clear that epoch info generation consists of couple independent steps. Couple notes: * FixStakingThreshold isn't stabilised so we can use whatever protocol version. Version of epoch being generated makes much more sense. * I expected chunk validators set to be the biggest, but there is subtle case when we can't select chunk validators due to small stake ratio but can select chunk producers instead. It adds a bit of complexity. * There is subtle change in validator indexing and I believe the new indexing - basically, sorting by descending stake in majority of cases - makes more sense. --------- Co-authored-by: Longarithm --- chain/chain/src/test_utils/kv_runtime.rs | 3 - chain/epoch-manager/src/proposals.rs | 10 - chain/epoch-manager/src/shard_assignment.rs | 31 +- chain/epoch-manager/src/test_utils.rs | 9 +- .../epoch-manager/src/validator_selection.rs | 538 ++++++++++++------ .../jsonrpc-tests/res/genesis_config.json | 4 +- core/chain-configs/src/genesis_config.rs | 19 + core/chain-configs/src/test_genesis.rs | 14 + core/primitives/src/epoch_manager.rs | 46 +- tools/fork-network/src/cli.rs | 2 + 10 files changed, 453 insertions(+), 223 deletions(-) diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index fb5f6388ad1..f5dbb9f8e83 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -530,9 +530,6 @@ impl EpochManagerAdapter for MockEpochManager { validator_to_index, bp_settlement, cp_settlement, - vec![], - vec![], - HashMap::new(), BTreeMap::new(), HashMap::new(), HashMap::new(), diff --git a/chain/epoch-manager/src/proposals.rs b/chain/epoch-manager/src/proposals.rs index 0e1d874f564..3242ebc59fa 100644 --- a/chain/epoch-manager/src/proposals.rs +++ b/chain/epoch-manager/src/proposals.rs @@ -57,7 +57,6 @@ pub fn proposals_to_epoch_info( validator_kickout, validator_reward, minted_amount, - current_version, next_version, ); } else { @@ -237,12 +236,6 @@ mod old_validator_selection { chunk_producers_settlement.push(shard_settlement); } - let fishermen_to_index = fishermen - .iter() - .enumerate() - .map(|(index, s)| (s.account_id().clone(), index as ValidatorId)) - .collect::>(); - let validator_to_index = final_proposals .iter() .enumerate() @@ -258,9 +251,6 @@ mod old_validator_selection { validator_to_index, block_producers_settlement, chunk_producers_settlement, - vec![], - fishermen, - fishermen_to_index, stake_change, validator_reward, validator_kickout, diff --git a/chain/epoch-manager/src/shard_assignment.rs b/chain/epoch-manager/src/shard_assignment.rs index f280ea4d74d..b42d2b7e2f0 100644 --- a/chain/epoch-manager/src/shard_assignment.rs +++ b/chain/epoch-manager/src/shard_assignment.rs @@ -2,6 +2,11 @@ use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{Balance, NumShards, ShardId}; use near_primitives::utils::min_heap::{MinHeap, PeekMut}; +/// Marker struct to communicate the error where you try to assign validators to shards +/// and there are not enough to even meet the minimum per shard. +#[derive(Debug)] +pub struct NotEnoughValidators; + /// Assign chunk producers (a.k.a. validators) to shards. The i-th element /// of the output corresponds to the validators assigned to the i-th shard. /// @@ -14,13 +19,20 @@ use near_primitives::utils::min_heap::{MinHeap, PeekMut}; /// producer will be assigned to a single shard. If there are fewer producers, /// some of them will be assigned to multiple shards. /// -/// Panics if chunk_producers vector is not sorted in descending order by -/// producer’s stake. +/// Returns error if `chunk_producers.len() < min_validators_per_shard`. +/// Panics if chunk_producers vector is not sorted in descending order by stake. pub fn assign_shards( chunk_producers: Vec, num_shards: NumShards, min_validators_per_shard: usize, ) -> Result>, NotEnoughValidators> { + // If there's not enough chunk producers to fill up a single shard there’s + // nothing we can do. Return with an error. + let num_chunk_producers = chunk_producers.len(); + if num_chunk_producers < min_validators_per_shard { + return Err(NotEnoughValidators); + } + for (idx, pair) in chunk_producers.windows(2).enumerate() { assert!( pair[0].get_stake() >= pair[1].get_stake(), @@ -29,13 +41,6 @@ pub fn assign_shards( ); } - // If there’s not enough chunk producers to fill up a single shard there’s - // nothing we can do. Return with an error. - let num_chunk_producers = chunk_producers.len(); - if num_chunk_producers < min_validators_per_shard { - return Err(NotEnoughValidators); - } - let mut result: Vec> = (0..num_shards).map(|_| Vec::new()).collect(); // Initially, sort by number of validators first so we fill shards up. @@ -135,11 +140,6 @@ fn assign_with_possible_repeats } } -/// Marker struct to communicate the error where you try to assign validators to shards -/// and there are not enough to even meet the minimum per shard. -#[derive(Debug)] -pub struct NotEnoughValidators; - pub trait HasStake { fn get_stake(&self) -> Balance; } @@ -152,6 +152,7 @@ impl HasStake for ValidatorStake { #[cfg(test)] mod tests { + use crate::shard_assignment::NotEnoughValidators; use near_primitives::types::{Balance, NumShards}; use std::collections::HashSet; @@ -226,7 +227,7 @@ mod tests { stakes: &[Balance], num_shards: NumShards, min_validators_per_shard: usize, - ) -> Result, super::NotEnoughValidators> { + ) -> Result, NotEnoughValidators> { let chunk_producers = stakes.iter().copied().enumerate().collect(); let assignments = super::assign_shards(chunk_producers, num_shards, min_validators_per_shard)?; diff --git a/chain/epoch-manager/src/test_utils.rs b/chain/epoch-manager/src/test_utils.rs index 441e8d4903d..5fa5e0e4344 100644 --- a/chain/epoch-manager/src/test_utils.rs +++ b/chain/epoch-manager/src/test_utils.rs @@ -76,8 +76,8 @@ pub fn epoch_info_with_num_seats( mut accounts: Vec<(AccountId, Balance)>, block_producers_settlement: Vec, chunk_producers_settlement: Vec>, - hidden_validators_settlement: Vec, - fishermen: Vec<(AccountId, Balance)>, + _hidden_validators_settlement: Vec, + _fishermen: Vec<(AccountId, Balance)>, stake_change: BTreeMap, validator_kickout: Vec<(AccountId, ValidatorKickoutReason)>, validator_reward: HashMap, @@ -91,8 +91,6 @@ pub fn epoch_info_with_num_seats( acc.insert(x.0.clone(), i as u64); acc }); - let fishermen_to_index = - fishermen.iter().enumerate().map(|(i, (s, _))| (s.clone(), i as ValidatorId)).collect(); let account_to_validators = |accounts: Vec<(AccountId, Balance)>| -> Vec { accounts .into_iter() @@ -121,9 +119,6 @@ pub fn epoch_info_with_num_seats( validator_to_index, block_producers_settlement, chunk_producers_settlement, - hidden_validators_settlement, - account_to_validators(fishermen), - fishermen_to_index, stake_change, validator_reward, validator_kickout.into_iter().collect(), diff --git a/chain/epoch-manager/src/validator_selection.rs b/chain/epoch-manager/src/validator_selection.rs index cdc7095fad1..ae9effcf2d1 100644 --- a/chain/epoch-manager/src/validator_selection.rs +++ b/chain/epoch-manager/src/validator_selection.rs @@ -14,6 +14,93 @@ use std::cmp::{self, Ordering}; use std::collections::hash_map; use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet}; +/// Helper struct which is a result of proposals processing. +struct ValidatorRoles { + /// Proposals which were not given any role. + unselected_proposals: BinaryHeap, + /// Validators which are assigned to produce chunks. + chunk_producers: Vec, + /// Validators which are assigned to produce blocks. + block_producers: Vec, + /// Validators which are assigned to validate chunks. + chunk_validators: Vec, + /// Stake threshold to become a validator. + threshold: Balance, +} + +/// Helper struct which is a result of assigning chunk producers to shards. +struct ChunkProducersAssignment { + /// List of all validators in the epoch. + /// Note that it doesn't belong here, but in the legacy code it is computed + /// together with chunk producers assignment. + all_validators: Vec, + /// Maps validator account names to local indices throughout the epoch. + validator_to_index: HashMap, + /// Maps chunk producers to shards, where i-th list contains local indices + /// of validators producing chunks for i-th shard. + chunk_producers_settlement: Vec>, +} + +/// Selects validator roles for the given proposals. +fn select_validators_from_proposals( + epoch_config: &EpochConfig, + proposals: HashMap, + next_version: ProtocolVersion, +) -> ValidatorRoles { + let shard_ids: Vec<_> = epoch_config.shard_layout.shard_ids().collect(); + let min_stake_ratio = { + let rational = epoch_config.validator_selection_config.minimum_stake_ratio; + Ratio::new(*rational.numer() as u128, *rational.denom() as u128) + }; + + let chunk_producer_proposals = order_proposals(proposals.values().cloned()); + let (chunk_producers, _, cp_stake_threshold) = select_chunk_producers( + chunk_producer_proposals, + epoch_config.validator_selection_config.num_chunk_producer_seats as usize, + min_stake_ratio, + shard_ids.len() as NumShards, + next_version, + ); + + let block_producer_proposals = order_proposals(proposals.values().cloned()); + let (block_producers, _, bp_stake_threshold) = select_block_producers( + block_producer_proposals, + epoch_config.num_block_producer_seats as usize, + min_stake_ratio, + next_version, + ); + + let chunk_validator_proposals = order_proposals(proposals.values().cloned()); + let (chunk_validators, _, cv_stake_threshold) = select_validators( + chunk_validator_proposals, + epoch_config.validator_selection_config.num_chunk_validator_seats as usize, + min_stake_ratio, + next_version, + ); + + let mut unselected_proposals = BinaryHeap::new(); + for proposal in order_proposals(proposals.into_values()) { + if chunk_producers.contains(&proposal.0) { + continue; + } + if block_producers.contains(&proposal.0) { + continue; + } + if chunk_validators.contains(&proposal.0) { + continue; + } + unselected_proposals.push(proposal); + } + let threshold = bp_stake_threshold.min(cp_stake_threshold).min(cv_stake_threshold); + ValidatorRoles { + unselected_proposals, + chunk_producers, + block_producers, + chunk_validators, + threshold, + } +} + /// Select validators for next epoch and generate epoch info pub fn proposals_to_epoch_info( epoch_config: &EpochConfig, @@ -23,7 +110,6 @@ pub fn proposals_to_epoch_info( mut validator_kickout: HashMap, validator_reward: HashMap, minted_amount: Balance, - current_version: ProtocolVersion, next_version: ProtocolVersion, ) -> Result { debug_assert!( @@ -33,49 +119,37 @@ pub fn proposals_to_epoch_info( ); let shard_ids: Vec<_> = epoch_config.shard_layout.shard_ids().collect(); - let min_stake_ratio = { - let rational = epoch_config.validator_selection_config.minimum_stake_ratio; - Ratio::new(*rational.numer() as u128, *rational.denom() as u128) - }; - let max_bp_selected = epoch_config.num_block_producer_seats as usize; let mut stake_change = BTreeMap::new(); - let proposals = proposals_with_rollover( + let proposals = apply_epoch_update_to_proposals( proposals, prev_epoch_info, &validator_reward, &validator_kickout, &mut stake_change, ); - let mut block_producer_proposals = order_proposals(proposals.values().cloned()); - let (block_producers, bp_stake_threshold) = select_block_producers( - &mut block_producer_proposals, - max_bp_selected, - min_stake_ratio, - current_version, - ); - let (chunk_producer_proposals, chunk_producers, cp_stake_threshold) = - if checked_feature!("stable", ChunkOnlyProducers, next_version) { - let mut chunk_producer_proposals = order_proposals(proposals.into_values()); - let max_cp_selected = max_bp_selected - + (epoch_config.validator_selection_config.num_chunk_only_producer_seats as usize); - let (chunk_producers, cp_stake_threshold) = select_chunk_producers( - &mut chunk_producer_proposals, - max_cp_selected, - min_stake_ratio, - shard_ids.len() as NumShards, - current_version, - ); - (chunk_producer_proposals, chunk_producers, cp_stake_threshold) - } else { - (block_producer_proposals, block_producers.clone(), bp_stake_threshold) - }; - // since block producer proposals could become chunk producers, their actual stake threshold - // is the smaller of the two thresholds - let threshold = cmp::min(bp_stake_threshold, cp_stake_threshold); + // Select validators for the next epoch. + // Returns unselected proposals, validator lists for all roles and stake + // threshold to become a validator. + let ValidatorRoles { + unselected_proposals, + chunk_producers, + block_producers, + chunk_validators, + threshold, + } = if checked_feature!("stable", StatelessValidationV0, next_version) { + select_validators_from_proposals(epoch_config, proposals, next_version) + } else { + old_validator_selection::select_validators_from_proposals( + epoch_config, + proposals, + next_version, + ) + }; - // process remaining chunk_producer_proposals that were not selected for either role - for OrderedValidatorStake(p) in chunk_producer_proposals { + // Add kickouts for validators which fell out of validator set. + // Used for querying epoch info by RPC. + for OrderedValidatorStake(p) in unselected_proposals { let stake = p.stake(); let account_id = p.account_id(); *stake_change.get_mut(account_id).unwrap() = 0; @@ -87,21 +161,43 @@ pub fn proposals_to_epoch_info( } } - let num_chunk_producers = chunk_producers.len(); - // Constructing `all_validators` such that a validators position corresponds to its `ValidatorId`. - let mut all_validators: Vec = Vec::with_capacity(num_chunk_producers); - let mut validator_to_index = HashMap::new(); - let mut block_producers_settlement = Vec::with_capacity(block_producers.len()); - - for (i, bp) in block_producers.into_iter().enumerate() { - let id = i as ValidatorId; - validator_to_index.insert(bp.account_id().clone(), id); - block_producers_settlement.push(id); - all_validators.push(bp); - } + // Constructing `validator_to_index` and `all_validators` mapping validator + // account names to local indices throughout the epoch and vice versa, for + // convenience of epoch manager. + // Assign chunk producers to shards using local validator indices. + // TODO: this happens together because assigment logic is more subtle for + // older protocol versions, consider decoupling it. + let ChunkProducersAssignment { + all_validators, + validator_to_index, + mut chunk_producers_settlement, + } = if checked_feature!("stable", StatelessValidationV0, next_version) { + // Construct local validator indices. + // Note that if there are too few validators and too many shards, + // assigning chunk producers to shards is more aggressive, so it + // is not enough to iterate over chunk validators. + // We assign local indices in the order of roles priority and then + // in decreasing order of stake. + let max_validators_for_role = cmp::max( + chunk_producers.len(), + cmp::max(block_producers.len(), chunk_validators.len()), + ); + let mut all_validators: Vec = Vec::with_capacity(max_validators_for_role); + let mut validator_to_index = HashMap::new(); + for validators_for_role in [&chunk_producers, &block_producers, &chunk_validators].iter() { + for validator in validators_for_role.iter() { + let account_id = validator.account_id().clone(); + if validator_to_index.contains_key(&account_id) { + continue; + } + let id = all_validators.len() as ValidatorId; + validator_to_index.insert(account_id, id); + all_validators.push(validator.clone()); + } + } - let chunk_producers_settlement = if checked_feature!("stable", ChunkOnlyProducers, next_version) - { + // Assign chunk producers to shards. + let num_chunk_producers = chunk_producers.len(); let minimum_validators_per_shard = epoch_config.validator_selection_config.minimum_validators_per_shard as usize; let shard_assignment = assign_shards( @@ -114,47 +210,36 @@ pub fn proposals_to_epoch_info( num_shards: shard_ids.len() as NumShards, })?; - let mut chunk_producers_settlement: Vec> = - shard_assignment.iter().map(|vs| Vec::with_capacity(vs.len())).collect(); - let mut i = all_validators.len(); - // Here we assign validator ids to all chunk only validators - for (shard_validators, shard_validator_ids) in - shard_assignment.into_iter().zip(chunk_producers_settlement.iter_mut()) - { - for validator in shard_validators { - debug_assert_eq!(i, all_validators.len()); - match validator_to_index.entry(validator.account_id().clone()) { - hash_map::Entry::Vacant(entry) => { - let validator_id = i as ValidatorId; - entry.insert(validator_id); - shard_validator_ids.push(validator_id); - all_validators.push(validator); - i += 1; - } - // Validators which have an entry in the validator_to_index map - // have already been inserted into `all_validators`. - hash_map::Entry::Occupied(entry) => { - let validator_id = *entry.get(); - shard_validator_ids.push(validator_id); - } - } - } - } - - if epoch_config.validator_selection_config.shuffle_shard_assignment_for_chunk_producers { - chunk_producers_settlement - .shuffle(&mut EpochInfo::shard_assignment_shuffling_rng(&rng_seed)); - } + let chunk_producers_settlement = shard_assignment + .into_iter() + .map(|vs| vs.into_iter().map(|v| validator_to_index[v.account_id()]).collect()) + .collect(); - chunk_producers_settlement + ChunkProducersAssignment { all_validators, validator_to_index, chunk_producers_settlement } + } else if checked_feature!("stable", ChunkOnlyProducers, next_version) { + old_validator_selection::assign_chunk_producers_to_shards_chunk_only( + epoch_config, + chunk_producers, + &block_producers, + )? } else { old_validator_selection::assign_chunk_producers_to_shards( epoch_config, chunk_producers, - &block_producers_settlement, + &block_producers, )? }; + if epoch_config.validator_selection_config.shuffle_shard_assignment_for_chunk_producers { + chunk_producers_settlement + .shuffle(&mut EpochInfo::shard_assignment_shuffling_rng(&rng_seed)); + } + + // Get local indices for block producers. + let block_producers_settlement = + block_producers.into_iter().map(|bp| validator_to_index[bp.account_id()]).collect(); + + // Assign chunk validators to shards using validator mandates abstraction. let validator_mandates = if checked_feature!("stable", StatelessValidationV0, next_version) { // Value chosen based on calculations for the security of the protocol. // With this number of mandates per shard and 6 shards, the theory calculations predict the @@ -176,9 +261,6 @@ pub fn proposals_to_epoch_info( validator_to_index, block_producers_settlement, chunk_producers_settlement, - vec![], - vec![], - Default::default(), stake_change, validator_reward, validator_kickout, @@ -190,18 +272,17 @@ pub fn proposals_to_epoch_info( )) } -/// Generates proposals based on new proposals, last epoch validators/fishermen and validator -/// kickouts -/// For each account that was validator or fisherman in last epoch or made stake action last epoch -/// we apply the following in the order of priority -/// 1. If account is in validator_kickout it cannot be validator or fisherman for the next epoch, -/// we will not include it in proposals or fishermen +/// Generates proposals based on proposals generated throughout last epoch, +/// last epoch validators and validator kickouts. +/// For each account that was validator in last epoch or made stake action last epoch +/// we apply the following in the order of priority: +/// 1. If account is in validator_kickout it cannot be validator for the next epoch, +/// we will not include it in proposals /// 2. If account made staking action last epoch, it will be included in proposals with stake /// adjusted by rewards from last epoch, if any /// 3. If account was validator last epoch, it will be included in proposals with the same stake /// as last epoch, adjusted by rewards from last epoch, if any -/// 4. If account was fisherman last epoch, it is included in fishermen -fn proposals_with_rollover( +fn apply_epoch_update_to_proposals( proposals: Vec, prev_epoch_info: &EpochInfo, validator_reward: &HashMap, @@ -243,21 +324,21 @@ fn order_proposals>( } fn select_block_producers( - block_producer_proposals: &mut BinaryHeap, + block_producer_proposals: BinaryHeap, max_num_selected: usize, min_stake_ratio: Ratio, protocol_version: ProtocolVersion, -) -> (Vec, Balance) { +) -> (Vec, BinaryHeap, Balance) { select_validators(block_producer_proposals, max_num_selected, min_stake_ratio, protocol_version) } fn select_chunk_producers( - all_proposals: &mut BinaryHeap, + all_proposals: BinaryHeap, max_num_selected: usize, min_stake_ratio: Ratio, num_shards: u64, protocol_version: ProtocolVersion, -) -> (Vec, Balance) { +) -> (Vec, BinaryHeap, Balance) { select_validators( all_proposals, max_num_selected, @@ -271,11 +352,11 @@ fn select_chunk_producers( // slots are filled, or the stake ratio falls too low, the threshold stake to be included // is also returned. fn select_validators( - proposals: &mut BinaryHeap, + mut proposals: BinaryHeap, max_number_selected: usize, min_stake_ratio: Ratio, protocol_version: ProtocolVersion, -) -> (Vec, Balance) { +) -> (Vec, BinaryHeap, Balance) { let mut total_stake = 0; let n = cmp::min(max_number_selected, proposals.len()); let mut validators = Vec::with_capacity(n); @@ -296,7 +377,7 @@ fn select_validators( // all slots were filled, so the threshold stake is 1 more than the current // smallest stake let threshold = validators.last().unwrap().stake() + 1; - (validators, threshold) + (validators, proposals, threshold) } else { // the stake ratio condition prevented all slots from being filled, // or there were fewer proposals than available slots, @@ -313,7 +394,7 @@ fn select_validators( } else { (min_stake_ratio * Ratio::new(total_stake, 1)).ceil().to_integer() }; - (validators, threshold) + (validators, proposals, threshold) } } @@ -338,14 +419,142 @@ impl Ord for OrderedValidatorStake { } } +/// Helpers to generate new epoch info for older protocol versions. mod old_validator_selection { use super::*; - pub fn assign_chunk_producers_to_shards( + /// Selects validator roles for the given proposals. + pub(crate) fn select_validators_from_proposals( + epoch_config: &EpochConfig, + proposals: HashMap, + next_version: ProtocolVersion, + ) -> ValidatorRoles { + let max_bp_selected = epoch_config.num_block_producer_seats as usize; + let min_stake_ratio = { + let rational = epoch_config.validator_selection_config.minimum_stake_ratio; + Ratio::new(*rational.numer() as u128, *rational.denom() as u128) + }; + + let block_producer_proposals = order_proposals(proposals.values().cloned()); + let (block_producers, not_block_producers, bp_stake_threshold) = select_block_producers( + block_producer_proposals, + max_bp_selected, + min_stake_ratio, + next_version, + ); + let (chunk_producer_proposals, chunk_producers, cp_stake_threshold) = + if checked_feature!("stable", ChunkOnlyProducers, next_version) { + let chunk_producer_proposals = order_proposals(proposals.into_values()); + let max_cp_selected = max_bp_selected + + (epoch_config.validator_selection_config.num_chunk_only_producer_seats + as usize); + let num_shards = epoch_config.shard_layout.shard_ids().count() as NumShards; + let (chunk_producers, not_chunk_producers, cp_stake_threshold) = + select_chunk_producers( + chunk_producer_proposals, + max_cp_selected, + min_stake_ratio, + num_shards, + next_version, + ); + (not_chunk_producers, chunk_producers, cp_stake_threshold) + } else { + (not_block_producers, block_producers.clone(), bp_stake_threshold) + }; + + // since block producer proposals could become chunk producers, their actual stake threshold + // is the smaller of the two thresholds + let threshold = cmp::min(bp_stake_threshold, cp_stake_threshold); + + ValidatorRoles { + unselected_proposals: chunk_producer_proposals, + chunk_producers, + block_producers, + chunk_validators: vec![], // chunk validators are not used for older protocol versions + threshold, + } + } + + /// Assigns chunk producers to shards for the given proposals when chunk + /// only producers were enabled, but before stateless validation. + pub(crate) fn assign_chunk_producers_to_shards_chunk_only( + epoch_config: &EpochConfig, + chunk_producers: Vec, + block_producers: &[ValidatorStake], + ) -> Result { + let num_chunk_producers = chunk_producers.len(); + let mut all_validators: Vec = Vec::with_capacity(num_chunk_producers); + let mut validator_to_index = HashMap::new(); + for (i, bp) in block_producers.iter().enumerate() { + let id = i as ValidatorId; + validator_to_index.insert(bp.account_id().clone(), id); + all_validators.push(bp.clone()); + } + + let shard_ids: Vec<_> = epoch_config.shard_layout.shard_ids().collect(); + let minimum_validators_per_shard = + epoch_config.validator_selection_config.minimum_validators_per_shard as usize; + let shard_assignment = assign_shards( + chunk_producers, + shard_ids.len() as NumShards, + minimum_validators_per_shard, + ) + .map_err(|_| EpochError::NotEnoughValidators { + num_validators: num_chunk_producers as u64, + num_shards: shard_ids.len() as NumShards, + })?; + + let mut chunk_producers_settlement: Vec> = + shard_assignment.iter().map(|vs| Vec::with_capacity(vs.len())).collect(); + let mut i = all_validators.len(); + // Here we assign validator ids to all chunk only validators + for (shard_validators, shard_validator_ids) in + shard_assignment.into_iter().zip(chunk_producers_settlement.iter_mut()) + { + for validator in shard_validators { + debug_assert_eq!(i, all_validators.len()); + match validator_to_index.entry(validator.account_id().clone()) { + hash_map::Entry::Vacant(entry) => { + let validator_id = i as ValidatorId; + entry.insert(validator_id); + shard_validator_ids.push(validator_id); + all_validators.push(validator); + i += 1; + } + // Validators which have an entry in the validator_to_index map + // have already been inserted into `all_validators`. + hash_map::Entry::Occupied(entry) => { + let validator_id = *entry.get(); + shard_validator_ids.push(validator_id); + } + } + } + } + + Ok(ChunkProducersAssignment { + all_validators, + validator_to_index, + chunk_producers_settlement, + }) + } + + /// Assigns chunk producers to shards given chunk and block producers before + /// chunk only producers were enabled. + pub(crate) fn assign_chunk_producers_to_shards( epoch_config: &EpochConfig, chunk_producers: Vec, - block_producers_settlement: &[ValidatorId], - ) -> Result>, EpochError> { + block_producers: &[ValidatorStake], + ) -> Result { + let mut all_validators: Vec = Vec::with_capacity(chunk_producers.len()); + let mut validator_to_index = HashMap::new(); + let mut block_producers_settlement = Vec::with_capacity(block_producers.len()); + for (i, bp) in block_producers.into_iter().enumerate() { + let id = i as ValidatorId; + validator_to_index.insert(bp.account_id().clone(), id); + block_producers_settlement.push(id); + all_validators.push(bp.clone()); + } + let shard_ids: Vec<_> = epoch_config.shard_layout.shard_ids().collect(); if chunk_producers.is_empty() { // All validators tried to unstake? @@ -354,12 +563,13 @@ mod old_validator_selection { num_shards: shard_ids.len() as NumShards, }); } + let mut id = 0usize; // Here we assign validators to chunks (we try to keep number of shards assigned for // each validator as even as possible). Note that in prod configuration number of seats // per shard is the same as maximal number of block producers, so normally all // validators would be assigned to all chunks - Ok(shard_ids + let chunk_producers_settlement = shard_ids .iter() .map(|&shard_id| shard_id as usize) .map(|shard_id| { @@ -372,7 +582,13 @@ mod old_validator_selection { }) .collect() }) - .collect()) + .collect(); + + Ok(ChunkProducersAssignment { + all_validators, + validator_to_index, + chunk_producers_settlement, + }) } } @@ -393,7 +609,7 @@ mod tests { // A simple sanity test. Given fewer proposals than the number of seats, // none of which has too little stake, they all get assigned as block and // chunk producers. - let epoch_config = create_epoch_config(2, 100, 0, Default::default()); + let epoch_config = create_epoch_config(2, 100, Default::default()); let prev_epoch_height = 7; let prev_epoch_info = create_prev_epoch_info(prev_epoch_height, &["test1", "test2"], &[]); let proposals = create_proposals(&[("test1", 1000), ("test2", 2000), ("test3", 300)]); @@ -406,7 +622,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -436,9 +651,9 @@ mod tests { let epoch_config = create_epoch_config( 2, num_bp_seats, - // purposely set the fishermen threshold high so that none become fishermen - 10_000, ValidatorSelectionConfig { + num_chunk_producer_seats: num_bp_seats + num_cp_seats, + num_chunk_validator_seats: num_bp_seats + num_cp_seats, num_chunk_only_producer_seats: num_cp_seats, minimum_validators_per_shard: 1, minimum_stake_ratio: Ratio::new(160, 1_000_000), @@ -479,7 +694,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -529,14 +743,20 @@ mod tests { // depending on the `shuffle_shard_assignment_for_chunk_producers` flag. #[test] fn test_validator_assignment_with_chunk_only_producers_with_shard_shuffling() { + // Don't run test without stateless validation because it has slight + // changes in validator epoch indexing. + if !checked_feature!("stable", StatelessValidationV0, PROTOCOL_VERSION) { + return; + } + let num_bp_seats = 10; let num_cp_seats = 30; let mut epoch_config = create_epoch_config( 6, num_bp_seats, - // purposely set the fishermen threshold high so that none become fishermen - 10_000, ValidatorSelectionConfig { + num_chunk_producer_seats: num_bp_seats + num_cp_seats, + num_chunk_validator_seats: num_bp_seats + num_cp_seats, num_chunk_only_producer_seats: num_cp_seats, minimum_validators_per_shard: 1, minimum_stake_ratio: Ratio::new(160, 1_000_000), @@ -565,7 +785,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); let epoch_info_no_shuffling_different_seed = proposals_to_epoch_info( @@ -577,7 +796,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -591,7 +809,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); let epoch_info_with_shuffling_different_seed = proposals_to_epoch_info( @@ -603,49 +820,37 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); - assert_eq!( - epoch_info_no_shuffling.chunk_producers_settlement(), - vec![ - vec![0, 10, 11, 12, 13, 14, 15], - vec![1, 16, 17, 18, 19, 20, 21], - vec![2, 9, 22, 23, 24, 25, 26], - vec![3, 8, 27, 28, 29, 30, 31], - vec![4, 7, 32, 33, 34, 35], - vec![5, 6, 36, 37, 38, 39], - ], - ); + let target_settlement = vec![ + vec![0, 11, 12, 23, 24, 35, 36], + vec![1, 10, 13, 22, 25, 34, 37], + vec![2, 9, 14, 21, 26, 33, 38], + vec![3, 8, 15, 20, 27, 32, 39], + vec![4, 7, 16, 19, 28, 31], + vec![5, 6, 17, 18, 29, 30], + ]; + assert_eq!(epoch_info_no_shuffling.chunk_producers_settlement(), target_settlement,); assert_eq!( epoch_info_no_shuffling.chunk_producers_settlement(), epoch_info_no_shuffling_different_seed.chunk_producers_settlement() ); - assert_eq!( - epoch_info_with_shuffling.chunk_producers_settlement(), - vec![ - vec![4, 7, 32, 33, 34, 35], - vec![2, 9, 22, 23, 24, 25, 26], - vec![1, 16, 17, 18, 19, 20, 21], - vec![0, 10, 11, 12, 13, 14, 15], - vec![5, 6, 36, 37, 38, 39], - vec![3, 8, 27, 28, 29, 30, 31], - ], - ); + let shuffled_settlement = [4, 2, 1, 0, 5, 3] + .into_iter() + .map(|i| target_settlement[i].clone()) + .collect::>(); + assert_eq!(epoch_info_with_shuffling.chunk_producers_settlement(), shuffled_settlement); + let shuffled_settlement = [3, 1, 0, 2, 5, 4] + .into_iter() + .map(|i| target_settlement[i].clone()) + .collect::>(); assert_eq!( epoch_info_with_shuffling_different_seed.chunk_producers_settlement(), - vec![ - vec![3, 8, 27, 28, 29, 30, 31], - vec![1, 16, 17, 18, 19, 20, 21], - vec![0, 10, 11, 12, 13, 14, 15], - vec![2, 9, 22, 23, 24, 25, 26], - vec![5, 6, 36, 37, 38, 39], - vec![4, 7, 32, 33, 34, 35], - ], + shuffled_settlement, ); } @@ -655,8 +860,9 @@ mod tests { let epoch_config = create_epoch_config( num_shards, 2, - 0, ValidatorSelectionConfig { + num_chunk_producer_seats: 2, + num_chunk_validator_seats: 2, num_chunk_only_producer_seats: 0, minimum_validators_per_shard: 1, minimum_stake_ratio: Ratio::new(160, 1_000_000), @@ -676,7 +882,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -697,8 +902,9 @@ mod tests { let epoch_config = create_epoch_config( num_shards, 2 * num_shards, - 0, ValidatorSelectionConfig { + num_chunk_producer_seats: 2 * num_shards, + num_chunk_validator_seats: 2 * num_shards, num_chunk_only_producer_seats: 0, minimum_validators_per_shard: 1, minimum_stake_ratio: Ratio::new(160, 1_000_000), @@ -719,7 +925,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -747,7 +952,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -765,14 +969,14 @@ mod tests { } } - #[cfg(feature = "nightly")] fn get_epoch_info_for_chunk_validators_sampling() -> EpochInfo { let num_shards = 4; let epoch_config = create_epoch_config( num_shards, 2 * num_shards, - 0, ValidatorSelectionConfig { + num_chunk_producer_seats: 2 * num_shards, + num_chunk_validator_seats: 2 * num_shards, num_chunk_only_producer_seats: 0, minimum_validators_per_shard: 1, // for example purposes, we choose a higher ratio than in production @@ -807,7 +1011,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -818,8 +1021,11 @@ mod tests { /// `EpochInfo`. The internals of mandate assignment are tested in the module containing /// [`ValidatorMandates`]. #[test] - #[cfg(feature = "nightly")] fn test_chunk_validators_sampling() { + if !checked_feature!("stable", StatelessValidationV0, PROTOCOL_VERSION) { + return; + } + let epoch_info = get_epoch_info_for_chunk_validators_sampling(); // Given `epoch_info` and `proposals` above, the sample at a given height is deterministic. let height = 42; @@ -833,8 +1039,11 @@ mod tests { } #[test] - #[cfg(feature = "nightly")] fn test_deterministic_chunk_validators_sampling() { + if !checked_feature!("stable", StatelessValidationV0, PROTOCOL_VERSION) { + return; + } + let epoch_info = get_epoch_info_for_chunk_validators_sampling(); let height = 42; let assignment1 = epoch_info.sample_chunk_validators(height); @@ -852,8 +1061,9 @@ mod tests { let epoch_config = create_epoch_config( 1, 100, - 150, ValidatorSelectionConfig { + num_chunk_producer_seats: 300, + num_chunk_validator_seats: 300, num_chunk_only_producer_seats: 300, minimum_validators_per_shard: 1, // for example purposes, we choose a higher ratio than in production @@ -881,7 +1091,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -925,7 +1134,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); #[cfg(feature = "protocol_feature_fix_staking_threshold")] @@ -949,7 +1157,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); assert_eq!(num_validators, epoch_info.validators_iter().len()); @@ -958,7 +1165,7 @@ mod tests { #[test] fn test_validator_assignment_with_kickout() { // kicked out validators are not selected - let epoch_config = create_epoch_config(1, 100, 0, Default::default()); + let epoch_config = create_epoch_config(1, 100, Default::default()); let prev_epoch_height = 7; let prev_epoch_info = create_prev_epoch_info( prev_epoch_height, @@ -977,7 +1184,6 @@ mod tests { Default::default(), 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -990,7 +1196,7 @@ mod tests { // validator balances are updated based on their rewards let validators = [("test1", 3000), ("test2", 2000), ("test3", 1000)]; let rewards: [u128; 3] = [7, 8, 9]; - let epoch_config = create_epoch_config(1, 100, 0, Default::default()); + let epoch_config = create_epoch_config(1, 100, Default::default()); let prev_epoch_height = 7; let prev_epoch_info = create_prev_epoch_info(prev_epoch_height, &validators, &[]); let rewards_map = validators @@ -1007,7 +1213,6 @@ mod tests { rewards_map, 0, PROTOCOL_VERSION, - PROTOCOL_VERSION, ) .unwrap(); @@ -1029,7 +1234,6 @@ mod tests { fn create_epoch_config( num_shards: u64, num_block_producer_seats: u64, - fishermen_threshold: Balance, validator_selection_config: ValidatorSelectionConfig, ) -> EpochConfig { EpochConfig { @@ -1042,7 +1246,7 @@ mod tests { validator_max_kickout_stake_perc: 100, online_min_threshold: 0.into(), online_max_threshold: 0.into(), - fishermen_threshold, + fishermen_threshold: 0, minimum_stake_divisor: 0, protocol_upgrade_stake_threshold: 0.into(), shard_layout: ShardLayout::v0(num_shards, 0), diff --git a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json index f2673819468..850645e6e89 100644 --- a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json +++ b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json @@ -69,5 +69,7 @@ ], "shuffle_shard_assignment_for_chunk_producers": false, "use_production_config": false, + "num_chunk_producer_seats": 100, + "num_chunk_validator_seats": 300, "records": [] -} +} \ No newline at end of file diff --git a/core/chain-configs/src/genesis_config.rs b/core/chain-configs/src/genesis_config.rs index 1e160d90abe..1c31d117733 100644 --- a/core/chain-configs/src/genesis_config.rs +++ b/core/chain-configs/src/genesis_config.rs @@ -68,6 +68,14 @@ fn default_minimum_validators_per_shard() -> u64 { 1 } +fn default_num_chunk_producer_seats() -> u64 { + 100 +} + +fn default_num_chunk_validator_seats() -> u64 { + 300 +} + fn default_num_chunk_only_producer_seats() -> u64 { 300 } @@ -162,6 +170,7 @@ pub struct GenesisConfig { pub shard_layout: ShardLayout, #[serde(default = "default_num_chunk_only_producer_seats")] #[default(300)] + /// Deprecated. pub num_chunk_only_producer_seats: NumSeats, /// The minimum number of validators each shard must have #[serde(default = "default_minimum_validators_per_shard")] @@ -189,6 +198,14 @@ pub struct GenesisConfig { /// in AllEpochConfig, and we want to have a way to test that code path. This flag is for that. /// If set to true, the node will use the same config override path as mainnet and testnet. pub use_production_config: bool, + #[serde(default = "default_num_chunk_producer_seats")] + #[default(100)] + /// Number of chunk producers. + /// Don't mess it up with chunk-only producers feature which is deprecated. + pub num_chunk_producer_seats: NumSeats, + #[serde(default = "default_num_chunk_validator_seats")] + #[default(300)] + pub num_chunk_validator_seats: NumSeats, } impl GenesisConfig { @@ -217,6 +234,8 @@ impl From<&GenesisConfig> for EpochConfig { minimum_stake_divisor: config.minimum_stake_divisor, shard_layout: config.shard_layout.clone(), validator_selection_config: near_primitives::epoch_manager::ValidatorSelectionConfig { + num_chunk_producer_seats: config.num_chunk_producer_seats, + num_chunk_validator_seats: config.num_chunk_validator_seats, num_chunk_only_producer_seats: config.num_chunk_only_producer_seats, minimum_validators_per_shard: config.minimum_validators_per_shard, minimum_stake_ratio: config.minimum_stake_ratio, diff --git a/core/chain-configs/src/test_genesis.rs b/core/chain-configs/src/test_genesis.rs index b97f2eb8bc5..40e3741aa46 100644 --- a/core/chain-configs/src/test_genesis.rs +++ b/core/chain-configs/src/test_genesis.rs @@ -57,6 +57,8 @@ enum ValidatorsSpec { validators: Vec, num_block_producer_seats: NumSeats, num_chunk_only_producer_seats: NumSeats, + num_chunk_producer_seats: NumSeats, + num_chunk_validator_seats: NumSeats, }, } @@ -188,6 +190,8 @@ impl TestGenesisBuilder { validators, num_block_producer_seats, num_chunk_only_producer_seats, + num_chunk_producer_seats: num_block_producer_seats, + num_chunk_validator_seats: num_block_producer_seats + num_chunk_only_producer_seats, }); self } @@ -474,6 +478,8 @@ impl TestGenesisBuilder { max_inflation_rate: Rational32::new(1, 1), protocol_upgrade_stake_threshold: Rational32::new(8, 10), use_production_config: false, + num_chunk_producer_seats: derived_validator_setup.num_chunk_producer_seats, + num_chunk_validator_seats: derived_validator_setup.num_chunk_validator_seats, }; Genesis { @@ -487,6 +493,8 @@ struct DerivedValidatorSetup { validators: Vec, num_block_producer_seats: NumSeats, num_chunk_only_producer_seats: NumSeats, + num_chunk_producer_seats: NumSeats, + num_chunk_validator_seats: NumSeats, minimum_stake_ratio: Rational32, } @@ -523,6 +531,8 @@ fn derive_validator_setup(specs: ValidatorsSpec) -> DerivedValidatorSetup { validators, num_block_producer_seats, num_chunk_only_producer_seats, + num_chunk_producer_seats: num_block_producer_seats, + num_chunk_validator_seats: num_block_producer_seats + num_chunk_only_producer_seats, minimum_stake_ratio, } } @@ -530,10 +540,14 @@ fn derive_validator_setup(specs: ValidatorsSpec) -> DerivedValidatorSetup { validators, num_block_producer_seats, num_chunk_only_producer_seats, + num_chunk_producer_seats, + num_chunk_validator_seats, } => DerivedValidatorSetup { validators, num_block_producer_seats, num_chunk_only_producer_seats, + num_chunk_producer_seats, + num_chunk_validator_seats, minimum_stake_ratio: Rational32::new(160, 1000000), }, } diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index fe36145dcc0..3053acb5992 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -295,6 +295,12 @@ impl AllEpochConfig { /// algorithm. See for details. #[derive(Debug, Clone, SmartDefault, PartialEq, Eq)] pub struct ValidatorSelectionConfig { + #[default(100)] + pub num_chunk_producer_seats: NumSeats, + #[default(300)] + pub num_chunk_validator_seats: NumSeats, + // TODO (#11267): deprecate after StatelessValidationV0 is in place. + // Use 300 for older protocol versions. #[default(300)] pub num_chunk_only_producer_seats: NumSeats, #[default(1)] @@ -732,9 +738,12 @@ pub mod epoch_info { pub validator_to_index: HashMap, pub block_producers_settlement: Vec, pub chunk_producers_settlement: Vec>, - pub hidden_validators_settlement: Vec, - pub fishermen: Vec, - pub fishermen_to_index: HashMap, + /// Deprecated. + pub _hidden_validators_settlement: Vec, + /// Deprecated. + pub _fishermen: Vec, + /// Deprecated. + pub _fishermen_to_index: HashMap, pub stake_change: BTreeMap, pub validator_reward: HashMap, pub validator_kickout: HashMap, @@ -757,9 +766,6 @@ pub mod epoch_info { validator_to_index: HashMap, block_producers_settlement: Vec, chunk_producers_settlement: Vec>, - hidden_validators_settlement: Vec, - fishermen: Vec, - fishermen_to_index: HashMap, stake_change: BTreeMap, validator_reward: HashMap, validator_kickout: HashMap, @@ -785,15 +791,15 @@ pub mod epoch_info { Self::V4(EpochInfoV4 { epoch_height, validators, - fishermen, + _fishermen: Default::default(), validator_to_index, block_producers_settlement, chunk_producers_settlement, - hidden_validators_settlement, + _hidden_validators_settlement: Default::default(), stake_change, validator_reward, validator_kickout, - fishermen_to_index, + _fishermen_to_index: Default::default(), minted_amount, seat_price, protocol_version, @@ -806,15 +812,15 @@ pub mod epoch_info { Self::V3(EpochInfoV3 { epoch_height, validators, - fishermen, + fishermen: Default::default(), validator_to_index, block_producers_settlement, chunk_producers_settlement, - hidden_validators_settlement, + hidden_validators_settlement: Default::default(), stake_change, validator_reward, validator_kickout, - fishermen_to_index, + fishermen_to_index: Default::default(), minted_amount, seat_price, protocol_version, @@ -827,15 +833,15 @@ pub mod epoch_info { Self::V2(EpochInfoV2 { epoch_height, validators, - fishermen, + fishermen: Default::default(), validator_to_index, block_producers_settlement, chunk_producers_settlement, - hidden_validators_settlement, + hidden_validators_settlement: Default::default(), stake_change, validator_reward, validator_kickout, - fishermen_to_index, + fishermen_to_index: Default::default(), minted_amount, seat_price, protocol_version, @@ -993,7 +999,7 @@ pub mod epoch_info { Self::V1(v1) => ValidatorStakeIter::v1(&v1.fishermen), Self::V2(v2) => ValidatorStakeIter::new(&v2.fishermen), Self::V3(v3) => ValidatorStakeIter::new(&v3.fishermen), - Self::V4(v4) => ValidatorStakeIter::new(&v4.fishermen), + Self::V4(v4) => ValidatorStakeIter::new(&v4._fishermen), } } @@ -1072,7 +1078,7 @@ pub mod epoch_info { Self::V1(v1) => v1.fishermen_to_index.contains_key(account_id), Self::V2(v2) => v2.fishermen_to_index.contains_key(account_id), Self::V3(v3) => v3.fishermen_to_index.contains_key(account_id), - Self::V4(v4) => v4.fishermen_to_index.contains_key(account_id), + Self::V4(v4) => v4._fishermen_to_index.contains_key(account_id), } } @@ -1090,9 +1096,9 @@ pub mod epoch_info { .get(account_id) .map(|validator_id| v3.fishermen[*validator_id as usize].clone()), Self::V4(v4) => v4 - .fishermen_to_index + ._fishermen_to_index .get(account_id) - .map(|validator_id| v4.fishermen[*validator_id as usize].clone()), + .map(|validator_id| v4._fishermen[*validator_id as usize].clone()), } } @@ -1102,7 +1108,7 @@ pub mod epoch_info { Self::V1(v1) => ValidatorStake::V1(v1.fishermen[fisherman_id as usize].clone()), Self::V2(v2) => v2.fishermen[fisherman_id as usize].clone(), Self::V3(v3) => v3.fishermen[fisherman_id as usize].clone(), - Self::V4(v4) => v4.fishermen[fisherman_id as usize].clone(), + Self::V4(v4) => v4._fishermen[fisherman_id as usize].clone(), } } diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index 8a00340394a..0090d22b5c1 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -822,6 +822,8 @@ impl ForkNetworkCommand { total_supply: original_config.total_supply, transaction_validity_period: original_config.transaction_validity_period, use_production_config: original_config.use_production_config, + num_chunk_producer_seats: original_config.num_chunk_producer_seats, + num_chunk_validator_seats: original_config.num_chunk_validator_seats, }; let genesis = Genesis::new_from_state_roots(new_config, new_state_roots);