pallet_scored_pool/lib.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//! # Scored Pool Pallet
19//!
20//! The pallet maintains a scored membership pool. Each entity in the
21//! pool can be attributed a `Score`. From this pool a set `Members`
22//! is constructed. This set contains the `MemberCount` highest
23//! scoring entities. Unscored entities are never part of `Members`.
24//!
25//! If an entity wants to be part of the pool a deposit is required.
26//! The deposit is returned when the entity withdraws or when it
27//! is removed by an entity with the appropriate authority.
28//!
29//! Every `Period` blocks the set of `Members` is refreshed from the
30//! highest scoring members in the pool and, no matter if changes
31//! occurred, `T::MembershipChanged::set_members_sorted` is invoked.
32//! On first load `T::MembershipInitialized::initialize_members` is
33//! invoked with the initial `Members` set.
34//!
35//! It is possible to withdraw candidacy/resign your membership at any
36//! time. If an entity is currently a member, this results in removal
37//! from the `Pool` and `Members`; the entity is immediately replaced
38//! by the next highest scoring candidate in the pool, if available.
39//!
40//! - [`Config`]
41//! - [`Call`]
42//! - [`Pallet`]
43//!
44//! ## Interface
45//!
46//! ### Public Functions
47//!
48//! - `submit_candidacy` - Submit candidacy to become a member. Requires a deposit.
49//! - `withdraw_candidacy` - Withdraw candidacy. Deposit is returned.
50//! - `score` - Attribute a quantitative score to an entity.
51//! - `kick` - Remove an entity from the pool and members. Deposit is returned.
52//! - `change_member_count` - Changes the amount of candidates taken into `Members`.
53//!
54//! ## Usage
55//!
56//! ```
57//! use pallet_scored_pool::{self as scored_pool};
58//!
59//! #[frame_support::pallet]
60//! pub mod pallet {
61//! use super::*;
62//! use frame_support::pallet_prelude::*;
63//! use frame_system::pallet_prelude::*;
64//!
65//! #[pallet::pallet]
66//! pub struct Pallet<T>(_);
67//!
68//! #[pallet::config]
69//! pub trait Config: frame_system::Config + scored_pool::Config {}
70//!
71//! #[pallet::call]
72//! impl<T: Config> Pallet<T> {
73//! #[pallet::weight({0})]
74//! pub fn candidate(origin: OriginFor<T>) -> DispatchResult {
75//! let who = ensure_signed(origin)?;
76//!
77//! let _ = <scored_pool::Pallet<T>>::submit_candidacy(
78//! T::RuntimeOrigin::from(Some(who.clone()).into())
79//! );
80//! Ok(())
81//! }
82//! }
83//! }
84//!
85//! # fn main() { }
86//! ```
87//!
88//! ## Dependencies
89//!
90//! This pallet depends on the [System pallet](../frame_system/index.html).
91
92// Ensure we're `no_std` when compiling for Wasm.
93#![cfg_attr(not(feature = "std"), no_std)]
94
95#[cfg(test)]
96// We do not declare all features used by `construct_runtime`
97#[allow(unexpected_cfgs)]
98mod mock;
99
100#[cfg(test)]
101mod tests;
102
103extern crate alloc;
104
105use alloc::vec::Vec;
106use codec::{FullCodec, MaxEncodedLen};
107use core::{cmp::Reverse, fmt::Debug};
108use frame_support::{
109 ensure,
110 traits::{ChangeMembers, Currency, Get, InitializeMembers, ReservableCurrency},
111 BoundedVec,
112};
113pub use pallet::*;
114use sp_runtime::traits::{AtLeast32Bit, StaticLookup, Zero};
115
116type BalanceOf<T, I> =
117 <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
118type PoolT<T, I> = BoundedVec<
119 (<T as frame_system::Config>::AccountId, Option<<T as Config<I>>::Score>),
120 <T as Config<I>>::MaximumMembers,
121>;
122type MembersT<T, I> =
123 BoundedVec<<T as frame_system::Config>::AccountId, <T as Config<I>>::MaximumMembers>;
124type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
125
126/// The enum is supplied when refreshing the members set.
127/// Depending on the enum variant the corresponding associated
128/// type function will be invoked.
129enum ChangeReceiver {
130 /// Should call `T::MembershipInitialized`.
131 MembershipInitialized,
132 /// Should call `T::MembershipChanged`.
133 MembershipChanged,
134}
135
136#[frame_support::pallet]
137pub mod pallet {
138 use super::*;
139 use frame_support::pallet_prelude::*;
140 use frame_system::pallet_prelude::*;
141
142 #[pallet::pallet]
143 pub struct Pallet<T, I = ()>(_);
144
145 #[pallet::config]
146 pub trait Config<I: 'static = ()>: frame_system::Config {
147 /// The currency used for deposits.
148 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
149
150 /// Maximum members length allowed.
151 #[pallet::constant]
152 type MaximumMembers: Get<u32>;
153
154 /// The score attributed to a member or candidate.
155 type Score: AtLeast32Bit
156 + Clone
157 + Copy
158 + Default
159 + FullCodec
160 + MaybeSerializeDeserialize
161 + Debug
162 + scale_info::TypeInfo
163 + MaxEncodedLen;
164
165 /// The overarching event type.
166 #[allow(deprecated)]
167 type RuntimeEvent: From<Event<Self, I>>
168 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
169
170 // The deposit which is reserved from candidates if they want to
171 // start a candidacy. The deposit gets returned when the candidacy is
172 // withdrawn or when the candidate is kicked.
173 #[pallet::constant]
174 type CandidateDeposit: Get<BalanceOf<Self, I>>;
175
176 /// Every `Period` blocks the `Members` are filled with the highest scoring
177 /// members in the `Pool`.
178 #[pallet::constant]
179 type Period: Get<BlockNumberFor<Self>>;
180
181 /// The receiver of the signal for when the membership has been initialized.
182 /// This happens pre-genesis and will usually be the same as `MembershipChanged`.
183 /// If you need to do something different on initialization, then you can change
184 /// this accordingly.
185 type MembershipInitialized: InitializeMembers<Self::AccountId>;
186
187 /// The receiver of the signal for when the members have changed.
188 type MembershipChanged: ChangeMembers<Self::AccountId>;
189
190 /// Allows a configurable origin type to set a score to a candidate in the pool.
191 type ScoreOrigin: EnsureOrigin<Self::RuntimeOrigin>;
192
193 /// Required origin for removing a member (though can always be Root).
194 /// Configurable origin which enables removing an entity. If the entity
195 /// is part of the `Members` it is immediately replaced by the next
196 /// highest scoring candidate, if available.
197 type KickOrigin: EnsureOrigin<Self::RuntimeOrigin>;
198 }
199
200 #[pallet::event]
201 #[pallet::generate_deposit(pub(super) fn deposit_event)]
202 pub enum Event<T: Config<I>, I: 'static = ()> {
203 /// The given member was removed. See the transaction for who.
204 MemberRemoved,
205 /// An entity has issued a candidacy. See the transaction for who.
206 CandidateAdded,
207 /// An entity withdrew candidacy. See the transaction for who.
208 CandidateWithdrew,
209 /// The candidacy was forcefully removed for an entity.
210 /// See the transaction for who.
211 CandidateKicked,
212 /// A score was attributed to the candidate.
213 /// See the transaction for who.
214 CandidateScored,
215 }
216
217 /// Error for the scored-pool pallet.
218 #[pallet::error]
219 pub enum Error<T, I = ()> {
220 /// Already a member.
221 AlreadyInPool,
222 /// Index out of bounds.
223 InvalidIndex,
224 /// Index does not match requested account.
225 WrongAccountIndex,
226 /// Number of members exceeds `MaximumMembers`.
227 TooManyMembers,
228 }
229
230 /// The current pool of candidates, stored as an ordered Bounded Vec
231 /// (ordered descending by score, `None` last, highest first).
232 #[pallet::storage]
233 #[pallet::getter(fn pool)]
234 pub(crate) type Pool<T: Config<I>, I: 'static = ()> = StorageValue<_, PoolT<T, I>, ValueQuery>;
235
236 /// A Map of the candidates. The information in this Map is redundant
237 /// to the information in the `Pool`. But the Map enables us to easily
238 /// check if a candidate is already in the pool, without having to
239 /// iterate over the entire pool (the `Pool` is not sorted by
240 /// `T::AccountId`, but by `T::Score` instead).
241 #[pallet::storage]
242 #[pallet::getter(fn candidate_exists)]
243 pub(crate) type CandidateExists<T: Config<I>, I: 'static = ()> =
244 StorageMap<_, Twox64Concat, T::AccountId, bool, ValueQuery>;
245
246 /// The current membership, stored as an ordered Vec.
247 #[pallet::storage]
248 #[pallet::getter(fn members)]
249 pub(crate) type Members<T: Config<I>, I: 'static = ()> =
250 StorageValue<_, MembersT<T, I>, ValueQuery>;
251
252 /// Size of the `Members` set.
253 #[pallet::storage]
254 #[pallet::getter(fn member_count)]
255 pub(crate) type MemberCount<T, I = ()> = StorageValue<_, u32, ValueQuery>;
256
257 #[pallet::genesis_config]
258 #[derive(frame_support::DefaultNoBound)]
259 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
260 pub pool: PoolT<T, I>,
261 pub member_count: u32,
262 }
263
264 #[pallet::genesis_build]
265 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
266 fn build(&self) {
267 let mut pool = self.pool.clone();
268
269 // reserve balance for each candidate in the pool.
270 // panicking here is ok, since this just happens one time, pre-genesis.
271 pool.iter().for_each(|(who, _)| {
272 T::Currency::reserve(who, T::CandidateDeposit::get())
273 .expect("balance too low to create candidacy");
274 <CandidateExists<T, I>>::insert(who, true);
275 });
276
277 // Sorts the `Pool` by score in a descending order. Entities which
278 // have a score of `None` are sorted to the end of the bounded vec.
279 pool.sort_by_key(|(_, maybe_score)| Reverse(maybe_score.unwrap_or_default()));
280 <Pallet<T, I>>::update_member_count(self.member_count)
281 .expect("Number of allowed members exceeded");
282 <Pool<T, I>>::put(&pool);
283 <Pallet<T, I>>::refresh_members(pool, ChangeReceiver::MembershipInitialized);
284 }
285 }
286
287 #[pallet::hooks]
288 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
289 /// Every `Period` blocks the `Members` set is refreshed from the
290 /// highest scoring members in the pool.
291 fn on_initialize(n: frame_system::pallet_prelude::BlockNumberFor<T>) -> Weight {
292 if n % T::Period::get() == Zero::zero() {
293 let pool = <Pool<T, I>>::get();
294 <Pallet<T, I>>::refresh_members(pool, ChangeReceiver::MembershipChanged);
295 }
296 Weight::zero()
297 }
298 }
299
300 #[pallet::call]
301 impl<T: Config<I>, I: 'static> Pallet<T, I> {
302 /// Add `origin` to the pool of candidates.
303 ///
304 /// This results in `CandidateDeposit` being reserved from
305 /// the `origin` account. The deposit is returned once
306 /// candidacy is withdrawn by the candidate or the entity
307 /// is kicked by `KickOrigin`.
308 ///
309 /// The dispatch origin of this function must be signed.
310 ///
311 /// The `index` parameter of this function must be set to
312 /// the index of the transactor in the `Pool`.
313 #[pallet::call_index(0)]
314 #[pallet::weight({0})]
315 pub fn submit_candidacy(origin: OriginFor<T>) -> DispatchResult {
316 let who = ensure_signed(origin)?;
317 ensure!(!<CandidateExists<T, I>>::contains_key(&who), Error::<T, I>::AlreadyInPool);
318
319 let deposit = T::CandidateDeposit::get();
320 T::Currency::reserve(&who, deposit)?;
321
322 // can be inserted as last element in pool, since entities with
323 // `None` are always sorted to the end.
324 <Pool<T, I>>::try_append((who.clone(), Option::<<T as Config<I>>::Score>::None))
325 .map_err(|_| Error::<T, I>::TooManyMembers)?;
326
327 <CandidateExists<T, I>>::insert(&who, true);
328
329 Self::deposit_event(Event::<T, I>::CandidateAdded);
330 Ok(())
331 }
332
333 /// An entity withdraws candidacy and gets its deposit back.
334 ///
335 /// If the entity is part of the `Members`, then the highest member
336 /// of the `Pool` that is not currently in `Members` is immediately
337 /// placed in the set instead.
338 ///
339 /// The dispatch origin of this function must be signed.
340 ///
341 /// The `index` parameter of this function must be set to
342 /// the index of the transactor in the `Pool`.
343 #[pallet::call_index(1)]
344 #[pallet::weight({0})]
345 pub fn withdraw_candidacy(origin: OriginFor<T>, index: u32) -> DispatchResult {
346 let who = ensure_signed(origin)?;
347
348 let pool = <Pool<T, I>>::get();
349 Self::ensure_index(&pool, &who, index)?;
350
351 Self::remove_member(pool, who, index)?;
352 Self::deposit_event(Event::<T, I>::CandidateWithdrew);
353 Ok(())
354 }
355
356 /// Kick a member `who` from the set.
357 ///
358 /// May only be called from `T::KickOrigin`.
359 ///
360 /// The `index` parameter of this function must be set to
361 /// the index of `dest` in the `Pool`.
362 #[pallet::call_index(2)]
363 #[pallet::weight({0})]
364 pub fn kick(
365 origin: OriginFor<T>,
366 dest: AccountIdLookupOf<T>,
367 index: u32,
368 ) -> DispatchResult {
369 T::KickOrigin::ensure_origin(origin)?;
370
371 let who = T::Lookup::lookup(dest)?;
372
373 let pool = <Pool<T, I>>::get();
374 Self::ensure_index(&pool, &who, index)?;
375
376 Self::remove_member(pool, who, index)?;
377 Self::deposit_event(Event::<T, I>::CandidateKicked);
378 Ok(())
379 }
380
381 /// Score a member `who` with `score`.
382 ///
383 /// May only be called from `T::ScoreOrigin`.
384 ///
385 /// The `index` parameter of this function must be set to
386 /// the index of the `dest` in the `Pool`.
387 #[pallet::call_index(3)]
388 #[pallet::weight({0})]
389 pub fn score(
390 origin: OriginFor<T>,
391 dest: AccountIdLookupOf<T>,
392 index: u32,
393 score: T::Score,
394 ) -> DispatchResult {
395 T::ScoreOrigin::ensure_origin(origin)?;
396
397 let who = T::Lookup::lookup(dest)?;
398
399 let mut pool = <Pool<T, I>>::get();
400 Self::ensure_index(&pool, &who, index)?;
401
402 pool.remove(index as usize);
403
404 // we binary search the pool (which is sorted descending by score).
405 // if there is already an element with `score`, we insert
406 // right before that. if not, the search returns a location
407 // where we can insert while maintaining order.
408 let item = (who, Some(score));
409 let location = pool
410 .binary_search_by_key(&Reverse(score), |(_, maybe_score)| {
411 Reverse(maybe_score.unwrap_or_default())
412 })
413 .unwrap_or_else(|l| l);
414 pool.try_insert(location, item).map_err(|_| Error::<T, I>::TooManyMembers)?;
415
416 <Pool<T, I>>::put(&pool);
417 Self::deposit_event(Event::<T, I>::CandidateScored);
418 Ok(())
419 }
420
421 /// Dispatchable call to change `MemberCount`.
422 ///
423 /// This will only have an effect the next time a refresh happens
424 /// (this happens each `Period`).
425 ///
426 /// May only be called from root.
427 #[pallet::call_index(4)]
428 #[pallet::weight({0})]
429 pub fn change_member_count(origin: OriginFor<T>, count: u32) -> DispatchResult {
430 ensure_root(origin)?;
431 Self::update_member_count(count).map_err(Into::into)
432 }
433 }
434}
435
436impl<T: Config<I>, I: 'static> Pallet<T, I> {
437 /// Fetches the `MemberCount` highest scoring members from
438 /// `Pool` and puts them into `Members`.
439 ///
440 /// The `notify` parameter is used to deduct which associated
441 /// type function to invoke at the end of the method.
442 fn refresh_members(pool: PoolT<T, I>, notify: ChangeReceiver) {
443 let count = MemberCount::<T, I>::get();
444 let old_members = <Members<T, I>>::get();
445
446 let new_members: Vec<T::AccountId> = pool
447 .into_iter()
448 .filter(|(_, score)| score.is_some())
449 .take(count as usize)
450 .map(|(account_id, _)| account_id)
451 .collect();
452
453 // It's safe to truncate_from at this point since MemberCount
454 // is verified that it does not exceed the MaximumMembers value
455 let mut new_members_bounded: MembersT<T, I> = BoundedVec::truncate_from(new_members);
456
457 new_members_bounded.sort();
458
459 <Members<T, I>>::put(&new_members_bounded);
460
461 match notify {
462 ChangeReceiver::MembershipInitialized =>
463 T::MembershipInitialized::initialize_members(&new_members_bounded),
464 ChangeReceiver::MembershipChanged =>
465 T::MembershipChanged::set_members_sorted(&new_members_bounded[..], &old_members[..]),
466 }
467 }
468
469 /// Removes an entity `remove` at `index` from the `Pool`.
470 ///
471 /// If the entity is a member it is also removed from `Members` and
472 /// the deposit is returned.
473 fn remove_member(
474 mut pool: PoolT<T, I>,
475 remove: T::AccountId,
476 index: u32,
477 ) -> Result<(), Error<T, I>> {
478 // all callers of this function in this pallet also check
479 // the index for validity before calling this function.
480 // nevertheless we check again here, to assert that there was
481 // no mistake when invoking this sensible function.
482 Self::ensure_index(&pool, &remove, index)?;
483
484 pool.remove(index as usize);
485 <Pool<T, I>>::put(&pool);
486
487 // remove from set, if it was in there
488 let members = <Members<T, I>>::get();
489 if members.binary_search(&remove).is_ok() {
490 Self::refresh_members(pool, ChangeReceiver::MembershipChanged);
491 }
492
493 <CandidateExists<T, I>>::remove(&remove);
494
495 T::Currency::unreserve(&remove, T::CandidateDeposit::get());
496
497 Self::deposit_event(Event::<T, I>::MemberRemoved);
498 Ok(())
499 }
500
501 /// Checks if `index` is a valid number and if the element found
502 /// at `index` in `Pool` is equal to `who`.
503 fn ensure_index(pool: &PoolT<T, I>, who: &T::AccountId, index: u32) -> Result<(), Error<T, I>> {
504 ensure!(index < pool.len() as u32, Error::<T, I>::InvalidIndex);
505
506 let (index_who, _index_score) = &pool[index as usize];
507 ensure!(index_who == who, Error::<T, I>::WrongAccountIndex);
508
509 Ok(())
510 }
511
512 /// Make sure the new member count value does not exceed the MaximumMembers
513 fn update_member_count(new_member_count: u32) -> Result<(), Error<T, I>> {
514 ensure!(new_member_count <= T::MaximumMembers::get(), Error::<T, I>::TooManyMembers);
515 <MemberCount<T, I>>::put(new_member_count);
516 Ok(())
517 }
518}