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::{
130 traits::Get,
131 weights::{Weight, WeightMeter},
132};
133use frame_system::ensure_signed;
134use sp_runtime::traits::{AtLeast32BitUnsigned, Bounded, StaticLookup};
135
136#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
137use sp_runtime::TryRuntimeError;
138
139#[cfg(any(feature = "runtime-benchmarks", test))]
140mod benchmarks;
141
142pub mod list;
143pub mod migrations;
144#[cfg(any(test, feature = "fuzz"))]
145pub mod mock;
146#[cfg(test)]
147mod tests;
148pub mod weights;
149
150pub use list::{notional_bag_for, Bag, List, ListError, Node};
151pub use pallet::*;
152pub use weights::WeightInfo;
153
154pub(crate) const LOG_TARGET: &str = "runtime::bags-list";
155
156#[macro_export]
158macro_rules! log {
159 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
160 log::$level!(
161 target: crate::LOG_TARGET,
162 concat!("[{:?}] ๐ [{}]", $patter),
163 <frame_system::Pallet<T>>::block_number(),
164 <crate::Pallet::<T, I> as frame_support::traits::PalletInfoAccess>::name()
165 $(, $values)*
166 )
167 };
168}
169
170type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
171
172#[frame_support::pallet]
173pub mod pallet {
174 use super::*;
175 use frame_support::pallet_prelude::*;
176 use frame_system::pallet_prelude::*;
177
178 #[pallet::pallet]
179 pub struct Pallet<T, I = ()>(_);
180
181 #[pallet::config]
182 pub trait Config<I: 'static = ()>: frame_system::Config {
183 #[allow(deprecated)]
185 type RuntimeEvent: From<Event<Self, I>>
186 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
187
188 type WeightInfo: weights::WeightInfo;
190
191 type ScoreProvider: ScoreProvider<Self::AccountId, Score = Self::Score>;
193
194 #[pallet::constant]
238 type BagThresholds: Get<&'static [Self::Score]>;
239
240 #[pallet::constant]
245 type MaxAutoRebagPerBlock: Get<u32>;
246
247 type Score: Clone
249 + Default
250 + PartialEq
251 + Eq
252 + Ord
253 + PartialOrd
254 + core::fmt::Debug
255 + Copy
256 + AtLeast32BitUnsigned
257 + Bounded
258 + TypeInfo
259 + FullCodec
260 + MaxEncodedLen;
261 }
262
263 #[pallet::storage]
267 pub type ListNodes<T: Config<I>, I: 'static = ()> =
268 CountedStorageMap<_, Twox64Concat, T::AccountId, list::Node<T, I>>;
269
270 #[pallet::storage]
274 pub type ListBags<T: Config<I>, I: 'static = ()> =
275 StorageMap<_, Twox64Concat, T::Score, list::Bag<T, I>>;
276
277 #[pallet::storage]
280 pub type NextNodeAutoRebagged<T: Config<I>, I: 'static = ()> =
281 StorageValue<_, T::AccountId, OptionQuery>;
282
283 #[pallet::storage]
288 pub type Lock<T: Config<I>, I: 'static = ()> = StorageValue<_, (), OptionQuery>;
289
290 #[pallet::storage]
305 pub type PendingRebag<T: Config<I>, I: 'static = ()> =
306 CountedStorageMap<_, Twox64Concat, T::AccountId, ()>;
307
308 #[pallet::event]
309 #[pallet::generate_deposit(pub(crate) fn deposit_event)]
310 pub enum Event<T: Config<I>, I: 'static = ()> {
311 Rebagged { who: T::AccountId, from: T::Score, to: T::Score },
313 ScoreUpdated { who: T::AccountId, new_score: T::Score },
315 }
316
317 #[pallet::error]
318 pub enum Error<T, I = ()> {
319 List(ListError),
321 Locked,
323 }
324
325 impl<T, I> From<ListError> for Error<T, I> {
326 fn from(t: ListError) -> Self {
327 Error::<T, I>::List(t)
328 }
329 }
330
331 #[pallet::view_functions]
332 impl<T: Config<I>, I: 'static> Pallet<T, I> {
333 pub fn scores(who: T::AccountId) -> (Option<T::Score>, Option<T::Score>) {
342 (ListNodes::<T, I>::get(&who).map(|node| node.score), T::ScoreProvider::score(&who))
343 }
344 }
345
346 #[pallet::call]
347 impl<T: Config<I>, I: 'static> Pallet<T, I> {
348 #[pallet::call_index(0)]
359 #[pallet::weight(T::WeightInfo::rebag_non_terminal().max(T::WeightInfo::rebag_terminal()))]
360 pub fn rebag(origin: OriginFor<T>, dislocated: AccountIdLookupOf<T>) -> DispatchResult {
361 ensure_signed(origin)?;
362 let dislocated = T::Lookup::lookup(dislocated)?;
363 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
364
365 Self::rebag_internal(&dislocated).map_err::<DispatchError, _>(Into::into)?;
366
367 Ok(())
368 }
369
370 #[pallet::call_index(1)]
381 #[pallet::weight(T::WeightInfo::put_in_front_of())]
382 pub fn put_in_front_of(
383 origin: OriginFor<T>,
384 lighter: AccountIdLookupOf<T>,
385 ) -> DispatchResult {
386 let heavier = ensure_signed(origin)?;
387 let lighter = T::Lookup::lookup(lighter)?;
388 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
389 List::<T, I>::put_in_front_of(&lighter, &heavier)
390 .map_err::<Error<T, I>, _>(Into::into)
391 .map_err::<DispatchError, _>(Into::into)
392 }
393
394 #[pallet::call_index(2)]
398 #[pallet::weight(T::WeightInfo::put_in_front_of())]
399 pub fn put_in_front_of_other(
400 origin: OriginFor<T>,
401 heavier: AccountIdLookupOf<T>,
402 lighter: AccountIdLookupOf<T>,
403 ) -> DispatchResult {
404 ensure_signed(origin)?;
405 let lighter = T::Lookup::lookup(lighter)?;
406 let heavier = T::Lookup::lookup(heavier)?;
407 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
408 List::<T, I>::put_in_front_of(&lighter, &heavier)
409 .map_err::<Error<T, I>, _>(Into::into)
410 .map_err::<DispatchError, _>(Into::into)
411 }
412 }
413
414 #[pallet::hooks]
415 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
416 fn integrity_test() {
417 assert!(
420 T::BagThresholds::get().windows(2).all(|window| window[1] > window[0]),
421 "thresholds must strictly increase, and have no duplicates",
422 );
423 }
424
425 #[cfg(feature = "try-runtime")]
426 fn try_state(_: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
427 <Self as SortedListProvider<T::AccountId>>::try_state()
428 }
429
430 fn on_idle(_n: BlockNumberFor<T>, limit: Weight) -> Weight {
441 let mut meter = WeightMeter::with_limit(limit);
442 if meter.try_consume(T::WeightInfo::on_idle()).is_err() {
445 log!(debug, "Not enough Weight for on_idle. Skipping rebugging.");
446 return Weight::zero();
447 }
448
449 let rebag_budget = T::MaxAutoRebagPerBlock::get();
450 if rebag_budget == 0 {
451 log!(debug, "Auto-rebag skipped: rebag_budget=0");
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 account in to_process {
513 let pending_value =
514 if PendingRebag::<T, I>::contains_key(&account) { 1 } else { 0 };
515
516 match Self::rebag_internal(&account) {
517 Err(Error::<T, I>::Locked) => {
518 defensive!("Pallet became locked during auto-rebag, stopping");
519 break;
520 },
521 Err(e) => {
522 log!(warn, "Error during rebagging: {:?}", e);
523 failed_rebags += 1;
524 },
525 Ok(Some((from, to))) => {
526 log!(debug, "Rebagged {:?}: moved from {:?} to {:?}", account, from, to);
527 successful_rebags += 1;
528 pending_processed += pending_value;
529 },
530 Ok(None) => {
531 log!(debug, "Rebagging not needed for {:?}", account);
532 pending_processed += pending_value;
533 },
534 }
535
536 processed += 1;
537 if processed == rebag_budget {
538 break;
539 }
540 }
541
542 let next_regular_account =
544 next_cursor.iter().find(|account| !PendingRebag::<T, I>::contains_key(account));
545
546 match next_regular_account {
547 Some(next) if to_process.contains(next) => {
552 NextNodeAutoRebagged::<T, I>::kill();
553 defensive!("Loop detected: {:?} already processed โ cursor killed", next);
554 },
555 Some(next) => {
557 NextNodeAutoRebagged::<T, I>::put(next);
558 log!(debug, "Saved next node to be processed in rebag cursor: {:?}", next);
559 },
560 None => {
568 NextNodeAutoRebagged::<T, I>::kill();
569 log!(debug, "End of regular list reached โ cursor killed");
570 },
571 }
572
573 let weight_used = meter.consumed();
574 log!(
575 debug,
576 "Auto-rebag finished: processed={}, successful_rebags={}, errors={}, pending_processed={}, weight_used={:?}",
577 processed,
578 successful_rebags,
579 failed_rebags,
580 pending_processed,
581 weight_used
582 );
583
584 weight_used
585 }
586 }
587}
588
589#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
590impl<T: Config<I>, I: 'static> Pallet<T, I> {
591 pub fn do_try_state() -> Result<(), TryRuntimeError> {
592 List::<T, I>::do_try_state()
593 }
594}
595
596impl<T: Config<I>, I: 'static> Pallet<T, I> {
597 pub fn do_rebag(
601 account: &T::AccountId,
602 new_score: T::Score,
603 ) -> Result<Option<(T::Score, T::Score)>, ListError> {
604 let node = list::Node::<T, I>::get(&account).ok_or(ListError::NodeNotFound)?;
606 if node.score != new_score {
607 Self::deposit_event(Event::<T, I>::ScoreUpdated { who: account.clone(), new_score });
608 }
609 let maybe_movement = List::update_position_for(node, new_score);
610 if let Some((from, to)) = maybe_movement {
611 Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
612 };
613 Ok(maybe_movement)
614 }
615
616 fn ensure_unlocked() -> Result<(), ListError> {
617 match Lock::<T, I>::get() {
618 None => Ok(()),
619 Some(()) => Err(ListError::Locked),
620 }
621 }
622
623 #[cfg(feature = "std")]
625 pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
626 ListBags::get(score)
627 }
628
629 fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
634 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
636
637 PendingRebag::<T, I>::remove(account);
638
639 let existed = ListNodes::<T, I>::contains_key(account);
641 let maybe_score = T::ScoreProvider::score(account);
642
643 match (existed, maybe_score) {
644 (true, Some(current_score)) => {
645 log!(debug, "Attempting to rebag node {:?}", account);
647 Pallet::<T, I>::do_rebag(account, current_score)
648 .map_err::<Error<T, I>, _>(Into::into)
649 },
650 (false, Some(current_score)) => {
651 log!(debug, "Inserting node {:?} with score {:?}", account, current_score);
653 List::<T, I>::insert(account.clone(), current_score)
654 .map_err::<Error<T, I>, _>(Into::into)?;
655 Ok(None)
656 },
657 (true, None) => {
658 log!(debug, "Removing node {:?}", account);
660 List::<T, I>::remove(account).map_err::<Error<T, I>, _>(Into::into)?;
661 Ok(None)
662 },
663 (false, None) => {
664 Err(Error::<T, I>::List(ListError::NodeNotFound))
666 },
667 }
668 }
669}
670
671impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
672 type Error = ListError;
673 type Score = T::Score;
674
675 fn range() -> (Self::Score, Self::Score) {
676 use frame_support::traits::Get;
677 (
678 T::BagThresholds::get().first().cloned().unwrap_or_default(),
679 T::BagThresholds::get().last().cloned().unwrap_or_default(),
680 )
681 }
682
683 fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
684 Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
685 }
686
687 fn lock() {
688 Lock::<T, I>::put(())
689 }
690
691 fn unlock() {
692 Lock::<T, I>::kill()
693 }
694
695 fn iter_from(
696 start: &T::AccountId,
697 ) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
698 let iter = List::<T, I>::iter_from(start)?;
699 Ok(Box::new(iter.map(|n| n.id().clone())))
700 }
701
702 fn count() -> u32 {
703 ListNodes::<T, I>::count()
704 }
705
706 fn contains(id: &T::AccountId) -> bool {
707 List::<T, I>::contains(id)
708 }
709
710 fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
711 Pallet::<T, I>::ensure_unlocked().inspect_err(|_| {
712 if T::MaxAutoRebagPerBlock::get() > 0u32 {
715 PendingRebag::<T, I>::insert(&id, ());
716 }
717 })?;
718 List::<T, I>::insert(id, score)
719 }
720
721 fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> {
722 Pallet::<T, I>::ensure_unlocked()?;
723 Pallet::<T, I>::do_rebag(id, new_score).map(|_| ())
724 }
725
726 fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
727 List::<T, I>::get_score(id)
728 }
729
730 fn on_remove(id: &T::AccountId) -> Result<(), ListError> {
731 Pallet::<T, I>::ensure_unlocked()?;
732 List::<T, I>::remove(id)
733 }
734
735 fn unsafe_regenerate(
736 all: impl IntoIterator<Item = T::AccountId>,
737 score_of: Box<dyn Fn(&T::AccountId) -> Option<T::Score>>,
738 ) -> u32 {
739 List::<T, I>::unsafe_regenerate(all, score_of)
743 }
744
745 fn unsafe_clear() {
746 List::<T, I>::unsafe_clear()
750 }
751
752 #[cfg(feature = "try-runtime")]
753 fn try_state() -> Result<(), TryRuntimeError> {
754 Self::do_try_state()
755 }
756
757 frame_election_provider_support::runtime_benchmarks_enabled! {
758 fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
759 use frame_support::traits::Get as _;
760 let thresholds = T::BagThresholds::get();
761 let node = list::Node::<T, I>::get(who).unwrap();
762 let current_bag_idx = thresholds
763 .iter()
764 .chain(core::iter::once(&T::Score::max_value()))
765 .position(|w| w == &node.bag_upper)
766 .unwrap();
767
768 if is_increase {
769 let next_threshold_idx = current_bag_idx + 1;
770 assert!(thresholds.len() > next_threshold_idx);
771 thresholds[next_threshold_idx]
772 } else {
773 assert!(current_bag_idx != 0);
774 let prev_threshold_idx = current_bag_idx - 1;
775 thresholds[prev_threshold_idx]
776 }
777 }
778 }
779}
780
781impl<T: Config<I>, I: 'static> ScoreProvider<T::AccountId> for Pallet<T, I> {
782 type Score = <Pallet<T, I> as SortedListProvider<T::AccountId>>::Score;
783
784 fn score(id: &T::AccountId) -> Option<T::Score> {
785 Node::<T, I>::get(id).map(|node| node.score())
786 }
787
788 frame_election_provider_support::runtime_benchmarks_or_std_enabled! {
789 fn set_score_of(id: &T::AccountId, new_score: T::Score) {
790 ListNodes::<T, I>::mutate(id, |maybe_node| {
791 if let Some(node) = maybe_node.as_mut() {
792 node.score = new_score;
793 } else {
794 panic!("trying to mutate {:?} which does not exists", id);
795 }
796 })
797 }
798 }
799}