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
139mod 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 let maybe_movement = List::update_position_for(node, new_score);
557 if let Some((from, to)) = maybe_movement {
558 Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
559 };
560 Self::deposit_event(Event::<T, I>::ScoreUpdated { who: account.clone(), new_score });
561 Ok(maybe_movement)
562 }
563
564 fn ensure_unlocked() -> Result<(), ListError> {
565 match Lock::<T, I>::get() {
566 None => Ok(()),
567 Some(()) => Err(ListError::Locked),
568 }
569 }
570
571 #[cfg(feature = "std")]
573 pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
574 ListBags::get(score)
575 }
576
577 fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
582 Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
584
585 let existed = ListNodes::<T, I>::contains_key(account);
587 let score_provider: fn(&T::AccountId) -> Option<T::Score> = T::ScoreProvider::score;
588 let maybe_score = score_provider(account);
589
590 match (existed, maybe_score) {
591 (true, Some(current_score)) => {
592 log!(debug, "Attempting to rebag node {:?}", account);
594 Pallet::<T, I>::do_rebag(account, current_score)
595 .map_err::<Error<T, I>, _>(Into::into)
596 },
597 (false, Some(current_score)) => {
598 log!(debug, "Inserting node {:?} with score {:?}", account, current_score);
600 List::<T, I>::insert(account.clone(), current_score)
601 .map_err::<Error<T, I>, _>(Into::into)?;
602 Ok(None)
603 },
604 (true, None) => {
605 log!(debug, "Removing node {:?}", account);
607 List::<T, I>::remove(account).map_err::<Error<T, I>, _>(Into::into)?;
608 Ok(None)
609 },
610 (false, None) => {
611 Err(Error::<T, I>::List(ListError::NodeNotFound))
613 },
614 }
615 }
616}
617
618impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
619 type Error = ListError;
620 type Score = T::Score;
621
622 fn range() -> (Self::Score, Self::Score) {
623 use frame_support::traits::Get;
624 (
625 T::BagThresholds::get().first().cloned().unwrap_or_default(),
626 T::BagThresholds::get().last().cloned().unwrap_or_default(),
627 )
628 }
629
630 fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
631 Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
632 }
633
634 fn lock() {
635 Lock::<T, I>::put(())
636 }
637
638 fn unlock() {
639 Lock::<T, I>::kill()
640 }
641
642 fn iter_from(
643 start: &T::AccountId,
644 ) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
645 let iter = List::<T, I>::iter_from(start)?;
646 Ok(Box::new(iter.map(|n| n.id().clone())))
647 }
648
649 fn count() -> u32 {
650 ListNodes::<T, I>::count()
651 }
652
653 fn contains(id: &T::AccountId) -> bool {
654 List::<T, I>::contains(id)
655 }
656
657 fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
658 Pallet::<T, I>::ensure_unlocked()?;
659 List::<T, I>::insert(id, score)
660 }
661
662 fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> {
663 Pallet::<T, I>::ensure_unlocked()?;
664 Pallet::<T, I>::do_rebag(id, new_score).map(|_| ())
665 }
666
667 fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
668 List::<T, I>::get_score(id)
669 }
670
671 fn on_remove(id: &T::AccountId) -> Result<(), ListError> {
672 Pallet::<T, I>::ensure_unlocked()?;
673 List::<T, I>::remove(id)
674 }
675
676 fn unsafe_regenerate(
677 all: impl IntoIterator<Item = T::AccountId>,
678 score_of: Box<dyn Fn(&T::AccountId) -> Option<T::Score>>,
679 ) -> u32 {
680 List::<T, I>::unsafe_regenerate(all, score_of)
684 }
685
686 fn unsafe_clear() {
687 List::<T, I>::unsafe_clear()
691 }
692
693 #[cfg(feature = "try-runtime")]
694 fn try_state() -> Result<(), TryRuntimeError> {
695 Self::do_try_state()
696 }
697
698 frame_election_provider_support::runtime_benchmarks_enabled! {
699 fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
700 use frame_support::traits::Get as _;
701 let thresholds = T::BagThresholds::get();
702 let node = list::Node::<T, I>::get(who).unwrap();
703 let current_bag_idx = thresholds
704 .iter()
705 .chain(core::iter::once(&T::Score::max_value()))
706 .position(|w| w == &node.bag_upper)
707 .unwrap();
708
709 if is_increase {
710 let next_threshold_idx = current_bag_idx + 1;
711 assert!(thresholds.len() > next_threshold_idx);
712 thresholds[next_threshold_idx]
713 } else {
714 assert!(current_bag_idx != 0);
715 let prev_threshold_idx = current_bag_idx - 1;
716 thresholds[prev_threshold_idx]
717 }
718 }
719 }
720}
721
722impl<T: Config<I>, I: 'static> ScoreProvider<T::AccountId> for Pallet<T, I> {
723 type Score = <Pallet<T, I> as SortedListProvider<T::AccountId>>::Score;
724
725 fn score(id: &T::AccountId) -> Option<T::Score> {
726 Node::<T, I>::get(id).map(|node| node.score())
727 }
728
729 frame_election_provider_support::runtime_benchmarks_or_std_enabled! {
730 fn set_score_of(id: &T::AccountId, new_score: T::Score) {
731 ListNodes::<T, I>::mutate(id, |maybe_node| {
732 if let Some(node) = maybe_node.as_mut() {
733 node.score = new_score;
734 } else {
735 panic!("trying to mutate {:?} which does not exists", id);
736 }
737 })
738 }
739 }
740}