Newer
Older
let mut unapplied = <Self as Store>::UnappliedSlashes::get(&era);
let last_item = slash_indices[slash_indices.len() - 1];
ensure!((last_item as usize) < unapplied.len(), Error::<T>::InvalidSlashIndex);
for (removed, index) in slash_indices.into_iter().enumerate() {
let index = (index as usize) - removed;
unapplied.remove(index);
}
<Self as Store>::UnappliedSlashes::insert(&era, &unapplied);
}
/// Pay out all the stakers behind a single validator for a single era.
///
/// - `validator_stash` is the stash account of the validator. Their nominators, up to
/// `T::MaxNominatorRewardedPerValidator`, will also receive their rewards.
/// - `era` may be any era between `[current_era - history_depth; current_era]`.
///
/// The origin of this call must be _Signed_. Any account can call this function, even if
/// it is not one of the stakers.
///
/// This can only be called when [`EraElectionStatus`] is `Closed`.
///
/// # <weight>
/// - Time complexity: at most O(MaxNominatorRewardedPerValidator).
/// - Contains a limited number of reads and writes.
/// -----------
/// N is the Number of payouts for the validator (including the validator)
/// Weight:
/// - Reward Destination Staked: O(N)
/// - Reward Destination Controller (Creating): O(N)
/// - Read: EraElectionStatus, CurrentEra, HistoryDepth, ErasValidatorReward,
/// ErasStakersClipped, ErasRewardPoints, ErasValidatorPrefs (8 items)
/// - Read Each: Bonded, Ledger, Payee, Locks, System Account (5 items)
/// - Write Each: System Account, Locks, Ledger (3 items)
///
/// NOTE: weights are assuming that payouts are made to alive stash account (Staked).
/// Paying even a dead controller is cheaper weight-wise. We don't do any refunds here.
#[weight = T::WeightInfo::payout_stakers_alive_staked(T::MaxNominatorRewardedPerValidator::get())]
fn payout_stakers(origin, validator_stash: T::AccountId, era: EraIndex) -> DispatchResult {
ensure!(Self::era_election_status().is_closed(), Error::<T>::CallNotAllowed);
ensure_signed(origin)?;
Self::do_payout_stakers(validator_stash, era)
}
/// Rebond a portion of the stash scheduled to be unlocked.
///
/// The dispatch origin must be signed by the controller, and it can be only called when
/// [`EraElectionStatus`] is `Closed`.
///
/// - Time complexity: O(L), where L is unlocking chunks
/// - Bounded by `MAX_UNLOCKING_CHUNKS`.
/// - Storage changes: Can't increase storage, only decrease it.
/// ---------------
/// - DB Weight:
/// - Reads: EraElectionStatus, Ledger, Locks, [Origin Account]
/// - Writes: [Origin Account], Locks, Ledger
#[weight = T::WeightInfo::rebond(MAX_UNLOCKING_CHUNKS as u32)]
fn rebond(origin, #[compact] value: BalanceOf<T>) -> DispatchResultWithPostInfo {
ensure!(Self::era_election_status().is_closed(), Error::<T>::CallNotAllowed);
let controller = ensure_signed(origin)?;
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
ensure!(!ledger.unlocking.is_empty(), Error::<T>::NoUnlockChunk);
// last check: the new active amount of ledger must be more than ED.
ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientValue);
Ok(Some(
35 * WEIGHT_PER_MICROS
+ 50 * WEIGHT_PER_NANOS * (ledger.unlocking.len() as Weight)
+ T::DbWeight::get().reads_writes(3, 2)
).into())
/// Set `HistoryDepth` value. This function will delete any history information
/// when `HistoryDepth` is reduced.
///
/// Parameters:
/// - `new_history_depth`: The new history depth you would like to set.
/// - `era_items_deleted`: The number of items that will be deleted by this dispatch.
/// This should report all the storage items that will be deleted by clearing old
/// era history. Needed to report an accurate weight for the dispatch. Trusted by
/// `Root` to report an accurate number.
///
/// # <weight>
/// - E: Number of history depths removed, i.e. 10 -> 7 = 3
/// - DB Weight:
/// - Reads: Current Era, History Depth
/// - Writes: History Depth
/// - Clear Prefix Each: Era Stakers, EraStakersClipped, ErasValidatorPrefs
/// - Writes Each: ErasValidatorReward, ErasRewardPoints, ErasTotalStake, ErasStartSessionIndex
/// # </weight>
#[weight = T::WeightInfo::set_history_depth(*_era_items_deleted)]
fn set_history_depth(origin,
#[compact] new_history_depth: EraIndex,
#[compact] _era_items_deleted: u32,
) {
ensure_root(origin)?;
if let Some(current_era) = Self::current_era() {
HistoryDepth::mutate(|history_depth| {
let last_kept = current_era.checked_sub(*history_depth).unwrap_or(0);
let new_last_kept = current_era.checked_sub(new_history_depth).unwrap_or(0);
for era_index in last_kept..new_last_kept {
Self::clear_era_information(era_index);
}
*history_depth = new_history_depth
})
}
}
/// Remove all data structure concerning a staker/stash once its balance is at the minimum.
/// This is essentially equivalent to `withdraw_unbonded` except it can be called by anyone
/// and the target `stash` must have no funds left beyond the ED.
///
/// This can be called from any origin.
///
/// - `stash`: The stash account to reap. Its balance must be zero.
///
/// # <weight>
/// Complexity: O(S) where S is the number of slashing spans on the account.
/// DB Weight:
/// - Reads: Stash Account, Bonded, Slashing Spans, Locks
/// - Writes: Bonded, Slashing Spans (if S > 0), Ledger, Payee, Validators, Nominators, Stash Account, Locks
/// - Writes Each: SpanSlash * S
/// # </weight>
#[weight = T::WeightInfo::reap_stash(*num_slashing_spans)]
fn reap_stash(_origin, stash: T::AccountId, num_slashing_spans: u32) {
let at_minimum = T::Currency::total_balance(&stash) == T::Currency::minimum_balance();
ensure!(at_minimum, Error::<T>::FundedTarget);
Self::kill_stash(&stash, num_slashing_spans)?;
/// Submit an election result to the chain. If the solution:
///
/// 1. is valid.
/// 2. has a better score than a potentially existing solution on chain.
///
/// then, it will be _put_ on chain.
///
/// A solution consists of two pieces of data:
///
/// 1. `winners`: a flat vector of all the winners of the round.
/// 2. `assignments`: the compact version of an assignment vector that encodes the edge
/// weights.
///
/// Both of which may be computed using _phragmen_, or any other algorithm.
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
///
/// Additionally, the submitter must provide:
///
/// - The `score` that they claim their solution has.
///
/// Both validators and nominators will be represented by indices in the solution. The
/// indices should respect the corresponding types ([`ValidatorIndex`] and
/// [`NominatorIndex`]). Moreover, they should be valid when used to index into
/// [`SnapshotValidators`] and [`SnapshotNominators`]. Any invalid index will cause the
/// solution to be rejected. These two storage items are set during the election window and
/// may be used to determine the indices.
///
/// A solution is valid if:
///
/// 0. It is submitted when [`EraElectionStatus`] is `Open`.
/// 1. Its claimed score is equal to the score computed on-chain.
/// 2. Presents the correct number of winners.
/// 3. All indexes must be value according to the snapshot vectors. All edge values must
/// also be correct and should not overflow the granularity of the ratio type (i.e. 256
/// or billion).
/// 4. For each edge, all targets are actually nominated by the voter.
/// 5. Has correct self-votes.
///
/// A solutions score is consisted of 3 parameters:
///
/// 1. `min { support.total }` for each support of a winner. This value should be maximized.
/// 2. `sum { support.total }` for each support of a winner. This value should be minimized.
/// 3. `sum { support.total^2 }` for each support of a winner. This value should be
/// minimized (to ensure less variance)
///
/// # <weight>
/// The transaction is assumed to be the longest path, a better solution.
/// - Initial solution is almost the same.
/// - Worse solution is retraced in pre-dispatch-checks which sets its own weight.
#[weight = T::WeightInfo::submit_solution_better(
size.validators.into(),
size.nominators.into(),
compact.voter_count() as u32,
winners.len() as u32,
)]
pub fn submit_election_solution(
origin,
winners: Vec<ValidatorIndex>,
compact: CompactAssignments,
size: ElectionSize,
) -> DispatchResultWithPostInfo {
let _who = ensure_signed(origin)?;
Self::check_and_replace_solution(
winners,
ElectionCompute::Signed,
score,
era,
}
/// Unsigned version of `submit_election_solution`.
///
/// Note that this must pass the [`ValidateUnsigned`] check which only allows transactions
/// from the local node to be included. In other words, only the block author can include a
/// transaction in the block.
///
/// # <weight>
/// See [`submit_election_solution`].
#[weight = T::WeightInfo::submit_solution_better(
size.validators.into(),
size.nominators.into(),
compact.voter_count() as u32,
winners.len() as u32,
)]
pub fn submit_election_solution_unsigned(
origin,
winners: Vec<ValidatorIndex>,
compact: CompactAssignments,
size: ElectionSize,
) -> DispatchResultWithPostInfo {
let adjustments = Self::check_and_replace_solution(
ElectionCompute::Unsigned,
score,
era,
).expect(
"An unsigned solution can only be submitted by validators; A validator should \
always produce correct solutions, else this block should not be imported, thus \
effectively depriving the validators from their authoring reward. Hence, this panic
is expected."
);
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
/// Remove the given nominations from the calling validator.
///
/// Effects will be felt at the beginning of the next era.
///
/// The dispatch origin for this call must be _Signed_ by the controller, not the stash.
/// And, it can be only called when [`EraElectionStatus`] is `Closed`. The controller
/// account should represent a validator.
///
/// - `who`: A list of nominator stash accounts who are nominating this validator which
/// should no longer be nominating this validator.
///
/// Note: Making this call only makes sense if you first set the validator preferences to
/// block any further nominations.
#[weight = T::WeightInfo::kick(who.len() as u32)]
pub fn kick(origin, who: Vec<<T::Lookup as StaticLookup>::Source>) -> DispatchResult {
let controller = ensure_signed(origin)?;
ensure!(Self::era_election_status().is_closed(), Error::<T>::CallNotAllowed);
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
let stash = &ledger.stash;
for nom_stash in who.into_iter()
.map(T::Lookup::lookup)
.collect::<Result<Vec<T::AccountId>, _>>()?
.into_iter()
{
Nominators::<T>::mutate(&nom_stash, |maybe_nom| if let Some(ref mut nom) = maybe_nom {
if let Some(pos) = nom.targets.iter().position(|v| v == stash) {
nom.targets.swap_remove(pos);
Self::deposit_event(RawEvent::Kicked(nom_stash.clone(), stash.clone()));
}
});
}
Ok(())
}
impl<T: Config> Module<T> {
/// The total balance that can be slashed from a stash account as of right now.
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
// Weight note: consider making the stake accessible through stash.
Self::bonded(stash).and_then(Self::ledger).map(|l| l.active).unwrap_or_default()
/// Internal impl of [`Self::slashable_balance_of`] that returns [`VoteWeight`].
pub fn slashable_balance_of_vote_weight(
stash: &T::AccountId,
issuance: BalanceOf<T>,
) -> VoteWeight {
T::CurrencyToVote::to_vote(Self::slashable_balance_of(stash), issuance)
}
/// Returns a closure around `slashable_balance_of_vote_weight` that can be passed around.
///
/// This prevents call sites from repeatedly requesting `total_issuance` from backend. But it is
/// important to be only used while the total issuance is not changing.
pub fn slashable_balance_of_fn() -> Box<dyn Fn(&T::AccountId) -> VoteWeight> {
// NOTE: changing this to unboxed `impl Fn(..)` return type and the module will still
// compile, while some types in mock fail to resolve.
let issuance = T::Currency::total_issuance();
Box::new(move |who: &T::AccountId| -> VoteWeight {
Self::slashable_balance_of_vote_weight(who, issuance)
})
}
/// Dump the list of validators and nominators into vectors and keep them on-chain.
///
/// This data is used to efficiently evaluate election results. returns `true` if the operation
/// is successful.
pub fn create_stakers_snapshot() -> (bool, Weight) {
let mut consumed_weight = 0;
let mut add_db_reads_writes = |reads, writes| {
consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
};
let validators = <Validators<T>>::iter().map(|(v, _)| v).collect::<Vec<_>>();
let mut nominators = <Nominators<T>>::iter().map(|(n, _)| n).collect::<Vec<_>>();
let num_validators = validators.len();
let num_nominators = nominators.len();
add_db_reads_writes((num_validators + num_nominators) as Weight, 0);
if
num_validators > MAX_VALIDATORS ||
num_nominators.saturating_add(num_validators) > MAX_NOMINATORS
{
log!(
warn,
"Snapshot size too big [{} <> {}][{} <> {}].",
num_validators,
MAX_VALIDATORS,
num_nominators,
MAX_NOMINATORS,
);
} else {
// all validators nominate themselves;
nominators.extend(validators.clone());
<SnapshotValidators<T>>::put(validators);
<SnapshotNominators<T>>::put(nominators);
add_db_reads_writes(0, 2);
(true, consumed_weight)
}
}
/// Clears both snapshots of stakers.
fn kill_stakers_snapshot() {
<SnapshotValidators<T>>::kill();
<SnapshotNominators<T>>::kill();
}
fn do_payout_stakers(validator_stash: T::AccountId, era: EraIndex) -> DispatchResult {
// Validate input data
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
let current_era = CurrentEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
ensure!(era <= current_era, Error::<T>::InvalidEraToReward);
let history_depth = Self::history_depth();
ensure!(era >= current_era.saturating_sub(history_depth), Error::<T>::InvalidEraToReward);
// Note: if era has no reward to be claimed, era may be future. better not to update
// `ledger.claimed_rewards` in this case.
let era_payout = <ErasValidatorReward<T>>::get(&era)
.ok_or_else(|| Error::<T>::InvalidEraToReward)?;
let controller = Self::bonded(&validator_stash).ok_or(Error::<T>::NotStash)?;
let mut ledger = <Ledger<T>>::get(&controller).ok_or_else(|| Error::<T>::NotController)?;
ledger.claimed_rewards.retain(|&x| x >= current_era.saturating_sub(history_depth));
match ledger.claimed_rewards.binary_search(&era) {
Ok(_) => Err(Error::<T>::AlreadyClaimed)?,
Err(pos) => ledger.claimed_rewards.insert(pos, era),
}
let exposure = <ErasStakersClipped<T>>::get(&era, &ledger.stash);
/* Input data seems good, no errors allowed after this point */
<Ledger<T>>::insert(&controller, &ledger);
// Get Era reward points. It has TOTAL and INDIVIDUAL
// Find the fraction of the era reward that belongs to the validator
// Take that fraction of the eras rewards to split to nominator and validator
//
// Then look at the validator, figure out the proportion of their reward
// which goes to them and each of their nominators.
let era_reward_points = <ErasRewardPoints<T>>::get(&era);
let total_reward_points = era_reward_points.total;
let validator_reward_points = era_reward_points.individual.get(&ledger.stash)
.map(|points| *points)
.unwrap_or_else(|| Zero::zero());
// Nothing to do if they have no reward points.
if validator_reward_points.is_zero() { return Ok(())}
// This is the fraction of the total reward that the validator and the
// nominators will get.
let validator_total_reward_part = Perbill::from_rational_approximation(
validator_reward_points,
total_reward_points,
);
// This is how much validator + nominators are entitled to.
let validator_total_payout = validator_total_reward_part * era_payout;
let validator_prefs = Self::eras_validator_prefs(&era, &validator_stash);
// Validator first gets a cut off the top.
let validator_commission = validator_prefs.commission;
let validator_commission_payout = validator_commission * validator_total_payout;
let validator_leftover_payout = validator_total_payout - validator_commission_payout;
// Now let's calculate how this is split to the validator.
let validator_exposure_part = Perbill::from_rational_approximation(
exposure.own,
exposure.total,
);
let validator_staking_payout = validator_exposure_part * validator_leftover_payout;
// We can now make total validator payout:
if let Some(imbalance) = Self::make_payout(
&ledger.stash,
validator_staking_payout + validator_commission_payout
) {
Self::deposit_event(RawEvent::Reward(ledger.stash, imbalance.peek()));
}
// Lets now calculate how this is split to the nominators.
// Reward only the clipped exposures. Note this is not necessarily sorted.
for nominator in exposure.others.iter() {
let nominator_exposure_part = Perbill::from_rational_approximation(
nominator.value,
exposure.total,
);
let nominator_reward: BalanceOf<T> = nominator_exposure_part * validator_leftover_payout;
// We can now make nominator payout:
if let Some(imbalance) = Self::make_payout(&nominator.who, nominator_reward) {
Self::deposit_event(RawEvent::Reward(nominator.who.clone(), imbalance.peek()));
}
}
Ok(())
}
/// Update the ledger for a controller.
///
/// This will also update the stash lock.
fn update_ledger(
controller: &T::AccountId,
ledger: &StakingLedger<T::AccountId, BalanceOf<T>>
) {
T::Currency::set_lock(
STAKING_ID,
&ledger.stash,
ledger.total,
<Ledger<T>>::insert(controller, ledger);
}
/// Chill a stash account.
fn chill_stash(stash: &T::AccountId) {
<Validators<T>>::remove(stash);
<Nominators<T>>::remove(stash);
}
/// Actually make a payment to a staker. This uses the currency's reward function
/// to pay the right payee for the given staker account.
fn make_payout(stash: &T::AccountId, amount: BalanceOf<T>) -> Option<PositiveImbalanceOf<T>> {
let dest = Self::payee(stash);
match dest {
RewardDestination::Controller => Self::bonded(stash)
.and_then(|controller|
Some(T::Currency::deposit_creating(&controller, amount))
),
RewardDestination::Stash =>
T::Currency::deposit_into_existing(stash, amount).ok(),
RewardDestination::Staked => Self::bonded(stash)
.and_then(|c| Self::ledger(&c).map(|l| (c, l)))
.and_then(|(controller, mut l)| {
l.active += amount;
l.total += amount;
let r = T::Currency::deposit_into_existing(stash, amount).ok();
Self::update_ledger(&controller, &l);
RewardDestination::Account(dest_account) => {
Some(T::Currency::deposit_creating(&dest_account, amount))
Wei Tang
committed
},
RewardDestination::None => None,
/// Plan a new session potentially trigger a new era.
fn new_session(session_index: SessionIndex) -> Option<Vec<T::AccountId>> {
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("Error: start_session_index must be set for current_era");
0
});
let era_length = session_index.checked_sub(current_era_start_session_index)
.unwrap_or(0); // Must never happen.
match ForceEra::get() {
Forcing::ForceNew => ForceEra::kill(),
Forcing::ForceAlways => (),
Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (),
// Either `ForceNone`, or `NotForcing && era_length < T::SessionsPerEra::get()`.
if era_length + 1 == T::SessionsPerEra::get() {
} else if era_length >= T::SessionsPerEra::get() {
// Should only happen when we are ready to trigger an era but we have ForceNone,
// otherwise previous arm would short circuit.
Self::close_election_window();
Self::new_era(session_index)
} else {
// Set initial era
Self::new_era(session_index)
/// Basic and cheap checks that we perform in validate unsigned, and in the execution.
///
/// State reads: ElectionState, CurrentEr, QueuedScore.
///
/// This function does weight refund in case of errors, which is based upon the fact that it is
/// called at the very beginning of the call site's function.
pub fn pre_dispatch_checks(score: ElectionScore, era: EraIndex) -> DispatchResultWithPostInfo {
// discard solutions that are not in-time
// check window open
ensure!(
Self::era_election_status().is_open(),
Error::<T>::OffchainElectionEarlySubmission.with_weight(T::DbWeight::get().reads(1)),
if let Some(current_era) = Self::current_era() {
Error::<T>::OffchainElectionEarlySubmission.with_weight(T::DbWeight::get().reads(2)),
)
}
// assume the given score is valid. Is it better than what we have on-chain, if we have any?
if let Some(queued_score) = Self::queued_score() {
ensure!(
Kian Paimani
committed
is_score_better(score, queued_score, T::MinSolutionScoreBump::get()),
Error::<T>::OffchainElectionWeakSubmission.with_weight(T::DbWeight::get().reads(3)),
}
/// Checks a given solution and if correct and improved, writes it on chain as the queued result
/// of the next round. This may be called by both a signed and an unsigned transaction.
pub fn check_and_replace_solution(
winners: Vec<ValidatorIndex>,
compact_assignments: CompactAssignments,
compute: ElectionCompute,
claimed_score: ElectionScore,
election_size: ElectionSize,
) -> DispatchResultWithPostInfo {
// Do the basic checks. era, claimed score and window open.
let _ = Self::pre_dispatch_checks(claimed_score, era)?;
// before we read any further state, we check that the unique targets in compact is same as
// compact. is a all in-memory check and easy to do. Moreover, it ensures that the solution
// is not full of bogus edges that can cause lots of reads to SlashingSpans. Thus, we can
// assume that the storage access of this function is always O(|winners|), not
// O(|compact.edge_count()|).
ensure!(
compact_assignments.unique_targets().len() == winners.len(),
Error::<T>::OffchainElectionBogusWinnerCount,
// Check that the number of presented winners is sane. Most often we have more candidates
// than we need. Then it should be `Self::validator_count()`. Else it should be all the
let snapshot_validators_length = <SnapshotValidators<T>>::decode_len()
.map(|l| l as u32)
.ok_or_else(|| Error::<T>::SnapshotUnavailable)?;
// size of the solution must be correct.
ensure!(
snapshot_validators_length == u32::from(election_size.validators),
Error::<T>::OffchainElectionBogusElectionSize,
// check the winner length only here and when we know the length of the snapshot validators
// length.
let desired_winners = Self::validator_count().min(snapshot_validators_length);
ensure!(winners.len() as u32 == desired_winners, Error::<T>::OffchainElectionBogusWinnerCount);
let snapshot_nominators_len = <SnapshotNominators<T>>::decode_len()
.map(|l| l as u32)
.ok_or_else(|| Error::<T>::SnapshotUnavailable)?;
// rest of the size of the solution must be correct.
ensure!(
snapshot_nominators_len == election_size.nominators,
Error::<T>::OffchainElectionBogusElectionSize,
// decode snapshot validators.
let snapshot_validators = Self::snapshot_validators()
.ok_or(Error::<T>::SnapshotUnavailable)?;
// check if all winners were legit; this is rather cheap. Replace with accountId.
let winners = winners.into_iter().map(|widx| {
// NOTE: at the moment, since staking is explicitly blocking any offence until election
// is closed, we don't check here if the account id at `snapshot_validators[widx]` is
// actually a validator. If this ever changes, this loop needs to also check this.
snapshot_validators.get(widx as usize).cloned().ok_or(Error::<T>::OffchainElectionBogusWinner)
}).collect::<Result<Vec<T::AccountId>, Error<T>>>()?;
// decode the rest of the snapshot.
let snapshot_nominators = Self::snapshot_nominators()
.ok_or(Error::<T>::SnapshotUnavailable)?;
// helpers
let nominator_at = |i: NominatorIndex| -> Option<T::AccountId> {
snapshot_nominators.get(i as usize).cloned()
};
let validator_at = |i: ValidatorIndex| -> Option<T::AccountId> {
snapshot_validators.get(i as usize).cloned()
};
// un-compact.
let assignments = compact_assignments.into_assignment(
nominator_at,
validator_at,
).map_err(|e| {
// log the error since it is not propagated into the runtime error.
log!(warn, "un-compacting solution failed due to {:?}", e);
Error::<T>::OffchainElectionBogusCompact
})?;
// check all nominators actually including the claimed vote. Also check correct self votes.
// Note that we assume all validators and nominators in `assignments` are properly bonded,
// because they are coming from the snapshot via a given index.
for Assignment { who, distribution } in assignments.iter() {
let is_validator = <Validators<T>>::contains_key(&who);
let maybe_nomination = Self::nominators(&who);
if !(maybe_nomination.is_some() ^ is_validator) {
// all of the indices must map to either a validator or a nominator. If this is ever
// not the case, then the locking system of staking is most likely faulty, or we
// have bigger problems.
log!(error, "detected an error in the staking locking and snapshot.");
return Err(Error::<T>::OffchainElectionBogusNominator.into());
}
if !is_validator {
// a normal vote
let nomination = maybe_nomination.expect(
"exactly one of `maybe_validator` and `maybe_nomination.is_some` is true. \
is_validator is false; maybe_nomination is some; qed"
);
// NOTE: we don't really have to check here if the sum of all edges are the
// nominator correct. Un-compacting assures this by definition.
for (t, _) in distribution {
// each target in the provided distribution must be actually nominated by the
// nominator after the last non-zero slash.
if nomination.targets.iter().find(|&tt| tt == t).is_none() {
return Err(Error::<T>::OffchainElectionBogusNomination.into());
}
if <Self as Store>::SlashingSpans::get(&t).map_or(
false,
|spans| nomination.submitted_in < spans.last_nonzero_slash(),
) {
return Err(Error::<T>::OffchainElectionSlashedNomination.into());
}
}
} else {
// a self vote
ensure!(distribution.len() == 1, Error::<T>::OffchainElectionBogusSelfVote);
ensure!(distribution[0].0 == *who, Error::<T>::OffchainElectionBogusSelfVote);
// defensive only. A compact assignment of length one does NOT encode the weight and
// it is always created to be 100%.
ensure!(
distribution[0].1 == OffchainAccuracy::one(),
Error::<T>::OffchainElectionBogusSelfVote,
);
}
}
// convert into staked assignments.
let staked_assignments = sp_npos_elections::assignment_ratio_to_staked(
);
// build the support map thereof in order to evaluate.
let supports = to_supports(&winners, &staked_assignments)
.map_err(|_| Error::<T>::OffchainElectionBogusEdge)?;
// Check if the score is the same as the claimed one.
let submitted_score = (&supports).evaluate();
ensure!(submitted_score == claimed_score, Error::<T>::OffchainElectionBogusScore);
// At last, alles Ok. Exposures and store the result.
let exposures = Self::collect_exposures(supports);
log!(
info,
"A better solution (with compute {:?} and score {:?}) has been validated and stored \
on chain.",
);
// write new results.
<QueuedElected<T>>::put(ElectionResult {
elected_stashes: winners,
exposures,
});
QueuedScore::put(submitted_score);
// emit event.
Self::deposit_event(RawEvent::SolutionStored(compute));
/// Start a session potentially starting an era.
fn start_session(start_session: SessionIndex) {
let next_active_era = Self::active_era().map(|e| e.index + 1).unwrap_or(0);
// This is only `Some` when current era has already progressed to the next era, while the
// active era is one behind (i.e. in the *last session of the active era*, or *first session
// of the new current era*, depending on how you look at it).
if let Some(next_active_era_start_session_index) =
Self::eras_start_session_index(next_active_era)
{
if next_active_era_start_session_index == start_session {
Self::start_era(start_session);
} else if next_active_era_start_session_index < start_session {
// This arm should never happen, but better handle it than to stall the staking
// pallet.
frame_support::print("Warning: A session appears to have been skipped.");
Self::start_era(start_session);
}
}
/// End a session potentially ending an era.
fn end_session(session_index: SessionIndex) {
if let Some(active_era) = Self::active_era() {
if let Some(next_active_era_start_session_index) =
{
if next_active_era_start_session_index == session_index + 1 {
Self::end_era(active_era, session_index);
}
/// * Increment `active_era.index`,
/// * reset `active_era.start`,
/// * update `BondedEras` and apply slashes.
fn start_era(start_session: SessionIndex) {
let active_era = ActiveEra::mutate(|active_era| {
let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0);
*active_era = Some(ActiveEraInfo {
index: new_index,
// Set new active era start in next `on_finalize`. To guarantee usage of `Time`
start: None,
});
new_index
let bonding_duration = T::BondingDuration::get();
BondedEras::mutate(|bonded| {
if active_era > bonding_duration {
let first_kept = active_era - bonding_duration;
// 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.
for (pruned_era, _) in bonded.drain(..n_to_prune) {
slashing::clear_era_metadata::<T>(pruned_era);
}
if let Some(&(_, first_session)) = bonded.first() {
T::SessionInterface::prune_historical_up_to(first_session);
}
Self::apply_unapplied_slashes(active_era);
}
/// Compute payout for era.
fn end_era(active_era: ActiveEraInfo, _session_index: SessionIndex) {
// Note: active_era_start can be None if end era is called during genesis config.
if let Some(active_era_start) = active_era.start {
let now_as_millis_u64 = T::UnixTime::now().as_millis().saturated_into::<u64>();
let era_duration = now_as_millis_u64 - active_era_start;
let (validator_payout, max_payout) = inflation::compute_total_payout(
&T::RewardCurve::get(),
Self::eras_total_stake(&active_era.index),
T::Currency::total_issuance(),
// Duration of era; more than u64::MAX is rewarded as u64::MAX.
era_duration.saturated_into::<u64>(),
);
let rest = max_payout.saturating_sub(validator_payout);
Self::deposit_event(RawEvent::EraPayout(active_era.index, validator_payout, rest));
<ErasValidatorReward<T>>::insert(&active_era.index, validator_payout);
T::RewardRemainder::on_unbalanced(T::Currency::issue(rest));
}
}
/// Plan a new era. Return the potential new staking set.
fn new_era(start_session_index: SessionIndex) -> Option<Vec<T::AccountId>> {
// Increment or set current era.
let current_era = CurrentEra::mutate(|s| {
*s = Some(s.map(|s| s + 1).unwrap_or(0));
s.unwrap()
});
ErasStartSessionIndex::insert(¤t_era, &start_session_index);
// Clean old era information.
if let Some(old_era) = current_era.checked_sub(Self::history_depth() + 1) {
Self::clear_era_information(old_era);
}
// Set staking information for new era.
let maybe_new_validators = Self::select_and_update_validators(current_era);
// TWO_PHASE_NOTE: use this later on.
let _unused_new_validators = Self::enact_election(current_era);
/// Remove all the storage items associated with the election.
fn close_election_window() {
// Close window.
<EraElectionStatus<T>>::put(ElectionStatus::Closed);
// Kill snapshots.
Self::kill_stakers_snapshot();
// Don't track final session.
IsCurrentSessionFinal::put(false);
}
/// Select the new validator set at the end of the era.
///
/// Runs [`try_do_phragmen`] and updates the following storage items:
/// - [`EraElectionStatus`]: with `None`.
/// - [`ErasStakers`]: with the new staker set.
/// - [`ErasStakersClipped`].
/// - [`ErasValidatorPrefs`].
/// - [`ErasTotalStake`]: with the new total stake.
/// - [`SnapshotValidators`] and [`SnapshotNominators`] are both removed.
///
/// Internally, [`QueuedElected`], snapshots and [`QueuedScore`] are also consumed.
///
/// If the election has been successful, It passes the new set upwards.
///
/// This should only be called at the end of an era.
fn select_and_update_validators(current_era: EraIndex) -> Option<Vec<T::AccountId>> {
if let Some(ElectionResult::<T::AccountId, BalanceOf<T>> {
elected_stashes,
exposures,
compute,
}) = Self::try_do_election() {
// Totally close the election round and data.
Self::close_election_window();
// Populate Stakers and write slot stake.
let mut total_stake: BalanceOf<T> = Zero::zero();
exposures.into_iter().for_each(|(stash, exposure)| {
total_stake = total_stake.saturating_add(exposure.total);
<ErasStakers<T>>::insert(current_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.value.cmp(&b.value).reverse());
exposure_clipped.others.truncate(clipped_max_len);
<ErasStakersClipped<T>>::insert(¤t_era, &stash, exposure_clipped);
});
// Insert current era staking information
<ErasTotalStake<T>>::insert(¤t_era, total_stake);
// collect the pref of all winners
for stash in &elected_stashes {
let pref = Self::validators(stash);
<ErasValidatorPrefs<T>>::insert(¤t_era, stash, pref);
// emit event
Self::deposit_event(RawEvent::StakingElection(compute));
log!(
info,
"new validator set of size {:?} has been elected via {:?} for staring era {:?}",
elected_stashes.len(),
compute,
current_era,
);
Some(elected_stashes)
} else {
None
}
}
/// Select a new validator set from the assembled stakers and their role preferences. It tries
/// first to peek into [`QueuedElected`]. Otherwise, it runs a new on-chain phragmen election.
///
/// If [`QueuedElected`] and [`QueuedScore`] exists, they are both removed. No further storage
/// is updated.
fn try_do_election() -> Option<ElectionResult<T::AccountId, BalanceOf<T>>> {
// an election result from either a stored submission or locally executed one.
let next_result = <QueuedElected<T>>::take().or_else(||
);
// either way, kill this. We remove it here to make sure it always has the exact same
// lifetime as `QueuedElected`.
QueuedScore::kill();
next_result
/// Execute election and return the new results. The edge weights are processed into support
/// This is basically a wrapper around [`Self::do_phragmen`] which translates
/// `PrimitiveElectionResult` into `ElectionResult`.
pub fn do_on_chain_phragmen() -> Option<ElectionResult<T::AccountId, BalanceOf<T>>> {
if let Some(phragmen_result) = Self::do_phragmen::<ChainAccuracy>(0) {
let elected_stashes = phragmen_result.winners.iter()
.map(|(s, _)| s.clone())
.collect::<Vec<T::AccountId>>();
let assignments = phragmen_result.assignments;
let staked_assignments = sp_npos_elections::assignment_ratio_to_staked(
let supports = to_supports(
&elected_stashes,
&staked_assignments,
"on-chain phragmen is failing due to a problem in the result. This must be a bug."