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