1#![cfg_attr(not(feature = "std"), no_std)]
86
87extern crate alloc;
88#[cfg(doc)]
89#[cfg_attr(doc, aquamarine::aquamarine)]
90#[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
139pub mod 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#[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 #[allow(deprecated)]
182 type RuntimeEvent: From<Event<Self, I>>
183 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
184
185 type WeightInfo: weights::WeightInfo;
187
188 type ScoreProvider: ScoreProvider<Self::AccountId, Score = Self::Score>;
190
191 #[pallet::constant]
235 type BagThresholds: Get<&'static [Self::Score]>;
236
237 #[pallet::constant]
242 type MaxAutoRebagPerBlock: Get<u32>;
243
244 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 #[pallet::storage]
264 pub type ListNodes<T: Config<I>, I: 'static = ()> =
265 CountedStorageMap<_, Twox64Concat, T::AccountId, list::Node<T, I>>;
266
267 #[pallet::storage]
271 pub type ListBags<T: Config<I>, I: 'static = ()> =
272 StorageMap<_, Twox64Concat, T::Score, list::Bag<T, I>>;
273
274 #[pallet::storage]
277 pub type NextNodeAutoRebagged<T: Config<I>, I: 'static = ()> =
278 StorageValue<_, T::AccountId, OptionQuery>;
279
280 #[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 Rebagged { who: T::AccountId, from: T::Score, to: T::Score },
292 ScoreUpdated { who: T::AccountId, new_score: T::Score },
294 }
295
296 #[pallet::error]
297 pub enum Error<T, I = ()> {
298 List(ListError),
300 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 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 #[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 #[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 #[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 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 fn on_idle(_n: BlockNumberFor<T>, limit: Weight) -> Weight {
416 let mut meter = WeightMeter::with_limit(limit);
417 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 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 let (to_process, next_cursor) = if accounts.len() <= rebag_budget as usize {
467 (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 Some(next) if to_process.contains(next) => {
509 NextNodeAutoRebagged::<T, I>::kill();
510 defensive!("Loop detected: {:?} already processed — cursor killed", next);
511 },
512 Some(next) => {
514 NextNodeAutoRebagged::<T, I>::put(next);
515 log!(debug, "Saved next node to be processed in rebag cursor: {:?}", next);
516 },
517 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 pub fn do_rebag(
551 account: &T::AccountId,
552 new_score: T::Score,
553 ) -> Result<Option<(T::Score, T::Score)>, ListError> {
554 let node = list::Node::<T, I>::get(&account).ok_or(ListError::NodeNotFound)?;
556 if node.score != new_score {
557 Self::deposit_event(Event::<T, I>::ScoreUpdated { who: account.clone(), new_score });
558 }
559 let maybe_movement = List::update_position_for(node, new_score);
560 if let Some((from, to)) = maybe_movement {
561 Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
562 };
563 Ok(maybe_movement)
564 }
565
566 fn ensure_unlocked() -> Result<(), ListError> {
567 match Lock::<T, I>::get() {
568 None => Ok(()),
569 Some(()) => Err(ListError::Locked),
570 }
571 }
572
573 #[cfg(feature = "std")]
575 pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
576 ListBags::get(score)
577 }
578
579 fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
584 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
586
587 let existed = ListNodes::<T, I>::contains_key(account);
589 let score_provider: fn(&T::AccountId) -> Option<T::Score> = T::ScoreProvider::score;
590 let maybe_score = score_provider(account);
591
592 match (existed, maybe_score) {
593 (true, Some(current_score)) => {
594 log!(debug, "Attempting to rebag node {:?}", account);
596 Pallet::<T, I>::do_rebag(account, current_score)
597 .map_err::<Error<T, I>, _>(Into::into)
598 },
599 (false, Some(current_score)) => {
600 log!(debug, "Inserting node {:?} with score {:?}", account, current_score);
602 List::<T, I>::insert(account.clone(), current_score)
603 .map_err::<Error<T, I>, _>(Into::into)?;
604 Ok(None)
605 },
606 (true, None) => {
607 log!(debug, "Removing node {:?}", account);
609 List::<T, I>::remove(account).map_err::<Error<T, I>, _>(Into::into)?;
610 Ok(None)
611 },
612 (false, None) => {
613 Err(Error::<T, I>::List(ListError::NodeNotFound))
615 },
616 }
617 }
618}
619
620impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
621 type Error = ListError;
622 type Score = T::Score;
623
624 fn range() -> (Self::Score, Self::Score) {
625 use frame_support::traits::Get;
626 (
627 T::BagThresholds::get().first().cloned().unwrap_or_default(),
628 T::BagThresholds::get().last().cloned().unwrap_or_default(),
629 )
630 }
631
632 fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
633 Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
634 }
635
636 fn lock() {
637 Lock::<T, I>::put(())
638 }
639
640 fn unlock() {
641 Lock::<T, I>::kill()
642 }
643
644 fn iter_from(
645 start: &T::AccountId,
646 ) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
647 let iter = List::<T, I>::iter_from(start)?;
648 Ok(Box::new(iter.map(|n| n.id().clone())))
649 }
650
651 fn count() -> u32 {
652 ListNodes::<T, I>::count()
653 }
654
655 fn contains(id: &T::AccountId) -> bool {
656 List::<T, I>::contains(id)
657 }
658
659 fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
660 Pallet::<T, I>::ensure_unlocked()?;
661 List::<T, I>::insert(id, score)
662 }
663
664 fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> {
665 Pallet::<T, I>::ensure_unlocked()?;
666 Pallet::<T, I>::do_rebag(id, new_score).map(|_| ())
667 }
668
669 fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
670 List::<T, I>::get_score(id)
671 }
672
673 fn on_remove(id: &T::AccountId) -> Result<(), ListError> {
674 Pallet::<T, I>::ensure_unlocked()?;
675 List::<T, I>::remove(id)
676 }
677
678 fn unsafe_regenerate(
679 all: impl IntoIterator<Item = T::AccountId>,
680 score_of: Box<dyn Fn(&T::AccountId) -> Option<T::Score>>,
681 ) -> u32 {
682 List::<T, I>::unsafe_regenerate(all, score_of)
686 }
687
688 fn unsafe_clear() {
689 List::<T, I>::unsafe_clear()
693 }
694
695 #[cfg(feature = "try-runtime")]
696 fn try_state() -> Result<(), TryRuntimeError> {
697 Self::do_try_state()
698 }
699
700 frame_election_provider_support::runtime_benchmarks_enabled! {
701 fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
702 use frame_support::traits::Get as _;
703 let thresholds = T::BagThresholds::get();
704 let node = list::Node::<T, I>::get(who).unwrap();
705 let current_bag_idx = thresholds
706 .iter()
707 .chain(core::iter::once(&T::Score::max_value()))
708 .position(|w| w == &node.bag_upper)
709 .unwrap();
710
711 if is_increase {
712 let next_threshold_idx = current_bag_idx + 1;
713 assert!(thresholds.len() > next_threshold_idx);
714 thresholds[next_threshold_idx]
715 } else {
716 assert!(current_bag_idx != 0);
717 let prev_threshold_idx = current_bag_idx - 1;
718 thresholds[prev_threshold_idx]
719 }
720 }
721 }
722}
723
724impl<T: Config<I>, I: 'static> ScoreProvider<T::AccountId> for Pallet<T, I> {
725 type Score = <Pallet<T, I> as SortedListProvider<T::AccountId>>::Score;
726
727 fn score(id: &T::AccountId) -> Option<T::Score> {
728 Node::<T, I>::get(id).map(|node| node.score())
729 }
730
731 frame_election_provider_support::runtime_benchmarks_or_std_enabled! {
732 fn set_score_of(id: &T::AccountId, new_score: T::Score) {
733 ListNodes::<T, I>::mutate(id, |maybe_node| {
734 if let Some(node) = maybe_node.as_mut() {
735 node.score = new_score;
736 } else {
737 panic!("trying to mutate {:?} which does not exists", id);
738 }
739 })
740 }
741 }
742}