1#![cfg_attr(not(feature = "std"), no_std)]
83
84extern crate alloc;
85#[cfg(doc)]
86#[cfg_attr(doc, aquamarine::aquamarine)]
87#[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#[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 #[allow(deprecated)]
181 type RuntimeEvent: From<Event<Self, I>>
182 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
183
184 type WeightInfo: weights::WeightInfo;
186
187 type ScoreProvider: ScoreProvider<Self::AccountId, Score = Self::Score>;
189
190 #[pallet::constant]
234 type BagThresholds: Get<&'static [Self::Score]>;
235
236 #[pallet::constant]
241 type MaxAutoRebagPerBlock: Get<u32>;
242
243 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 #[pallet::storage]
263 pub type ListNodes<T: Config<I>, I: 'static = ()> =
264 CountedStorageMap<_, Twox64Concat, T::AccountId, list::Node<T, I>>;
265
266 #[pallet::storage]
270 pub type ListBags<T: Config<I>, I: 'static = ()> =
271 StorageMap<_, Twox64Concat, T::Score, list::Bag<T, I>>;
272
273 #[pallet::storage]
276 pub type NextNodeAutoRebagged<T: Config<I>, I: 'static = ()> =
277 StorageValue<_, T::AccountId, OptionQuery>;
278
279 #[pallet::storage]
284 pub type Lock<T: Config<I>, I: 'static = ()> = StorageValue<_, (), OptionQuery>;
285
286 #[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 Rebagged { who: T::AccountId, from: T::Score, to: T::Score },
309 ScoreUpdated { who: T::AccountId, new_score: T::Score },
311 }
312
313 #[pallet::error]
314 pub enum Error<T, I = ()> {
315 List(ListError),
317 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 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 #[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 #[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 #[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 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 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 let overhead = T::DbWeight::get().reads_writes(5, 1);
449
450 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 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 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 let (to_process, next_cursor) = if accounts.len() <= rebag_budget as usize {
500 (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 for (i, account) in to_process.iter().enumerate() {
514 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 let next_regular_account =
549 next_cursor.iter().find(|account| !PendingRebag::<T, I>::contains_key(account));
550
551 match next_regular_account {
552 Some(next) if to_process.contains(next) => {
557 NextNodeAutoRebagged::<T, I>::kill();
558 defensive!("Loop detected: {:?} already processed โ cursor killed", next);
559 },
560 Some(next) => {
562 NextNodeAutoRebagged::<T, I>::put(next);
563 log!(debug, "Saved next node to be processed in rebag cursor: {:?}", next);
564 },
565 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 pub fn do_rebag(
606 account: &T::AccountId,
607 new_score: T::Score,
608 ) -> Result<Option<(T::Score, T::Score)>, ListError> {
609 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 #[cfg(feature = "std")]
630 pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
631 ListBags::get(score)
632 }
633
634 fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
639 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
641
642 PendingRebag::<T, I>::remove(account);
643
644 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 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 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 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 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 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 List::<T, I>::unsafe_regenerate(all, score_of)
751 }
752
753 fn unsafe_clear() {
754 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}