1use crate::*;
80use alloc::{boxed::Box, vec::Vec};
81use frame_election_provider_support::{BoundedSupportsOf, ElectionProvider, PageIndex};
82use frame_support::{
83 pallet_prelude::*,
84 traits::{Defensive, DefensiveMax, DefensiveSaturating, OnUnbalanced, TryCollect},
85 weights::WeightMeter,
86};
87use pallet_staking_async_rc_client::RcClientInterface;
88use sp_runtime::{Perbill, Percent, Saturating};
89use sp_staking::{
90 currency_to_vote::CurrencyToVote, Exposure, Page, PagedExposureMetadata, SessionIndex,
91 StakerRewardCalculator,
92};
93
94pub struct Eras<T: Config>(core::marker::PhantomData<T>);
106
107impl<T: Config> Eras<T> {
108 pub(crate) fn set_validator_prefs(era: EraIndex, stash: &T::AccountId, prefs: ValidatorPrefs) {
109 debug_assert_eq!(era, Rotator::<T>::planned_era(), "we only set prefs for planning era");
110 <ErasValidatorPrefs<T>>::insert(era, stash, prefs);
111 }
112
113 pub(crate) fn get_validator_prefs(era: EraIndex, stash: &T::AccountId) -> ValidatorPrefs {
114 <ErasValidatorPrefs<T>>::get(era, stash)
115 }
116
117 pub(crate) fn get_validator_commission(era: EraIndex, stash: &T::AccountId) -> Perbill {
119 Self::get_validator_prefs(era, stash).commission
120 }
121
122 pub(crate) fn pending_rewards(era: EraIndex, validator: &T::AccountId) -> bool {
125 let Some(overview) = <ErasStakersOverview<T>>::get(&era, validator) else {
126 return false;
128 };
129
130 if Self::get_reward_points_for_validator(era, validator).is_zero() {
132 return false;
133 }
134
135 ClaimedRewards::<T>::get(era, validator).len() < overview.page_count as usize
136 }
137
138 pub(crate) fn get_paged_exposure(
146 era: EraIndex,
147 validator: &T::AccountId,
148 page: Page,
149 ) -> Option<PagedExposure<T::AccountId, BalanceOf<T>>> {
150 let overview = <ErasStakersOverview<T>>::get(&era, validator)?;
151
152 let validator_stake = if page == 0 { overview.own } else { Zero::zero() };
154
155 let exposure_page = <ErasStakersPaged<T>>::get((era, validator, page)).unwrap_or_default();
158
159 Some(PagedExposure {
161 exposure_metadata: PagedExposureMetadata { own: validator_stake, ..overview },
162 exposure_page: exposure_page.into(),
163 })
164 }
165
166 pub(crate) fn get_full_exposure(
168 era: EraIndex,
169 validator: &T::AccountId,
170 ) -> Exposure<T::AccountId, BalanceOf<T>> {
171 let Some(overview) = <ErasStakersOverview<T>>::get(&era, validator) else {
172 return Exposure::default();
173 };
174
175 let mut others = Vec::with_capacity(overview.nominator_count as usize);
176 for page in 0..overview.page_count {
177 let nominators = <ErasStakersPaged<T>>::get((era, validator, page));
178 others.append(&mut nominators.map(|n| n.others.clone()).defensive_unwrap_or_default());
179 }
180
181 Exposure { total: overview.total, own: overview.own, others }
182 }
183
184 pub(crate) fn exposure_page_count(era: EraIndex, validator: &T::AccountId) -> Page {
188 <ErasStakersOverview<T>>::get(&era, validator)
189 .map(|overview| {
190 if overview.page_count == 0 && overview.own > Zero::zero() {
191 1
194 } else {
195 overview.page_count
196 }
197 })
198 .unwrap_or(1)
201 }
202
203 pub(crate) fn was_validator_exposed(era: EraIndex, validator: &T::AccountId) -> bool {
205 <ErasStakersOverview<T>>::contains_key(era, validator)
206 }
207
208 pub(crate) fn get_next_claimable_page(era: EraIndex, validator: &T::AccountId) -> Option<Page> {
210 let page_count = Self::exposure_page_count(era, validator);
212 let all_claimable_pages: Vec<Page> = (0..page_count).collect();
213 let claimed_pages = ClaimedRewards::<T>::get(era, validator);
214
215 all_claimable_pages.into_iter().find(|p| !claimed_pages.contains(p))
216 }
217
218 pub(crate) fn are_nominators_slashable(era: EraIndex) -> bool {
224 ErasNominatorsSlashable::<T>::get(era).unwrap_or(true)
225 }
226
227 pub(crate) fn set_rewards_as_claimed(era: EraIndex, validator: &T::AccountId, page: Page) {
230 let mut claimed_pages = ClaimedRewards::<T>::get(era, validator).into_inner();
231
232 if claimed_pages.contains(&page) {
234 defensive!("Trying to set an already claimed reward");
235 return;
237 }
238
239 claimed_pages.push(page);
241 ClaimedRewards::<T>::insert(
242 era,
243 validator,
244 WeakBoundedVec::<_, _>::force_from(claimed_pages, Some("set_rewards_as_claimed")),
245 );
246 }
247
248 pub fn upsert_exposure(
255 era: EraIndex,
256 validator: &T::AccountId,
257 mut exposure: Exposure<T::AccountId, BalanceOf<T>>,
258 ) {
259 let page_size = T::MaxExposurePageSize::get().defensive_max(1);
260 if cfg!(debug_assertions) && cfg!(not(feature = "runtime-benchmarks")) {
261 let expected_total = exposure
264 .others
265 .iter()
266 .map(|ie| ie.value)
267 .fold::<BalanceOf<T>, _>(Default::default(), |acc, x| acc + x)
268 .saturating_add(exposure.own);
269 debug_assert_eq!(expected_total, exposure.total, "exposure total must equal own + sum(others) for (era: {:?}, validator: {:?}, exposure: {:?})", era, validator, exposure);
270 }
271
272 if let Some(overview) = ErasStakersOverview::<T>::get(era, &validator) {
273 let last_page_idx = overview.page_count.saturating_sub(1);
275 let mut last_page =
276 ErasStakersPaged::<T>::get((era, validator, last_page_idx)).unwrap_or_default();
277 let last_page_empty_slots =
278 T::MaxExposurePageSize::get().saturating_sub(last_page.others.len() as u32);
279
280 let new_stake_added = exposure.total;
283 let new_nominators_added = exposure.others.len() as u32;
284 let mut updated_overview = overview
285 .update_with::<T::MaxExposurePageSize>(new_stake_added, new_nominators_added);
286
287 match (updated_overview.own.is_zero(), exposure.own.is_zero()) {
289 (true, false) => {
290 updated_overview.own = exposure.own;
293 },
294 (true, true) | (false, true) => {
295 },
297 (false, false) => {
298 debug_assert!(
299 false,
300 "validator own stake already set in overview for (era: {:?}, validator: {:?}, current overview: {:?}, new exposure: {:?})",
301 era,
302 validator,
303 updated_overview,
304 exposure,
305 );
306 defensive!("duplicate validator self stake in election");
307 },
308 };
309
310 ErasStakersOverview::<T>::insert(era, &validator, updated_overview);
311 exposure.total = exposure.total.saturating_sub(exposure.own);
323 exposure.own = Zero::zero();
324
325 let append_to_last_page = exposure.split_others(last_page_empty_slots);
329 let put_in_new_pages = exposure;
330
331 last_page.page_total = last_page.page_total.saturating_add(append_to_last_page.total);
335 last_page.others.extend(append_to_last_page.others);
336 ErasStakersPaged::<T>::insert((era, &validator, last_page_idx), last_page);
337
338 let (_unused_metadata, put_in_new_pages_chunks) =
341 put_in_new_pages.into_pages(page_size);
342
343 put_in_new_pages_chunks
344 .into_iter()
345 .enumerate()
346 .for_each(|(idx, paged_exposure)| {
347 let append_at =
348 (last_page_idx.saturating_add(1).saturating_add(idx as u32)) as Page;
349 <ErasStakersPaged<T>>::insert((era, &validator, append_at), paged_exposure);
350 });
351 } else {
352 let expected_page_count = exposure
354 .others
355 .len()
356 .defensive_saturating_add((page_size as usize).defensive_saturating_sub(1))
357 .saturating_div(page_size as usize);
358
359 let (exposure_metadata, exposure_pages) = exposure.into_pages(page_size);
362 defensive_assert!(exposure_pages.len() == expected_page_count, "unexpected page count");
363
364 ErasStakersOverview::<T>::insert(era, &validator, exposure_metadata);
366
367 LastValidatorEra::<T>::insert(validator, era);
369
370 exposure_pages.into_iter().enumerate().for_each(|(idx, paged_exposure)| {
372 let append_at = idx as Page;
373 <ErasStakersPaged<T>>::insert((era, &validator, append_at), paged_exposure);
374 });
375 };
376 }
377
378 pub(crate) fn set_stakers_reward(era: EraIndex, amount: BalanceOf<T>) {
379 ErasValidatorReward::<T>::insert(era, amount);
380 }
381
382 pub(crate) fn get_stakers_reward(era: EraIndex) -> Option<BalanceOf<T>> {
383 ErasValidatorReward::<T>::get(era)
384 }
385
386 pub(crate) fn set_validator_incentive_budget(era: EraIndex, amount: BalanceOf<T>) {
387 ErasValidatorIncentiveBudget::<T>::insert(era, amount);
388 }
389
390 pub(crate) fn get_validator_incentive_budget(era: EraIndex) -> BalanceOf<T> {
391 ErasValidatorIncentiveBudget::<T>::get(era)
392 }
393
394 pub(crate) fn add_sum_validator_incentive_weight(
395 era: EraIndex,
396 incentive_weight: BalanceOf<T>,
397 ) {
398 <ErasSumValidatorIncentiveWeight<T>>::mutate(era, |sum| {
399 *sum = sum.saturating_add(incentive_weight);
400 });
401 }
402
403 pub(crate) fn add_total_stake(era: EraIndex, stake: BalanceOf<T>) {
405 <ErasTotalStake<T>>::mutate(era, |total_stake| {
406 *total_stake += stake;
407 });
408 }
409
410 pub(crate) fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: Page) -> bool {
412 ClaimedRewards::<T>::get(era, validator).contains(&page)
413 }
414
415 pub(crate) fn reward_active_era(
422 validators_points: impl IntoIterator<Item = (T::AccountId, u32)>,
423 ) {
424 if let Some(active_era) = ActiveEra::<T>::get() {
425 let mut sum_weighted_points_delta: BalanceOf<T> = Zero::zero();
426 <ErasRewardPoints<T>>::mutate(active_era.index, |era_rewards| {
427 for (validator, points) in validators_points.into_iter() {
428 let weight =
429 ErasValidatorIncentiveWeight::<T>::get(active_era.index, &validator)
430 .unwrap_or_else(Zero::zero);
431
432 let recorded = match era_rewards.individual.get_mut(&validator) {
433 Some(individual) => {
434 individual.saturating_accrue(points);
435 true
436 },
437 None => {
438 era_rewards.individual.try_insert(validator, points).defensive().is_ok()
441 },
442 };
443
444 if recorded && !weight.is_zero() {
449 sum_weighted_points_delta = sum_weighted_points_delta.saturating_add(
450 weight.saturating_mul(IncentiveWeight::<T>::from(points)),
451 );
452 }
453
454 era_rewards.total.saturating_accrue(points);
455 }
456 });
457 if !sum_weighted_points_delta.is_zero() {
458 ErasSumWeightedPoints::<T>::mutate(active_era.index, |sum| {
459 *sum = sum.saturating_add(sum_weighted_points_delta);
460 });
461 }
462 }
463 }
464
465 pub(crate) fn get_reward_points(era: EraIndex) -> EraRewardPoints<T> {
466 ErasRewardPoints::<T>::get(era)
467 }
468
469 pub(crate) fn get_reward_points_for_validator(
470 era: EraIndex,
471 validator: &T::AccountId,
472 ) -> RewardPoint {
473 let points = ErasRewardPoints::<T>::get(era);
474 points.individual.get(validator).copied().unwrap_or_default()
475 }
476
477 pub(crate) fn uses_weighted_points(era: EraIndex) -> bool {
493 crate::WeightedPointsFormulaStartEra::<T>::get().map_or(true, |start| era >= start)
494 }
495}
496
497#[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks"))]
498#[allow(unused)]
499impl<T: Config> Eras<T> {
500 pub(crate) fn era_fully_present(era: EraIndex) -> Result<(), sp_runtime::TryRuntimeError> {
502 let e0 = ErasValidatorPrefs::<T>::iter_prefix_values(era).count() != 0;
504 let e1 = ErasStakersOverview::<T>::iter_prefix_values(era).count() != 0;
506 ensure!(e0 == e1, "ErasValidatorPrefs and ErasStakersOverview should be consistent");
507
508 let e2 = ErasTotalStake::<T>::contains_key(era);
510
511 let active_era = Rotator::<T>::active_era();
512 let e4 = if era.saturating_sub(1) > 0 &&
513 era.saturating_sub(1) > active_era.saturating_sub(T::HistoryDepth::get() + 1)
514 {
515 ErasValidatorReward::<T>::contains_key(era.saturating_sub(1))
519 } else {
520 e2
522 };
523
524 ensure!(e2 == e4, "era info presence not consistent");
525
526 if e2 {
527 Ok(())
528 } else {
529 Err("era presence mismatch".into())
530 }
531 }
532
533 pub(crate) fn era_pruning_in_progress(era: EraIndex) -> bool {
535 EraPruningState::<T>::contains_key(era)
536 }
537
538 pub(crate) fn era_absent_or_pruning(era: EraIndex) -> Result<(), sp_runtime::TryRuntimeError> {
540 if Self::era_pruning_in_progress(era) {
541 Ok(())
542 } else {
543 Self::era_absent(era)
544 }
545 }
546
547 pub(crate) fn era_absent(era: EraIndex) -> Result<(), sp_runtime::TryRuntimeError> {
550 let e0 = ErasValidatorPrefs::<T>::iter_prefix_values(era).count() != 0;
552 let e1 = ErasStakersPaged::<T>::iter_prefix_values((era,)).count() != 0;
553 let e2 = ErasStakersOverview::<T>::iter_prefix_values(era).count() != 0;
554
555 let e3 = ErasValidatorReward::<T>::contains_key(era);
558 let e4 = ErasTotalStake::<T>::contains_key(era);
559
560 let e6 = ClaimedRewards::<T>::iter_prefix_values(era).count() != 0;
562 let e7 = ErasRewardPoints::<T>::contains_key(era);
563
564 if !vec![e0, e1, e2, e3, e4, e6, e7].windows(2).all(|w| w[0] == w[1]) {
566 return Err("era info absence not consistent - partial pruning state".into());
567 }
568
569 if !e0 {
570 Ok(())
571 } else {
572 Err("era absence mismatch".into())
573 }
574 }
575
576 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
577 let active_era = Rotator::<T>::active_era();
579 let oldest_present_era = active_era.saturating_sub(T::HistoryDepth::get()).max(1);
582
583 for e in oldest_present_era..=active_era {
584 Self::era_fully_present(e)?;
585 Self::check_validator_incentive_weight_consistency(e)?;
586 if Self::uses_weighted_points(e) {
591 Self::check_sum_weighted_points_consistency(e)?;
592 }
593 }
594
595 ensure!(
598 (1..oldest_present_era).all(|e| Self::era_absent_or_pruning(e).is_ok()),
599 "All old eras must be either fully pruned or marked for pruning"
600 );
601
602 Ok(())
603 }
604
605 fn check_validator_incentive_weight_consistency(
607 era: EraIndex,
608 ) -> Result<(), sp_runtime::TryRuntimeError> {
609 use sp_runtime::traits::Zero;
610
611 let stored_total = ErasSumValidatorIncentiveWeight::<T>::get(era);
612 let computed_total: BalanceOf<T> = ErasValidatorIncentiveWeight::<T>::iter_prefix(era)
613 .fold(BalanceOf::<T>::zero(), |acc, (_, w)| acc.saturating_add(w));
614
615 ensure!(
616 stored_total == computed_total,
617 "ErasSumValidatorIncentiveWeight mismatch: \
618 stored vs computed individual weights do not match"
619 );
620
621 Ok(())
622 }
623
624 fn check_sum_weighted_points_consistency(
627 era: EraIndex,
628 ) -> Result<(), sp_runtime::TryRuntimeError> {
629 use sp_runtime::traits::Zero;
630
631 let stored = ErasSumWeightedPoints::<T>::get(era);
632 let reward_points = ErasRewardPoints::<T>::get(era);
633 let computed: BalanceOf<T> =
634 reward_points.individual.iter().fold(BalanceOf::<T>::zero(), |acc, (v, &ep)| {
635 let weight =
636 ErasValidatorIncentiveWeight::<T>::get(era, v).unwrap_or_else(Zero::zero);
637 acc.saturating_add(weight.saturating_mul(BalanceOf::<T>::from(ep)))
638 });
639
640 ensure!(
641 stored == computed,
642 "ErasSumWeightedPoints mismatch: \
643 stored vs computed (Σ weight · era_points) do not match"
644 );
645
646 Ok(())
647 }
648}
649
650pub struct Rotator<T: Config>(core::marker::PhantomData<T>);
659
660impl<T: Config> Rotator<T> {
661 #[cfg(feature = "runtime-benchmarks")]
662 pub(crate) fn legacy_insta_plan_era() -> Vec<T::AccountId> {
663 Self::plan_new_era();
665 <<T as Config>::ElectionProvider as ElectionProvider>::asap();
667 let msp = <T::ElectionProvider as ElectionProvider>::msp();
670 let lsp = 0;
671 for p in (lsp..=msp).rev() {
672 EraElectionPlanner::<T>::do_elect_paged(p);
673 }
674
675 crate::ElectableStashes::<T>::take().into_iter().collect()
676 }
677
678 #[cfg(any(feature = "try-runtime", test))]
679 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
680 let active_era = ActiveEra::<T>::get();
682 let planned_era = CurrentEra::<T>::get();
683
684 let bonded = BondedEras::<T>::get();
685
686 match (&active_era, &planned_era) {
687 (None, None) => {
688 ensure!(bonded.is_empty(), "BondedEras must be empty when ActiveEra is None");
690 },
691 (Some(active), Some(planned)) => {
692 ensure!(
694 *planned == active.index || *planned == active.index + 1,
695 "planned era is always equal or one more than active"
696 );
697
698 let bonded_eras: Vec<_> = bonded.iter().map(|(era, _sess)| *era).collect();
701 ensure!(
702 bonded_eras ==
703 (active.index.saturating_sub(T::BondingDuration::get())..=active.index)
704 .collect::<Vec<_>>(),
705 "BondedEras range incorrect"
706 );
707
708 let oldest_allowed_era = active.index.saturating_sub(T::HistoryDepth::get()).max(1);
713 for (era, _) in ErasNominatorsSlashable::<T>::iter() {
714 let being_pruned = EraPruningState::<T>::contains_key(era);
716 ensure!(
717 (era >= oldest_allowed_era && era <= active.index) || being_pruned,
718 "ErasNominatorsSlashable entry exists for era outside history depth range and not being pruned"
719 );
720 }
721 },
722 _ => {
723 ensure!(false, "ActiveEra and CurrentEra must both be None or both be Some");
724 },
725 }
726
727 Ok(())
728 }
729
730 #[cfg(any(feature = "try-runtime", feature = "std", feature = "runtime-benchmarks", test))]
731 pub fn assert_election_ongoing() {
732 assert!(Self::is_planning().is_some(), "planning era must exist");
733 assert!(
734 T::ElectionProvider::status().is_ok(),
735 "Election provider must be in a good state during election"
736 );
737 }
738
739 pub fn planned_era() -> EraIndex {
747 CurrentEra::<T>::get().unwrap_or(0)
748 }
749
750 pub fn active_era() -> EraIndex {
751 ActiveEra::<T>::get().map(|a| a.index).defensive_unwrap_or(0)
752 }
753
754 pub fn is_planning() -> Option<EraIndex> {
758 let (active, planned) = (Self::active_era(), Self::planned_era());
759 if planned.defensive_saturating_sub(active) > 1 {
760 defensive!("planned era must always be equal or one more than active");
761 }
762
763 (planned > active).then_some(planned)
764 }
765
766 pub(crate) fn end_session(
768 end_index: SessionIndex,
769 activation_timestamp: Option<(u64, u32)>,
770 rewarded_validators: u32,
771 ) -> Weight {
772 let weight = T::WeightInfo::rc_on_session_report(rewarded_validators);
774
775 let Some(active_era) = ActiveEra::<T>::get() else {
776 defensive!("Active era must always be available.");
777 return weight;
778 };
779 let current_planned_era = Self::is_planning();
780 let starting = end_index + 1;
781 let planning = starting + 1;
783
784 log!(
785 info,
786 "Session: end {:?}, start {:?} (ts: {:?}), planning {:?}",
787 end_index,
788 starting,
789 activation_timestamp,
790 planning
791 );
792 log!(info, "Era: active {:?}, planned {:?}", active_era.index, current_planned_era);
793
794 match activation_timestamp {
795 Some((time, id)) if Some(id) == current_planned_era => {
796 Self::start_era(active_era, starting, time);
798 },
799 Some((_time, id)) => {
800 crate::log!(
802 warn,
803 "received wrong ID with activation timestamp. Got {}, expected {:?}",
804 id,
805 current_planned_era
806 );
807 Pallet::<T>::deposit_event(Event::Unexpected(
808 UnexpectedKind::UnknownValidatorActivation,
809 ));
810 },
811 None => (),
812 }
813
814 let should_plan_era = match ForceEra::<T>::get() {
816 Forcing::NotForcing => Self::is_plan_era_deadline(starting),
818 Forcing::ForceNew => {
820 ForceEra::<T>::put(Forcing::NotForcing);
821 true
822 },
823 Forcing::ForceAlways => true,
825 Forcing::ForceNone => false,
827 };
828
829 let has_pending_era = Self::is_planning().is_some();
832 match (should_plan_era, has_pending_era) {
833 (false, _) => {
834 },
836 (true, false) => {
837 Self::plan_new_era();
839 },
840 (true, true) => {
841 crate::log!(
844 debug,
845 "time to plan a new era {:?}, but waiting for the activation of the previous.",
846 current_planned_era
847 );
848 },
849 }
850
851 Pallet::<T>::deposit_event(Event::SessionRotated {
852 starting_session: starting,
853 active_era: Self::active_era(),
854 planned_era: Self::planned_era(),
855 });
856
857 weight
858 }
859
860 pub(crate) fn start_era(
861 ending_era: ActiveEraInfo,
862 starting_session: SessionIndex,
863 new_era_start_timestamp: u64,
864 ) {
865 debug_assert!(CurrentEra::<T>::get().unwrap_or(0) == ending_era.index + 1);
867
868 let starting_era = ending_era.index + 1;
869
870 Self::end_era(&ending_era, new_era_start_timestamp);
872
873 Self::start_era_inc_active_era(new_era_start_timestamp);
875 Self::start_era_update_bonded_eras(starting_era, starting_session);
876
877 ErasNominatorsSlashable::<T>::insert(starting_era, AreNominatorsSlashable::<T>::get());
880
881 EraElectionPlanner::<T>::cleanup();
883
884 if let Some(old_era) = starting_era.checked_sub(T::HistoryDepth::get() + 1) {
886 reward::EraRewardManager::<T>::cleanup_era(old_era);
887 log!(debug, "Marking era {:?} for lazy pruning", old_era);
888 EraPruningState::<T>::insert(old_era, PruningStep::ErasStakersPaged);
889 }
890 }
891
892 fn start_era_inc_active_era(start_timestamp: u64) {
893 ActiveEra::<T>::mutate(|active_era| {
894 let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0);
895 log!(
896 debug,
897 "starting active era {:?} with RC-provided timestamp {:?}",
898 new_index,
899 start_timestamp
900 );
901 *active_era = Some(ActiveEraInfo { index: new_index, start: Some(start_timestamp) });
902 });
903 }
904
905 pub fn active_era_start_session_index() -> SessionIndex {
909 Self::era_start_session_index(Self::active_era()).defensive_unwrap_or(0)
910 }
911
912 pub fn era_start_session_index(era: EraIndex) -> Option<SessionIndex> {
914 BondedEras::<T>::get()
915 .into_iter()
916 .rev()
917 .find_map(|(e, s)| if e == era { Some(s) } else { None })
918 }
919
920 fn start_era_update_bonded_eras(starting_era: EraIndex, start_session: SessionIndex) {
921 let bonding_duration = T::BondingDuration::get();
922
923 BondedEras::<T>::mutate(|bonded| {
924 if bonded.is_full() {
925 let (era_removed, _) = bonded.remove(0);
927 debug_assert!(
928 era_removed <= (starting_era.saturating_sub(bonding_duration)),
929 "should not delete an era that is not older than bonding duration"
930 );
931 }
932
933 let _ = bonded.try_push((starting_era, start_session)).defensive();
935 });
936 }
937
938 fn end_era(ending_era: &ActiveEraInfo, new_era_start: u64) {
939 if T::DisableMinting::get() {
940 Self::end_era_dap(ending_era);
941 } else {
942 Self::end_era_legacy(ending_era, new_era_start);
943 }
944 }
945
946 fn end_era_legacy(ending_era: &ActiveEraInfo, new_era_start: u64) {
948 let previous_era_start = ending_era.start.defensive_unwrap_or(new_era_start);
949 let era_duration = new_era_start.saturating_sub(previous_era_start);
950
951 let cap = T::MaxEraDuration::get();
952 let era_duration = if cap == 0 || era_duration <= cap {
953 era_duration
954 } else {
955 Pallet::<T>::deposit_event(Event::Unexpected(UnexpectedKind::EraDurationBoundExceeded));
956 log!(
957 warn,
958 "capping era duration for era {:?} from {:?} to max {:?}",
959 ending_era.index,
960 era_duration,
961 cap
962 );
963 cap
964 };
965
966 let staked = ErasTotalStake::<T>::get(ending_era.index);
967 let issuance = asset::total_issuance::<T>();
968 let (validator_payout, remainder) =
969 T::EraPayout::era_payout(staked, issuance, era_duration);
970
971 let total_payout = validator_payout.saturating_add(remainder);
972 let max_staked_rewards = MaxStakedRewards::<T>::get().unwrap_or(Percent::from_percent(100));
973
974 let validator_payout = validator_payout.min(max_staked_rewards * total_payout);
975 let remainder = total_payout.saturating_sub(validator_payout);
976
977 Pallet::<T>::deposit_event(Event::<T>::EraPaid {
978 era_index: ending_era.index,
979 validator_payout,
980 remainder,
981 });
982
983 Eras::<T>::set_stakers_reward(ending_era.index, validator_payout);
984 T::RewardRemainder::on_unbalanced(asset::issue::<T>(remainder));
985 }
986
987 fn end_era_dap(ending_era: &ActiveEraInfo) {
992 let allocation = reward::EraRewardManager::<T>::snapshot_era_rewards(ending_era.index);
993
994 if allocation.staker_rewards.is_zero() {
995 log!(warn, "Era {:?} has zero staker rewards in general pot", ending_era.index);
996 }
997
998 Eras::<T>::set_stakers_reward(ending_era.index, allocation.staker_rewards);
999 Eras::<T>::set_validator_incentive_budget(ending_era.index, allocation.validator_incentive);
1000
1001 Pallet::<T>::deposit_event(Event::<T>::EraPaid {
1003 era_index: ending_era.index,
1004 validator_payout: allocation
1005 .staker_rewards
1006 .saturating_add(allocation.validator_incentive),
1007 remainder: Zero::zero(),
1008 });
1009
1010 if DisableMintingGuard::<T>::get().is_none() {
1011 DisableMintingGuard::<T>::put(ending_era.index);
1012 }
1013 }
1014
1015 fn plan_new_era() {
1019 let _ = CurrentEra::<T>::try_mutate(|x| {
1020 log!(info, "Planning new era: {:?}, sending election start signal", x.unwrap_or(0));
1021 let could_start_election = EraElectionPlanner::<T>::plan_new_election();
1022 *x = Some(x.unwrap_or(0) + 1);
1023 could_start_election
1024 });
1025 }
1026
1027 fn is_plan_era_deadline(start_session: SessionIndex) -> bool {
1029 let planning_era_offset = T::PlanningEraOffset::get().min(T::SessionsPerEra::get());
1030 let target_plan_era_session = T::SessionsPerEra::get().saturating_sub(planning_era_offset);
1032 let era_start_session = Self::active_era_start_session_index();
1033
1034 let session_progress = start_session.defensive_saturating_sub(era_start_session);
1036
1037 log!(
1038 debug,
1039 "Session progress within era: {:?}, target_plan_era_session: {:?}",
1040 session_progress,
1041 target_plan_era_session
1042 );
1043 session_progress >= target_plan_era_session
1044 }
1045}
1046
1047pub(crate) struct EraElectionPlanner<T: Config>(PhantomData<T>);
1073impl<T: Config> EraElectionPlanner<T> {
1074 pub(crate) fn cleanup() {
1076 VoterSnapshotStatus::<T>::kill();
1077 NextElectionPage::<T>::kill();
1078 ElectableStashes::<T>::kill();
1079 Pallet::<T>::register_weight(T::DbWeight::get().writes(3));
1080 }
1081
1082 pub(crate) fn election_pages() -> u32 {
1084 <<T as Config>::ElectionProvider as ElectionProvider>::Pages::get()
1085 }
1086
1087 pub(crate) fn plan_new_election() -> Result<(), <T::ElectionProvider as ElectionProvider>::Error>
1089 {
1090 T::ElectionProvider::start()
1091 .inspect_err(|e| log!(warn, "Election provider failed to start: {:?}", e))
1092 }
1093
1094 pub(crate) fn maybe_fetch_election_results() -> (Weight, Box<dyn Fn(&mut WeightMeter)>) {
1095 let Ok(Some(mut required_weight)) = T::ElectionProvider::status() else {
1096 let weight = T::DbWeight::get().reads(1);
1098 return (weight, Box::new(move |meter: &mut WeightMeter| meter.consume(weight)));
1099 };
1100
1101 required_weight.saturating_accrue(T::DbWeight::get().reads_writes(3, 2));
1110
1111 let exec = Box::new(move |meter: &mut WeightMeter| {
1112 crate::log!(
1113 debug,
1114 "Election provider is ready, our status is {:?}",
1115 NextElectionPage::<T>::get()
1116 );
1117
1118 debug_assert!(
1119 CurrentEra::<T>::get().unwrap_or(0) ==
1120 ActiveEra::<T>::get().map_or(0, |a| a.index) + 1,
1121 "Next era must be already planned."
1122 );
1123
1124 let current_page = NextElectionPage::<T>::get()
1125 .unwrap_or(Self::election_pages().defensive_saturating_sub(1));
1126 let maybe_next_page = current_page.checked_sub(1);
1127 crate::log!(debug, "fetching page {:?}, next {:?}", current_page, maybe_next_page);
1128
1129 Self::do_elect_paged(current_page);
1130 NextElectionPage::<T>::set(maybe_next_page);
1131
1132 if maybe_next_page.is_none() {
1133 let id = CurrentEra::<T>::get().defensive_unwrap_or(0);
1134 let prune_up_to = Self::get_prune_up_to();
1135 let rc_validators = ElectableStashes::<T>::take().into_iter().collect::<Vec<_>>();
1136
1137 crate::log!(
1138 info,
1139 "Sending new validator set of size {:?} to RC. ID: {:?}, prune_up_to: {:?}",
1140 rc_validators.len(),
1141 id,
1142 prune_up_to
1143 );
1144 T::RcClientInterface::validator_set(rc_validators, id, prune_up_to);
1145 }
1146
1147 meter.consume(required_weight)
1149 });
1150
1151 (required_weight, exec)
1152 }
1153
1154 fn get_prune_up_to() -> Option<SessionIndex> {
1157 let bonded_eras = BondedEras::<T>::get();
1158
1159 if bonded_eras.is_full() {
1161 bonded_eras.first().map(|(_, first_session)| first_session.saturating_sub(1))
1162 } else {
1163 None
1164 }
1165 }
1166
1167 pub(crate) fn do_elect_paged(page: PageIndex) {
1179 let election_result = T::ElectionProvider::elect(page);
1180 match election_result {
1181 Ok(supports) => {
1182 let inner_processing_results = Self::do_elect_paged_inner(supports);
1183 if let Err(not_included) = inner_processing_results {
1184 defensive!(
1185 "electable stashes exceeded limit, unexpected but election proceeds.\
1186 {} stashes from election result discarded",
1187 not_included
1188 );
1189 };
1190
1191 Pallet::<T>::deposit_event(Event::PagedElectionProceeded {
1192 page,
1193 result: inner_processing_results.map(|x| x as u32).map_err(|x| x as u32),
1194 });
1195 },
1196 Err(e) => {
1197 log!(warn, "election provider page failed due to {:?} (page: {})", e, page);
1198 Pallet::<T>::deposit_event(Event::PagedElectionProceeded { page, result: Err(0) });
1199 },
1200 }
1201 }
1202
1203 pub(crate) fn do_elect_paged_inner(
1209 mut supports: BoundedSupportsOf<T::ElectionProvider>,
1210 ) -> Result<usize, usize> {
1211 let planning_era = Rotator::<T>::planned_era();
1212
1213 match Self::add_electables(supports.iter().map(|(s, _)| s.clone())) {
1214 Ok(added) => {
1215 let exposures = Self::collect_exposures(supports);
1216 let _ = Self::store_stakers_info(exposures, planning_era);
1217 Ok(added)
1218 },
1219 Err(not_included_idx) => {
1220 let not_included = supports.len().saturating_sub(not_included_idx);
1221
1222 log!(
1223 warn,
1224 "not all winners fit within the electable stashes, excluding {:?} accounts from solution.",
1225 not_included,
1226 );
1227
1228 supports.truncate(not_included_idx);
1231 let exposures = Self::collect_exposures(supports);
1232 let _ = Self::store_stakers_info(exposures, planning_era);
1233
1234 Err(not_included)
1235 },
1236 }
1237 }
1238
1239 pub(crate) fn store_stakers_info(
1243 exposures: BoundedExposuresOf<T>,
1244 new_planned_era: EraIndex,
1245 ) -> BoundedVec<T::AccountId, MaxWinnersPerPageOf<T::ElectionProvider>> {
1246 let mut total_stake_page: BalanceOf<T> = Zero::zero();
1248 let mut elected_stashes_page = Vec::with_capacity(exposures.len());
1249 let mut total_backers = 0u32;
1250
1251 let mut total_incentive_weight_page: BalanceOf<T> = Zero::zero();
1252
1253 exposures.into_iter().for_each(|(stash, exposure)| {
1254 log!(
1255 trace,
1256 "storing exposure for stash {:?} with {:?} own-stake and {:?} backers",
1257 stash,
1258 exposure.own,
1259 exposure.others.len()
1260 );
1261 elected_stashes_page.push(stash.clone());
1263 total_stake_page = total_stake_page.saturating_add(exposure.total);
1265 total_backers += exposure.others.len() as u32;
1266 let own = exposure.own;
1267 Eras::<T>::upsert_exposure(new_planned_era, &stash, exposure);
1268
1269 if !own.is_zero() {
1273 if ErasValidatorIncentiveWeight::<T>::contains_key(new_planned_era, &stash) {
1274 defensive!(
1275 "validator own-stake seen twice in the same era across election pages"
1276 );
1277 } else {
1278 let incentive_weight =
1279 T::StakerRewardCalculator::calculate_validator_incentive_weight(own);
1280 if !incentive_weight.is_zero() {
1281 total_incentive_weight_page =
1282 total_incentive_weight_page.saturating_add(incentive_weight);
1283 ErasValidatorIncentiveWeight::<T>::insert(
1284 new_planned_era,
1285 &stash,
1286 incentive_weight,
1287 );
1288 }
1289 }
1290 }
1291 });
1292
1293 let elected_stashes: BoundedVec<_, MaxWinnersPerPageOf<T::ElectionProvider>> =
1294 elected_stashes_page
1295 .try_into()
1296 .expect("both types are bounded by MaxWinnersPerPageOf; qed");
1297
1298 Eras::<T>::add_total_stake(new_planned_era, total_stake_page);
1300
1301 Eras::<T>::add_sum_validator_incentive_weight(new_planned_era, total_incentive_weight_page);
1303
1304 for stash in &elected_stashes {
1308 let pref = Validators::<T>::get(stash);
1309 Eras::<T>::set_validator_prefs(new_planned_era, stash, pref);
1310 }
1311
1312 log!(
1313 debug,
1314 "stored a page of stakers with {:?} validators and {:?} total backers for era {:?}",
1315 elected_stashes.len(),
1316 total_backers,
1317 new_planned_era,
1318 );
1319
1320 elected_stashes
1321 }
1322
1323 fn collect_exposures(
1329 supports: BoundedSupportsOf<T::ElectionProvider>,
1330 ) -> BoundedExposuresOf<T> {
1331 let total_issuance = asset::total_issuance::<T>();
1332 let to_currency = |e: frame_election_provider_support::ExtendedBalance| {
1333 T::CurrencyToVote::to_currency(e, total_issuance)
1334 };
1335
1336 supports
1337 .into_iter()
1338 .map(|(validator, support)| {
1339 let mut others = Vec::with_capacity(support.voters.len());
1341 let mut own: BalanceOf<T> = Zero::zero();
1342 let mut total: BalanceOf<T> = Zero::zero();
1343 support
1344 .voters
1345 .into_iter()
1346 .map(|(nominator, weight)| (nominator, to_currency(weight)))
1347 .for_each(|(nominator, stake)| {
1348 if nominator == validator {
1349 defensive_assert!(own == Zero::zero(), "own stake should be unique");
1350 own = own.saturating_add(stake);
1351 } else {
1352 others.push(IndividualExposure { who: nominator, value: stake });
1353 }
1354 total = total.saturating_add(stake);
1355 });
1356
1357 let exposure = Exposure { own, others, total };
1358 (validator, exposure)
1359 })
1360 .try_collect()
1361 .expect("we only map through support vector which cannot change the size; qed")
1362 }
1363
1364 pub(crate) fn add_electables(
1371 new_stashes: impl Iterator<Item = T::AccountId>,
1372 ) -> Result<usize, usize> {
1373 ElectableStashes::<T>::mutate(|electable| {
1374 let pre_size = electable.len();
1375
1376 for (idx, stash) in new_stashes.enumerate() {
1377 if electable.try_insert(stash).is_err() {
1378 return Err(idx);
1379 }
1380 }
1381
1382 Ok(electable.len() - pre_size)
1383 })
1384 }
1385}