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(4, 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 let pending_len = core::cmp::min(pending_count as usize, accounts.len());
503
504 let mut processed = 0u32;
505 let mut successful_rebags = 0u32;
506 let mut failed_rebags = 0u32;
507 let mut pending_processed = 0u32;
508
509 for (i, account) in accounts.iter().enumerate() {
511 if i > 0 && meter.try_consume(per_item).is_err() {
513 break;
514 }
515
516 let pending_value = if PendingRebag::<T, I>::contains_key(account) { 1 } else { 0 };
517
518 match Self::rebag_internal(account) {
519 Err(Error::<T, I>::Locked) => {
520 defensive!("Pallet became locked during auto-rebag, stopping");
521 break;
522 },
523 Err(e) => {
524 log!(warn, "Error during rebagging: {:?}", e);
525 failed_rebags += 1;
526 },
527 Ok(Some((from, to))) => {
528 log!(debug, "Rebagged {:?}: moved from {:?} to {:?}", account, from, to);
529 successful_rebags += 1;
530 pending_processed += pending_value;
531 },
532 Ok(None) => {
533 log!(debug, "Rebagging not needed for {:?}", account);
534 pending_processed += pending_value;
535 },
536 }
537
538 processed += 1;
539 if processed == rebag_budget {
540 break;
541 }
542 }
543
544 let next_index = core::cmp::max(processed as usize, pending_len);
548
549 match accounts.get(next_index) {
550 Some(next) => {
552 NextNodeAutoRebagged::<T, I>::put(next);
553 log!(debug, "Saved next node to be processed in rebag cursor: {:?}", next);
554 },
555 None => {
558 NextNodeAutoRebagged::<T, I>::kill();
559 log!(debug, "End of regular list reached โ cursor killed");
560 },
561 }
562
563 let weight_used = meter.consumed();
564 log!(
565 debug,
566 "Auto-rebag finished: processed={}, successful_rebags={}, errors={}, pending_processed={}, weight_used={:?}",
567 processed,
568 successful_rebags,
569 failed_rebags,
570 pending_processed,
571 weight_used
572 );
573
574 weight_used
575 }
576 }
577}
578
579#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
580impl<T: Config<I>, I: 'static> Pallet<T, I> {
581 pub fn do_try_state() -> Result<(), TryRuntimeError> {
582 List::<T, I>::do_try_state()
583 }
584}
585
586impl<T: Config<I>, I: 'static> Pallet<T, I> {
587 pub fn do_rebag(
591 account: &T::AccountId,
592 new_score: T::Score,
593 ) -> Result<Option<(T::Score, T::Score)>, ListError> {
594 let node = list::Node::<T, I>::get(&account).ok_or(ListError::NodeNotFound)?;
596 if node.score != new_score {
597 Self::deposit_event(Event::<T, I>::ScoreUpdated { who: account.clone(), new_score });
598 }
599 let maybe_movement = List::update_position_for(node, new_score);
600 if let Some((from, to)) = maybe_movement {
601 Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
602 };
603 Ok(maybe_movement)
604 }
605
606 fn ensure_unlocked() -> Result<(), ListError> {
607 match Lock::<T, I>::get() {
608 None => Ok(()),
609 Some(()) => Err(ListError::Locked),
610 }
611 }
612
613 #[cfg(feature = "std")]
615 pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
616 ListBags::get(score)
617 }
618
619 fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
624 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
626
627 PendingRebag::<T, I>::remove(account);
628
629 let existed = ListNodes::<T, I>::contains_key(account);
631 let maybe_score = T::ScoreProvider::score(account);
632
633 match (existed, maybe_score) {
634 (true, Some(current_score)) => {
635 log!(debug, "Attempting to rebag node {:?}", account);
637 Pallet::<T, I>::do_rebag(account, current_score)
638 .map_err::<Error<T, I>, _>(Into::into)
639 },
640 (false, Some(current_score)) => {
641 log!(debug, "Inserting node {:?} with score {:?}", account, current_score);
643 List::<T, I>::insert(account.clone(), current_score)
644 .map_err::<Error<T, I>, _>(Into::into)?;
645 Ok(None)
646 },
647 (true, None) => {
648 log!(debug, "Removing node {:?}", account);
650 List::<T, I>::remove(account).map_err::<Error<T, I>, _>(Into::into)?;
651 Ok(None)
652 },
653 (false, None) => {
654 Err(Error::<T, I>::List(ListError::NodeNotFound))
656 },
657 }
658 }
659}
660
661impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
662 type Error = ListError;
663 type Score = T::Score;
664
665 fn range() -> (Self::Score, Self::Score) {
666 use frame_support::traits::Get;
667 (
668 T::BagThresholds::get().first().cloned().unwrap_or_default(),
669 T::BagThresholds::get().last().cloned().unwrap_or_default(),
670 )
671 }
672
673 fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
674 Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
675 }
676
677 fn lock() {
678 Lock::<T, I>::put(())
679 }
680
681 fn unlock() {
682 Lock::<T, I>::kill()
683 }
684
685 fn iter_from(
686 start: &T::AccountId,
687 ) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
688 let iter = List::<T, I>::iter_from(start)?;
689 Ok(Box::new(iter.map(|n| n.id().clone())))
690 }
691
692 fn count() -> u32 {
693 ListNodes::<T, I>::count()
694 }
695
696 fn contains(id: &T::AccountId) -> bool {
697 List::<T, I>::contains(id)
698 }
699
700 fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
701 if Pallet::<T, I>::ensure_unlocked().is_err() {
702 if T::MaxAutoRebagPerBlock::get() > 0u32 {
705 PendingRebag::<T, I>::insert(&id, ());
706 return Ok(());
707 }
708
709 return Err(ListError::Locked);
710 };
711 List::<T, I>::insert(id, score)
712 }
713
714 fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> {
715 Pallet::<T, I>::ensure_unlocked()?;
716 Pallet::<T, I>::do_rebag(id, new_score).map(|_| ())
717 }
718
719 fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
720 List::<T, I>::get_score(id)
721 }
722
723 fn on_remove(id: &T::AccountId) -> Result<(), ListError> {
724 Pallet::<T, I>::ensure_unlocked()?;
725 List::<T, I>::remove(id)
726 }
727
728 fn unsafe_regenerate(
729 all: impl IntoIterator<Item = T::AccountId>,
730 score_of: Box<dyn Fn(&T::AccountId) -> Option<T::Score>>,
731 ) -> u32 {
732 List::<T, I>::unsafe_regenerate(all, score_of)
736 }
737
738 fn unsafe_clear() {
739 List::<T, I>::unsafe_clear()
743 }
744
745 #[cfg(feature = "try-runtime")]
746 fn try_state() -> Result<(), TryRuntimeError> {
747 Self::do_try_state()
748 }
749
750 frame_election_provider_support::runtime_benchmarks_enabled! {
751 fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
752 use frame_support::traits::Get as _;
753 let thresholds = T::BagThresholds::get();
754 let node = list::Node::<T, I>::get(who).unwrap();
755 let current_bag_idx = thresholds
756 .iter()
757 .chain(core::iter::once(&T::Score::max_value()))
758 .position(|w| w == &node.bag_upper)
759 .unwrap();
760
761 if is_increase {
762 let next_threshold_idx = current_bag_idx + 1;
763 assert!(thresholds.len() > next_threshold_idx);
764 thresholds[next_threshold_idx]
765 } else {
766 assert!(current_bag_idx != 0);
767 let prev_threshold_idx = current_bag_idx - 1;
768 thresholds[prev_threshold_idx]
769 }
770 }
771 }
772}
773
774impl<T: Config<I>, I: 'static> ScoreProvider<T::AccountId> for Pallet<T, I> {
775 type Score = <Pallet<T, I> as SortedListProvider<T::AccountId>>::Score;
776
777 fn score(id: &T::AccountId) -> Option<T::Score> {
778 Node::<T, I>::get(id).map(|node| node.score())
779 }
780
781 frame_election_provider_support::runtime_benchmarks_or_std_enabled! {
782 fn set_score_of(id: &T::AccountId, new_score: T::Score) {
783 ListNodes::<T, I>::mutate(id, |maybe_node| {
784 if let Some(node) = maybe_node.as_mut() {
785 node.score = new_score;
786 } else {
787 panic!("trying to mutate {:?} which does not exists", id);
788 }
789 })
790 }
791 }
792}