diff --git a/substrate/frame/election-provider-multi-block/src/lib.rs b/substrate/frame/election-provider-multi-block/src/lib.rs index 094542dd722b7..4b9d774899ef8 100644 --- a/substrate/frame/election-provider-multi-block/src/lib.rs +++ b/substrate/frame/election-provider-multi-block/src/lib.rs @@ -224,12 +224,12 @@ pub mod helpers; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; -/// The common logginv prefix of all pallets in this crate. +/// The common logging prefix of all pallets in this crate. pub const LOG_PREFIX: &'static str = "runtime::multiblock-election"; -macro_rules! clear_paged_map { - ($map: ty) => {{ - let __r = <$map>::clear(u32::MAX, None); +macro_rules! clear_round_based_map { + ($map: ty, $round: expr) => {{ + let __r = <$map>::clear_prefix($round, u32::MAX, None); debug_assert!(__r.unique <= T::Pages::get(), "clearing map caused too many removals") }}; } @@ -476,9 +476,38 @@ pub enum AdminOperation { SetMinUntrustedScore(ElectionScore), } +/// Trait to notify other sub-systems that a round has ended. +pub trait OnRoundRotation { + /// `ending` round has ended. Implies we are now at round `ending + 1` + fn on_round_rotation(ending: u32); +} + +impl OnRoundRotation for () { + fn on_round_rotation(_: u32) {} +} + +/// An implementation of [`OnRoundRotation`] that immediately deletes all the data in all the +/// pallets, once the round is over. +/// +/// This is intended to be phased out once we move to fully lazy deletion system to spare more PoV. +/// In that case, simply use `()` on [`pallet::Config::OnRoundRotation`]. +pub struct CleanRound(core::marker::PhantomData); +impl OnRoundRotation for CleanRound { + fn on_round_rotation(_ending: u32) { + // Kill everything in the verifier. + T::Verifier::kill(); + + // Kill the snapshot. + pallet::Snapshot::::kill(); + + // Nothing to do in the signed pallet -- it is already in lazy-deletion mode. + } +} + #[frame_support::pallet] pub mod pallet { use super::*; + #[pallet::config] pub trait Config: frame_system::Config { /// Duration of the unsigned phase. @@ -558,6 +587,10 @@ pub mod pallet { /// The weight of the pallet. type WeightInfo: WeightInfo; + + /// Single type that implement [`super::OnRoundRotation`] to do something when the round + /// ends. + type OnRoundRotation: super::OnRoundRotation; } #[pallet::call] @@ -794,6 +827,14 @@ pub mod pallet { /// - `PagedTargetSnapshot`: Paginated map of targets. /// - `PagedTargetSnapshotHash`: Hash of the aforementioned. /// + /// ### Round + /// + /// All inner storage items are keyed by the round number. Yet, none of the interface in this + /// type expose this. This is because a snapshot is really only ever meaningful in the current + /// round. Moreover, doing this will allow us to possibly lazy-delete the old round data, such + /// as the sizeable snapshot, in a lazy manner. If any of these storage items, key-ed by a round + /// index, are in a round that has passed, now they can be lazy deleted. + /// /// ### Invariants /// /// The following invariants must be met at **all times** for this storage item to be "correct". @@ -816,48 +857,48 @@ pub mod pallet { impl Snapshot { // ----------- mutable methods pub(crate) fn set_desired_targets(d: u32) { - DesiredTargets::::put(d); + DesiredTargets::::insert(Self::round(), d); } pub(crate) fn set_targets(targets: BoundedVec) { let hash = Self::write_storage_with_pre_allocate( - &PagedTargetSnapshot::::hashed_key_for(Pallet::::msp()), + &PagedTargetSnapshot::::hashed_key_for(Self::round(), Pallet::::msp()), targets, ); - PagedTargetSnapshotHash::::insert(Pallet::::msp(), hash); + PagedTargetSnapshotHash::::insert(Self::round(), Pallet::::msp(), hash); } pub(crate) fn set_voters(page: PageIndex, voters: VoterPageOf) { let hash = Self::write_storage_with_pre_allocate( - &PagedVoterSnapshot::::hashed_key_for(page), + &PagedVoterSnapshot::::hashed_key_for(Self::round(), page), voters, ); - PagedVoterSnapshotHash::::insert(page, hash); + PagedVoterSnapshotHash::::insert(Self::round(), page, hash); } /// Destroy the entire snapshot. /// /// Should be called only once we transition to [`Phase::Off`]. pub(crate) fn kill() { - DesiredTargets::::kill(); - clear_paged_map!(PagedVoterSnapshot::); - clear_paged_map!(PagedVoterSnapshotHash::); - clear_paged_map!(PagedTargetSnapshot::); - clear_paged_map!(PagedTargetSnapshotHash::); + DesiredTargets::::remove(Self::round()); + clear_round_based_map!(PagedVoterSnapshot::, Self::round()); + clear_round_based_map!(PagedVoterSnapshotHash::, Self::round()); + clear_round_based_map!(PagedTargetSnapshot::, Self::round()); + clear_round_based_map!(PagedTargetSnapshotHash::, Self::round()); } // ----------- non-mutables pub(crate) fn desired_targets() -> Option { - DesiredTargets::::get() + DesiredTargets::::get(Self::round()) } pub(crate) fn voters(page: PageIndex) -> Option> { - PagedVoterSnapshot::::get(page) + PagedVoterSnapshot::::get(Self::round(), page) } pub(crate) fn targets() -> Option> { // NOTE: targets always have one index, which is 0, aka lsp. - PagedTargetSnapshot::::get(Pallet::::msp()) + PagedTargetSnapshot::::get(Self::round(), Pallet::::msp()) } /// Get a fingerprint of the snapshot, from all the hashes that are stored for each page of @@ -870,7 +911,7 @@ pub mod pallet { let mut hashed_target_and_voters = Self::targets_hash().unwrap_or_default().as_ref().to_vec(); let hashed_voters = (Pallet::::msp()..=Pallet::::lsp()) - .map(|i| PagedVoterSnapshotHash::::get(i).unwrap_or_default()) + .map(|i| PagedVoterSnapshotHash::::get(Self::round(), i).unwrap_or_default()) .flat_map(|hash| >::as_ref(&hash).to_owned()) .collect::>(); hashed_target_and_voters.extend(hashed_voters); @@ -894,7 +935,11 @@ pub mod pallet { } pub(crate) fn targets_hash() -> Option { - PagedTargetSnapshotHash::::get(Pallet::::msp()) + PagedTargetSnapshotHash::::get(Self::round(), Pallet::::msp()) + } + + fn round() -> u32 { + Pallet::::round() } } @@ -985,15 +1030,15 @@ pub mod pallet { } pub(crate) fn voters_decode_len(page: PageIndex) -> Option { - PagedVoterSnapshot::::decode_len(page) + PagedVoterSnapshot::::decode_len(Self::round(), page) } pub(crate) fn targets_decode_len() -> Option { - PagedTargetSnapshot::::decode_len(Pallet::::msp()) + PagedTargetSnapshot::::decode_len(Self::round(), Pallet::::msp()) } pub(crate) fn voters_hash(page: PageIndex) -> Option { - PagedVoterSnapshotHash::::get(page) + PagedVoterSnapshotHash::::get(Self::round(), page) } pub(crate) fn sanity_check() -> Result<(), &'static str> { @@ -1043,60 +1088,79 @@ pub mod pallet { (crate::Pallet::::lsp()..=crate::Pallet::::msp()).collect::>(); key_range .into_iter() - .flat_map(|k| PagedVoterSnapshot::::get(k).unwrap_or_default()) + .flat_map(|k| PagedVoterSnapshot::::get(Self::round(), k).unwrap_or_default()) } pub(crate) fn remove_voter_page(page: PageIndex) { - PagedVoterSnapshot::::remove(page); + PagedVoterSnapshot::::remove(Self::round(), page); } pub(crate) fn kill_desired_targets() { - DesiredTargets::::kill(); + DesiredTargets::::remove(Self::round()); } pub(crate) fn remove_target_page() { - PagedTargetSnapshot::::remove(Pallet::::msp()); + PagedTargetSnapshot::::remove(Self::round(), Pallet::::msp()); } pub(crate) fn remove_target(at: usize) { - PagedTargetSnapshot::::mutate(crate::Pallet::::msp(), |maybe_targets| { - if let Some(targets) = maybe_targets { - targets.remove(at); - // and update the hash. - PagedTargetSnapshotHash::::insert( - crate::Pallet::::msp(), - T::Hashing::hash(&targets.encode()), - ) - } else { - unreachable!(); - } - }) + PagedTargetSnapshot::::mutate( + Self::round(), + crate::Pallet::::msp(), + |maybe_targets| { + if let Some(targets) = maybe_targets { + targets.remove(at); + // and update the hash. + PagedTargetSnapshotHash::::insert( + Self::round(), + crate::Pallet::::msp(), + T::Hashing::hash(&targets.encode()), + ) + } else { + unreachable!(); + } + }, + ) } } /// Desired number of targets to elect for this round. #[pallet::storage] - type DesiredTargets = StorageValue<_, u32>; + type DesiredTargets = StorageMap<_, Twox64Concat, u32, u32>; /// Paginated voter snapshot. At most [`T::Pages`] keys will exist. #[pallet::storage] - type PagedVoterSnapshot = - StorageMap<_, Twox64Concat, PageIndex, VoterPageOf>; + type PagedVoterSnapshot = StorageDoubleMap< + _, + Twox64Concat, + u32, + Twox64Concat, + PageIndex, + VoterPageOf, + >; /// Same as [`PagedVoterSnapshot`], but it will store the hash of the snapshot. /// /// The hash is generated using [`frame_system::Config::Hashing`]. #[pallet::storage] - type PagedVoterSnapshotHash = StorageMap<_, Twox64Concat, PageIndex, T::Hash>; + type PagedVoterSnapshotHash = + StorageDoubleMap<_, Twox64Concat, u32, Twox64Concat, PageIndex, T::Hash>; /// Paginated target snapshot. /// /// For the time being, since we assume one pages of targets, at most ONE key will exist. #[pallet::storage] - type PagedTargetSnapshot = - StorageMap<_, Twox64Concat, PageIndex, BoundedVec>; + type PagedTargetSnapshot = StorageDoubleMap< + _, + Twox64Concat, + u32, + Twox64Concat, + PageIndex, + BoundedVec, + >; /// Same as [`PagedTargetSnapshot`], but it will store the hash of the snapshot. /// /// The hash is generated using [`frame_system::Config::Hashing`]. #[pallet::storage] - type PagedTargetSnapshotHash = StorageMap<_, Twox64Concat, PageIndex, T::Hash>; + type PagedTargetSnapshotHash = + StorageDoubleMap<_, Twox64Concat, u32, Twox64Concat, PageIndex, T::Hash>; #[pallet::pallet] pub struct Pallet(PhantomData); @@ -1238,16 +1302,14 @@ impl Pallet { /// 3. Clear all snapshot data. pub(crate) fn rotate_round() { // Inc round. - >::mutate(|r| *r += 1); + >::mutate(|r| { + // Notify the rest of the world + T::OnRoundRotation::on_round_rotation(*r); + *r += 1 + }); // Phase is off now. Self::phase_transition(Phase::Off); - - // Kill everything in the verifier. - T::Verifier::kill(); - - // Kill the snapshot. - Snapshot::::kill(); } /// Call fallback for the given page. diff --git a/substrate/frame/election-provider-multi-block/src/mock/mod.rs b/substrate/frame/election-provider-multi-block/src/mock/mod.rs index a03e726d2f8b3..1a208c8bb46a7 100644 --- a/substrate/frame/election-provider-multi-block/src/mock/mod.rs +++ b/substrate/frame/election-provider-multi-block/src/mock/mod.rs @@ -227,6 +227,7 @@ impl crate::Config for Runtime { type AdminOrigin = EnsureRoot; type Pages = Pages; type AreWeDone = AreWeDone; + type OnRoundRotation = CleanRound; } parameter_types! { diff --git a/substrate/frame/election-provider-multi-block/src/verifier/impls.rs b/substrate/frame/election-provider-multi-block/src/verifier/impls.rs index 0236fe58fa1a2..034ece20073d9 100644 --- a/substrate/frame/election-provider-multi-block/src/verifier/impls.rs +++ b/substrate/frame/election-provider-multi-block/src/verifier/impls.rs @@ -180,6 +180,14 @@ pub(crate) mod pallet { /// INVALID variant we mean either of these two storage items, based on the value of /// `QueuedValidVariant`. /// + /// ### Round Index + /// + /// Much like `Snapshot` in the parent crate, these storage items are mapping whereby their + /// _first_ key is the round index. None of the APIs in [`QueuedSolution`] expose this, as + /// on-chain, we should ONLY ever be reading the current round's associated data. + /// + /// Having this extra key paves the way for lazy deletion in the future. + /// /// ### Invariants /// /// The following conditions must be met at all times for this group of storage items to be @@ -215,6 +223,10 @@ pub(crate) mod pallet { r } + fn round() -> u32 { + crate::Pallet::::round() + } + /// Finalize a correct solution. /// /// Should be called at the end of a verification process, once we are sure that a certain @@ -228,13 +240,13 @@ pub(crate) mod pallet { info, "verifier", "finalizing verification a correct solution, replacing old score {:?} with {:?}", - QueuedSolutionScore::::get(), + QueuedSolutionScore::::get(Self::round()), score ); Self::mutate_checked(|| { - QueuedValidVariant::::mutate(|v| *v = v.other()); - QueuedSolutionScore::::put(score); + QueuedValidVariant::::mutate(Self::round(), |v| *v = v.other()); + QueuedSolutionScore::::insert(Self::round(), score); // Clear what was previously the valid variant. Also clears the partial backings. Self::clear_invalid_and_backings_unchecked(); @@ -254,10 +266,10 @@ pub(crate) mod pallet { pub(crate) fn clear_invalid_and_backings_unchecked() { // clear is safe as we delete at most `Pages` entries, and `Pages` is bounded. match Self::invalid() { - ValidSolution::X => clear_paged_map!(QueuedSolutionX::), - ValidSolution::Y => clear_paged_map!(QueuedSolutionY::), + ValidSolution::X => clear_round_based_map!(QueuedSolutionX::, Self::round()), + ValidSolution::Y => clear_round_based_map!(QueuedSolutionY::, Self::round()), }; - clear_paged_map!(QueuedSolutionBackings::); + clear_round_based_map!(QueuedSolutionBackings::, Self::round()); } /// Write a single page of a valid solution into the `invalid` variant of the storage. @@ -276,11 +288,11 @@ pub(crate) mod pallet { .map(|(x, s)| (x.clone(), PartialBackings { total: s.total, backers: s.voters.len() as u32 } )) .try_collect() .expect("`SupportsOfVerifier` is bounded by as Verifier>::MaxWinnersPerPage, which is assured to be the same as `T::MaxWinnersPerPage` in an integrity test"); - QueuedSolutionBackings::::insert(page, backings); + QueuedSolutionBackings::::insert(Self::round(), page, backings); match Self::invalid() { - ValidSolution::X => QueuedSolutionX::::insert(page, supports), - ValidSolution::Y => QueuedSolutionY::::insert(page, supports), + ValidSolution::X => QueuedSolutionX::::insert(Self::round(), page, supports), + ValidSolution::Y => QueuedSolutionY::::insert(Self::round(), page, supports), } }) } @@ -299,19 +311,19 @@ pub(crate) mod pallet { Self::mutate_checked(|| { // clear everything about valid solutions. match Self::valid() { - ValidSolution::X => clear_paged_map!(QueuedSolutionX::), - ValidSolution::Y => clear_paged_map!(QueuedSolutionY::), + ValidSolution::X => clear_round_based_map!(QueuedSolutionX::, Self::round()), + ValidSolution::Y => clear_round_based_map!(QueuedSolutionY::, Self::round()), }; - QueuedSolutionScore::::kill(); + QueuedSolutionScore::::remove(Self::round()); // write a single new page. match Self::valid() { - ValidSolution::X => QueuedSolutionX::::insert(page, supports), - ValidSolution::Y => QueuedSolutionY::::insert(page, supports), + ValidSolution::X => QueuedSolutionX::::insert(Self::round(), page, supports), + ValidSolution::Y => QueuedSolutionY::::insert(Self::round(), page, supports), } // write the score. - QueuedSolutionScore::::put(score); + QueuedSolutionScore::::insert(Self::round(), score); }) } @@ -325,19 +337,21 @@ pub(crate) mod pallet { Self::mutate_checked(|| { // clear everything about valid solutions. match Self::valid() { - ValidSolution::X => clear_paged_map!(QueuedSolutionX::), - ValidSolution::Y => clear_paged_map!(QueuedSolutionY::), + ValidSolution::X => clear_round_based_map!(QueuedSolutionX::, Self::round()), + ValidSolution::Y => clear_round_based_map!(QueuedSolutionY::, Self::round()), }; - QueuedSolutionScore::::kill(); + QueuedSolutionScore::::remove(Self::round()); // store the valid pages for (support, page) in supports.into_iter().zip(pages.iter()) { match Self::valid() { - ValidSolution::X => QueuedSolutionX::::insert(page, support), - ValidSolution::Y => QueuedSolutionY::::insert(page, support), + ValidSolution::X => + QueuedSolutionX::::insert(Self::round(), page, support), + ValidSolution::Y => + QueuedSolutionY::::insert(Self::round(), page, support), } } - QueuedSolutionScore::::put(score); + QueuedSolutionScore::::insert(Self::round(), score); }); } @@ -346,11 +360,11 @@ pub(crate) mod pallet { /// Should only be called once everything is done. pub(crate) fn kill() { Self::mutate_checked(|| { - clear_paged_map!(QueuedSolutionX::); - clear_paged_map!(QueuedSolutionY::); - QueuedValidVariant::::kill(); - clear_paged_map!(QueuedSolutionBackings::); - QueuedSolutionScore::::kill(); + clear_round_based_map!(QueuedSolutionX::, Self::round()); + clear_round_based_map!(QueuedSolutionY::, Self::round()); + QueuedValidVariant::::remove(Self::round()); + clear_round_based_map!(QueuedSolutionBackings::, Self::round()); + QueuedSolutionScore::::remove(Self::round()); }) } @@ -367,13 +381,15 @@ pub(crate) mod pallet { /// should never become `valid`. pub(crate) fn compute_invalid_score() -> Result<(ElectionScore, u32), FeasibilityError> { // ensure that this is only called when all pages are verified individually. - if QueuedSolutionBackings::::iter_keys().count() != T::Pages::get() as usize { + if QueuedSolutionBackings::::iter_key_prefix(Self::round()).count() != + T::Pages::get() as usize + { return Err(FeasibilityError::Incomplete) } let mut total_supports: BTreeMap = Default::default(); for (who, PartialBackings { backers, total }) in - QueuedSolutionBackings::::iter().flat_map(|(_, pb)| pb) + QueuedSolutionBackings::::iter_prefix(Self::round()).flat_map(|(_, pb)| pb) { let entry = total_supports.entry(who).or_default(); entry.total = entry.total.saturating_add(total); @@ -392,7 +408,7 @@ pub(crate) mod pallet { /// The score of the current best solution, if any. pub(crate) fn queued_score() -> Option { - QueuedSolutionScore::::get() + QueuedSolutionScore::::get(Self::round()) } /// Get a page of the current queued (aka valid) solution. @@ -400,13 +416,13 @@ pub(crate) mod pallet { page: PageIndex, ) -> Option>> { match Self::valid() { - ValidSolution::X => QueuedSolutionX::::get(page), - ValidSolution::Y => QueuedSolutionY::::get(page), + ValidSolution::X => QueuedSolutionX::::get(Self::round(), page), + ValidSolution::Y => QueuedSolutionY::::get(Self::round(), page), } } fn valid() -> ValidSolution { - QueuedValidVariant::::get() + QueuedValidVariant::::get(Self::round()) } fn invalid() -> ValidSolution { @@ -420,30 +436,30 @@ pub(crate) mod pallet { pub(crate) fn valid_iter( ) -> impl Iterator>)> { match Self::valid() { - ValidSolution::X => QueuedSolutionX::::iter(), - ValidSolution::Y => QueuedSolutionY::::iter(), + ValidSolution::X => QueuedSolutionX::::iter_prefix(Self::round()), + ValidSolution::Y => QueuedSolutionY::::iter_prefix(Self::round()), } } pub(crate) fn invalid_iter( ) -> impl Iterator>)> { match Self::invalid() { - ValidSolution::X => QueuedSolutionX::::iter(), - ValidSolution::Y => QueuedSolutionY::::iter(), + ValidSolution::X => QueuedSolutionX::::iter_prefix(Self::round()), + ValidSolution::Y => QueuedSolutionY::::iter_prefix(Self::round()), } } pub(crate) fn get_valid_page(page: PageIndex) -> Option>> { match Self::valid() { - ValidSolution::X => QueuedSolutionX::::get(page), - ValidSolution::Y => QueuedSolutionY::::get(page), + ValidSolution::X => QueuedSolutionX::::get(Self::round(), page), + ValidSolution::Y => QueuedSolutionY::::get(Self::round(), page), } } pub(crate) fn backing_iter() -> impl Iterator< Item = (PageIndex, BoundedVec<(T::AccountId, PartialBackings), T::MaxWinnersPerPage>), > { - QueuedSolutionBackings::::iter() + QueuedSolutionBackings::::iter_prefix(Self::round()) } /// Ensure that all the storage items managed by this struct are in `kill` state, meaning @@ -481,7 +497,8 @@ pub(crate) mod pallet { // The number of existing keys in `QueuedSolutionBackings` must always match that of // the INVALID variant. ensure!( - QueuedSolutionBackings::::iter().count() == Self::invalid_iter().count(), + QueuedSolutionBackings::::iter_prefix(Self::round()).count() == + Self::invalid_iter().count(), "incorrect number of backings pages", ); @@ -503,18 +520,31 @@ pub(crate) mod pallet { /// Writing them to a bugger and copying at the ned is slightly better, but expensive. This flag /// system is best of both worlds. #[pallet::storage] - type QueuedSolutionX = - StorageMap<_, Twox64Concat, PageIndex, SupportsOfVerifier>>; + type QueuedSolutionX = StorageDoubleMap< + _, + Twox64Concat, + u32, + Twox64Concat, + PageIndex, + SupportsOfVerifier>, + >; /// The `Y` variant of the current queued solution. Might be the valid one or not. #[pallet::storage] - type QueuedSolutionY = - StorageMap<_, Twox64Concat, PageIndex, SupportsOfVerifier>>; + type QueuedSolutionY = StorageDoubleMap< + _, + Twox64Concat, + u32, + Twox64Concat, + PageIndex, + SupportsOfVerifier>, + >; /// Pointer to the variant of [`QueuedSolutionX`] or [`QueuedSolutionY`] that is currently /// valid. #[pallet::storage] - type QueuedValidVariant = StorageValue<_, ValidSolution, ValueQuery>; + type QueuedValidVariant = + StorageMap<_, Twox64Concat, u32, ValidSolution, ValueQuery>; /// The `(amount, count)` of backings, divided per page. /// @@ -525,9 +555,11 @@ pub(crate) mod pallet { /// need this information anymore; the score is already computed once in /// [`QueuedSolutionScore`], and the backing counts are checked. #[pallet::storage] - type QueuedSolutionBackings = StorageMap< + type QueuedSolutionBackings = StorageDoubleMap< _, Twox64Concat, + u32, + Twox64Concat, PageIndex, BoundedVec<(T::AccountId, PartialBackings), T::MaxWinnersPerPage>, >; @@ -536,7 +568,7 @@ pub(crate) mod pallet { /// /// This only ever lives for the `valid` variant. #[pallet::storage] - type QueuedSolutionScore = StorageValue<_, ElectionScore>; + type QueuedSolutionScore = StorageMap<_, Twox64Concat, u32, ElectionScore>; // -- ^^ private storage items, managed by `QueuedSolution`. diff --git a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs index a255f6105c284..ab3c5d0392887 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs @@ -269,6 +269,7 @@ impl multi_block::Config for Runtime { type VoterSnapshotPerBlock = VoterSnapshotPerBlock; type Verifier = MultiBlockVerifier; type AreWeDone = multi_block::ProceedRegardlessOf; + type OnRoundRotation = multi_block::CleanRound; type WeightInfo = multi_block::weights::AllZeroWeights; } diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index 86225a7402be0..d1f2a71015e15 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -117,6 +117,7 @@ impl multi_block::Config for Runtime { type Fallback = frame_election_provider_support::onchain::OnChainExecution; type MinerConfig = Self; type Verifier = MultiBlockVerifier; + type OnRoundRotation = multi_block::CleanRound; type WeightInfo = measured::pallet_election_provider_multi_block::SubstrateWeight; }