--- title: Notes on refactoring `pallet-elections-phragmen` to use `ElectionProvider` tags: parity, FRAME, substrate, polkadot, npos, staking --- ## Notes on refactoring `pallet-elections-phragmen` to use `ElectionProvider` - [paritytech/substrate issue - Use ElectionProvider for pallet-elections-phragmen #8250 ](https://github.com/paritytech/substrate/issues/8250) ### 1. High level The `#[pallet::config]` proc macro works over a trait named `Config` that the every pallet must implement. It is where the pallet's developer set the requirements in terms of which external traits must the runtime pallet have as an associate type and/or implement. Note that the the pallet's `Config` should extend (be a super trait of) `frame_system::Config`. It is also possible to constrain the generic types of an associated type in the `Config` definition. For example, in the `pallet-staking` (and in the future in the `pallet-elections-phragmen`), the `Config` traits define the requirement for an associated type `ElectionDataProvider`, which will handle the election logic. E.g. ```rust #[pallet::config] pub trait Config: frame_system::Config { // ... type ElectionProvider: ElectionProvider< AccountId = Self::AccountId, BlockNumber = Self::BlockNumber, DataProvider = Pallet<Self>, >; } ``` In the code above, the pallet's `Config` trait has an associated type that must implement the `frame_election_support::ElectionProvider` trait. We also constrain the allowed types for the the concrete implementor of the `ElectionProvider`. For example, through this definition, we constrain the `ElectionProvider::DataProvider` to be the pallet itself. In practice, this means that the `Pallet` struct must implement the `frame_election_support::DataProvider` and used as the `DataProvider` of the type `Pallet<Config>::ElectionProvider`. In terms of mocking the runtime for testing, we must define the concrete types in the `Config` trait for the testing concrete type that will be our mock pallet. For example: ```rust pub struct Test; // required for all pallets/mock pallets (note that pallet::Config has this trait as a supertrait) impl frame_system::Config for Test { type AccountId = u64; type BlockNumber = u64 } impl Config for Test { // define the concrete types associated with the mock pallet type ElectionProvider: ElectionProviderTest<Test>; } pub struct<T: Config> ElectionProviderTest<T>(PhantomData<_>); impl<T: Config> ElectionProviderBase for ElectionProviderTest<T> { type AccountId = u64; type BlockNumber = u64 type Error = Error<T>; // this type will match the constraints defined in the pallet's config trait // definition, ie.e `DataProvider = Pallet<Self>`, provided that the pallet // implements the DataProvider trait. type DataProvider = Pallet<T>; fn on_going() -> bool { false } } impl<T: Config> ElectionProvider for ElectionProviderTest<T> { fn elect() -> Result<Supports<Self::AccountId>, Self::Error> { //... } } ``` ### 2. `ElectionDataProvider`, `InstantElectionProvider` and `NposSolver` traits and usage The `election_provider_support` crate defines traits that enable implementing and extending generic election logic in FRAME. In addition, the crate also defines a `NposSolver` trait that computes the solution of a NpoS election. The relevant trait definitions in the `election_provider_support` crate are: - `ElectionProviderBase`: Based trait for election provider types. It has an associated type `ElectionDataProvider` that is used internally to fetch the data necessary to calculate the results of the election; - `ElectionProvider`: Supertrait of `ElectionProviderBase`. The associated methods do not provide any way to bound the election (ie. to set the maximum number of voters, votes per voter, candidates and winners). In cases where the election must run on-chain, it is not advisable to use an `ElectionProvider` (use only at genesis or testing). - `BoundedElectionProvider`: Supertrait of `ElectionProviderBase`, similar to `ElectionProvider` but it returns a bounded vector of winners, bound by `Self::MaxWinners`. - `InstantElectionProvider`: Supertrait of `ElectionProvider`, where both the max number of voters and candidates is defined through the associated method `elect_with_bounds(max_voters, max_targets)`. The maximum voters and targets are fetched by the `ElectionDataProvider`. The module `election_provider_support::onchain` implements a couple of concrete `ElectionProvider` s that use the `NposSolver` trait to perform the election. The relevant type implementations are the following: - `onchain::BoundedExecution`: Is an implements of a bounded `ElectionProvider` which extends the `InstantElectionProvider` trait. The type is generic over a `BoundedConfig` trait that requires the a `VoterBound` and `TargetsBound` getter types, which extends the `onchain::Config` trait. - `onchain::UnboundedExecution`: An unbounded variant of `BoundedExecution`. The `onchain::Config` trait is used to define the types and parameters of the `BoundedExection` or `UnboundedExecution` election providers. The most relevant associated types of the trait are the `DataProvider` to run the election and the `NposSolver`, which is used by the types to calculate the solution of the election. The `election_provider_support` also provides the `NposSolver` wrappers of the NpoS algorithms defined in `sp_npos_elections`, namely `np_npos_elections::seq_phragmen()` and `sp_npos_elections::phragmms()`: - `SequentialPhragmen` ```rust pub struct SequentialPhragmen<AccountId, Accuracy, Balancing = ()>( sp_std::marker::PhantomData<(AccountId, Accuracy, Balancing)>, ); ``` - `PhragMMS` ```rust pub struct PhragMMS<AccountId, Accuracy, Balancing = ()>( sp_std::marker::PhantomData<(AccountId, Accuracy, Balancing)>, ); ``` Although an `ElectionProvider` doesn't necessarily need to have or implement a `NposSolver`, arguably it keeps the implementation cleaner with well defined interfaces and responsibilities between the different types at the expense of adding a one more degree of indirection: ie. `ElectionProvider has a DataProvider to fetch election data and it has a NposSolver that performs the election calculation.` The `ElectionProvider` would then work as a glue between the data layer and the execution, controlling the election bounds etc. ### 3. Plan for [paritytech/substrate issue#8250](https://github.com/paritytech/substrate/issues/8250) - [ ] Rename the pallet from `pallet-elections-phragmen` to `pallet-elections-generic` - [ ] Implement the `ElectionDataProvider` trait on the pallet - [ ] Implement the mocks and make sure the current tests pass - [ ] Implement tests on election bounds if more bounds are added in this PR (namely `max_voters` and `max_targets`) - [ ] Implement an `InstantElectionProvider` to be used by the pallet - Option 1. use the `onchain::BoundedExecution` with the `onchain::SequentialPhragmen` NpoS solver. - Option 2. use `ElectionProvider` without `NposSolver` - Option 3. rename and refactor the `do_phragmen()` method currently implemented in the phragmen elections pallet to use a `NposSolver` The advantages of Option 1 are described in the section above: increased readability and cleaner responsibilities concrete types. However, this is subjective, since it increases slightly the complexity of the code by adding the `NposSolver`. In addition, using the `onchain::BoundedExecurtion` trait also make it clear where the election bounds are defined and allow for dynamic bounds in runtime Finally, the implementation of the `pallet-elections-generic` would be closer to that of the `pallet-staking`, which could be used as a convention across the code base. The advantages of Option 3. are simplicity (less machinery to achieve the same goal, ie. abstract the pallet). In addition, it requires less data to be fetched from runtime, which makes the election computation cheaper (although this added computation may not be a deal breaker). Given that the `pallet-elections-generic` may be deprecated in the near future (may depend on section below), simplicity could be the feature that tips the scale to Option 3 -- unless we don't deprecate this pallet. ### 4. Parachains using `pallet-elections-phragmen` The `pallet-staking` has its own logic to handle the NpoS elections, which may deprecate the `pallet-elections-phragmen`. It is important, though, to know if there are any other projects/parachains using the `pallet-elections-phragmen` and whether they have any input/ requirements regarding the refactoring being worked out in [paritytech/substrate issue#8250](https://github.com/paritytech/substrate/issues/8250). - [Moonbean](https://github.com/PureStake/moonbeam) uses their own [staking pallet](https://github.com/PureStake/moonbeam/blob/master/pallets/parachain-staking/src/lib.rs) (non-phagmen based); They do not rely on the `ElectionProvider` or `NposSolver` abstractions. - [Acala](https://github.com/AcalaNetwork/Acala) They do not rely on the `ElectionProvider` or `NposSolver` abstractions. - [Interlay](https://github.com/interlay) - [Parallel](https://github.com/parallel-finance/parallel) - [Astar](https://github.com/AstarNetwork/Astar) - [Phala](https://github.com/Phala-Network/khala-parachain) - [Darwinia Network](https://github.com/darwinia-network/darwinia) - [Clover](https://github.com/clover-network/clover) - [Equilibrium](https://github.com/equilibrium-eosdt) - [Bifrost](https://github.com/bifrost-finance/bifrost) - [Centrifuge](https://github.com/centrifuge/centrifuge-chain)