referrerpolicy=no-referrer-when-downgrade

pallet_bags_list/
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//! > Made with *Substrate*, for *Polkadot*.
19//!
20//! [![github]](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/bags-list) -
21//! [![polkadot]](https://polkadot.com)
22//!
23//! [polkadot]:
24//!     https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white
25//! [github]:
26//!     https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
27//!
28//!  # Bags-List Pallet
29//!
30//! An onchain implementation of a semi-sorted linked list, with permissionless sorting and update
31//! operations.
32//!
33//! ## Pallet API
34//!
35//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
36//! including its configuration trait, dispatchables, storage items, events and errors.
37//!
38//! This pallet provides an implementation of
39//! [`frame_election_provider_support::SortedListProvider`] and it can typically be used by another
40//! pallet via this API.
41//!
42//! ## Overview
43//!
44//! This pallet splits `AccountId`s into different bags. Within a bag, these `AccountId`s are stored
45//! as nodes in a linked-list manner. This pallet then provides iteration over all bags, which
46//! basically allows an infinitely large list of items to be kept in a sorted manner.
47//!
48//! Each bags has a upper and lower range of scores, denoted by [`Config::BagThresholds`]. All nodes
49//! within a bag must be within the range of the bag. If not, the permissionless [`Pallet::rebag`]
50//! can be used to move any node to the right bag.
51//!
52//! Once a `rebag` happens, the order within a node is still not enforced. To move a node to the
53//! optimal position in a bag, the [`Pallet::put_in_front_of`] or [`Pallet::put_in_front_of_other`]
54//! can be used.
55//!
56//! Additional reading, about how this pallet is used in the context of Polkadot's staking system:
57//! <https://polkadot.com/blog/staking-update-september-2021/#bags-list-in-depth>
58//!
59//! ## Examples
60//!
61//! See [`example`] for a diagram of `rebag` and `put_in_front_of` operations.
62//!
63//! ## Low Level / Implementation Details
64//!
65//! The data structure exposed by this pallet aims to be optimized for:
66//!
67//! - insertions and removals.
68//! - iteration over the top* N items by score, where the precise ordering of items doesn't
69//!   particularly matter.
70//!
71//! ### Further Details
72//!
73//! - items are kept in bags, which are delineated by their range of score (See
74//!   [`Config::BagThresholds`]).
75//! - for iteration, bags are chained together from highest to lowest and elements within the bag
76//!   are iterated from head to tail.
77//! - items within a bag are iterated in order of insertion. Thus removing an item and re-inserting
78//!   it will worsen its position in list iteration; this reduces incentives for some types of spam
79//!   that involve consistently removing and inserting for better position. Further, ordering
80//!   granularity is thus dictated by range between each bag threshold.
81//! - if an item's score changes to a value no longer within the range of its current bag the item's
82//!   position will need to be updated by an external actor with rebag (update), or removal and
83//!   insertion.
84
85#![cfg_attr(not(feature = "std"), no_std)]
86
87extern crate alloc;
88#[cfg(doc)]
89#[cfg_attr(doc, aquamarine::aquamarine)]
90///
91/// In this example, assuming each node has an equal id and score (eg. node 21 has a score of 21),
92/// the node 22 can be moved from bag 1 to bag 0 with the `rebag` operation.
93///
94/// Once the whole list is iterated, assuming the above above rebag happens, the order of iteration
95/// would be: `25, 21, 22, 12, 22, 5, 7, 3`.
96///
97/// Moreover, in bag2, node 7 can be moved to the front of node 5 with the `put_in_front_of`, as it
98/// has a higher score.
99///
100/// ```mermaid
101/// graph LR
102/// 	Bag0 --> Bag1 --> Bag2
103///
104/// 	subgraph Bag0[Bag 0: 21-30 DOT]
105/// 		direction LR
106/// 		25 --> 21 --> 22X[22]
107/// 	end
108///
109/// 	subgraph Bag1[Bag 1: 11-20 DOT]
110/// 		direction LR
111/// 		12 --> 22
112/// 	end
113///
114/// 	subgraph Bag2[Bag 2: 0-10 DOT]
115/// 		direction LR
116/// 		5 --> 7 --> 3
117/// 	end
118///
119/// 	style 22X stroke-dasharray: 5 5,opacity:50%
120/// ```
121///
122/// The equivalent of this in code would be:
123#[doc = docify::embed!("src/tests.rs", examples_work)]
124pub mod example {}
125
126use alloc::{boxed::Box, vec::Vec};
127use codec::FullCodec;
128use frame_election_provider_support::{ScoreProvider, SortedListProvider};
129use frame_support::weights::{Weight, WeightMeter};
130use frame_system::ensure_signed;
131use sp_runtime::traits::{AtLeast32BitUnsigned, Bounded, StaticLookup};
132
133#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
134use sp_runtime::TryRuntimeError;
135
136#[cfg(any(feature = "runtime-benchmarks", test))]
137mod benchmarks;
138
139mod list;
140pub mod migrations;
141#[cfg(any(test, feature = "fuzz"))]
142pub mod mock;
143#[cfg(test)]
144mod tests;
145pub mod weights;
146
147pub use list::{notional_bag_for, Bag, List, ListError, Node};
148pub use pallet::*;
149pub use weights::WeightInfo;
150
151pub(crate) const LOG_TARGET: &str = "runtime::bags-list";
152
153// syntactic sugar for logging.
154#[macro_export]
155macro_rules! log {
156	($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
157		log::$level!(
158			target: crate::LOG_TARGET,
159			concat!("[{:?}] 👜 [{}]", $patter),
160			<frame_system::Pallet<T>>::block_number(),
161			<crate::Pallet::<T, I> as frame_support::traits::PalletInfoAccess>::name()
162			$(, $values)*
163		)
164	};
165}
166
167type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
168
169#[frame_support::pallet]
170pub mod pallet {
171	use super::*;
172	use frame_support::pallet_prelude::*;
173	use frame_system::pallet_prelude::*;
174
175	#[pallet::pallet]
176	pub struct Pallet<T, I = ()>(_);
177
178	#[pallet::config]
179	pub trait Config<I: 'static = ()>: frame_system::Config {
180		/// The overarching event type.
181		#[allow(deprecated)]
182		type RuntimeEvent: From<Event<Self, I>>
183			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
184
185		/// Weight information for extrinsics in this pallet.
186		type WeightInfo: weights::WeightInfo;
187
188		/// Something that provides the scores of ids.
189		type ScoreProvider: ScoreProvider<Self::AccountId, Score = Self::Score>;
190
191		/// The list of thresholds separating the various bags.
192		///
193		/// Ids are separated into unsorted bags according to their score. This specifies the
194		/// thresholds separating the bags. An id's bag is the largest bag for which the id's score
195		/// is less than or equal to its upper threshold.
196		///
197		/// When ids are iterated, higher bags are iterated completely before lower bags. This means
198		/// that iteration is _semi-sorted_: ids of higher score tend to come before ids of lower
199		/// score, but peer ids within a particular bag are sorted in insertion order.
200		///
201		/// # Expressing the constant
202		///
203		/// This constant must be sorted in strictly increasing order. Duplicate items are not
204		/// permitted.
205		///
206		/// There is an implied upper limit of `Score::MAX`; that value does not need to be
207		/// specified within the bag. For any two threshold lists, if one ends with
208		/// `Score::MAX`, the other one does not, and they are otherwise equal, the two
209		/// lists will behave identically.
210		///
211		/// # Calculation
212		///
213		/// It is recommended to generate the set of thresholds in a geometric series, such that
214		/// there exists some constant ratio such that `threshold[k + 1] == (threshold[k] *
215		/// constant_ratio).max(threshold[k] + 1)` for all `k`.
216		///
217		/// The helpers in the `/utils/frame/generate-bags` module can simplify this calculation.
218		///
219		/// # Examples
220		///
221		/// - If `BagThresholds::get().is_empty()`, then all ids are put into the same bag, and
222		///   iteration is strictly in insertion order.
223		/// - If `BagThresholds::get().len() == 64`, and the thresholds are determined according to
224		///   the procedure given above, then the constant ratio is equal to 2.
225		/// - If `BagThresholds::get().len() == 200`, and the thresholds are determined according to
226		///   the procedure given above, then the constant ratio is approximately equal to 1.248.
227		/// - If the threshold list begins `[1, 2, 3, ...]`, then an id with score 0 or 1 will fall
228		///   into bag 0, an id with score 2 will fall into bag 1, etc.
229		///
230		/// # Migration
231		///
232		/// In the event that this list ever changes, a copy of the old bags list must be retained.
233		/// With that `List::migrate` can be called, which will perform the appropriate migration.
234		#[pallet::constant]
235		type BagThresholds: Get<&'static [Self::Score]>;
236
237		/// Maximum number of accounts that may be re-bagged automatically in `on_idle`.
238		///
239		/// A value of `0` (obtained by configuring `type MaxAutoRebagPerBlock = ();`) disables
240		/// the feature.
241		#[pallet::constant]
242		type MaxAutoRebagPerBlock: Get<u32>;
243
244		/// The type used to dictate a node position relative to other nodes.
245		type Score: Clone
246			+ Default
247			+ PartialEq
248			+ Eq
249			+ Ord
250			+ PartialOrd
251			+ core::fmt::Debug
252			+ Copy
253			+ AtLeast32BitUnsigned
254			+ Bounded
255			+ TypeInfo
256			+ FullCodec
257			+ MaxEncodedLen;
258	}
259
260	/// A single node, within some bag.
261	///
262	/// Nodes store links forward and back within their respective bags.
263	#[pallet::storage]
264	pub type ListNodes<T: Config<I>, I: 'static = ()> =
265		CountedStorageMap<_, Twox64Concat, T::AccountId, list::Node<T, I>>;
266
267	/// A bag stored in storage.
268	///
269	/// Stores a `Bag` struct, which stores head and tail pointers to itself.
270	#[pallet::storage]
271	pub type ListBags<T: Config<I>, I: 'static = ()> =
272		StorageMap<_, Twox64Concat, T::Score, list::Bag<T, I>>;
273
274	/// Pointer that remembers the next node that will be auto-rebagged.
275	/// When `None`, the next scan will start from the list head again.
276	#[pallet::storage]
277	pub type NextNodeAutoRebagged<T: Config<I>, I: 'static = ()> =
278		StorageValue<_, T::AccountId, OptionQuery>;
279
280	/// Lock all updates to this pallet.
281	///
282	/// If any nodes needs updating, removal or addition due to a temporary lock, the
283	/// [`Call::rebag`] can be used.
284	#[pallet::storage]
285	pub type Lock<T: Config<I>, I: 'static = ()> = StorageValue<_, (), OptionQuery>;
286
287	#[pallet::event]
288	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
289	pub enum Event<T: Config<I>, I: 'static = ()> {
290		/// Moved an account from one bag to another.
291		Rebagged { who: T::AccountId, from: T::Score, to: T::Score },
292		/// Updated the score of some account to the given amount.
293		ScoreUpdated { who: T::AccountId, new_score: T::Score },
294	}
295
296	#[pallet::error]
297	pub enum Error<T, I = ()> {
298		/// A error in the list interface implementation.
299		List(ListError),
300		/// Could not update a node, because the pallet is locked.
301		Locked,
302	}
303
304	impl<T, I> From<ListError> for Error<T, I> {
305		fn from(t: ListError) -> Self {
306			Error::<T, I>::List(t)
307		}
308	}
309
310	#[pallet::view_functions]
311	impl<T: Config<I>, I: 'static> Pallet<T, I> {
312		/// Get the current `score` of a given account.
313		///
314		/// Returns `(current, real_score)`, the former being the current score that this pallet is
315		/// aware of, which may or may not be up to date, and the latter being the real score, as
316		/// provided by
317		// [`Config::ScoreProvider`].
318		///
319		/// If the two differ, it means this node is eligible for [`Call::rebag`].
320		pub fn scores(who: T::AccountId) -> (Option<T::Score>, Option<T::Score>) {
321			(ListNodes::<T, I>::get(&who).map(|node| node.score), T::ScoreProvider::score(&who))
322		}
323	}
324
325	#[pallet::call]
326	impl<T: Config<I>, I: 'static> Pallet<T, I> {
327		/// Declare that some `dislocated` account has, through rewards or penalties, sufficiently
328		/// changed its score that it should properly fall into a different bag than its current
329		/// one.
330		///
331		/// Anyone can call this function about any potentially dislocated account.
332		///
333		/// Will always update the stored score of `dislocated` to the correct score, based on
334		/// `ScoreProvider`.
335		///
336		/// If `dislocated` does not exists, it returns an error.
337		#[pallet::call_index(0)]
338		#[pallet::weight(T::WeightInfo::rebag_non_terminal().max(T::WeightInfo::rebag_terminal()))]
339		pub fn rebag(origin: OriginFor<T>, dislocated: AccountIdLookupOf<T>) -> DispatchResult {
340			ensure_signed(origin)?;
341			let dislocated = T::Lookup::lookup(dislocated)?;
342			Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
343
344			Self::rebag_internal(&dislocated).map_err::<DispatchError, _>(Into::into)?;
345
346			Ok(())
347		}
348
349		/// Move the caller's Id directly in front of `lighter`.
350		///
351		/// The dispatch origin for this call must be _Signed_ and can only be called by the Id of
352		/// the account going in front of `lighter`. Fee is payed by the origin under all
353		/// circumstances.
354		///
355		/// Only works if:
356		///
357		/// - both nodes are within the same bag,
358		/// - and `origin` has a greater `Score` than `lighter`.
359		#[pallet::call_index(1)]
360		#[pallet::weight(T::WeightInfo::put_in_front_of())]
361		pub fn put_in_front_of(
362			origin: OriginFor<T>,
363			lighter: AccountIdLookupOf<T>,
364		) -> DispatchResult {
365			let heavier = ensure_signed(origin)?;
366			let lighter = T::Lookup::lookup(lighter)?;
367			Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
368			List::<T, I>::put_in_front_of(&lighter, &heavier)
369				.map_err::<Error<T, I>, _>(Into::into)
370				.map_err::<DispatchError, _>(Into::into)
371		}
372
373		/// Same as [`Pallet::put_in_front_of`], but it can be called by anyone.
374		///
375		/// Fee is paid by the origin under all circumstances.
376		#[pallet::call_index(2)]
377		#[pallet::weight(T::WeightInfo::put_in_front_of())]
378		pub fn put_in_front_of_other(
379			origin: OriginFor<T>,
380			heavier: AccountIdLookupOf<T>,
381			lighter: AccountIdLookupOf<T>,
382		) -> DispatchResult {
383			ensure_signed(origin)?;
384			let lighter = T::Lookup::lookup(lighter)?;
385			let heavier = T::Lookup::lookup(heavier)?;
386			Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
387			List::<T, I>::put_in_front_of(&lighter, &heavier)
388				.map_err::<Error<T, I>, _>(Into::into)
389				.map_err::<DispatchError, _>(Into::into)
390		}
391	}
392
393	#[pallet::hooks]
394	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
395		fn integrity_test() {
396			// to ensure they are strictly increasing, this also implies that duplicates are
397			// detected.
398			assert!(
399				T::BagThresholds::get().windows(2).all(|window| window[1] > window[0]),
400				"thresholds must strictly increase, and have no duplicates",
401			);
402		}
403
404		#[cfg(feature = "try-runtime")]
405		fn try_state(_: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
406			<Self as SortedListProvider<T::AccountId>>::try_state()
407		}
408
409		/// Called during the idle phase of block execution.
410		/// Automatically performs a limited number of `rebag` operations each block,
411		/// incrementally correcting the position of accounts within the bags-list.
412		///
413		/// Guarantees processing as many nodes as possible without failing on errors.
414		/// It stores a persistent cursor to continue across blocks.
415		fn on_idle(_n: BlockNumberFor<T>, limit: Weight) -> Weight {
416			let mut meter = WeightMeter::with_limit(limit);
417			// This weight assumes worst-case usage of `MaxAutoRebagPerBlock`.
418			// Changing the runtime value requires re-running the benchmarks.
419			if meter.try_consume(T::WeightInfo::on_idle()).is_err() {
420				log!(debug, "Not enough Weight for on_idle. Skipping rebugging.");
421				return Weight::zero();
422			}
423
424			let rebag_budget = T::MaxAutoRebagPerBlock::get();
425			if rebag_budget == 0 {
426				log!(debug, "Auto-rebag skipped: rebag_budget=0");
427				return meter.consumed();
428			}
429
430			let total_nodes = ListNodes::<T, I>::count();
431			if total_nodes == 0 {
432				log!(debug, "Auto-rebag skipped: total_nodes=0");
433				return meter.consumed();
434			}
435
436			if Self::ensure_unlocked().is_err() {
437				log!(debug, "Auto-rebag skipped: pallet is locked");
438				return meter.consumed();
439			}
440
441			log!(
442				debug,
443				"Starting auto-rebag. Budget: {} accounts/block, total_nodes={}.",
444				rebag_budget,
445				total_nodes
446			);
447
448			let cursor = NextNodeAutoRebagged::<T, I>::get();
449			let iter = match cursor {
450				Some(ref last) => {
451					log!(debug, "Next node from previous block: {:?}", last);
452
453					// Build an iterator that yields `last` first, then everything *after* it.
454					let tail = Self::iter_from(last).unwrap_or_else(|_| Self::iter());
455					let head_and_tail = core::iter::once(last.clone()).chain(tail);
456					Box::new(head_and_tail) as Box<dyn Iterator<Item = T::AccountId>>
457				},
458				None => {
459					log!(debug, "No NextNodeAutoRebagged found. Starting from head of the list");
460					Self::iter()
461				},
462			};
463			let accounts: Vec<_> = iter.take((rebag_budget + 1) as usize).collect();
464
465			// Safe split: if we reached (or passed) the tail of the list, we don’t want to panic.
466			let (to_process, next_cursor) = if accounts.len() <= rebag_budget as usize {
467				// This guarantees we either get the next account to process
468				// or gracefully receive None.
469				(accounts.as_slice(), &[][..])
470			} else {
471				accounts.split_at(rebag_budget as usize)
472			};
473
474			let mut processed = 0u32;
475			let mut successful_rebags = 0u32;
476			let mut failed_rebags = 0u32;
477
478			for account in to_process {
479				match Self::rebag_internal(&account) {
480					Err(Error::<T, I>::Locked) => {
481						defensive!("Pallet became locked during auto-rebag, stopping");
482						break;
483					},
484					Err(e) => {
485						log!(warn, "Error during rebagging: {:?}", e);
486						failed_rebags += 1;
487					},
488					Ok(Some((from, to))) => {
489						log!(debug, "Rebagged {:?}: moved from {:?} to {:?}", account, from, to);
490						successful_rebags += 1;
491					},
492					Ok(None) => {
493						log!(debug, "Rebagging not needed for {:?}", account);
494					},
495				}
496
497				processed += 1;
498				if processed == rebag_budget {
499					break;
500				}
501			}
502
503			match next_cursor.first() {
504				// Defensive check: prevents re-processing the same node multiple times within a
505				// single block. This situation should not occur during normal execution, but
506				// can happen in test environments or if `on_idle()` is invoked more than once
507				// per block (e.g. via custom test harnesses or manual calls).
508				Some(next) if to_process.contains(next) => {
509					NextNodeAutoRebagged::<T, I>::kill();
510					defensive!("Loop detected: {:?} already processed — cursor killed", next);
511				},
512				// Normal case: save the next node as a cursor for the following block.
513				Some(next) => {
514					NextNodeAutoRebagged::<T, I>::put(next);
515					log!(debug, "Saved next node to be processed in rebag cursor: {:?}", next);
516				},
517				// End of a list: no cursor needed.
518				None => {
519					NextNodeAutoRebagged::<T, I>::kill();
520					log!(debug, "End of list — cursor killed");
521				},
522			}
523
524			let weight_used = meter.consumed();
525			log!(
526				debug,
527				"Auto-rebag finished: processed={}, successful_rebags={}, errors={}, weight_used={:?}",
528				processed,
529				successful_rebags,
530				failed_rebags,
531				weight_used
532			);
533
534			weight_used
535		}
536	}
537}
538
539#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
540impl<T: Config<I>, I: 'static> Pallet<T, I> {
541	pub fn do_try_state() -> Result<(), TryRuntimeError> {
542		List::<T, I>::do_try_state()
543	}
544}
545
546impl<T: Config<I>, I: 'static> Pallet<T, I> {
547	/// Move an account from one bag to another, depositing an event on success.
548	///
549	/// If the account changed bags, returns `Ok(Some((from, to)))`.
550	pub fn do_rebag(
551		account: &T::AccountId,
552		new_score: T::Score,
553	) -> Result<Option<(T::Score, T::Score)>, ListError> {
554		// If no voter at that node, don't do anything. the caller just wasted the fee to call this.
555		let node = list::Node::<T, I>::get(&account).ok_or(ListError::NodeNotFound)?;
556		let maybe_movement = List::update_position_for(node, new_score);
557		if let Some((from, to)) = maybe_movement {
558			Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
559		};
560		Self::deposit_event(Event::<T, I>::ScoreUpdated { who: account.clone(), new_score });
561		Ok(maybe_movement)
562	}
563
564	fn ensure_unlocked() -> Result<(), ListError> {
565		match Lock::<T, I>::get() {
566			None => Ok(()),
567			Some(()) => Err(ListError::Locked),
568		}
569	}
570
571	/// Equivalent to `ListBags::get`, but public. Useful for tests in outside of this crate.
572	#[cfg(feature = "std")]
573	pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
574		ListBags::get(score)
575	}
576
577	/// Perform the internal rebagging logic for an account based on its updated score.
578	/// This function does not handle origin checks or higher-level dispatch logic.
579	///
580	/// Returns `Ok(Some((from, to)))` if rebagging occurred, or `Ok(None)` if nothing changed.
581	fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
582		// Ensure the pallet is not locked
583		Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
584
585		// Check if the account exists and retrieve its current score
586		let existed = ListNodes::<T, I>::contains_key(account);
587		let score_provider: fn(&T::AccountId) -> Option<T::Score> = T::ScoreProvider::score;
588		let maybe_score = score_provider(account);
589
590		match (existed, maybe_score) {
591			(true, Some(current_score)) => {
592				// The account exists and has a valid score, so try to rebag
593				log!(debug, "Attempting to rebag node {:?}", account);
594				Pallet::<T, I>::do_rebag(account, current_score)
595					.map_err::<Error<T, I>, _>(Into::into)
596			},
597			(false, Some(current_score)) => {
598				// The account doesn't exist, but it has a valid score - insert it
599				log!(debug, "Inserting node {:?} with score {:?}", account, current_score);
600				List::<T, I>::insert(account.clone(), current_score)
601					.map_err::<Error<T, I>, _>(Into::into)?;
602				Ok(None)
603			},
604			(true, None) => {
605				// The account exists but no longer has a valid score, so remove it
606				log!(debug, "Removing node {:?}", account);
607				List::<T, I>::remove(account).map_err::<Error<T, I>, _>(Into::into)?;
608				Ok(None)
609			},
610			(false, None) => {
611				// The account doesn't exist and has no valid score - do nothing
612				Err(Error::<T, I>::List(ListError::NodeNotFound))
613			},
614		}
615	}
616}
617
618impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
619	type Error = ListError;
620	type Score = T::Score;
621
622	fn range() -> (Self::Score, Self::Score) {
623		use frame_support::traits::Get;
624		(
625			T::BagThresholds::get().first().cloned().unwrap_or_default(),
626			T::BagThresholds::get().last().cloned().unwrap_or_default(),
627		)
628	}
629
630	fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
631		Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
632	}
633
634	fn lock() {
635		Lock::<T, I>::put(())
636	}
637
638	fn unlock() {
639		Lock::<T, I>::kill()
640	}
641
642	fn iter_from(
643		start: &T::AccountId,
644	) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
645		let iter = List::<T, I>::iter_from(start)?;
646		Ok(Box::new(iter.map(|n| n.id().clone())))
647	}
648
649	fn count() -> u32 {
650		ListNodes::<T, I>::count()
651	}
652
653	fn contains(id: &T::AccountId) -> bool {
654		List::<T, I>::contains(id)
655	}
656
657	fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
658		Pallet::<T, I>::ensure_unlocked()?;
659		List::<T, I>::insert(id, score)
660	}
661
662	fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> {
663		Pallet::<T, I>::ensure_unlocked()?;
664		Pallet::<T, I>::do_rebag(id, new_score).map(|_| ())
665	}
666
667	fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
668		List::<T, I>::get_score(id)
669	}
670
671	fn on_remove(id: &T::AccountId) -> Result<(), ListError> {
672		Pallet::<T, I>::ensure_unlocked()?;
673		List::<T, I>::remove(id)
674	}
675
676	fn unsafe_regenerate(
677		all: impl IntoIterator<Item = T::AccountId>,
678		score_of: Box<dyn Fn(&T::AccountId) -> Option<T::Score>>,
679	) -> u32 {
680		// NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_regenerate.
681		// I.e. because it can lead to many storage accesses.
682		// So it is ok to call it as caller must ensure the conditions.
683		List::<T, I>::unsafe_regenerate(all, score_of)
684	}
685
686	fn unsafe_clear() {
687		// NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_clear.
688		// I.e. because it can lead to many storage accesses.
689		// So it is ok to call it as caller must ensure the conditions.
690		List::<T, I>::unsafe_clear()
691	}
692
693	#[cfg(feature = "try-runtime")]
694	fn try_state() -> Result<(), TryRuntimeError> {
695		Self::do_try_state()
696	}
697
698	frame_election_provider_support::runtime_benchmarks_enabled! {
699		fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
700			use frame_support::traits::Get as _;
701			let thresholds = T::BagThresholds::get();
702			let node = list::Node::<T, I>::get(who).unwrap();
703			let current_bag_idx = thresholds
704				.iter()
705				.chain(core::iter::once(&T::Score::max_value()))
706				.position(|w| w == &node.bag_upper)
707				.unwrap();
708
709			if is_increase {
710				let next_threshold_idx = current_bag_idx + 1;
711				assert!(thresholds.len() > next_threshold_idx);
712				thresholds[next_threshold_idx]
713			} else {
714				assert!(current_bag_idx != 0);
715				let prev_threshold_idx = current_bag_idx - 1;
716				thresholds[prev_threshold_idx]
717			}
718		}
719	}
720}
721
722impl<T: Config<I>, I: 'static> ScoreProvider<T::AccountId> for Pallet<T, I> {
723	type Score = <Pallet<T, I> as SortedListProvider<T::AccountId>>::Score;
724
725	fn score(id: &T::AccountId) -> Option<T::Score> {
726		Node::<T, I>::get(id).map(|node| node.score())
727	}
728
729	frame_election_provider_support::runtime_benchmarks_or_std_enabled! {
730		fn set_score_of(id: &T::AccountId, new_score: T::Score) {
731			ListNodes::<T, I>::mutate(id, |maybe_node| {
732				if let Some(node) = maybe_node.as_mut() {
733					node.score = new_score;
734				} else {
735					panic!("trying to mutate {:?} which does not exists", id);
736				}
737			})
738		}
739	}
740}