referrerpolicy=no-referrer-when-downgrade

frame_election_provider_support/
onchain.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! An implementation of [`ElectionProvider`] that uses an `NposSolver` to do the election. As the
19//! name suggests, this is meant to be used onchain. Given how heavy the calculations are, please be
20//! careful when using it onchain.
21
22use crate::{
23	bounds::{ElectionBounds, ElectionBoundsBuilder},
24	BoundedSupportsOf, Debug, ElectionDataProvider, ElectionProvider, InstantElectionProvider,
25	NposSolver, PageIndex, VoterOf, WeightInfo,
26};
27use alloc::{collections::btree_map::BTreeMap, vec::Vec};
28use core::marker::PhantomData;
29use frame_support::{dispatch::DispatchClass, traits::Get};
30use frame_system::pallet_prelude::BlockNumberFor;
31use sp_npos_elections::{
32	assignment_ratio_to_staked_normalized, to_supports, ElectionResult, VoteWeight,
33};
34
35/// Errors of the on-chain election.
36#[derive(Eq, PartialEq, Debug, Clone)]
37pub enum Error {
38	/// An internal error in the NPoS elections crate.
39	NposElections(sp_npos_elections::Error),
40	/// Errors from the data provider.
41	DataProvider(&'static str),
42	/// Results failed to meet the bounds.
43	FailedToBound,
44}
45
46impl From<sp_npos_elections::Error> for Error {
47	fn from(e: sp_npos_elections::Error) -> Self {
48		Error::NposElections(e)
49	}
50}
51
52/// A simple on-chain implementation of the election provider trait.
53///
54/// This implements both `ElectionProvider` and `InstantElectionProvider`.
55///
56/// This type has some utilities to make it safe. Nonetheless, it should be used with utmost care. A
57/// thoughtful value must be set as [`Config::Bounds`] to ensure the size of the input is sensible.
58pub struct OnChainExecution<T: Config>(PhantomData<T>);
59
60#[deprecated(note = "use OnChainExecution, which is bounded by default")]
61pub type BoundedExecution<T> = OnChainExecution<T>;
62
63/// Configuration trait for an onchain election execution.
64pub trait Config {
65	/// Whether to try and sort or not.
66	///
67	/// If `true`, the supports will be sorted by descending total support to meet the bounds. If
68	/// `false`, `FailedToBound` error may be returned.
69	type Sort: Get<bool>;
70
71	/// Needed for weight registration.
72	type System: frame_system::Config;
73
74	/// `NposSolver` that should be used, an example would be `PhragMMS`.
75	type Solver: NposSolver<
76		AccountId = <Self::System as frame_system::Config>::AccountId,
77		Error = sp_npos_elections::Error,
78	>;
79
80	/// Maximum number of backers allowed per target.
81	///
82	/// If the bounds are exceeded due to the data returned by the data provider, the election will
83	/// fail.
84	type MaxBackersPerWinner: Get<u32>;
85
86	/// Maximum number of winners in an election.
87	///
88	/// If the bounds are exceeded due to the data returned by the data provider, the election will
89	/// fail.
90	type MaxWinnersPerPage: Get<u32>;
91
92	/// Something that provides the data for election.
93	type DataProvider: ElectionDataProvider<
94		AccountId = <Self::System as frame_system::Config>::AccountId,
95		BlockNumber = frame_system::pallet_prelude::BlockNumberFor<Self::System>,
96	>;
97
98	/// Weight information for extrinsics in this pallet.
99	type WeightInfo: WeightInfo;
100
101	/// Elections bounds, to use when calling into [`Config::DataProvider`]. It might be overwritten
102	/// in the `InstantElectionProvider` impl.
103	type Bounds: Get<ElectionBounds>;
104}
105
106impl<T: Config> OnChainExecution<T> {
107	fn elect_with_snapshot(
108		voters: Vec<VoterOf<T::DataProvider>>,
109		targets: Vec<<T::System as frame_system::Config>::AccountId>,
110		desired_targets: u32,
111	) -> Result<BoundedSupportsOf<Self>, Error> {
112		if (desired_targets > T::MaxWinnersPerPage::get()) && !T::Sort::get() {
113			// early exit what will fail in the last line anyways.
114			return Err(Error::FailedToBound)
115		}
116
117		let voters_len = voters.len() as u32;
118		let targets_len = targets.len() as u32;
119
120		let stake_map: BTreeMap<_, _> = voters
121			.iter()
122			.map(|(validator, vote_weight, _)| (validator.clone(), *vote_weight))
123			.collect();
124
125		let stake_of = |w: &<T::System as frame_system::Config>::AccountId| -> VoteWeight {
126			stake_map.get(w).cloned().unwrap_or_default()
127		};
128
129		let ElectionResult { winners: _, assignments } =
130			T::Solver::solve(desired_targets as usize, targets, voters).map_err(Error::from)?;
131
132		let staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)?;
133
134		let weight = T::Solver::weight::<T::WeightInfo>(
135			voters_len,
136			targets_len,
137			<T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get(),
138		);
139		frame_system::Pallet::<T::System>::register_extra_weight_unchecked(
140			weight,
141			DispatchClass::Mandatory,
142		);
143
144		let unbounded = to_supports(&staked);
145		let bounded = if T::Sort::get() {
146			let (bounded, _winners_removed, _backers_removed) =
147				BoundedSupportsOf::<Self>::sorted_truncate_from(unbounded);
148			bounded
149		} else {
150			unbounded.try_into().map_err(|_| Error::FailedToBound)?
151		};
152		Ok(bounded)
153	}
154
155	fn elect_with(
156		bounds: ElectionBounds,
157		page: PageIndex,
158	) -> Result<BoundedSupportsOf<Self>, Error> {
159		let (voters, targets) = T::DataProvider::electing_voters(bounds.voters, page)
160			.and_then(|voters| {
161				Ok((voters, T::DataProvider::electable_targets(bounds.targets, page)?))
162			})
163			.map_err(Error::DataProvider)?;
164		let desired_targets = T::DataProvider::desired_targets().map_err(Error::DataProvider)?;
165		Self::elect_with_snapshot(voters, targets, desired_targets)
166	}
167}
168
169impl<T: Config> InstantElectionProvider for OnChainExecution<T> {
170	fn instant_elect(
171		voters: Vec<VoterOf<T::DataProvider>>,
172		targets: Vec<<T::System as frame_system::Config>::AccountId>,
173		desired_targets: u32,
174	) -> Result<BoundedSupportsOf<Self>, Self::Error> {
175		Self::elect_with_snapshot(voters, targets, desired_targets)
176	}
177
178	fn bother() -> bool {
179		true
180	}
181}
182
183impl<T: Config> ElectionProvider for OnChainExecution<T> {
184	type AccountId = <T::System as frame_system::Config>::AccountId;
185	type BlockNumber = BlockNumberFor<T::System>;
186	type Error = Error;
187	type MaxWinnersPerPage = T::MaxWinnersPerPage;
188	type MaxBackersPerWinnerFinal = T::MaxWinnersPerPage;
189	type MaxBackersPerWinner = T::MaxBackersPerWinner;
190	// can support any number of pages, as this is meant to be called "instantly". We don't care
191	// about this value here.
192	type Pages = sp_core::ConstU32<1>;
193	type DataProvider = T::DataProvider;
194
195	fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> {
196		let election_bounds = ElectionBoundsBuilder::from(T::Bounds::get()).build();
197		Self::elect_with(election_bounds, page)
198	}
199
200	fn start() -> Result<(), Self::Error> {
201		// noop, we are always ready!
202		Ok(())
203	}
204
205	fn duration() -> Self::BlockNumber {
206		sp_runtime::traits::Zero::zero()
207	}
208
209	fn status() -> Result<bool, ()> {
210		Ok(true)
211	}
212}
213
214#[cfg(test)]
215mod tests {
216	use super::*;
217	use crate::{ElectionProvider, PhragMMS, SequentialPhragmen};
218	use frame_support::{assert_noop, derive_impl, parameter_types};
219	use sp_io::TestExternalities;
220	use sp_npos_elections::Support;
221	use sp_runtime::Perbill;
222	type AccountId = u64;
223	type Nonce = u64;
224	type BlockNumber = u64;
225
226	pub type Header = sp_runtime::generic::Header<BlockNumber, sp_runtime::traits::BlakeTwo256>;
227	pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic<AccountId, (), (), ()>;
228	pub type Block = sp_runtime::generic::Block<Header, UncheckedExtrinsic>;
229
230	frame_support::construct_runtime!(
231		pub enum Runtime {
232			System: frame_system,
233		}
234	);
235
236	#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
237	impl frame_system::Config for Runtime {
238		type SS58Prefix = ();
239		type BaseCallFilter = frame_support::traits::Everything;
240		type RuntimeOrigin = RuntimeOrigin;
241		type Nonce = Nonce;
242		type RuntimeCall = RuntimeCall;
243		type Hash = sp_core::H256;
244		type Hashing = sp_runtime::traits::BlakeTwo256;
245		type AccountId = AccountId;
246		type Lookup = sp_runtime::traits::IdentityLookup<Self::AccountId>;
247		type Block = Block;
248		type RuntimeEvent = ();
249		type BlockHashCount = ();
250		type DbWeight = ();
251		type BlockLength = ();
252		type BlockWeights = ();
253		type Version = ();
254		type PalletInfo = PalletInfo;
255		type AccountData = ();
256		type OnNewAccount = ();
257		type OnKilledAccount = ();
258		type SystemWeightInfo = ();
259		type OnSetCode = ();
260		type MaxConsumers = frame_support::traits::ConstU32<16>;
261	}
262
263	struct PhragmenParams;
264	struct PhragMMSParams;
265
266	parameter_types! {
267		pub static MaxWinnersPerPage: u32 = 10;
268		pub static MaxBackersPerWinner: u32 = 20;
269		pub static DesiredTargets: u32 = 2;
270		pub static Sort: bool = false;
271		pub static Bounds: ElectionBounds = ElectionBoundsBuilder::default().voters_count(600.into()).targets_count(400.into()).build();
272	}
273
274	impl Config for PhragmenParams {
275		type Sort = Sort;
276		type System = Runtime;
277		type Solver = SequentialPhragmen<AccountId, Perbill>;
278		type DataProvider = mock_data_provider::DataProvider;
279		type MaxWinnersPerPage = MaxWinnersPerPage;
280		type MaxBackersPerWinner = MaxBackersPerWinner;
281		type Bounds = Bounds;
282		type WeightInfo = ();
283	}
284
285	impl Config for PhragMMSParams {
286		type Sort = Sort;
287		type System = Runtime;
288		type Solver = PhragMMS<AccountId, Perbill>;
289		type DataProvider = mock_data_provider::DataProvider;
290		type MaxWinnersPerPage = MaxWinnersPerPage;
291		type MaxBackersPerWinner = MaxBackersPerWinner;
292		type WeightInfo = ();
293		type Bounds = Bounds;
294	}
295
296	mod mock_data_provider {
297		use super::*;
298		use crate::{data_provider, DataProviderBounds, PageIndex, VoterOf};
299		use frame_support::traits::ConstU32;
300		use sp_runtime::bounded_vec;
301
302		pub struct DataProvider;
303		impl ElectionDataProvider for DataProvider {
304			type AccountId = AccountId;
305			type BlockNumber = BlockNumber;
306			type MaxVotesPerVoter = ConstU32<2>;
307			fn electing_voters(
308				_: DataProviderBounds,
309				_page: PageIndex,
310			) -> data_provider::Result<Vec<VoterOf<Self>>> {
311				Ok(vec![
312					(1, 10, bounded_vec![10, 20]),
313					(2, 20, bounded_vec![30, 20]),
314					(3, 30, bounded_vec![10, 30]),
315				])
316			}
317
318			fn electable_targets(
319				_: DataProviderBounds,
320				_page: PageIndex,
321			) -> data_provider::Result<Vec<AccountId>> {
322				Ok(vec![10, 20, 30])
323			}
324
325			fn desired_targets() -> data_provider::Result<u32> {
326				Ok(DesiredTargets::get())
327			}
328
329			fn next_election_prediction(_: BlockNumber) -> BlockNumber {
330				0
331			}
332		}
333	}
334
335	#[test]
336	fn onchain_seq_phragmen_works() {
337		TestExternalities::new_empty().execute_with(|| {
338			let expected_supports = vec![
339				(
340					10 as AccountId,
341					Support { total: 25, voters: vec![(1 as AccountId, 10), (3, 15)] },
342				),
343				(30, Support { total: 35, voters: vec![(2, 20), (3, 15)] }),
344			]
345			.try_into()
346			.unwrap();
347
348			assert_eq!(
349				<OnChainExecution::<PhragmenParams> as ElectionProvider>::elect(0).unwrap(),
350				expected_supports,
351			);
352		})
353	}
354
355	#[test]
356	fn sorting_false_works() {
357		TestExternalities::new_empty().execute_with(|| {
358			// Default results would have 3 targets, but we allow for only 2.
359			DesiredTargets::set(3);
360			MaxWinnersPerPage::set(2);
361
362			assert_noop!(
363				<OnChainExecution::<PhragmenParams> as ElectionProvider>::elect(0),
364				Error::FailedToBound,
365			);
366		});
367
368		TestExternalities::new_empty().execute_with(|| {
369			// Default results would have 2 backers per winner
370			MaxBackersPerWinner::set(1);
371
372			assert_noop!(
373				<OnChainExecution::<PhragmenParams> as ElectionProvider>::elect(0),
374				Error::FailedToBound,
375			);
376		});
377	}
378
379	#[test]
380	fn sorting_true_works_winners() {
381		Sort::set(true);
382
383		TestExternalities::new_empty().execute_with(|| {
384			let expected_supports =
385				vec![(30, Support { total: 35, voters: vec![(2, 20), (3, 15)] })]
386					.try_into()
387					.unwrap();
388
389			// we want to allow 1 winner only, and allow sorting.
390			MaxWinnersPerPage::set(1);
391
392			assert_eq!(
393				<OnChainExecution::<PhragmenParams> as ElectionProvider>::elect(0).unwrap(),
394				expected_supports,
395			);
396		});
397
398		MaxWinnersPerPage::set(10);
399
400		TestExternalities::new_empty().execute_with(|| {
401			let expected_supports = vec![
402				(30, Support { total: 20, voters: vec![(2, 20)] }),
403				(10 as AccountId, Support { total: 15, voters: vec![(3 as AccountId, 15)] }),
404			]
405			.try_into()
406			.unwrap();
407
408			// we want to allow 2 winners only but 1 backer each, and allow sorting.
409			MaxBackersPerWinner::set(1);
410
411			assert_eq!(
412				<OnChainExecution::<PhragmenParams> as ElectionProvider>::elect(0).unwrap(),
413				expected_supports,
414			);
415		})
416	}
417
418	#[test]
419	fn onchain_phragmms_works() {
420		TestExternalities::new_empty().execute_with(|| {
421			assert_eq!(
422				<OnChainExecution::<PhragMMSParams> as ElectionProvider>::elect(0).unwrap(),
423				vec![
424					(
425						10 as AccountId,
426						Support { total: 25, voters: vec![(1 as AccountId, 10), (3, 15)] }
427					),
428					(30, Support { total: 35, voters: vec![(2, 20), (3, 15)] })
429				]
430				.try_into()
431				.unwrap()
432			);
433		})
434	}
435}