From b9aa8817cf93847caf36a450df1c654ca04933ef Mon Sep 17 00:00:00 2001 From: kianenigma <kian@parity.io> Date: Mon, 27 Jan 2025 10:41:29 +0000 Subject: [PATCH] stateless data provider so that EPM and EPMB can work together --- substrate/bin/node/runtime/src/lib.rs | 28 +++++- .../election-provider-multi-block/src/lib.rs | 4 +- .../election-provider-multi-phase/src/lib.rs | 5 +- .../election-provider-support/src/lib.rs | 16 ++++ .../election-provider-support/src/onchain.rs | 5 +- substrate/frame/staking/src/benchmarking.rs | 6 +- substrate/frame/staking/src/pallet/impls.rs | 93 ++++++++++--------- substrate/frame/staking/src/tests.rs | 4 +- 8 files changed, 103 insertions(+), 58 deletions(-) diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 8f9c2d2e295..efb7cabdfd0 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -831,6 +831,31 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig { type MaxValidators = ConstU32<1000>; } +use frame_election_provider_support::{BoundedSupportsOf, ElectionProvider, PageIndex}; +pub struct MultiElectionProvider; +impl ElectionProvider for MultiElectionProvider { + type AccountId = <MultiBlock as ElectionProvider>::AccountId; + type BlockNumber = <MultiBlock as ElectionProvider>::BlockNumber; + type DataProvider = <MultiBlock as ElectionProvider>::DataProvider; + type Error = <MultiBlock as ElectionProvider>::Error; + type Pages = <MultiBlock as ElectionProvider>::Pages; + type MaxBackersPerWinner = <MultiBlock as ElectionProvider>::MaxBackersPerWinner; + type MaxWinnersPerPage = <MultiBlock as ElectionProvider>::MaxWinnersPerPage; + + fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> { + if page == 0 { + // TODO: later on, we can even compare the results of the multi-page and multi-block + // election like this. + let _ = ElectionProviderMultiPhase::elect(page); + } + MultiBlock::elect(page) + } + + fn ongoing() -> bool { + MultiBlock::ongoing() + } +} + impl pallet_staking::Config for Runtime { type OldCurrency = Balances; type Currency = Balances; @@ -855,8 +880,7 @@ impl pallet_staking::Config for Runtime { type NextNewSession = Session; type MaxExposurePageSize = MaxExposurePageSize; type MaxValidatorSet = MaxActiveValidators; - type ElectionProvider = MultiBlock; - // type ElectionProvider = ElectionProviderMultiPhase; + type ElectionProvider = MultiElectionProvider; type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type VoterList = VoterList; type NominationsQuota = pallet_staking::FixedNominationsQuota<MAX_QUOTA_NOMINATIONS>; diff --git a/substrate/frame/election-provider-multi-block/src/lib.rs b/substrate/frame/election-provider-multi-block/src/lib.rs index 6ba447726da..282c45b76c3 100644 --- a/substrate/frame/election-provider-multi-block/src/lib.rs +++ b/substrate/frame/election-provider-multi-block/src/lib.rs @@ -60,6 +60,8 @@ //! 3. signed //! 4. unsigned //! +//! This is critical for the phase transition to work. +//! //! This should be manually checked, there is not automated way to test it. //! //! ## Pagination @@ -552,8 +554,6 @@ pub mod pallet { Self::phase_transition(Phase::SignedValidation(now)); // we don't do anything else here. We expect the signed sub-pallet to handle // whatever else needs to be done. - // TODO: this notification system based on block numbers is 100% based on the - // on_initialize of the parent pallet is called before the rest of them. todo_weight }, diff --git a/substrate/frame/election-provider-multi-phase/src/lib.rs b/substrate/frame/election-provider-multi-phase/src/lib.rs index 0f4a5934196..09bb54221b5 100644 --- a/substrate/frame/election-provider-multi-phase/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/src/lib.rs @@ -768,7 +768,6 @@ pub mod pallet { #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn on_initialize(now: BlockNumberFor<T>) -> Weight { - return Default::default(); let next_election = T::DataProvider::next_election_prediction(now).max(now); let signed_deadline = T::SignedPhase::get() + T::UnsignedPhase::get(); @@ -1525,7 +1524,7 @@ impl<T: Config> Pallet<T> { fn create_snapshot_external( ) -> Result<(Vec<T::AccountId>, Vec<VoterOf<T>>, u32), ElectionError<T>> { let election_bounds = T::ElectionBounds::get(); - let targets = T::DataProvider::electable_targets(election_bounds.targets, SINGLE_PAGE) + let targets = T::DataProvider::electable_targets_stateless(election_bounds.targets) .and_then(|t| { election_bounds.ensure_targets_limits( CountBound(t.len() as u32), @@ -1535,7 +1534,7 @@ impl<T: Config> Pallet<T> { }) .map_err(ElectionError::DataProvider)?; - let voters = T::DataProvider::electing_voters(election_bounds.voters, SINGLE_PAGE) + let voters = T::DataProvider::electing_voters_stateless(election_bounds.voters) .and_then(|v| { election_bounds.ensure_voters_limits( CountBound(v.len() as u32), diff --git a/substrate/frame/election-provider-support/src/lib.rs b/substrate/frame/election-provider-support/src/lib.rs index e9055d456c4..234314d21d3 100644 --- a/substrate/frame/election-provider-support/src/lib.rs +++ b/substrate/frame/election-provider-support/src/lib.rs @@ -330,6 +330,15 @@ pub trait ElectionDataProvider { page: PageIndex, ) -> data_provider::Result<Vec<Self::AccountId>>; + /// A state-less version of [`Self::electing_targets`]. + /// + /// An election-provider that only uses 1 page should use this. + fn electable_targets_stateless( + bounds: DataProviderBounds, + ) -> data_provider::Result<Vec<Self::AccountId>> { + Self::electable_targets(bounds, 0) + } + /// All the voters that participate in the election associated with page `page`, thus /// "electing". /// @@ -342,6 +351,13 @@ pub trait ElectionDataProvider { page: PageIndex, ) -> data_provider::Result<Vec<VoterOf<Self>>>; + /// A state-less version of [`Self::electing_voters`]. + fn electing_voters_stateless( + bounds: DataProviderBounds, + ) -> data_provider::Result<Vec<VoterOf<Self>>> { + Self::electing_voters(bounds, 0) + } + /// The number of targets to elect. /// /// This should be implemented as a self-weighing function. The implementor should register its diff --git a/substrate/frame/election-provider-support/src/onchain.rs b/substrate/frame/election-provider-support/src/onchain.rs index 65f5bc05cf6..3fe8f3b4bc3 100644 --- a/substrate/frame/election-provider-support/src/onchain.rs +++ b/substrate/frame/election-provider-support/src/onchain.rs @@ -184,8 +184,9 @@ impl<T: Config> ElectionProvider for OnChainExecution<T> { type Error = Error; type MaxWinnersPerPage = T::MaxWinnersPerPage; type MaxBackersPerWinner = T::MaxBackersPerWinner; - // can support any number of pages, as this is meant to be called "instantly". - type Pages = sp_core::ConstU32<{ u32::MAX }>; + // can support any number of pages, as this is meant to be called "instantly". We don't care + // about this value here. + type Pages = sp_core::ConstU32<1>; type DataProvider = T::DataProvider; fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> { diff --git a/substrate/frame/staking/src/benchmarking.rs b/substrate/frame/staking/src/benchmarking.rs index 984c4aadcd7..4538556f56c 100644 --- a/substrate/frame/staking/src/benchmarking.rs +++ b/substrate/frame/staking/src/benchmarking.rs @@ -42,7 +42,6 @@ use frame_system::RawOrigin; const SEED: u32 = 0; const MAX_SPANS: u32 = 100; const MAX_SLASHES: u32 = 1000; -const SINGLE_PAGE: u32 = 0; type MaxValidators<T> = <<T as Config>::BenchmarkingConfig as BenchmarkingConfig>::MaxValidators; type MaxNominators<T> = <<T as Config>::BenchmarkingConfig as BenchmarkingConfig>::MaxNominators; @@ -1031,7 +1030,10 @@ mod benchmarks { let voters; #[block] { - voters = <Staking<T>>::get_npos_voters(DataProviderBounds::default(), SINGLE_PAGE); + voters = <Staking<T>>::get_npos_voters( + DataProviderBounds::default(), + &SnapshotStatus::<T::AccountId>::Waiting, + ); } assert_eq!(voters.len(), num_voters); diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 96336362334..a08f15b47b7 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1038,7 +1038,7 @@ impl<T: Config> Pallet<T> { /// This function is self-weighing as [`DispatchClass::Mandatory`]. pub(crate) fn get_npos_voters( bounds: DataProviderBounds, - page: PageIndex, + status: &SnapshotStatus<T::AccountId>, ) -> Vec<VoterOf<Self>> { let mut voters_size_tracker: StaticTracker<Self> = StaticTracker::default(); @@ -1057,7 +1057,7 @@ impl<T: Config> Pallet<T> { let mut nominators_taken = 0u32; let mut min_active_stake = u64::MAX; - let mut sorted_voters = match VoterSnapshotStatus::<T>::get() { + let mut sorted_voters = match status { // start the snapshot processing from the beginning. SnapshotStatus::Waiting => T::VoterList::iter(), // snapshot continues, start from the last iterated voter in the list. @@ -1140,29 +1140,6 @@ impl<T: Config> Pallet<T> { } } - // update the voter snapshot status. - VoterSnapshotStatus::<T>::mutate(|status| { - match (page, status.clone()) { - // last page, reset status for next round. - (0, _) => *status = SnapshotStatus::Waiting, - - (_, SnapshotStatus::Waiting) | (_, SnapshotStatus::Ongoing(_)) => { - let maybe_last = all_voters.last().map(|(x, _, _)| x).cloned(); - - if let Some(ref last) = maybe_last { - if maybe_last == T::VoterList::iter().last() { - // all voters in the voter list have been consumed. - *status = SnapshotStatus::Consumed; - } else { - *status = SnapshotStatus::Ongoing(last.clone()); - } - } - }, - // do nothing. - (_, SnapshotStatus::Consumed) => (), - } - }); - // all_voters should have not re-allocated. debug_assert!(all_voters.capacity() == page_len_prediction as usize); @@ -1173,17 +1150,6 @@ impl<T: Config> Pallet<T> { MinimumActiveStake::<T>::put(min_active_stake); - log!( - info, - "[page {}, status {:?}, bounds {:?}] generated {} npos voters, {} from validators and {} nominators", - page, - VoterSnapshotStatus::<T>::get(), - bounds, - all_voters.len(), - validators_taken, - nominators_taken - ); - all_voters } @@ -1463,13 +1429,58 @@ impl<T: Config> ElectionDataProvider for Pallet<T> { bounds: DataProviderBounds, page: PageIndex, ) -> data_provider::Result<Vec<VoterOf<Self>>> { - let voters = Self::get_npos_voters(bounds, page); + let mut status = VoterSnapshotStatus::<T>::get(); + let voters = Self::get_npos_voters(bounds, &status); + + // update the voter snapshot status. + match (page, &status) { + // last page, reset status for next round. + (0, _) => status = SnapshotStatus::Waiting, + + (_, SnapshotStatus::Waiting) | (_, SnapshotStatus::Ongoing(_)) => { + let maybe_last = voters.last().map(|(x, _, _)| x).cloned(); + + if let Some(ref last) = maybe_last { + if maybe_last == T::VoterList::iter().last() { + // all voters in the voter list have been consumed. + status = SnapshotStatus::Consumed; + } else { + status = SnapshotStatus::Ongoing(last.clone()); + } + } + }, + // do nothing. + (_, SnapshotStatus::Consumed) => (), + } + log!( + info, + "[page {}, status {:?}, bounds {:?}] generated {} npos voters", + page, + VoterSnapshotStatus::<T>::get(), + bounds, + voters.len(), + ); + VoterSnapshotStatus::<T>::put(status); debug_assert!(!bounds.slice_exhausted(&voters)); Ok(voters) } + fn electing_voters_stateless( + bounds: DataProviderBounds, + ) -> data_provider::Result<Vec<VoterOf<Self>>> { + let voters = Self::get_npos_voters(bounds, &SnapshotStatus::Waiting); + log!( + info, + "[stateless, status {:?}, bounds {:?}] generated {} npos voters", + VoterSnapshotStatus::<T>::get(), + bounds, + voters.len(), + ); + Ok(voters) + } + fn electable_targets( bounds: DataProviderBounds, page: PageIndex, @@ -2295,7 +2306,6 @@ impl<T: Config> Pallet<T> { /// /// - `NextElectionPage`: should only be set if pages > 1 and if we are within `pages-election /// -> election` - /// - `ElectableStashes`: should only be set in `pages-election -> election block` /// - `VoterSnapshotStatus`: cannot be argued about as we don't know when we get a call to data /// provider, but we know it should never be set if we have 1 page. /// @@ -2304,13 +2314,6 @@ impl<T: Config> Pallet<T> { let next_election = Self::next_election_prediction(now); let pages = Self::election_pages().saturated_into::<BlockNumberFor<T>>(); let election_prep_start = next_election - pages; - let is_mid_election = now >= election_prep_start && now < next_election; - if !is_mid_election { - ensure!( - ElectableStashes::<T>::get().is_empty(), - "ElectableStashes should not be empty mid election" - ); - } if pages > One::one() && now >= election_prep_start { ensure!( diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 200d4e2fd8d..152d623330e 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -5625,7 +5625,7 @@ mod election_data_provider { fn estimate_next_election_single_page_works() { ExtBuilder::default().session_per_era(5).period(5).build_and_execute(|| { // first session is always length 0. - for b in 1..20 { + for b in 1..19 { run_to_block(b); assert_eq!(Staking::next_election_prediction(System::block_number()), 20); } @@ -5635,7 +5635,7 @@ mod election_data_provider { assert_eq!(Staking::next_election_prediction(System::block_number()), 45); assert_eq!(*staking_events().last().unwrap(), Event::StakersElected); - for b in 21..45 { + for b in 21..44 { run_to_block(b); assert_eq!(Staking::next_election_prediction(System::block_number()), 45); } -- GitLab