From a9b6c424f3764b39d8377c5a485f2ffe3ff3d012 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 15 Nov 2021 21:29:05 +0800 Subject: [PATCH 1/3] Companion for Substrate#8912 --- frame/staking/src/lib.rs | 425 ++++++++++++------- frame/staking/src/mock.rs | 1 + frame/staking/src/substrate_tests.rs | 63 ++- frame/wormhole/backing/ethereum/src/mock.rs | 1 + node/runtime/pangolin/src/pallets/staking.rs | 3 + node/runtime/pangoro/src/pallets/staking.rs | 3 + 6 files changed, 332 insertions(+), 164 deletions(-) diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 0ef509d751..e58e2638ad 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -381,7 +381,7 @@ pub mod pallet { use sp_runtime::{ helpers_128bit, traits::{ - AccountIdConversion, AtLeast32BitUnsigned, CheckedSub, Convert, Saturating, + AccountIdConversion, AtLeast32BitUnsigned, Bounded, CheckedSub, Convert, Saturating, StaticLookup, Zero, }, Perbill, Percent, Perquintill, SaturatedConversion, @@ -424,6 +424,13 @@ pub mod pallet { DataProvider = Pallet, >; + /// Something that provides the election functionality at genesis. + type GenesisElectionProvider: ElectionProvider< + Self::AccountId, + Self::BlockNumber, + DataProvider = Pallet, + >; + /// Number of sessions per era. #[pallet::constant] type SessionsPerEra: Get; @@ -494,44 +501,47 @@ pub mod pallet { pub enum Event { /// The era payout has been set; the first balance is the validator-payout; the second is /// the remainder from the maximum amount of reward. - /// [era_index, validator_payout, remainder] + /// \[era_index, validator_payout, remainder\] EraPayout(EraIndex, RingBalance, RingBalance), - /// The staker has been rewarded by this amount. [stash, amount] + /// The staker has been rewarded by this amount. \[stash, amount\] Reward(AccountId, RingBalance), /// One validator (and its nominators) has been slashed by the given amount. - /// [validator, amount, amount] + /// \[validator, amount, amount\] Slash(AccountId, RingBalance, KtonBalance), /// An old slashing report from a prior era was discarded because it could - /// not be processed. [session_index] + /// not be processed. \[session_index\] OldSlashingReportDiscarded(SessionIndex), /// A new set of stakers was elected. StakingElection, - /// An account has bonded this amount. [amount, start, end] + /// An account has bonded this amount. \[amount, start, end\] /// /// NOTE: This event is only emitted when funds are bonded via a dispatchable. Notably, /// it will not be emitted for staking rewards when they are added to stake. BondRing(RingBalance, TsInMs, TsInMs), - /// An account has bonded this amount. [amount, start, end] + /// An account has bonded this amount. \[amount, start, end\] /// /// NOTE: This event is only emitted when funds are bonded via a dispatchable. Notably, /// it will not be emitted for staking rewards when they are added to stake. BondKton(KtonBalance), - /// An account has unbonded this amount. [amount, now] + /// An account has unbonded this amount. \[amount, now\] UnbondRing(RingBalance, BlockNumberFor), - /// An account has unbonded this amount. [amount, now] + /// An account has unbonded this amount. [amount, now\] UnbondKton(KtonBalance, BlockNumberFor), /// A nominator has been kicked from a validator. \[nominator, stash\] Kicked(AccountId, AccountId), - /// Someone claimed his deposits. [stash] + /// The election failed. No new era is planned. + StakingElectionFailed, + + /// Someone claimed his deposits. \[stash\] DepositsClaimed(AccountId), - /// Someone claimed his deposits with some *KTON*s punishment. [stash, forfeit] + /// Someone claimed his deposits with some *KTON*s punishment. \[stash, forfeit\] DepositsClaimedWithPunish(AccountId, KtonBalance), } @@ -1004,7 +1014,7 @@ pub mod pallet { origin: OriginFor, controller: ::Source, value: StakingBalanceT, - payee: RewardDestination, + payee: RewardDestination>, promise_month: u8, ) -> DispatchResult { let stash = ensure_signed(origin)?; @@ -1021,14 +1031,14 @@ pub mod pallet { match value { StakingBalance::RingBalance(value) => { - // reject a bond which is considered to be _dust_. + // Reject a bond which is considered to be _dust_. ensure!( value >= T::RingCurrency::minimum_balance(), >::InsufficientValue, ); } StakingBalance::KtonBalance(value) => { - // reject a bond which is considered to be _dust_. + // Reject a bond which is considered to be _dust_. ensure!( value >= T::KtonCurrency::minimum_balance(), >::InsufficientValue, @@ -1586,10 +1596,10 @@ pub mod pallet { } }) }) - .collect::, _>>()?; + .collect::>, _>>()?; let nominations = Nominations { targets, - // initial nominations are considered submitted at era 0. See `Nominations` doc + // Initial nominations are considered submitted at era 0. See `Nominations` doc submitted_in: Self::current_era().unwrap_or(0), suppressed: false, }; @@ -1646,7 +1656,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::set_payee())] pub fn set_payee( origin: OriginFor, - payee: RewardDestination, + payee: RewardDestination>, ) -> DispatchResult { let controller = ensure_signed(origin)?; let ledger = Self::ledger(&controller).ok_or(>::NotController)?; @@ -1758,6 +1768,12 @@ pub mod pallet { /// /// The dispatch origin must be Root. /// + /// # Warning + /// + /// The election process starts multiple blocks before the end of the era. + /// Thus the election process may be ongoing when this is called. In this case the + /// election will continue until the next era is triggered. + /// /// # /// - No arguments. /// - Weight: O(1) @@ -1777,6 +1793,12 @@ pub mod pallet { /// /// The dispatch origin must be Root. /// + /// # Warning + /// + /// The election process starts multiple blocks before the end of the era. + /// If this is called just before a new era is triggered, the election process may not + /// have enough blocks to get a result. + /// /// # /// - No arguments. /// - Weight: O(1) @@ -1802,7 +1824,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::set_invulnerables(invulnerables.len() as u32))] pub fn set_invulnerables( origin: OriginFor, - invulnerables: Vec, + invulnerables: Vec>, ) -> DispatchResult { ensure_root(origin)?; @@ -1824,15 +1846,15 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::force_unstake(*num_slashing_spans))] pub fn force_unstake( origin: OriginFor, - stash: T::AccountId, + stash: AccountId, num_slashing_spans: u32, ) -> DispatchResult { ensure_root(origin)?; - // remove all staking-related information. + // Remove all staking-related information. Self::kill_stash(&stash, num_slashing_spans)?; - // remove the lock. + // Remove the lock. T::RingCurrency::remove_lock(STAKING_ID, &stash); T::KtonCurrency::remove_lock(STAKING_ID, &stash); @@ -1843,6 +1865,12 @@ pub mod pallet { /// /// The dispatch origin must be Root. /// + /// # Warning + /// + /// The election process starts multiple blocks before the end of the era. + /// If this is called just before a new era is triggered, the election process may not + /// have enough blocks to get a result. + /// /// # /// - Weight: O(1) /// - Write: ForceEra @@ -1933,7 +1961,7 @@ pub mod pallet { ))] pub fn payout_stakers( origin: OriginFor, - validator_stash: T::AccountId, + validator_stash: AccountId, era: EraIndex, ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; @@ -1979,7 +2007,7 @@ pub mod pallet { ledger.rebond(plan_to_rebond_ring, plan_to_rebond_kton); - // last check: the new active amount of ledger must be more than ED. + // Last check: the new active amount of ledger must be more than ED. ensure!( ledger.active_ring >= T::RingCurrency::minimum_balance() || ledger.active_kton >= T::KtonCurrency::minimum_balance(), @@ -2071,7 +2099,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::reap_stash(*num_slashing_spans))] pub fn reap_stash( _origin: OriginFor, - stash: T::AccountId, + stash: AccountId, num_slashing_spans: u32, ) -> DispatchResult { let total_ring = T::RingCurrency::total_balance(&stash); @@ -2115,7 +2143,7 @@ pub mod pallet { for nom_stash in who .into_iter() .map(T::Lookup::lookup) - .collect::, _>>()? + .collect::>, _>>()? .into_iter() { >::mutate(&nom_stash, |maybe_nom| { @@ -2132,14 +2160,14 @@ pub mod pallet { } } impl Pallet { - pub fn account_id() -> T::AccountId { + pub fn account_id() -> AccountId { T::PalletId::get().into_account() } /// Update the ledger while bonding ring and compute the *KTON* reward pub fn bond_ring( - stash: &T::AccountId, - controller: &T::AccountId, + stash: &AccountId, + controller: &AccountId, value: RingBalance, promise_month: u8, mut ledger: StakingLedgerT, @@ -2157,14 +2185,14 @@ pub mod pallet { *active_ring = active_ring.saturating_add(value); - // last check: the new active amount of ledger must be more than ED. + // Last check: the new active amount of ledger must be more than ED. ensure!( *active_ring >= T::RingCurrency::minimum_balance() || *active_kton >= T::KtonCurrency::minimum_balance(), >::InsufficientValue ); - // if stash promise to an extra-lock + // If stash promise to an extra-lock // there will be extra reward (*KTON*), which can also be used for staking if promise_month > 0 { expire_time += promise_month as TsInMs * MONTH_IN_MILLISECONDS; @@ -2189,13 +2217,13 @@ pub mod pallet { /// Update the ledger while bonding controller with *KTON* pub fn bond_kton( - controller: &T::AccountId, + controller: &AccountId, value: KtonBalance, mut ledger: StakingLedgerT, ) -> DispatchResult { ledger.active_kton = ledger.active_kton.saturating_add(value); - // last check: the new active amount of ledger must be more than ED. + // Last check: the new active amount of ledger must be more than ED. ensure!( ledger.active_ring >= T::RingCurrency::minimum_balance() || ledger.active_kton >= T::KtonCurrency::minimum_balance(), @@ -2247,7 +2275,7 @@ pub mod pallet { } /// The total power that can be slashed from a stash account as of right now. - pub fn power_of(stash: &T::AccountId) -> Power { + pub fn power_of(stash: &AccountId) -> Power { // Weight note: consider making the stake accessible through stash. Self::bonded(stash) .and_then(Self::ledger) @@ -2260,13 +2288,13 @@ pub mod pallet { darwinia_support::impl_rpc! { pub fn power_of_rpc( - stash: impl sp_std::borrow::Borrow, + stash: impl sp_std::borrow::Borrow>, ) -> RuntimeDispatchInfo { RuntimeDispatchInfo { power: Self::power_of(stash.borrow()) } } } - pub fn stake_of(stash: &T::AccountId) -> (RingBalance, KtonBalance) { + pub fn stake_of(stash: &AccountId) -> (RingBalance, KtonBalance) { // Weight note: consider making the stake accessible through stash. Self::bonded(stash) .and_then(Self::ledger) @@ -2275,7 +2303,7 @@ pub mod pallet { } pub fn do_payout_stakers( - validator_stash: T::AccountId, + validator_stash: AccountId, era: EraIndex, ) -> DispatchResultWithPostInfo { // Validate input data @@ -2429,7 +2457,7 @@ pub mod pallet { /// BE CAREFUL: /// This will also update the stash lock. /// DO NOT modify the locks' staking amount outside this function. - pub fn update_ledger(controller: &T::AccountId, ledger: &mut StakingLedgerT) { + pub fn update_ledger(controller: &AccountId, ledger: &mut StakingLedgerT) { let StakingLedger { active_ring, active_kton, @@ -2484,7 +2512,7 @@ pub mod pallet { } /// Chill a stash account. - pub fn chill_stash(stash: &T::AccountId) { + pub fn chill_stash(stash: &AccountId) { >::remove(stash); >::remove(stash); } @@ -2492,7 +2520,7 @@ pub mod pallet { /// Actually make a payment to a staker. This uses the currency's reward function /// to pay the right payee for the given staker account. pub fn make_payout( - stash: &T::AccountId, + stash: &AccountId, amount: RingBalance, ) -> Option> { let dest = Self::payee(stash); @@ -2524,10 +2552,12 @@ pub mod pallet { } /// Plan a new session potentially trigger a new era. - pub fn new_session(session_index: SessionIndex) -> Option> { + pub fn new_session( + session_index: SessionIndex, + is_genesis: bool, + ) -> Option>> { if let Some(current_era) = Self::current_era() { // Initial era has been set. - let current_era_start_session_index = Self::eras_start_session_index(current_era) .unwrap_or_else(|| { frame_support::print( @@ -2541,25 +2571,32 @@ pub mod pallet { .unwrap_or(0); // Must never happen. match >::get() { - // Will set to default again, which is `NotForcing`. - Forcing::ForceNew => >::kill(), - // Short circuit to `new_era`. + // Will be set to `NotForcing` again if a new era has been triggered. + Forcing::ForceNew => (), + // Short circuit to `try_trigger_new_era`. Forcing::ForceAlways => (), - // Only go to `new_era` if deadline reached. + // Only go to `try_trigger_new_era` if deadline reached. Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (), _ => { - // either `Forcing::ForceNone`, + // Either `Forcing::ForceNone`, // or `Forcing::NotForcing if era_length >= T::SessionsPerEra::get()`. return None; } } - // new era. - Self::new_era(session_index) + // New era. + let maybe_new_era_validators = Self::try_trigger_new_era(session_index, is_genesis); + if maybe_new_era_validators.is_some() + && matches!(>::get(), Forcing::ForceNew) + { + >::put(Forcing::NotForcing); + } + + maybe_new_era_validators } else { - // Set initial era + // Set initial era. log!(debug, "Starting the first era."); - Self::new_era(session_index) + Self::try_trigger_new_era(session_index, is_genesis) } } @@ -2622,13 +2659,13 @@ pub mod pallet { if active_era > bonding_duration { let first_kept = active_era - bonding_duration; - // prune out everything that's from before the first-kept index. + // Prune out everything that's from before the first-kept index. let n_to_prune = bonded .iter() .take_while(|&&(era_idx, _)| era_idx < first_kept) .count(); - // kill slashing metadata. + // Kill slashing metadata. for (pruned_era, _) in bonded.drain(..n_to_prune) { slashing::clear_era_metadata::(pruned_era); } @@ -2668,122 +2705,162 @@ pub mod pallet { } } - /// Plan a new era. Return the potential new staking set. - pub fn new_era(start_session_index: SessionIndex) -> Option> { + /// Plan a new era. + /// + /// * Bump the current era storage (which holds the latest planned era). + /// * Store start session index for the new planned era. + /// * Clean old era information. + /// * Store staking information for the new planned era + /// + /// Returns the new validator set. + pub fn trigger_new_era( + start_session_index: SessionIndex, + exposures: Vec<(AccountId, ExposureT)>, + ) -> Vec> { // Increment or set current era. - let current_era = >::mutate(|s| { + let new_planned_era = >::mutate(|s| { *s = Some(s.map(|s| s + 1).unwrap_or(0)); s.unwrap() }); - >::insert(¤t_era, &start_session_index); + >::insert(&new_planned_era, &start_session_index); // Clean old era information. - if let Some(old_era) = current_era.checked_sub(Self::history_depth() + 1) { + if let Some(old_era) = new_planned_era.checked_sub(Self::history_depth() + 1) { Self::clear_era_information(old_era); } - let maybe_new_validators = Self::enact_election(current_era); - - maybe_new_validators + // Set staking information for the new era. + Self::store_stakers_info(exposures, new_planned_era) } - /// Enact and process the election using the `ElectionProvider` type. + /// Potentially plan a new era. /// - /// This will also process the election, as noted in [`process_election`]. - pub fn enact_election(current_era: EraIndex) -> Option> { - T::ElectionProvider::elect() - .map_err(|e| log!(warn, "election provider failed due to {:?}", e)) - .and_then(|(res, weight)| { - >::register_extra_weight_unchecked( - weight, - frame_support::weights::DispatchClass::Mandatory, - ); - Self::process_election(res, current_era) + /// Get election result from `T::ElectionProvider`. + /// In case election result has more than [`MinimumValidatorCount`] validator trigger a new era. + /// + /// In case a new era is planned, the new validator set is returned. + fn try_trigger_new_era( + start_session_index: SessionIndex, + is_genesis: bool, + ) -> Option>> { + let (election_result, weight) = if is_genesis { + T::GenesisElectionProvider::elect().map_err(|e| { + log!(warn, "genesis election provider failed due to {:?}", e); + + Self::deposit_event(Event::StakingElectionFailed); }) - .ok() + } else { + T::ElectionProvider::elect().map_err(|e| { + log!(warn, "election provider failed due to {:?}", e); + + Self::deposit_event(Event::StakingElectionFailed); + }) + } + .ok()?; + + >::register_extra_weight_unchecked( + weight, + frame_support::weights::DispatchClass::Mandatory, + ); + + let exposures = Self::collect_exposures(election_result); + + if (exposures.len() as u32) < Self::minimum_validator_count().max(1) { + // Session will panic if we ever return an empty validator set, thus max(1) ^^. + match CurrentEra::::get() { + Some(current_era) if current_era > 0 => log!( + warn, + "chain does not have enough staking candidates to operate for era {:?} ({} \ + elected, minimum is {})", + CurrentEra::::get().unwrap_or(0), + exposures.len(), + Self::minimum_validator_count(), + ), + None => { + // The initial era is allowed to have no exposures. + // In this case the SessionManager is expected to choose a sensible validator + // set. + // TODO: this should be simplified #8911 + >::put(0); + >::insert(0, &start_session_index); + } + _ => (), + } + + Self::deposit_event(Event::StakingElectionFailed); + + return None; + } + + Self::deposit_event(Event::StakingElection); + + Some(Self::trigger_new_era(start_session_index, exposures)) } /// Process the output of the election. /// - /// This ensures enough validators have been elected, converts all supports to exposures and - /// writes them to the associated storage. - /// - /// Returns `Err(())` if less than [`MinimumValidatorCount`] validators have been elected, `Ok` - /// otherwise. - pub fn process_election( - flat_supports: Supports, - current_era: EraIndex, - ) -> Result, ()> { - let exposures = Self::collect_exposures(flat_supports); + /// Store staking information for the new planned era + pub fn store_stakers_info( + exposures: Vec<(AccountId, ExposureT)>, + new_planned_era: EraIndex, + ) -> Vec> { let elected_stashes = exposures .iter() .cloned() .map(|(x, _)| x) .collect::>(); - - if (elected_stashes.len() as u32) < Self::minimum_validator_count().max(1) { - // Session will panic if we ever return an empty validator set, thus max(1) ^^. - if current_era > 0 { - log!( - warn, - "chain does not have enough staking candidates to operate for era {:?} ({} elected, minimum is {})", - current_era, - elected_stashes.len(), - Self::minimum_validator_count(), - ); - } - return Err(()); - } - // Populate stakers, exposures, and the snapshot of validator prefs. let mut total_stake = 0; + exposures.into_iter().for_each(|(stash, exposure)| { total_stake = total_stake.saturating_add(exposure.total_power); - >::insert(current_era, &stash, &exposure); + + >::insert(new_planned_era, &stash, &exposure); let mut exposure_clipped = exposure; let clipped_max_len = T::MaxNominatorRewardedPerValidator::get() as usize; + if exposure_clipped.others.len() > clipped_max_len { exposure_clipped .others .sort_by(|a, b| a.power.cmp(&b.power).reverse()); exposure_clipped.others.truncate(clipped_max_len); } - >::insert(¤t_era, &stash, exposure_clipped); + + >::insert(&new_planned_era, &stash, exposure_clipped); }); // Insert current era staking information - >::insert(¤t_era, total_stake); + >::insert(&new_planned_era, total_stake); - // collect the pref of all winners + // Collect the pref of all winners for stash in &elected_stashes { let pref = Self::validators(stash); - >::insert(¤t_era, stash, pref); - } - Self::deposit_event(Event::StakingElection); + >::insert(&new_planned_era, stash, pref); + } - if current_era > 0 { + if new_planned_era > 0 { log!( info, "new validator set of size {:?} has been processed for era {:?}", elected_stashes.len(), - current_era, + new_planned_era, ); } - Ok(elected_stashes) + elected_stashes } /// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a /// [`Exposure`]. pub fn collect_exposures( - supports: Supports, - ) -> Vec<(T::AccountId, ExposureT)> { + supports: Supports>, + ) -> Vec<(AccountId, ExposureT)> { supports .into_iter() .map(|(validator, support)| { - // build `struct exposure` from `support` + // Build `struct exposure` from `support` let mut own_ring_balance: RingBalance = Zero::zero(); let mut own_kton_balance: KtonBalance = Zero::zero(); let mut own_power = 0; @@ -2871,7 +2948,7 @@ pub mod pallet { /// This is called: /// - after a `withdraw_unbond()` call that frees all of a stash's bonded balance. /// - through `reap_stash()` if the balance has fallen to zero (through slashing). - pub fn kill_stash(stash: &T::AccountId, num_slashing_spans: u32) -> DispatchResult { + pub fn kill_stash(stash: &AccountId, num_slashing_spans: u32) -> DispatchResult { let controller = >::get(stash).ok_or(>::NotStash)?; slashing::clear_stash_metadata::(stash, num_slashing_spans)?; @@ -2929,7 +3006,7 @@ pub mod pallet { /// /// COMPLEXITY: Complexity is `number_of_validator_to_reward x current_elected_len`. /// If you need to reward lots of validator consider using `reward_by_indices`. - pub fn reward_by_ids(validators_points: impl IntoIterator) { + pub fn reward_by_ids(validators_points: impl IntoIterator, u32)>) { if let Some(active_era) = Self::active_era() { >::mutate(active_era.index, |era_rewards| { for (validator, points) in validators_points.into_iter() { @@ -2951,7 +3028,7 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] pub fn add_era_stakers( current_era: EraIndex, - controller: T::AccountId, + controller: AccountId, exposure: ExposureT, ) { >::insert(¤t_era, &controller, &exposure); @@ -2972,13 +3049,13 @@ pub mod pallet { /// auto-chilled. /// /// Note that this is VERY expensive. Use with care. - pub fn get_npos_voters() -> Vec<(T::AccountId, VoteWeight, Vec)> { + pub fn get_npos_voters() -> Vec<(AccountId, VoteWeight, Vec>)> { let weight_of = - |account_id: &T::AccountId| -> VoteWeight { Self::power_of(account_id) as _ }; + |account_id: &AccountId| -> VoteWeight { Self::power_of(account_id) as _ }; let mut all_voters = Vec::new(); for (validator, _) in >::iter() { - // append self vote + // Append self vote let self_vote = ( validator.clone(), weight_of(&validator), @@ -2987,7 +3064,7 @@ pub mod pallet { all_voters.push(self_vote); } - // collect all slashing spans into a BTreeMap for further queries. + // Collect all slashing spans into a BTreeMap for further queries. let slashing_spans = >::iter().collect::>(); for (nominator, nominations) in >::iter() { @@ -3014,11 +3091,11 @@ pub mod pallet { all_voters } - pub fn get_npos_targets() -> Vec { + pub fn get_npos_targets() -> Vec> { >::iter().map(|(v, _)| v).collect::>() } } - impl ElectionDataProvider for Pallet { + impl ElectionDataProvider, T::BlockNumber> for Pallet { const MAXIMUM_VOTES_PER_VOTER: u32 = T::MAX_NOMINATIONS; fn desired_targets() -> data_provider::Result<(u32, Weight)> { @@ -3030,7 +3107,7 @@ pub mod pallet { fn voters( maybe_max_len: Option, - ) -> data_provider::Result<(Vec<(T::AccountId, VoteWeight, Vec)>, Weight)> { + ) -> data_provider::Result<(Vec<(AccountId, VoteWeight, Vec>)>, Weight)> { // NOTE: reading these counts already needs to iterate a lot of storage keys, but they get // cached. This is okay for the case of `Ok(_)`, but bad for `Err(_)`, as the trait does not // report weight in failures. @@ -3053,7 +3130,7 @@ pub mod pallet { fn targets( maybe_max_len: Option, - ) -> data_provider::Result<(Vec, Weight)> { + ) -> data_provider::Result<(Vec>, Weight)> { let target_count = >::iter().count(); if maybe_max_len.map_or(false, |max_len| target_count > max_len) { @@ -3072,19 +3149,21 @@ pub mod pallet { let era_length = current_session .saturating_sub(current_era_start_session_index) .min(T::SessionsPerEra::get()); - - let session_length = T::NextNewSession::average_session_length(); - let until_this_session_end = T::NextNewSession::estimate_next_new_session(now) .0 .unwrap_or_default() .saturating_sub(now); - - let sessions_left: T::BlockNumber = T::SessionsPerEra::get() - .saturating_sub(era_length) - // one session is computed in this_session_end. - .saturating_sub(1) - .into(); + let session_length = T::NextNewSession::average_session_length(); + let sessions_left: T::BlockNumber = match ForceEra::::get() { + Forcing::ForceNone => Bounded::max_value(), + Forcing::ForceNew | Forcing::ForceAlways => Zero::zero(), + Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => Zero::zero(), + Forcing::NotForcing => T::SessionsPerEra::get() + .saturating_sub(era_length) + // One session is computed in this_session_end. + .saturating_sub(1) + .into(), + }; now.saturating_add( until_this_session_end.saturating_add(sessions_left.saturating_mul(session_length)), @@ -3093,8 +3172,8 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] fn put_snapshot( - voters: Vec<(T::AccountId, VoteWeight, Vec)>, - targets: Vec, + voters: Vec<(AccountId, VoteWeight, Vec>)>, + targets: Vec>, target_stake: Option, ) { use sp_std::convert::TryFrom; @@ -3148,29 +3227,36 @@ pub mod pallet { }); } } - impl pallet_session::SessionManager for Pallet { - fn new_session(new_index: SessionIndex) -> Option> { - log!(trace, "planning new_session({})", new_index); + impl pallet_session::SessionManager> for Pallet { + fn new_session(new_index: SessionIndex) -> Option>> { + log!(trace, "planning new session {}", new_index); >::put(new_index); - Self::new_session(new_index) + Self::new_session(new_index, false) } - fn end_session(end_index: SessionIndex) { - log!(trace, "ending end_session({})", end_index); + fn new_session_genesis(new_index: SessionIndex) -> Option>> { + log!(trace, "planning new session {} at genesis", new_index); - Self::end_session(end_index) + >::put(new_index); + + Self::new_session(new_index, true) } fn start_session(start_index: SessionIndex) { - log!(trace, "starting start_session({})", start_index); + log!(trace, "starting session {}", start_index); Self::start_session(start_index) } + fn end_session(end_index: SessionIndex) { + log!(trace, "ending session {}", end_index); + + Self::end_session(end_index) + } } - impl pallet_session::historical::SessionManager> + impl pallet_session::historical::SessionManager, ExposureT> for Pallet { - fn new_session(new_index: SessionIndex) -> Option)>> { + fn new_session(new_index: SessionIndex) -> Option, ExposureT)>> { >::new_session(new_index).map(|validators| { let current_era = Self::current_era() // Must be some as a new era has been created. @@ -3185,6 +3271,25 @@ pub mod pallet { .collect() }) } + fn new_session_genesis( + new_index: SessionIndex, + ) -> Option, ExposureT)>> { + >::new_session_genesis(new_index).map( + |validators| { + let current_era = Self::current_era() + // Must be some as a new era has been created. + .unwrap_or(0); + + validators + .into_iter() + .map(|v| { + let exposure = Self::eras_stakers(current_era, &v); + (v, exposure) + }) + .collect() + }, + ) + } fn start_session(start_index: SessionIndex) { >::start_session(start_index) } @@ -3194,7 +3299,7 @@ pub mod pallet { } /// This is intended to be used with `FilterHistoricalOffences`. impl - OnOffenceHandler, Weight> + OnOffenceHandler, pallet_session::historical::IdentificationTuple, Weight> for Pallet where T: Config @@ -3209,7 +3314,7 @@ pub mod pallet { { fn on_offence( offenders: &[OffenceDetails< - T::AccountId, + AccountId, pallet_session::historical::IdentificationTuple, >], slash_fraction: &[Perbill], @@ -3225,7 +3330,7 @@ pub mod pallet { let active_era = Self::active_era(); add_db_reads_writes(1, 0); if active_era.is_none() { - // this offence need not be re-submitted. + // This offence need not be re-submitted. return consumed_weight; } active_era @@ -3241,7 +3346,7 @@ pub mod pallet { let window_start = active_era.saturating_sub(T::BondingDurationInEra::get()); - // fast path for active-era report - most likely. + // Fast path for active-era report - most likely. // `slash_session` cannot be in a future active era. It must be in `active_era` or before. let slash_era = if slash_session >= active_era_start_session_index { active_era @@ -3249,7 +3354,7 @@ pub mod pallet { let eras = >::get(); add_db_reads_writes(1, 0); - // reverse because it's more likely to find reports from recent eras. + // Reverse because it's more likely to find reports from recent eras. match eras .iter() .rev() @@ -3257,7 +3362,7 @@ pub mod pallet { .next() { Some(&(ref slash_era, _)) => *slash_era, - // before bonding period. defensive - should be filtered out. + // Before bonding period. defensive - should be filtered out. None => return consumed_weight, } }; @@ -3303,7 +3408,7 @@ pub mod pallet { } unapplied.reporters = details.reporters.clone(); if slash_defer_duration == 0 { - // apply right away. + // Apply right away. slashing::apply_slash::(unapplied); { let slash_cost = (6, 5); @@ -3314,7 +3419,7 @@ pub mod pallet { ); } } else { - // defer to end of some `slash_defer_duration` from now. + // Defer to end of some `slash_defer_duration` from now. ::UnappliedSlashes::mutate(active_era, move |for_later| { for_later.push(unapplied) }); @@ -3328,10 +3433,10 @@ pub mod pallet { consumed_weight } } - impl OnDepositRedeem> for Pallet { + impl OnDepositRedeem, RingBalance> for Pallet { fn on_deposit_redeem( - backing: &T::AccountId, - stash: &T::AccountId, + backing: &AccountId, + stash: &AccountId, amount: RingBalance, start_time: TsInMs, months: u8, @@ -3413,14 +3518,14 @@ pub mod pallet { /// * 20 points to the block producer for producing a (non-uncle) block in the relay chain, /// * 2 points to the block producer for each reference to a previously unreferenced uncle, and /// * 1 point to the producer of each referenced uncle block. - impl pallet_authorship::EventHandler for Pallet + impl pallet_authorship::EventHandler, T::BlockNumber> for Pallet where T: Config + pallet_authorship::Config + pallet_session::Config, { - fn note_author(author: T::AccountId) { + fn note_author(author: AccountId) { Self::reward_by_ids(vec![(author, 20)]); } - fn note_uncle(author: T::AccountId, _age: T::BlockNumber) { + fn note_uncle(author: AccountId, _age: T::BlockNumber) { Self::reward_by_ids(vec![ (>::author(), 2), (author, 1), @@ -3845,6 +3950,8 @@ pub mod pallet { /// Not forcing anything - just let whatever happen. NotForcing, /// Force a new era, then reset to `NotForcing` as soon as it is done. + /// Note that this will force to trigger an election until a new era is triggered, if the + /// election failed, the next session end will trigger a new election again, until success. ForceNew, /// Avoid a new era indefinitely. ForceNone, @@ -3926,8 +4033,8 @@ pub mod pallet { /// A `Convert` implementation that finds the stash of the given controller account, /// if any. pub struct StashOf(PhantomData); - impl Convert> for StashOf { - fn convert(controller: T::AccountId) -> Option { + impl Convert, Option>> for StashOf { + fn convert(controller: AccountId) -> Option> { >::ledger(&controller).map(|l| l.stash) } } @@ -3944,7 +4051,7 @@ pub mod pallet { O: Offence, { fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { - // disallow any slashing from before the current bonding period. + // Disallow any slashing from before the current bonding period. let offence_session = offence.session_index(); let bonded_eras = >::get(); diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index 17cbe3b575..cfcdf7e101 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -261,6 +261,7 @@ impl Config for Test { type NextNewSession = Session; type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; type ElectionProvider = onchain::OnChainSequentialPhragmen; + type GenesisElectionProvider = Self::ElectionProvider; type RingCurrency = Ring; type RingRewardRemainder = RingRewardRemainderMock; type RingSlash = (); diff --git a/frame/staking/src/substrate_tests.rs b/frame/staking/src/substrate_tests.rs index 3c5a3f6b7b..646157aefc 100644 --- a/frame/staking/src/substrate_tests.rs +++ b/frame/staking/src/substrate_tests.rs @@ -545,13 +545,26 @@ fn no_candidate_emergency_condition() { assert_ok!(Staking::chill(Origin::signed(10))); - // trigger era - start_active_era(1); + let current_era = >::get(); - // Previous ones are elected. chill is invalidates. TODO: #2494 + // try trigger new era + mock::run_to_block(20); + assert_eq!( + *staking_events().last().unwrap(), + Event::StakingElectionFailed, + ); + // No new era is created + assert_eq!(current_era, >::get()); + + // Go to far further session to see if validator have changed + mock::run_to_block(100); + + // Previous ones are elected. chill is not effective in active era (as era hasn't changed) assert_eq_uvec!(validator_controllers(), vec![10, 20, 30, 40]); - // Though the validator preferences has been removed. - assert!(Staking::validators(11) != prefs); + // The chill is still pending. + assert!(!::Validators::contains_key(11)); + // No new era is created. + assert_eq!(current_era, >::get()); }); } @@ -4684,6 +4697,46 @@ mod election_data_provider { ); assert_eq!(staking_events().len(), 3); assert_eq!(*staking_events().last().unwrap(), Event::StakingElection); + + Staking::force_no_eras(Origin::root()).unwrap(); + assert_eq!( + Staking::next_election_prediction(System::block_number()), + u64::max_value() + ); + + Staking::force_new_era_always(Origin::root()).unwrap(); + assert_eq!( + Staking::next_election_prediction(System::block_number()), + 45 + 5 + ); + + Staking::force_new_era(Origin::root()).unwrap(); + assert_eq!( + Staking::next_election_prediction(System::block_number()), + 45 + 5 + ); + + // Do a fail election + MinimumValidatorCount::::put(1000); + run_to_block(50); + // Election: failed, next session is a new election + assert_eq!( + Staking::next_election_prediction(System::block_number()), + 50 + 5 + ); + // The new era is still forced until a new era is planned. + assert_eq!(ForceEra::::get(), Forcing::ForceNew); + + MinimumValidatorCount::::put(2); + run_to_block(55); + assert_eq!( + Staking::next_election_prediction(System::block_number()), + 55 + 25 + ); + assert_eq!(staking_events().len(), 6); + assert_eq!(*staking_events().last().unwrap(), Event::StakingElection); + // The new era has been planned, forcing is changed from `ForceNew` to `NotForcing`. + assert_eq!(ForceEra::::get(), Forcing::NotForcing); }) } } diff --git a/frame/wormhole/backing/ethereum/src/mock.rs b/frame/wormhole/backing/ethereum/src/mock.rs index 4f75457481..9cb062369b 100644 --- a/frame/wormhole/backing/ethereum/src/mock.rs +++ b/frame/wormhole/backing/ethereum/src/mock.rs @@ -181,6 +181,7 @@ impl darwinia_staking::Config for Test { type NextNewSession = Session; type MaxNominatorRewardedPerValidator = (); type ElectionProvider = onchain::OnChainSequentialPhragmen; + type GenesisElectionProvider = Self::ElectionProvider; type RingCurrency = Ring; type RingRewardRemainder = (); type RingSlash = (); diff --git a/node/runtime/pangolin/src/pallets/staking.rs b/node/runtime/pangolin/src/pallets/staking.rs index 645d07d57e..abc907322d 100644 --- a/node/runtime/pangolin/src/pallets/staking.rs +++ b/node/runtime/pangolin/src/pallets/staking.rs @@ -1,5 +1,7 @@ // --- paritytech --- +use frame_election_provider_support::onchain::OnChainSequentialPhragmen; use frame_support::PalletId; +use pallet_election_provider_multi_phase::OnChainConfig; use sp_npos_elections::CompactSolution; use sp_staking::SessionIndex; // --- darwinia-network --- @@ -34,6 +36,7 @@ impl Config for Runtime { type NextNewSession = Session; type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; type ElectionProvider = ElectionProviderMultiPhase; + type GenesisElectionProvider = OnChainSequentialPhragmen>; type RingCurrency = Ring; type RingRewardRemainder = Treasury; // send the slashed funds to the treasury. diff --git a/node/runtime/pangoro/src/pallets/staking.rs b/node/runtime/pangoro/src/pallets/staking.rs index 18566e7f61..b50957da31 100644 --- a/node/runtime/pangoro/src/pallets/staking.rs +++ b/node/runtime/pangoro/src/pallets/staking.rs @@ -1,6 +1,8 @@ // --- paritytech --- +use frame_election_provider_support::onchain::OnChainSequentialPhragmen; use frame_support::PalletId; use frame_system::EnsureRoot; +use pallet_election_provider_multi_phase::OnChainConfig; use sp_npos_elections::CompactSolution; use sp_staking::SessionIndex; // --- darwinia-network --- @@ -34,6 +36,7 @@ impl Config for Runtime { type NextNewSession = Session; type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; type ElectionProvider = ElectionProviderMultiPhase; + type GenesisElectionProvider = OnChainSequentialPhragmen>; type RingCurrency = Ring; type RingRewardRemainder = (); // send the slashed funds to the treasury. From d8b1b2664dff8e03afcbd526b032b66dbd181eb7 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 15 Nov 2021 21:29:05 +0800 Subject: [PATCH 2/3] Add Missing Crate --- Cargo.lock | 2 ++ node/runtime/pangolin/Cargo.toml | 2 ++ node/runtime/pangoro/Cargo.toml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 22c99b44f7..58123a84cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6175,6 +6175,7 @@ dependencies = [ "ethereum-primitives", "evm 0.27.0", "frame-benchmarking", + "frame-election-provider-support", "frame-executive", "frame-support", "frame-system", @@ -6283,6 +6284,7 @@ dependencies = [ "dp-fee", "dp-s2s", "frame-benchmarking", + "frame-election-provider-support", "frame-executive", "frame-support", "frame-system", diff --git a/node/runtime/pangolin/Cargo.toml b/node/runtime/pangolin/Cargo.toml index 69d5044043..277051e7cb 100644 --- a/node/runtime/pangolin/Cargo.toml +++ b/node/runtime/pangolin/Cargo.toml @@ -67,6 +67,7 @@ bp-messages = { default-features = false, git = " bp-runtime = { default-features = false, git = "https://github.com/darwinia-network/parity-bridges-common", tag = "darwinia-v0.11.6" } bridge-runtime-common = { default-features = false, git = "https://github.com/darwinia-network/parity-bridges-common", tag = "darwinia-v0.11.6" } frame-benchmarking = { optional = true, default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } +frame-election-provider-support = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } frame-executive = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } @@ -181,6 +182,7 @@ std = [ "bp-runtime/std", "bridge-runtime-common/std", "frame-benchmarking/std", + "frame-election-provider-support/std", "frame-executive/std", "frame-support/std", "frame-system/std", diff --git a/node/runtime/pangoro/Cargo.toml b/node/runtime/pangoro/Cargo.toml index ebf4041a09..299e6bb46e 100644 --- a/node/runtime/pangoro/Cargo.toml +++ b/node/runtime/pangoro/Cargo.toml @@ -38,6 +38,7 @@ bp-messages = { default-features = false, git = " bp-runtime = { default-features = false, git = "https://github.com/darwinia-network/parity-bridges-common", tag = "darwinia-v0.11.6" } bridge-runtime-common = { default-features = false, git = "https://github.com/darwinia-network/parity-bridges-common", tag = "darwinia-v0.11.6" } frame-benchmarking = { optional = true, default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } +frame-election-provider-support = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } frame-executive = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6" } @@ -115,6 +116,7 @@ std = [ "bp-runtime/std", "bridge-runtime-common/std", "frame-benchmarking/std", + "frame-election-provider-support/std", "frame-executive/std", "frame-support/std", "frame-system/std", From afd5a43224d981ec6c0ab4b1cf455928a1c8958d Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 15 Nov 2021 21:40:04 +0800 Subject: [PATCH 3/3] Companion for Substrate#9108 --- frame/staking/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index e58e2638ad..57fca8d577 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -3146,7 +3146,7 @@ pub mod pallet { let current_session = Self::current_planned_session(); let current_era_start_session_index = Self::eras_start_session_index(current_era).unwrap_or(0); - let era_length = current_session + let era_progress = current_session .saturating_sub(current_era_start_session_index) .min(T::SessionsPerEra::get()); let until_this_session_end = T::NextNewSession::estimate_next_new_session(now) @@ -3157,9 +3157,9 @@ pub mod pallet { let sessions_left: T::BlockNumber = match ForceEra::::get() { Forcing::ForceNone => Bounded::max_value(), Forcing::ForceNew | Forcing::ForceAlways => Zero::zero(), - Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => Zero::zero(), + Forcing::NotForcing if era_progress >= T::SessionsPerEra::get() => Zero::zero(), Forcing::NotForcing => T::SessionsPerEra::get() - .saturating_sub(era_length) + .saturating_sub(era_progress) // One session is computed in this_session_end. .saturating_sub(1) .into(),