diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index b2efcb196787d..cb7ee7c2277d6 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -24,7 +24,8 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_election_provider_support::{ - onchain, BalancingConfig, ElectionDataProvider, SequentialPhragmen, VoteWeight, + onchain, onchain::TruncateIntoBoundedSupportsOf, BalancingConfig, ElectionDataProvider, + SequentialPhragmen, TruncateIntoBoundedSupports, VoteWeight, }; use frame_support::{ construct_runtime, @@ -658,6 +659,10 @@ impl onchain::Config for OnChainSeqPhragmen { >; type DataProvider = ::DataProvider; type WeightInfo = frame_election_provider_support::weights::SubstrateWeight; + + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl onchain::BoundedConfig for OnChainSeqPhragmen { @@ -711,6 +716,8 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type ForceOrigin = EnsureRootOrHalfCouncil; type MaxElectableTargets = ConstU16<{ u16::MAX }>; type MaxElectingVoters = MaxElectingVoters; + // TODO what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; type BenchmarkingConfig = ElectionProviderBenchmarkConfig; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; } diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 5677eb7e28e49..867c2507410b2 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -19,7 +19,9 @@ use crate::{self as pallet_babe, Config, CurrentSlot}; use codec::Encode; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + onchain, onchain::TruncateIntoBoundedSupportsOf, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU128, ConstU32, ConstU64, GenesisBuild, KeyOwnerProofSystem, OnInitialize}, @@ -178,6 +180,9 @@ impl onchain::Config for OnChainSeqPhragmen { type Solver = SequentialPhragmen; type DataProvider = Staking; type WeightInfo = (); + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl pallet_staking::Config for Test { diff --git a/frame/election-provider-multi-phase/src/benchmarking.rs b/frame/election-provider-multi-phase/src/benchmarking.rs index a8195df7305ff..0f6b715c0330d 100644 --- a/frame/election-provider-multi-phase/src/benchmarking.rs +++ b/frame/election-provider-multi-phase/src/benchmarking.rs @@ -310,7 +310,7 @@ frame_benchmarking::benchmarks! { assert!(>::get().is_some()); assert!(>::get().is_some()); }: { - assert_ok!( as ElectionProvider>::elect()); + assert!( as ElectionProvider>::elect().is_ok()); } verify { assert!(>::queued_solution().is_none()); assert!(>::get().is_none()); @@ -404,7 +404,8 @@ frame_benchmarking::benchmarks! { assert_eq!(raw_solution.solution.voter_count() as u32, a); assert_eq!(raw_solution.solution.unique_targets().len() as u32, d); }: { - assert_ok!(>::feasibility_check(raw_solution, ElectionCompute::Unsigned)); + // TODO @ggwpez Why does format! work but not assert_ok? + assert!(>::feasibility_check(raw_solution, ElectionCompute::Unsigned).is_ok()); } // NOTE: this weight is not used anywhere, but the fact that it should succeed when execution in diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index e1d3cb8ed5dee..d79498d443975 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -229,15 +229,17 @@ #![cfg_attr(not(feature = "std"), no_std)] -use codec::{Decode, Encode}; +use codec::{Decode, Encode, MaxEncodedLen}; use frame_election_provider_support::{ - ElectionDataProvider, ElectionProvider, InstantElectionProvider, NposSolution, + BoundedSupport, BoundedSupports, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, + InstantElectionProvider, NposSolution, }; use frame_support::{ dispatch::DispatchResultWithPostInfo, ensure, traits::{Currency, Get, OnUnbalanced, ReservableCurrency}, weights::{DispatchClass, Weight}, + DefaultNoBound, PartialEqNoBound, }; use frame_system::{ensure_none, offchain::SendTransactionTypes}; use scale_info::TypeInfo; @@ -249,6 +251,7 @@ use sp_npos_elections::{ assignment_ratio_to_staked_normalized, ElectionScore, EvaluateSupport, Supports, VoteWeight, }; use sp_runtime::{ + traits::Convert, transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction, @@ -317,15 +320,16 @@ impl ElectionProvider for NoFallback { type BlockNumber = T::BlockNumber; type DataProvider = T::DataProvider; type Error = &'static str; + type MaxBackersPerWinner = T::MaxBackersPerWinner; - fn elect() -> Result, Self::Error> { + fn elect() -> Result, Self::Error> { // Do nothing, this will enable the emergency phase. Err("NoFallback.") } } impl InstantElectionProvider for NoFallback { - fn elect_with_bounds(_: usize, _: usize) -> Result, Self::Error> { + fn elect_with_bounds(_: usize, _: usize) -> Result, Self::Error> { Err("NoFallback.") } } @@ -393,7 +397,7 @@ impl Phase { } /// The type of `Computation` that provided this election data. -#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, Debug, TypeInfo)] +#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, Debug, MaxEncodedLen, TypeInfo)] pub enum ElectionCompute { /// Election was computed on-chain. OnChain, @@ -437,13 +441,14 @@ impl Default for RawSolution { } /// A checked solution, ready to be enacted. -#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default, TypeInfo)] -pub struct ReadySolution { +#[derive(PartialEqNoBound, Clone, Encode, Decode, RuntimeDebug, DefaultNoBound, TypeInfo)] +#[scale_info(skip_type_params(MaxBackersPerWinner))] +pub struct ReadySolution> { /// The final supports of the solution. /// /// This is target-major vector, storing each winners, total backing, and each individual /// backer. - pub supports: Supports, + pub supports: BoundedSupports, /// The score of the solution. /// /// This is needed to potentially challenge the solution. @@ -452,6 +457,31 @@ pub struct ReadySolution { pub compute: ElectionCompute, } +/// Try to convert the supports of a solution into a [`ReadySolution`]. +/// +/// Errors if any of the supports has more than [`MaxBackersPerWinner`] backers. +impl> + TryFrom<(Supports, ElectionScore, ElectionCompute)> + for ReadySolution +{ + type Error = &'static str; + + fn try_from( + supports: (Supports, ElectionScore, ElectionCompute), + ) -> Result { + let mut bounded_supports = BoundedSupports::default(); + for (winner, backing) in supports.0.into_iter() { + bounded_supports.push((winner, backing.try_into()?)); + } + + Ok(Self { supports: bounded_supports, score: supports.1, compute: supports.2 }) + } +} + +/// Convenience wrapper to create a [`ReadySolution`] from a [`ElectionProvider`]. +pub type ReadySolutionOf = + ReadySolution<::AccountId, ::MaxBackersPerWinner>; + /// A snapshot of all the data that is needed for en entire round. They are provided by /// [`ElectionDataProvider`] and are kept around until the round is finished. /// @@ -551,6 +581,8 @@ pub enum FeasibilityError { InvalidRound, /// Comparison against `MinimumUntrustedScore` failed. UntrustedScoreTooLow, + /// There are too many backers. + TooManyBackers, } impl From for FeasibilityError { @@ -681,6 +713,7 @@ pub mod pallet { AccountId = Self::AccountId, BlockNumber = Self::BlockNumber, DataProvider = Self::DataProvider, + MaxBackersPerWinner = Self::MaxBackersPerWinner, >; /// Configuration of the governance-only fallback. @@ -691,11 +724,16 @@ pub mod pallet { AccountId = Self::AccountId, BlockNumber = Self::BlockNumber, DataProvider = Self::DataProvider, + MaxBackersPerWinner = Self::MaxBackersPerWinner, >; /// OCW election solution miner algorithm implementation. type Solver: NposSolver; + /// Maximum number of backers per winner that this pallet should, as its implementation of + /// `ElectionProvider` return. + type MaxBackersPerWinner: Get; + /// Origin that can control this pallet. Note that any action taken by this origin (such) /// as providing an emergency solution is not checked. Thus, it must be a trusted origin. type ForceOrigin: EnsureOrigin; @@ -942,11 +980,10 @@ pub mod pallet { // Note: we don't `rotate_round` at this point; the next call to // `ElectionProvider::elect` will succeed and take care of that. - let solution = ReadySolution { - supports, - score: Default::default(), - compute: ElectionCompute::Emergency, - }; + let solution: ReadySolution<_, _> = + (supports, Default::default(), ElectionCompute::Emergency) + .try_into() + .map_err(|_| Error::::TooManyBackersPerWinner)?; Self::deposit_event(Event::SolutionStored { election_compute: ElectionCompute::Emergency, @@ -1122,6 +1159,8 @@ pub mod pallet { CallNotAllowed, /// The fallback failed FallbackFailed, + /// The solution has too many backers per winner. + TooManyBackersPerWinner, } #[pallet::validate_unsigned] @@ -1195,7 +1234,7 @@ pub mod pallet { /// Current best solution, signed or unsigned, queued to be returned upon `elect`. #[pallet::storage] #[pallet::getter(fn queued_solution)] - pub type QueuedSolution = StorageValue<_, ReadySolution>; + pub type QueuedSolution = StorageValue<_, ReadySolutionOf>>; /// Snapshot data of the round. /// @@ -1429,7 +1468,7 @@ impl Pallet { pub fn feasibility_check( raw_solution: RawSolution>, compute: ElectionCompute, - ) -> Result, FeasibilityError> { + ) -> Result, FeasibilityError> { let RawSolution { solution, score, round } = raw_solution; // First, check round. @@ -1502,7 +1541,9 @@ impl Pallet { let known_score = supports.evaluate(); ensure!(known_score == score, FeasibilityError::InvalidScore); - Ok(ReadySolution { supports, compute, score }) + (supports, score, compute) + .try_into() + .map_err(|_| FeasibilityError::TooManyBackers) } /// Perform the tasks to be done after a new `elect` has been triggered: @@ -1521,7 +1562,7 @@ impl Pallet { Self::kill_snapshot(); } - fn do_elect() -> Result, ElectionError> { + fn do_elect() -> Result, ElectionError> { // We have to unconditionally try finalizing the signed phase here. There are only two // possibilities: // @@ -1556,7 +1597,7 @@ impl Pallet { } /// record the weight of the given `supports`. - fn weigh_supports(supports: &Supports) { + fn weigh_supports(supports: &BoundedSupportsOf) { let active_voters = supports .iter() .map(|(_, x)| x) @@ -1570,14 +1611,16 @@ impl ElectionProvider for Pallet { type AccountId = T::AccountId; type BlockNumber = T::BlockNumber; type Error = ElectionError; + type MaxBackersPerWinner = T::MaxBackersPerWinner; type DataProvider = T::DataProvider; - fn elect() -> Result, Self::Error> { + fn elect() -> Result, Self::Error> { match Self::do_elect() { Ok(supports) => { // All went okay, record the weight, put sign to be Off, clean snapshot, etc. Self::weigh_supports(&supports); Self::rotate_round(); + Ok(supports) }, Err(why) => { @@ -1807,8 +1850,8 @@ mod tests { Phase, }; use frame_election_provider_support::ElectionProvider; - use frame_support::{assert_noop, assert_ok}; - use sp_npos_elections::{BalancingConfig, Support}; + use frame_support::{assert_noop, assert_ok, bounded_vec}; + use sp_npos_elections::BalancingConfig; #[test] fn phase_rotation_works() { @@ -2030,8 +2073,20 @@ mod tests { assert_eq!( supports, vec![ - (30, Support { total: 40, voters: vec![(2, 5), (4, 5), (30, 30)] }), - (40, Support { total: 60, voters: vec![(2, 5), (3, 10), (4, 5), (40, 40)] }) + ( + 30, + BoundedSupport { + total: 40, + voters: bounded_vec![(2, 5), (4, 5), (30, 30)] + } + ), + ( + 40, + BoundedSupport { + total: 60, + voters: bounded_vec![(2, 5), (3, 10), (4, 5), (40, 40)] + } + ) ] ) }); diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index 7eff70b47eba5..fcaf0608a94ca 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -19,8 +19,8 @@ use super::*; use crate::{self as multi_phase, unsigned::MinerConfig}; use frame_election_provider_support::{ data_provider, - onchain::{self, UnboundedExecution}, - ElectionDataProvider, NposSolution, SequentialPhragmen, + onchain::{self, TruncateIntoBoundedSupportsOf, UnboundedExecution}, + ElectionDataProvider, NposSolution, SequentialPhragmen, TruncateIntoBoundedSupports, }; pub use frame_support::{assert_noop, assert_ok, pallet_prelude::GetDefault}; use frame_support::{ @@ -44,7 +44,7 @@ use sp_npos_elections::{ }; use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + traits::{BlakeTwo256, Convert, IdentityLookup}, PerU16, }; use std::sync::Arc; @@ -292,6 +292,9 @@ impl onchain::Config for OnChainSeqPhragmen { type Solver = SequentialPhragmen, Balancing>; type DataProvider = StakingMock; type WeightInfo = (); + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } pub struct MockFallback; @@ -300,8 +303,9 @@ impl ElectionProvider for MockFallback { type BlockNumber = u64; type Error = &'static str; type DataProvider = StakingMock; + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; - fn elect() -> Result, Self::Error> { + fn elect() -> Result, Self::Error> { Self::elect_with_bounds(Bounded::max_value(), Bounded::max_value()) } } @@ -310,7 +314,7 @@ impl InstantElectionProvider for MockFallback { fn elect_with_bounds( max_voters: usize, max_targets: usize, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { if OnChainFallback::get() { onchain::UnboundedExecution::::elect_with_bounds( max_voters, @@ -385,6 +389,8 @@ impl crate::Config for Runtime { type ForceOrigin = frame_system::EnsureRoot; type MaxElectingVoters = MaxElectingVoters; type MaxElectableTargets = MaxElectableTargets; + // TODO what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; type MinerConfig = Self; type Solver = SequentialPhragmen, Balancing>; } diff --git a/frame/election-provider-multi-phase/src/signed.rs b/frame/election-provider-multi-phase/src/signed.rs index eca75139f925a..5e0d7574cc027 100644 --- a/frame/election-provider-multi-phase/src/signed.rs +++ b/frame/election-provider-multi-phase/src/signed.rs @@ -19,7 +19,7 @@ use crate::{ unsigned::MinerConfig, Config, ElectionCompute, Pallet, QueuedSolution, RawSolution, - ReadySolution, SignedSubmissionIndices, SignedSubmissionNextIndex, SignedSubmissionsMap, + ReadySolutionOf, SignedSubmissionIndices, SignedSubmissionNextIndex, SignedSubmissionsMap, SolutionOf, SolutionOrSnapshotSize, Weight, WeightInfo, }; use codec::{Decode, Encode, HasCompact}; @@ -451,7 +451,7 @@ impl Pallet { /// /// Infallible pub fn finalize_signed_phase_accept_solution( - ready_solution: ReadySolution, + ready_solution: ReadySolutionOf, who: &T::AccountId, deposit: BalanceOf, call_fee: BalanceOf, diff --git a/frame/election-provider-multi-phase/src/unsigned.rs b/frame/election-provider-multi-phase/src/unsigned.rs index de25355f0ca5b..be725a3f31c56 100644 --- a/frame/election-provider-multi-phase/src/unsigned.rs +++ b/frame/election-provider-multi-phase/src/unsigned.rs @@ -19,7 +19,7 @@ use crate::{ helpers, Call, Config, ElectionCompute, Error, FeasibilityError, Pallet, RawSolution, - ReadySolution, RoundSnapshot, SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight, + ReadySolutionOf, RoundSnapshot, SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight, }; use codec::Encode; use frame_election_provider_support::{NposSolution, NposSolver, PerThing128, VoteWeight}; @@ -351,7 +351,7 @@ impl Pallet { // ensure score is being improved. Panic henceforth. ensure!( - Self::queued_solution().map_or(true, |q: ReadySolution<_>| raw_solution + Self::queued_solution().map_or(true, |q: ReadySolutionOf| raw_solution .score .strict_threshold_better(q.score, T::BetterUnsignedThreshold::get())), Error::::PreDispatchWeakSubmission, @@ -873,7 +873,7 @@ mod tests { assert!(::pre_dispatch(&call).is_ok()); // set a better score - let ready = ReadySolution { + let ready = crate::ReadySolution { score: ElectionScore { minimal_stake: 10, ..Default::default() }, ..Default::default() }; diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index eee865d0b737b..61a063c4fa91f 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -168,7 +168,9 @@ pub mod onchain; pub mod traits; -use sp_runtime::traits::{Bounded, Saturating, Zero}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::ConstU32; +use sp_runtime::traits::{Bounded, Convert, Saturating, Zero}; use sp_std::{fmt::Debug, prelude::*}; /// Re-export the solution generation macro. @@ -356,7 +358,7 @@ pub trait ElectionDataProvider { /// implemented of this trait through [`ElectionProvider::DataProvider`]. pub trait ElectionProvider { /// The account identifier type. - type AccountId; + type AccountId: Eq + Clone; /// The block number type. type BlockNumber; @@ -370,6 +372,12 @@ pub trait ElectionProvider { BlockNumber = Self::BlockNumber, >; + /// Maximum number of backers that each winner will have in any call to + /// [`ElectionProvider::elect`]. + /// + /// It is up to any particular implementation to know how it is going to achieve this property. + type MaxBackersPerWinner: Get; + /// Elect a new set of winners, without specifying any bounds on the amount of data fetched from /// [`Self::DataProvider`]. An implementation could nonetheless impose its own custom limits. /// @@ -377,7 +385,7 @@ pub trait ElectionProvider { /// /// This should be implemented as a self-weighing function. The implementor should register its /// appropriate weight at the end of execution with the system pallet directly. - fn elect() -> Result, Self::Error>; + fn elect() -> Result, Self::Error>; } /// A sub-trait of the [`ElectionProvider`] for cases where we need to be sure an election needs to @@ -399,7 +407,7 @@ pub trait InstantElectionProvider: ElectionProvider { fn elect_with_bounds( max_voters: usize, max_targets: usize, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; } /// An election provider to be used only for testing. @@ -410,14 +418,16 @@ pub struct NoElection(sp_std::marker::PhantomData); impl ElectionProvider for NoElection<(AccountId, BlockNumber, DataProvider)> where + AccountId: Eq + Clone, DataProvider: ElectionDataProvider, { type AccountId = AccountId; type BlockNumber = BlockNumber; type Error = &'static str; type DataProvider = DataProvider; + type MaxBackersPerWinner = ConstU32<0>; - fn elect() -> Result, Self::Error> { + fn elect() -> Result, Self::Error> { Err(" cannot do anything.") } } @@ -618,3 +628,99 @@ pub type Voter = (AccountId, VoteWeight, BoundedVec = Voter<::AccountId, ::MaxVotesPerVoter>; + +/// A bounded equivalent to [`sp_npos_elections::Support`]. +#[derive(Default, RuntimeDebug, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)] +#[codec(mel_bound(AccountId: MaxEncodedLen, Bound: Get))] +#[scale_info(skip_type_params(Bound))] +pub struct BoundedSupport> { + /// Total support. + pub total: ExtendedBalance, + /// Support from voters. + pub voters: BoundedVec<(AccountId, ExtendedBalance), Bound>, +} + +pub type BoundedSupportOf = BoundedSupport< + ::AccountId, + ::MaxBackersPerWinner, +>; + +pub type BoundedSupports = + Vec<(AccountId, BoundedSupport)>; + +/// Convenience to create a [`BoundedSupport`] from a [`ElectionProvider`]. +pub type BoundedSupportsOf = BoundedSupports< + ::AccountId, + ::MaxBackersPerWinner, +>; + +impl> sp_npos_elections::Backings for &BoundedSupport { + fn total(&self) -> ExtendedBalance { + self.total + } +} + +impl> PartialEq for BoundedSupport { + fn eq(&self, other: &Self) -> bool { + self.total == other.total && self.voters == other.voters + } +} +impl> Eq for BoundedSupport {} + +impl> Clone for BoundedSupport { + fn clone(&self) -> Self { + Self { voters: self.voters.clone(), total: self.total } + } +} + +impl> From> for Support { + fn from(b: BoundedSupport) -> Self { + Support { total: b.total, voters: b.voters.into_inner() } + } +} + +impl> TryFrom> + for BoundedSupport +{ + type Error = &'static str; + fn try_from(s: sp_npos_elections::Support) -> Result { + let voters = s.voters.try_into().map_err(|_| "voters bound not respected")?; + Ok(Self { voters, total: s.total }) + } +} + +pub struct SortIntoBoundedSupports(sp_std::marker::PhantomData); + +impl Convert, BoundedSupportsOf> + for SortIntoBoundedSupports +{ + fn convert(a: sp_npos_elections::Supports) -> BoundedSupportsOf { + todo!(); + } +} + +/// Converts an unbounded support into a bounded support by truncating it. +pub struct TruncateIntoBoundedSupports>( + sp_std::marker::PhantomData<(AccountId, MaxBackersPerWinner)>, +); + +impl> + Convert, BoundedSupports> + for TruncateIntoBoundedSupports +{ + fn convert( + a: sp_npos_elections::Supports, + ) -> BoundedSupports { + a.into_iter() + .map(|(who, support)| { + ( + who, + BoundedSupport { + total: support.total, + voters: BoundedVec::truncate_from(support.voters), + }, + ) + }) + .collect() + } +} diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs index 62e76c3888822..c91db46bbf475 100644 --- a/frame/election-provider-support/src/onchain.rs +++ b/frame/election-provider-support/src/onchain.rs @@ -20,10 +20,12 @@ //! careful when using it onchain. use crate::{ - Debug, ElectionDataProvider, ElectionProvider, InstantElectionProvider, NposSolver, WeightInfo, + BoundedSupport, BoundedSupports, BoundedSupportsOf, Debug, ElectionDataProvider, + ElectionProvider, InstantElectionProvider, NposSolver, WeightInfo, }; use frame_support::{traits::Get, weights::DispatchClass}; use sp_npos_elections::*; +use sp_runtime::traits::Convert; use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, prelude::*}; /// Errors of the on-chain election. @@ -68,6 +70,12 @@ pub struct BoundedExecution(PhantomData); /// This can be very expensive to run frequently on-chain. Use with care. pub struct UnboundedExecution(PhantomData); +/// Convenience alias to create a [`TruncateIntoBoundedSupports`] from a [`Config`]. +pub type TruncateIntoBoundedSupportsOf = crate::TruncateIntoBoundedSupports< + <::System as frame_system::Config>::AccountId, + ::MaxBackersPerWinner, +>; + /// Configuration trait for an onchain election execution. pub trait Config { /// Needed for weight registration. @@ -77,6 +85,21 @@ pub trait Config { AccountId = ::AccountId, Error = sp_npos_elections::Error, >; + /// Maximum number of backers per winner to return. + type MaxBackersPerWinner: Get; + + /// Something that can convert the final `Supports` into a bounded version. + type Bounder: Convert< + sp_npos_elections::Supports<::AccountId>, + Vec<( + ::AccountId, + BoundedSupport< + ::AccountId, + Self::MaxBackersPerWinner, + >, + )>, + >; + /// Something that provides the data for election. type DataProvider: ElectionDataProvider< AccountId = ::AccountId, @@ -96,7 +119,10 @@ pub trait BoundedConfig: Config { fn elect_with( maybe_max_voters: Option, maybe_max_targets: Option, -) -> Result::AccountId>, Error> { +) -> Result< + BoundedSupports<::AccountId, T::MaxBackersPerWinner>, + Error, +> { let voters = T::DataProvider::electing_voters(maybe_max_voters).map_err(Error::DataProvider)?; let targets = T::DataProvider::electable_targets(maybe_max_targets).map_err(Error::DataProvider)?; @@ -129,16 +155,19 @@ fn elect_with( DispatchClass::Mandatory, ); - Ok(to_supports(&staked)) + // TODO: not very efficient, but cleaner API. Ideally we would possibly trim while + // `to_supports`. + Ok(to_supports(&staked)).map(|s| T::Bounder::convert(s)) } impl ElectionProvider for UnboundedExecution { type AccountId = ::AccountId; type BlockNumber = ::BlockNumber; type Error = Error; + type MaxBackersPerWinner = T::MaxBackersPerWinner; type DataProvider = T::DataProvider; - fn elect() -> Result, Self::Error> { + fn elect() -> Result, Self::Error> { // This should not be called if not in `std` mode (and therefore neither in genesis nor in // testing) if cfg!(not(feature = "std")) { @@ -156,7 +185,7 @@ impl InstantElectionProvider for UnboundedExecution { fn elect_with_bounds( max_voters: usize, max_targets: usize, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { elect_with::(Some(max_voters), Some(max_targets)) } } @@ -165,9 +194,10 @@ impl ElectionProvider for BoundedExecution { type AccountId = ::AccountId; type BlockNumber = ::BlockNumber; type Error = Error; + type MaxBackersPerWinner = T::MaxBackersPerWinner; type DataProvider = T::DataProvider; - fn elect() -> Result, Self::Error> { + fn elect() -> Result, Self::Error> { elect_with::(Some(T::VotersBound::get() as usize), Some(T::TargetsBound::get() as usize)) } } @@ -176,7 +206,7 @@ impl InstantElectionProvider for BoundedExecution { fn elect_with_bounds( max_voters: usize, max_targets: usize, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { elect_with::( Some(max_voters.min(T::VotersBound::get() as usize)), Some(max_targets.min(T::TargetsBound::get() as usize)), @@ -189,7 +219,6 @@ mod tests { use super::*; use crate::{PhragMMS, SequentialPhragmen}; use frame_support::traits::ConstU32; - use sp_npos_elections::Support; use sp_runtime::Perbill; type AccountId = u64; type BlockNumber = u64; @@ -243,6 +272,9 @@ mod tests { type Solver = SequentialPhragmen; type DataProvider = mock_data_provider::DataProvider; type WeightInfo = (); + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl BoundedConfig for PhragmenParams { @@ -255,6 +287,9 @@ mod tests { type Solver = PhragMMS; type DataProvider = mock_data_provider::DataProvider; type WeightInfo = (); + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl BoundedConfig for PhragMMSParams { @@ -297,12 +332,13 @@ mod tests { #[test] fn onchain_seq_phragmen_works() { + use frame_support::bounded_vec; sp_io::TestExternalities::new_empty().execute_with(|| { assert_eq!( BoundedExecution::::elect().unwrap(), vec![ - (10, Support { total: 25, voters: vec![(1, 10), (3, 15)] }), - (30, Support { total: 35, voters: vec![(2, 20), (3, 15)] }) + (10, BoundedSupport { total: 25, voters: bounded_vec![(1, 10), (3, 15)] }), + (30, BoundedSupport { total: 35, voters: bounded_vec![(2, 20), (3, 15)] }) ] ); }) @@ -310,12 +346,13 @@ mod tests { #[test] fn onchain_phragmms_works() { + use frame_support::bounded_vec; sp_io::TestExternalities::new_empty().execute_with(|| { assert_eq!( BoundedExecution::::elect().unwrap(), vec![ - (10, Support { total: 25, voters: vec![(1, 10), (3, 15)] }), - (30, Support { total: 35, voters: vec![(2, 20), (3, 15)] }) + (10, BoundedSupport { total: 25, voters: bounded_vec![(1, 10), (3, 15)] }), + (30, BoundedSupport { total: 35, voters: bounded_vec![(2, 20), (3, 15)] }) ] ); }) diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index 5e6c955c441c5..24b0100a817b0 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -22,7 +22,9 @@ use crate::{self as pallet_grandpa, AuthorityId, AuthorityList, Config, ConsensusLog}; use ::grandpa as finality_grandpa; use codec::Encode; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + onchain, onchain::TruncateIntoBoundedSupportsOf, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ @@ -182,6 +184,9 @@ impl onchain::Config for OnChainSeqPhragmen { type Solver = SequentialPhragmen; type DataProvider = Staking; type WeightInfo = (); + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl pallet_staking::Config for Test { diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index d51a81b1212c0..6dd5725af1bfa 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -20,7 +20,9 @@ #![cfg(test)] use super::*; -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + onchain, onchain::TruncateIntoBoundedSupportsOf, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU32, ConstU64}, @@ -154,6 +156,10 @@ impl onchain::Config for OnChainSeqPhragmen { type Solver = SequentialPhragmen; type DataProvider = Staking; type WeightInfo = (); + + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl pallet_staking::Config for Test { diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index c777f2c56de3a..69ee71e0d7e52 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -19,7 +19,9 @@ #![cfg(test)] -use frame_election_provider_support::{onchain, SequentialPhragmen}; +use frame_election_provider_support::{ + onchain, onchain::TruncateIntoBoundedSupportsOf, SequentialPhragmen, +}; use frame_support::{ parameter_types, traits::{ConstU32, ConstU64}, @@ -160,6 +162,10 @@ impl onchain::Config for OnChainSeqPhragmen { type Solver = SequentialPhragmen; type DataProvider = Staking; type WeightInfo = (); + + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } impl pallet_staking::Config for Test { diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index bd2d8cdc32ce9..7a7e9bfd643a0 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -19,7 +19,8 @@ use crate::{self as pallet_staking, *}; use frame_election_provider_support::{ - onchain, SequentialPhragmen, SortedListProvider, VoteWeight, + onchain, onchain::TruncateIntoBoundedSupportsOf, SequentialPhragmen, SortedListProvider, + VoteWeight, }; use frame_support::{ assert_ok, parameter_types, @@ -255,6 +256,9 @@ impl onchain::Config for OnChainSeqPhragmen { type Solver = SequentialPhragmen; type DataProvider = Staking; type WeightInfo = (); + // TODO no idea what to use here + type MaxBackersPerWinner = ConstU32<{ u32::MAX }>; + type Bounder = TruncateIntoBoundedSupportsOf; } pub struct MockReward {} diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index 7656eec80a5ff..d1971bb38f58a 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -18,8 +18,8 @@ //! Implementations for the Staking FRAME Pallet. use frame_election_provider_support::{ - data_provider, ElectionDataProvider, ElectionProvider, ScoreProvider, SortedListProvider, - Supports, VoteWeight, VoterOf, + data_provider, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ScoreProvider, + SortedListProvider, VoteWeight, VoterOf, }; use frame_support::{ pallet_prelude::*, @@ -526,7 +526,7 @@ impl Pallet { /// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a /// [`Exposure`]. fn collect_exposures( - supports: Supports, + supports: BoundedSupportsOf, ) -> Vec<(T::AccountId, Exposure>)> { let total_issuance = T::Currency::total_issuance(); let to_currency = |e: frame_election_provider_support::ExtendedBalance| { diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index e53464195de23..f0304d801dd20 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -118,6 +118,7 @@ pub mod pallet { AccountId = Self::AccountId, BlockNumber = Self::BlockNumber, DataProvider = Pallet, + MaxBackersPerWinner = ::MaxBackersPerWinner, >; /// Maximum number of nominations per nominator. diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index b76126f0c5d04..cc63ef33766a1 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -16,9 +16,10 @@ // limitations under the License. //! Tests for the module. +#![cfg(test)] use super::{ConfigOp, Event, MaxUnlockingChunks, *}; -use frame_election_provider_support::{ElectionProvider, SortedListProvider, Support}; +use frame_election_provider_support::{BoundedSupport, ElectionProvider, SortedListProvider}; use frame_support::{ assert_noop, assert_ok, assert_storage_noop, bounded_vec, dispatch::WithPostDispatchInfo, @@ -1963,8 +1964,20 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider() { assert_eq!( supports, vec![ - (21, Support { total: 1800, voters: vec![(21, 1000), (1, 400), (3, 400)] }), - (31, Support { total: 2200, voters: vec![(31, 1000), (1, 600), (3, 600)] }) + ( + 21, + BoundedSupport { + total: 1800, + voters: bounded_vec![(21, 1000), (1, 400), (3, 400)] + } + ), + ( + 31, + BoundedSupport { + total: 2200, + voters: bounded_vec![(31, 1000), (1, 600), (3, 600)] + } + ) ], ); }); @@ -2007,8 +2020,17 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider_elected() { assert_eq!( supports, vec![ - (11, Support { total: 1500, voters: vec![(11, 1000), (1, 500)] }), - (21, Support { total: 2500, voters: vec![(21, 1000), (1, 500), (3, 1000)] }) + ( + 11, + BoundedSupport { total: 1500, voters: bounded_vec![(11, 1000), (1, 500)] } + ), + ( + 21, + BoundedSupport { + total: 2500, + voters: bounded_vec![(21, 1000), (1, 500), (3, 1000)] + } + ) ], ); }); diff --git a/primitives/npos-elections/src/lib.rs b/primitives/npos-elections/src/lib.rs index dd2a9bf198f8d..106b5c7bf81a6 100644 --- a/primitives/npos-elections/src/lib.rs +++ b/primitives/npos-elections/src/lib.rs @@ -444,6 +444,18 @@ impl Default for Support { } } +/// Generic representation of a support. +pub trait Backings { + /// The total backing of an individual winner. + fn total(&self) -> ExtendedBalance; +} + +impl Backings for &Support { + fn total(&self) -> ExtendedBalance { + self.total + } +} + /// A target-major representation of the the election outcome. /// /// Essentially a flat variant of [`SupportMap`]. @@ -494,23 +506,28 @@ pub trait EvaluateSupport { impl EvaluateSupport for Supports { fn evaluate(&self) -> ElectionScore { - let mut minimal_stake = ExtendedBalance::max_value(); - let mut sum_stake: ExtendedBalance = Zero::zero(); - // NOTE: The third element might saturate but fine for now since this will run on-chain and - // need to be fast. - let mut sum_stake_squared: ExtendedBalance = Zero::zero(); - - for (_, support) in self { - sum_stake = sum_stake.saturating_add(support.total); - let squared = support.total.saturating_mul(support.total); - sum_stake_squared = sum_stake_squared.saturating_add(squared); - if support.total < minimal_stake { - minimal_stake = support.total; - } - } + evaluate_support_core(self.iter().map(|(_, s)| s)) + } +} - ElectionScore { minimal_stake, sum_stake, sum_stake_squared } +/// Core implementation of how to evaluate a support (in the most generic form), exported as a +/// free-standing function for easy re-use. +pub fn evaluate_support_core(backings: impl Iterator) -> ElectionScore { + let mut minimal_stake = ExtendedBalance::max_value(); + let mut sum_stake: ExtendedBalance = Zero::zero(); + // NOTE: The third element might saturate but fine for now since this will run on-chain and + // need to be fast. + let mut sum_stake_squared: ExtendedBalance = Zero::zero(); + + for backing in backings { + sum_stake = sum_stake.saturating_add(backing.total()); + let squared = backing.total().saturating_mul(backing.total()); + sum_stake_squared = sum_stake_squared.saturating_add(squared); + if backing.total() < minimal_stake { + minimal_stake = backing.total(); + } } + ElectionScore { minimal_stake, sum_stake, sum_stake_squared } } /// Converts raw inputs to types used in this crate.