mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 02:21:04 +00:00
Make bags-list generic over node value and instantiable (#10997)
* make instantiable * update * cargo fmt * Clean up * bags-list: Make it generic over node value * Respond to some feedback * Apply suggestions from code review Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Add back default impl for weight update worst case * Update to Score in more places' * Use VoteWeight, not u64 to reduce test diff * FMT * FullCodec implies Codec * formatting * Fixup bags list remote test Co-authored-by: doordashcon <jesse.chejieh@gmail.com> Co-authored-by: Doordashcon <90750465+Doordashcon@users.noreply.github.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
|
||||
//! Utilities for remote-testing pallet-bags-list.
|
||||
|
||||
use frame_election_provider_support::ScoreProvider;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
/// A common log target to use.
|
||||
@@ -55,8 +56,12 @@ pub fn display_and_check_bags<Runtime: RuntimeT>(currency_unit: u64, currency_na
|
||||
let mut rebaggable = 0;
|
||||
let mut active_bags = 0;
|
||||
for vote_weight_thresh in <Runtime as pallet_bags_list::Config>::BagThresholds::get() {
|
||||
let vote_weight_thresh_u64: u64 = (*vote_weight_thresh)
|
||||
.try_into()
|
||||
.map_err(|_| "runtime must configure score to at most u64 to use this test")
|
||||
.unwrap();
|
||||
// threshold in terms of UNITS (e.g. KSM, DOT etc)
|
||||
let vote_weight_thresh_as_unit = *vote_weight_thresh as f64 / currency_unit as f64;
|
||||
let vote_weight_thresh_as_unit = vote_weight_thresh_u64 as f64 / currency_unit as f64;
|
||||
let pretty_thresh = format!("Threshold: {}. {}", vote_weight_thresh_as_unit, currency_name);
|
||||
|
||||
let bag = match pallet_bags_list::Pallet::<Runtime>::list_bags_get(*vote_weight_thresh) {
|
||||
@@ -70,9 +75,13 @@ pub fn display_and_check_bags<Runtime: RuntimeT>(currency_unit: u64, currency_na
|
||||
active_bags += 1;
|
||||
|
||||
for id in bag.std_iter().map(|node| node.std_id().clone()) {
|
||||
let vote_weight = pallet_staking::Pallet::<Runtime>::weight_of(&id);
|
||||
let vote_weight = <Runtime as pallet_bags_list::Config>::ScoreProvider::score(&id);
|
||||
let vote_weight_thresh_u64: u64 = (*vote_weight_thresh)
|
||||
.try_into()
|
||||
.map_err(|_| "runtime must configure score to at most u64 to use this test")
|
||||
.unwrap();
|
||||
let vote_weight_as_balance: pallet_staking::BalanceOf<Runtime> =
|
||||
vote_weight.try_into().map_err(|_| "can't convert").unwrap();
|
||||
vote_weight_thresh_u64.try_into().map_err(|_| "can't convert").unwrap();
|
||||
|
||||
if vote_weight_as_balance < min_nominator_bond {
|
||||
log::trace!(
|
||||
@@ -87,13 +96,17 @@ pub fn display_and_check_bags<Runtime: RuntimeT>(currency_unit: u64, currency_na
|
||||
pallet_bags_list::Node::<Runtime>::get(&id).expect("node in bag must exist.");
|
||||
if node.is_misplaced(vote_weight) {
|
||||
rebaggable += 1;
|
||||
let notional_bag = pallet_bags_list::notional_bag_for::<Runtime, _>(vote_weight);
|
||||
let notional_bag_as_u64: u64 = notional_bag
|
||||
.try_into()
|
||||
.map_err(|_| "runtime must configure score to at most u64 to use this test")
|
||||
.unwrap();
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Account {:?} can be rebagged from {:?} to {:?}",
|
||||
id,
|
||||
vote_weight_thresh_as_unit,
|
||||
pallet_bags_list::notional_bag_for::<Runtime>(vote_weight) as f64 /
|
||||
currency_unit as f64
|
||||
notional_bag_as_u64 as f64 / currency_unit as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
use super::*;
|
||||
use crate::list::List;
|
||||
use frame_benchmarking::{account, whitelist_account, whitelisted_caller};
|
||||
use frame_election_provider_support::VoteWeightProvider;
|
||||
use frame_election_provider_support::ScoreProvider;
|
||||
use frame_support::{assert_ok, traits::Get};
|
||||
use frame_system::RawOrigin as SystemOrigin;
|
||||
use sp_runtime::traits::One;
|
||||
|
||||
frame_benchmarking::benchmarks! {
|
||||
rebag_non_terminal {
|
||||
@@ -36,7 +37,7 @@ frame_benchmarking::benchmarks! {
|
||||
|
||||
// clear any pre-existing storage.
|
||||
// NOTE: safe to call outside block production
|
||||
List::<T>::unsafe_clear();
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
// define our origin and destination thresholds.
|
||||
let origin_bag_thresh = T::BagThresholds::get()[0];
|
||||
@@ -44,21 +45,21 @@ frame_benchmarking::benchmarks! {
|
||||
|
||||
// seed items in the origin bag.
|
||||
let origin_head: T::AccountId = account("origin_head", 0, 0);
|
||||
assert_ok!(List::<T>::insert(origin_head.clone(), origin_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(origin_head.clone(), origin_bag_thresh));
|
||||
|
||||
let origin_middle: T::AccountId = account("origin_middle", 0, 0); // the node we rebag (_R_)
|
||||
assert_ok!(List::<T>::insert(origin_middle.clone(), origin_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(origin_middle.clone(), origin_bag_thresh));
|
||||
|
||||
let origin_tail: T::AccountId = account("origin_tail", 0, 0);
|
||||
assert_ok!(List::<T>::insert(origin_tail.clone(), origin_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(origin_tail.clone(), origin_bag_thresh));
|
||||
|
||||
// seed items in the destination bag.
|
||||
let dest_head: T::AccountId = account("dest_head", 0, 0);
|
||||
assert_ok!(List::<T>::insert(dest_head.clone(), dest_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(dest_head.clone(), dest_bag_thresh));
|
||||
|
||||
// the bags are in the expected state after initial setup.
|
||||
assert_eq!(
|
||||
List::<T>::get_bags(),
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(origin_bag_thresh, vec![origin_head.clone(), origin_middle.clone(), origin_tail.clone()]),
|
||||
(dest_bag_thresh, vec![dest_head.clone()])
|
||||
@@ -67,12 +68,12 @@ frame_benchmarking::benchmarks! {
|
||||
|
||||
let caller = whitelisted_caller();
|
||||
// update the weight of `origin_middle` to guarantee it will be rebagged into the destination.
|
||||
T::VoteWeightProvider::set_vote_weight_of(&origin_middle, dest_bag_thresh);
|
||||
T::ScoreProvider::set_score_of(&origin_middle, dest_bag_thresh);
|
||||
}: rebag(SystemOrigin::Signed(caller), origin_middle.clone())
|
||||
verify {
|
||||
// check the bags have updated as expected.
|
||||
assert_eq!(
|
||||
List::<T>::get_bags(),
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(
|
||||
origin_bag_thresh,
|
||||
@@ -104,18 +105,18 @@ frame_benchmarking::benchmarks! {
|
||||
|
||||
// seed items in the origin bag.
|
||||
let origin_head: T::AccountId = account("origin_head", 0, 0);
|
||||
assert_ok!(List::<T>::insert(origin_head.clone(), origin_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(origin_head.clone(), origin_bag_thresh));
|
||||
|
||||
let origin_tail: T::AccountId = account("origin_tail", 0, 0); // the node we rebag (_R_)
|
||||
assert_ok!(List::<T>::insert(origin_tail.clone(), origin_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(origin_tail.clone(), origin_bag_thresh));
|
||||
|
||||
// seed items in the destination bag.
|
||||
let dest_head: T::AccountId = account("dest_head", 0, 0);
|
||||
assert_ok!(List::<T>::insert(dest_head.clone(), dest_bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(dest_head.clone(), dest_bag_thresh));
|
||||
|
||||
// the bags are in the expected state after initial setup.
|
||||
assert_eq!(
|
||||
List::<T>::get_bags(),
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(origin_bag_thresh, vec![origin_head.clone(), origin_tail.clone()]),
|
||||
(dest_bag_thresh, vec![dest_head.clone()])
|
||||
@@ -124,12 +125,12 @@ frame_benchmarking::benchmarks! {
|
||||
|
||||
let caller = whitelisted_caller();
|
||||
// update the weight of `origin_tail` to guarantee it will be rebagged into the destination.
|
||||
T::VoteWeightProvider::set_vote_weight_of(&origin_tail, dest_bag_thresh);
|
||||
T::ScoreProvider::set_score_of(&origin_tail, dest_bag_thresh);
|
||||
}: rebag(SystemOrigin::Signed(caller), origin_tail.clone())
|
||||
verify {
|
||||
// check the bags have updated as expected.
|
||||
assert_eq!(
|
||||
List::<T>::get_bags(),
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(origin_bag_thresh, vec![origin_head.clone()]),
|
||||
(dest_bag_thresh, vec![dest_head.clone(), origin_tail.clone()])
|
||||
@@ -147,22 +148,22 @@ frame_benchmarking::benchmarks! {
|
||||
|
||||
// insert the nodes in order
|
||||
let lighter: T::AccountId = account("lighter", 0, 0);
|
||||
assert_ok!(List::<T>::insert(lighter.clone(), bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(lighter.clone(), bag_thresh));
|
||||
|
||||
let heavier_prev: T::AccountId = account("heavier_prev", 0, 0);
|
||||
assert_ok!(List::<T>::insert(heavier_prev.clone(), bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(heavier_prev.clone(), bag_thresh));
|
||||
|
||||
let heavier: T::AccountId = account("heavier", 0, 0);
|
||||
assert_ok!(List::<T>::insert(heavier.clone(), bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(heavier.clone(), bag_thresh));
|
||||
|
||||
let heavier_next: T::AccountId = account("heavier_next", 0, 0);
|
||||
assert_ok!(List::<T>::insert(heavier_next.clone(), bag_thresh));
|
||||
assert_ok!(List::<T, _>::insert(heavier_next.clone(), bag_thresh));
|
||||
|
||||
T::VoteWeightProvider::set_vote_weight_of(&lighter, bag_thresh - 1);
|
||||
T::VoteWeightProvider::set_vote_weight_of(&heavier, bag_thresh);
|
||||
T::ScoreProvider::set_score_of(&lighter, bag_thresh - One::one());
|
||||
T::ScoreProvider::set_score_of(&heavier, bag_thresh);
|
||||
|
||||
assert_eq!(
|
||||
List::<T>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
|
||||
List::<T, _>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
|
||||
vec![lighter.clone(), heavier_prev.clone(), heavier.clone(), heavier_next.clone()]
|
||||
);
|
||||
|
||||
@@ -170,7 +171,7 @@ frame_benchmarking::benchmarks! {
|
||||
}: _(SystemOrigin::Signed(heavier.clone()), lighter.clone())
|
||||
verify {
|
||||
assert_eq!(
|
||||
List::<T>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
|
||||
List::<T, _>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
|
||||
vec![heavier, lighter, heavier_prev, heavier_next]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
|
||||
//! # Bags-List Pallet
|
||||
//!
|
||||
//! A semi-sorted list, where items hold an `AccountId` based on some `VoteWeight`. The `AccountId`
|
||||
//! (`id` for short) might be synonym to a `voter` or `nominator` in some context, and `VoteWeight`
|
||||
//! signifies the chance of each id being included in the final [`SortedListProvider::iter`].
|
||||
//! A semi-sorted list, where items hold an `AccountId` based on some `Score`. The
|
||||
//! `AccountId` (`id` for short) might be synonym to a `voter` or `nominator` in some context, and
|
||||
//! `Score` signifies the chance of each id being included in the final
|
||||
//! [`SortedListProvider::iter`].
|
||||
//!
|
||||
//! It implements [`frame_election_provider_support::SortedListProvider`] to provide a semi-sorted
|
||||
//! list of accounts to another pallet. It needs some other pallet to give it some information about
|
||||
//! the weights of accounts via [`frame_election_provider_support::VoteWeightProvider`].
|
||||
//! the weights of accounts via [`frame_election_provider_support::ScoreProvider`].
|
||||
//!
|
||||
//! This pallet is not configurable at genesis. Whoever uses it should call appropriate functions of
|
||||
//! the `SortedListProvider` (e.g. `on_insert`, or `unsafe_regenerate`) at their genesis.
|
||||
@@ -33,12 +34,12 @@
|
||||
//! The data structure exposed by this pallet aims to be optimized for:
|
||||
//!
|
||||
//! - insertions and removals.
|
||||
//! - iteration over the top* N items by weight, where the precise ordering of items doesn't
|
||||
//! - iteration over the top* N items by score, where the precise ordering of items doesn't
|
||||
//! particularly matter.
|
||||
//!
|
||||
//! # Details
|
||||
//!
|
||||
//! - items are kept in bags, which are delineated by their range of weight (See
|
||||
//! - items are kept in bags, which are delineated by their range of score (See
|
||||
//! [`Config::BagThresholds`]).
|
||||
//! - for iteration, bags are chained together from highest to lowest and elements within the bag
|
||||
//! are iterated from head to tail.
|
||||
@@ -46,14 +47,16 @@
|
||||
//! it will worsen its position in list iteration; this reduces incentives for some types of spam
|
||||
//! that involve consistently removing and inserting for better position. Further, ordering
|
||||
//! granularity is thus dictated by range between each bag threshold.
|
||||
//! - if an item's weight changes to a value no longer within the range of its current bag the
|
||||
//! item's position will need to be updated by an external actor with rebag (update), or removal
|
||||
//! and insertion.
|
||||
//! - if an item's score changes to a value no longer within the range of its current bag the item's
|
||||
//! position will need to be updated by an external actor with rebag (update), or removal and
|
||||
//! insertion.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_election_provider_support::{SortedListProvider, VoteWeight, VoteWeightProvider};
|
||||
use codec::FullCodec;
|
||||
use frame_election_provider_support::{ScoreProvider, SortedListProvider};
|
||||
use frame_system::ensure_signed;
|
||||
use sp_runtime::traits::{AtLeast32BitUnsigned, Bounded};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
@@ -92,38 +95,38 @@ pub mod pallet {
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(crate) trait Store)]
|
||||
pub struct Pallet<T>(_);
|
||||
pub struct Pallet<T, I = ()>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
pub trait Config<I: 'static = ()>: frame_system::Config {
|
||||
/// The overarching event type.
|
||||
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
|
||||
type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>;
|
||||
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: weights::WeightInfo;
|
||||
|
||||
/// Something that provides the weights of ids.
|
||||
type VoteWeightProvider: VoteWeightProvider<Self::AccountId>;
|
||||
/// Something that provides the scores of ids.
|
||||
type ScoreProvider: ScoreProvider<Self::AccountId, Score = Self::Score>;
|
||||
|
||||
/// The list of thresholds separating the various bags.
|
||||
///
|
||||
/// Ids are separated into unsorted bags according to their vote weight. This specifies the
|
||||
/// thresholds separating the bags. An id's bag is the largest bag for which the id's weight
|
||||
/// Ids are separated into unsorted bags according to their score. This specifies the
|
||||
/// thresholds separating the bags. An id's bag is the largest bag for which the id's score
|
||||
/// is less than or equal to its upper threshold.
|
||||
///
|
||||
/// When ids are iterated, higher bags are iterated completely before lower bags. This means
|
||||
/// that iteration is _semi-sorted_: ids of higher weight tend to come before ids of lower
|
||||
/// weight, but peer ids within a particular bag are sorted in insertion order.
|
||||
/// that iteration is _semi-sorted_: ids of higher score tend to come before ids of lower
|
||||
/// score, but peer ids within a particular bag are sorted in insertion order.
|
||||
///
|
||||
/// # Expressing the constant
|
||||
///
|
||||
/// This constant must be sorted in strictly increasing order. Duplicate items are not
|
||||
/// permitted.
|
||||
///
|
||||
/// There is an implied upper limit of `VoteWeight::MAX`; that value does not need to be
|
||||
/// There is an implied upper limit of `Score::MAX`; that value does not need to be
|
||||
/// specified within the bag. For any two threshold lists, if one ends with
|
||||
/// `VoteWeight::MAX`, the other one does not, and they are otherwise equal, the two lists
|
||||
/// will behave identically.
|
||||
/// `Score::MAX`, the other one does not, and they are otherwise equal, the two
|
||||
/// lists will behave identically.
|
||||
///
|
||||
/// # Calculation
|
||||
///
|
||||
@@ -141,52 +144,68 @@ pub mod pallet {
|
||||
/// the procedure given above, then the constant ratio is equal to 2.
|
||||
/// - If `BagThresholds::get().len() == 200`, and the thresholds are determined according to
|
||||
/// the procedure given above, then the constant ratio is approximately equal to 1.248.
|
||||
/// - If the threshold list begins `[1, 2, 3, ...]`, then an id with weight 0 or 1 will fall
|
||||
/// into bag 0, an id with weight 2 will fall into bag 1, etc.
|
||||
/// - If the threshold list begins `[1, 2, 3, ...]`, then an id with score 0 or 1 will fall
|
||||
/// into bag 0, an id with score 2 will fall into bag 1, etc.
|
||||
///
|
||||
/// # Migration
|
||||
///
|
||||
/// In the event that this list ever changes, a copy of the old bags list must be retained.
|
||||
/// With that `List::migrate` can be called, which will perform the appropriate migration.
|
||||
#[pallet::constant]
|
||||
type BagThresholds: Get<&'static [VoteWeight]>;
|
||||
type BagThresholds: Get<&'static [Self::Score]>;
|
||||
|
||||
/// The type used to dictate a node position relative to other nodes.
|
||||
type Score: Clone
|
||||
+ Default
|
||||
+ PartialEq
|
||||
+ Eq
|
||||
+ Ord
|
||||
+ PartialOrd
|
||||
+ sp_std::fmt::Debug
|
||||
+ Copy
|
||||
+ AtLeast32BitUnsigned
|
||||
+ Bounded
|
||||
+ TypeInfo
|
||||
+ FullCodec
|
||||
+ MaxEncodedLen;
|
||||
}
|
||||
|
||||
/// A single node, within some bag.
|
||||
///
|
||||
/// Nodes store links forward and back within their respective bags.
|
||||
#[pallet::storage]
|
||||
pub(crate) type ListNodes<T: Config> =
|
||||
CountedStorageMap<_, Twox64Concat, T::AccountId, list::Node<T>>;
|
||||
pub(crate) type ListNodes<T: Config<I>, I: 'static = ()> =
|
||||
CountedStorageMap<_, Twox64Concat, T::AccountId, list::Node<T, I>>;
|
||||
|
||||
/// A bag stored in storage.
|
||||
///
|
||||
/// Stores a `Bag` struct, which stores head and tail pointers to itself.
|
||||
#[pallet::storage]
|
||||
pub(crate) type ListBags<T: Config> = StorageMap<_, Twox64Concat, VoteWeight, list::Bag<T>>;
|
||||
pub(crate) type ListBags<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Twox64Concat, T::Score, list::Bag<T, I>>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
/// Moved an account from one bag to another.
|
||||
Rebagged { who: T::AccountId, from: VoteWeight, to: VoteWeight },
|
||||
Rebagged { who: T::AccountId, from: T::Score, to: T::Score },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum Error<T> {
|
||||
pub enum Error<T, I = ()> {
|
||||
/// Attempted to place node in front of a node in another bag.
|
||||
NotInSameBag,
|
||||
/// Id not found in list.
|
||||
IdNotFound,
|
||||
/// An Id does not have a greater vote weight than another Id.
|
||||
/// An Id does not have a greater score than another Id.
|
||||
NotHeavier,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Declare that some `dislocated` account has, through rewards or penalties, sufficiently
|
||||
/// changed its weight that it should properly fall into a different bag than its current
|
||||
/// changed its score that it should properly fall into a different bag than its current
|
||||
/// one.
|
||||
///
|
||||
/// Anyone can call this function about any potentially dislocated account.
|
||||
@@ -196,8 +215,8 @@ pub mod pallet {
|
||||
#[pallet::weight(T::WeightInfo::rebag_non_terminal().max(T::WeightInfo::rebag_terminal()))]
|
||||
pub fn rebag(origin: OriginFor<T>, dislocated: T::AccountId) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
let current_weight = T::VoteWeightProvider::vote_weight(&dislocated);
|
||||
let _ = Pallet::<T>::do_rebag(&dislocated, current_weight);
|
||||
let current_score = T::ScoreProvider::score(&dislocated);
|
||||
let _ = Pallet::<T, I>::do_rebag(&dislocated, current_score);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -208,16 +227,16 @@ pub mod pallet {
|
||||
///
|
||||
/// Only works if
|
||||
/// - both nodes are within the same bag,
|
||||
/// - and `origin` has a greater `VoteWeight` than `lighter`.
|
||||
/// - and `origin` has a greater `Score` than `lighter`.
|
||||
#[pallet::weight(T::WeightInfo::put_in_front_of())]
|
||||
pub fn put_in_front_of(origin: OriginFor<T>, lighter: T::AccountId) -> DispatchResult {
|
||||
let heavier = ensure_signed(origin)?;
|
||||
List::<T>::put_in_front_of(&lighter, &heavier).map_err(Into::into)
|
||||
List::<T, I>::put_in_front_of(&lighter, &heavier).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
|
||||
fn integrity_test() {
|
||||
// ensure they are strictly increasing, this also implies that duplicates are detected.
|
||||
assert!(
|
||||
@@ -228,71 +247,70 @@ pub mod pallet {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Move an account from one bag to another, depositing an event on success.
|
||||
///
|
||||
/// If the account changed bags, returns `Some((from, to))`.
|
||||
pub fn do_rebag(
|
||||
account: &T::AccountId,
|
||||
new_weight: VoteWeight,
|
||||
) -> Option<(VoteWeight, VoteWeight)> {
|
||||
pub fn do_rebag(account: &T::AccountId, new_weight: T::Score) -> Option<(T::Score, T::Score)> {
|
||||
// if no voter at that node, don't do anything.
|
||||
// the caller just wasted the fee to call this.
|
||||
let maybe_movement = list::Node::<T>::get(&account)
|
||||
let maybe_movement = list::Node::<T, I>::get(&account)
|
||||
.and_then(|node| List::update_position_for(node, new_weight));
|
||||
if let Some((from, to)) = maybe_movement {
|
||||
Self::deposit_event(Event::<T>::Rebagged { who: account.clone(), from, to });
|
||||
Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
|
||||
};
|
||||
maybe_movement
|
||||
}
|
||||
|
||||
/// Equivalent to `ListBags::get`, but public. Useful for tests in outside of this crate.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn list_bags_get(weight: VoteWeight) -> Option<list::Bag<T>> {
|
||||
ListBags::get(weight)
|
||||
pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
|
||||
ListBags::get(score)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> SortedListProvider<T::AccountId> for Pallet<T> {
|
||||
impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
|
||||
type Error = Error;
|
||||
|
||||
type Score = T::Score;
|
||||
|
||||
fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
|
||||
Box::new(List::<T>::iter().map(|n| n.id().clone()))
|
||||
Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
|
||||
}
|
||||
|
||||
fn count() -> u32 {
|
||||
ListNodes::<T>::count()
|
||||
ListNodes::<T, I>::count()
|
||||
}
|
||||
|
||||
fn contains(id: &T::AccountId) -> bool {
|
||||
List::<T>::contains(id)
|
||||
List::<T, I>::contains(id)
|
||||
}
|
||||
|
||||
fn on_insert(id: T::AccountId, weight: VoteWeight) -> Result<(), Error> {
|
||||
List::<T>::insert(id, weight)
|
||||
fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), Error> {
|
||||
List::<T, I>::insert(id, score)
|
||||
}
|
||||
|
||||
fn on_update(id: &T::AccountId, new_weight: VoteWeight) {
|
||||
Pallet::<T>::do_rebag(id, new_weight);
|
||||
fn on_update(id: &T::AccountId, new_score: T::Score) {
|
||||
Pallet::<T, I>::do_rebag(id, new_score);
|
||||
}
|
||||
|
||||
fn on_remove(id: &T::AccountId) {
|
||||
List::<T>::remove(id)
|
||||
List::<T, I>::remove(id)
|
||||
}
|
||||
|
||||
fn unsafe_regenerate(
|
||||
all: impl IntoIterator<Item = T::AccountId>,
|
||||
weight_of: Box<dyn Fn(&T::AccountId) -> VoteWeight>,
|
||||
score_of: Box<dyn Fn(&T::AccountId) -> T::Score>,
|
||||
) -> u32 {
|
||||
// NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_regenerate.
|
||||
// I.e. because it can lead to many storage accesses.
|
||||
// So it is ok to call it as caller must ensure the conditions.
|
||||
List::<T>::unsafe_regenerate(all, weight_of)
|
||||
List::<T, I>::unsafe_regenerate(all, score_of)
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn sanity_check() -> Result<(), &'static str> {
|
||||
List::<T>::sanity_check()
|
||||
List::<T, I>::sanity_check()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
@@ -304,17 +322,17 @@ impl<T: Config> SortedListProvider<T::AccountId> for Pallet<T> {
|
||||
// NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_clear.
|
||||
// I.e. because it can lead to many storage accesses.
|
||||
// So it is ok to call it as caller must ensure the conditions.
|
||||
List::<T>::unsafe_clear()
|
||||
List::<T, I>::unsafe_clear()
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn weight_update_worst_case(who: &T::AccountId, is_increase: bool) -> VoteWeight {
|
||||
fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
|
||||
use frame_support::traits::Get as _;
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let node = list::Node::<T>::get(who).unwrap();
|
||||
let node = list::Node::<T, I>::get(who).unwrap();
|
||||
let current_bag_idx = thresholds
|
||||
.iter()
|
||||
.chain(sp_std::iter::once(&VoteWeight::MAX))
|
||||
.chain(sp_std::iter::once(&T::Score::max_value()))
|
||||
.position(|w| w == &node.bag_upper())
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
|
||||
use crate::Config;
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_election_provider_support::{VoteWeight, VoteWeightProvider};
|
||||
use frame_election_provider_support::ScoreProvider;
|
||||
use frame_support::{traits::Get, DefaultNoBound};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::traits::{Bounded, Zero};
|
||||
use sp_std::{
|
||||
boxed::Box,
|
||||
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
|
||||
@@ -46,36 +47,36 @@ pub enum Error {
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Given a certain vote weight, to which bag does it belong to?
|
||||
/// Given a certain score, to which bag does it belong to?
|
||||
///
|
||||
/// Bags are identified by their upper threshold; the value returned by this function is guaranteed
|
||||
/// to be a member of `T::BagThresholds`.
|
||||
///
|
||||
/// Note that even if the thresholds list does not have `VoteWeight::MAX` as its final member, this
|
||||
/// function behaves as if it does.
|
||||
pub fn notional_bag_for<T: Config>(weight: VoteWeight) -> VoteWeight {
|
||||
/// Note that even if the thresholds list does not have `T::Score::max_value()` as its final member,
|
||||
/// this function behaves as if it does.
|
||||
pub fn notional_bag_for<T: Config<I>, I: 'static>(score: T::Score) -> T::Score {
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let idx = thresholds.partition_point(|&threshold| weight > threshold);
|
||||
thresholds.get(idx).copied().unwrap_or(VoteWeight::MAX)
|
||||
let idx = thresholds.partition_point(|&threshold| score > threshold);
|
||||
thresholds.get(idx).copied().unwrap_or(T::Score::max_value())
|
||||
}
|
||||
|
||||
/// The **ONLY** entry point of this module. All operations to the bags-list should happen through
|
||||
/// this interface. It is forbidden to access other module members directly.
|
||||
//
|
||||
// Data structure providing efficient mostly-accurate selection of the top N id by `VoteWeight`.
|
||||
// Data structure providing efficient mostly-accurate selection of the top N id by `Score`.
|
||||
//
|
||||
// It's implemented as a set of linked lists. Each linked list comprises a bag of ids of
|
||||
// arbitrary and unbounded length, all having a vote weight within a particular constant range.
|
||||
// arbitrary and unbounded length, all having a score within a particular constant range.
|
||||
// This structure means that ids can be added and removed in `O(1)` time.
|
||||
//
|
||||
// Iteration is accomplished by chaining the iteration of each bag, from greatest to least. While
|
||||
// the users within any particular bag are sorted in an entirely arbitrary order, the overall vote
|
||||
// weight decreases as successive bags are reached. This means that it is valid to truncate
|
||||
// the users within any particular bag are sorted in an entirely arbitrary order, the overall score
|
||||
// decreases as successive bags are reached. This means that it is valid to truncate
|
||||
// iteration at any desired point; only those ids in the lowest bag can be excluded. This
|
||||
// satisfies both the desire for fairness and the requirement for efficiency.
|
||||
pub struct List<T: Config>(PhantomData<T>);
|
||||
pub struct List<T: Config<I>, I: 'static = ()>(PhantomData<(T, I)>);
|
||||
|
||||
impl<T: Config> List<T> {
|
||||
impl<T: Config<I>, I: 'static> List<T, I> {
|
||||
/// Remove all data associated with the list from storage.
|
||||
///
|
||||
/// ## WARNING
|
||||
@@ -83,8 +84,8 @@ impl<T: Config> List<T> {
|
||||
/// this function should generally not be used in production as it could lead to a very large
|
||||
/// number of storage accesses.
|
||||
pub(crate) fn unsafe_clear() {
|
||||
crate::ListBags::<T>::remove_all(None);
|
||||
crate::ListNodes::<T>::remove_all();
|
||||
crate::ListBags::<T, I>::remove_all(None);
|
||||
crate::ListNodes::<T, I>::remove_all();
|
||||
}
|
||||
|
||||
/// Regenerate all of the data from the given ids.
|
||||
@@ -98,13 +99,13 @@ impl<T: Config> List<T> {
|
||||
/// Returns the number of ids migrated.
|
||||
pub fn unsafe_regenerate(
|
||||
all: impl IntoIterator<Item = T::AccountId>,
|
||||
weight_of: Box<dyn Fn(&T::AccountId) -> VoteWeight>,
|
||||
score_of: Box<dyn Fn(&T::AccountId) -> T::Score>,
|
||||
) -> u32 {
|
||||
// NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_regenerate.
|
||||
// I.e. because it can lead to many storage accesses.
|
||||
// So it is ok to call it as caller must ensure the conditions.
|
||||
Self::unsafe_clear();
|
||||
Self::insert_many(all, weight_of)
|
||||
Self::insert_many(all, score_of)
|
||||
}
|
||||
|
||||
/// Migrate the list from one set of thresholds to another.
|
||||
@@ -127,7 +128,7 @@ impl<T: Config> List<T> {
|
||||
/// - ids whose bags change at all are implicitly rebagged into the appropriate bag in the new
|
||||
/// threshold set.
|
||||
#[allow(dead_code)]
|
||||
pub fn migrate(old_thresholds: &[VoteWeight]) -> u32 {
|
||||
pub fn migrate(old_thresholds: &[T::Score]) -> u32 {
|
||||
let new_thresholds = T::BagThresholds::get();
|
||||
if new_thresholds == old_thresholds {
|
||||
return 0
|
||||
@@ -135,11 +136,13 @@ impl<T: Config> List<T> {
|
||||
|
||||
// we can't check all preconditions, but we can check one
|
||||
debug_assert!(
|
||||
crate::ListBags::<T>::iter().all(|(threshold, _)| old_thresholds.contains(&threshold)),
|
||||
crate::ListBags::<T, I>::iter()
|
||||
.all(|(threshold, _)| old_thresholds.contains(&threshold)),
|
||||
"not all `bag_upper` currently in storage are members of `old_thresholds`",
|
||||
);
|
||||
debug_assert!(
|
||||
crate::ListNodes::<T>::iter().all(|(_, node)| old_thresholds.contains(&node.bag_upper)),
|
||||
crate::ListNodes::<T, I>::iter()
|
||||
.all(|(_, node)| old_thresholds.contains(&node.bag_upper)),
|
||||
"not all `node.bag_upper` currently in storage are members of `old_thresholds`",
|
||||
);
|
||||
|
||||
@@ -158,7 +161,7 @@ impl<T: Config> List<T> {
|
||||
let affected_bag = {
|
||||
// this recreates `notional_bag_for` logic, but with the old thresholds.
|
||||
let idx = old_thresholds.partition_point(|&threshold| inserted_bag > threshold);
|
||||
old_thresholds.get(idx).copied().unwrap_or(VoteWeight::MAX)
|
||||
old_thresholds.get(idx).copied().unwrap_or(T::Score::max_value())
|
||||
};
|
||||
if !affected_old_bags.insert(affected_bag) {
|
||||
// If the previous threshold list was [10, 20], and we insert [3, 5], then there's
|
||||
@@ -166,7 +169,7 @@ impl<T: Config> List<T> {
|
||||
continue
|
||||
}
|
||||
|
||||
if let Some(bag) = Bag::<T>::get(affected_bag) {
|
||||
if let Some(bag) = Bag::<T, I>::get(affected_bag) {
|
||||
affected_accounts.extend(bag.iter().map(|node| node.id));
|
||||
}
|
||||
}
|
||||
@@ -178,17 +181,17 @@ impl<T: Config> List<T> {
|
||||
continue
|
||||
}
|
||||
|
||||
if let Some(bag) = Bag::<T>::get(removed_bag) {
|
||||
if let Some(bag) = Bag::<T, I>::get(removed_bag) {
|
||||
affected_accounts.extend(bag.iter().map(|node| node.id));
|
||||
}
|
||||
}
|
||||
|
||||
// migrate the voters whose bag has changed
|
||||
let num_affected = affected_accounts.len() as u32;
|
||||
let weight_of = T::VoteWeightProvider::vote_weight;
|
||||
let score_of = T::ScoreProvider::score;
|
||||
let _removed = Self::remove_many(&affected_accounts);
|
||||
debug_assert_eq!(_removed, num_affected);
|
||||
let _inserted = Self::insert_many(affected_accounts.into_iter(), weight_of);
|
||||
let _inserted = Self::insert_many(affected_accounts.into_iter(), score_of);
|
||||
debug_assert_eq!(_inserted, num_affected);
|
||||
|
||||
// we couldn't previously remove the old bags because both insertion and removal assume that
|
||||
@@ -199,10 +202,10 @@ impl<T: Config> List<T> {
|
||||
// lookups.
|
||||
for removed_bag in removed_bags {
|
||||
debug_assert!(
|
||||
!crate::ListNodes::<T>::iter().any(|(_, node)| node.bag_upper == removed_bag),
|
||||
!crate::ListNodes::<T, I>::iter().any(|(_, node)| node.bag_upper == removed_bag),
|
||||
"no id should be present in a removed bag",
|
||||
);
|
||||
crate::ListBags::<T>::remove(removed_bag);
|
||||
crate::ListBags::<T, I>::remove(removed_bag);
|
||||
}
|
||||
|
||||
debug_assert_eq!(Self::sanity_check(), Ok(()));
|
||||
@@ -212,14 +215,14 @@ impl<T: Config> List<T> {
|
||||
|
||||
/// Returns `true` if the list contains `id`, otherwise returns `false`.
|
||||
pub(crate) fn contains(id: &T::AccountId) -> bool {
|
||||
crate::ListNodes::<T>::contains_key(id)
|
||||
crate::ListNodes::<T, I>::contains_key(id)
|
||||
}
|
||||
|
||||
/// Iterate over all nodes in all bags in the list.
|
||||
///
|
||||
/// Full iteration can be expensive; it's recommended to limit the number of items with
|
||||
/// `.take(n)`.
|
||||
pub(crate) fn iter() -> impl Iterator<Item = Node<T>> {
|
||||
pub(crate) fn iter() -> impl Iterator<Item = Node<T, I>> {
|
||||
// We need a touch of special handling here: because we permit `T::BagThresholds` to
|
||||
// omit the final bound, we need to ensure that we explicitly include that threshold in the
|
||||
// list.
|
||||
@@ -228,12 +231,14 @@ impl<T: Config> List<T> {
|
||||
// easier; they can just configure `type BagThresholds = ()`.
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let iter = thresholds.iter().copied();
|
||||
let iter: Box<dyn Iterator<Item = u64>> = if thresholds.last() == Some(&VoteWeight::MAX) {
|
||||
let iter: Box<dyn Iterator<Item = T::Score>> = if thresholds.last() ==
|
||||
Some(&T::Score::max_value())
|
||||
{
|
||||
// in the event that they included it, we can just pass the iterator through unchanged.
|
||||
Box::new(iter.rev())
|
||||
} else {
|
||||
// otherwise, insert it here.
|
||||
Box::new(iter.chain(iter::once(VoteWeight::MAX)).rev())
|
||||
Box::new(iter.chain(iter::once(T::Score::max_value())).rev())
|
||||
};
|
||||
|
||||
iter.filter_map(Bag::get).flat_map(|bag| bag.iter())
|
||||
@@ -245,12 +250,12 @@ impl<T: Config> List<T> {
|
||||
/// Returns the final count of number of ids inserted.
|
||||
fn insert_many(
|
||||
ids: impl IntoIterator<Item = T::AccountId>,
|
||||
weight_of: impl Fn(&T::AccountId) -> VoteWeight,
|
||||
score_of: impl Fn(&T::AccountId) -> T::Score,
|
||||
) -> u32 {
|
||||
let mut count = 0;
|
||||
ids.into_iter().for_each(|v| {
|
||||
let weight = weight_of(&v);
|
||||
if Self::insert(v, weight).is_ok() {
|
||||
let score = score_of(&v);
|
||||
if Self::insert(v, score).is_ok() {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
@@ -261,13 +266,13 @@ impl<T: Config> List<T> {
|
||||
/// Insert a new id into the appropriate bag in the list.
|
||||
///
|
||||
/// Returns an error if the list already contains `id`.
|
||||
pub(crate) fn insert(id: T::AccountId, weight: VoteWeight) -> Result<(), Error> {
|
||||
pub(crate) fn insert(id: T::AccountId, score: T::Score) -> Result<(), Error> {
|
||||
if Self::contains(&id) {
|
||||
return Err(Error::Duplicate)
|
||||
}
|
||||
|
||||
let bag_weight = notional_bag_for::<T>(weight);
|
||||
let mut bag = Bag::<T>::get_or_make(bag_weight);
|
||||
let bag_score = notional_bag_for::<T, I>(score);
|
||||
let mut bag = Bag::<T, I>::get_or_make(bag_score);
|
||||
// unchecked insertion is okay; we just got the correct `notional_bag_for`.
|
||||
bag.insert_unchecked(id.clone());
|
||||
|
||||
@@ -276,11 +281,12 @@ impl<T: Config> List<T> {
|
||||
|
||||
crate::log!(
|
||||
debug,
|
||||
"inserted {:?} with weight {} into bag {:?}, new count is {}",
|
||||
"inserted {:?} with score {:?
|
||||
} into bag {:?}, new count is {}",
|
||||
id,
|
||||
weight,
|
||||
bag_weight,
|
||||
crate::ListNodes::<T>::count(),
|
||||
score,
|
||||
bag_score,
|
||||
crate::ListNodes::<T, I>::count(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -301,7 +307,7 @@ impl<T: Config> List<T> {
|
||||
let mut count = 0;
|
||||
|
||||
for id in ids.into_iter() {
|
||||
let node = match Node::<T>::get(id) {
|
||||
let node = match Node::<T, I>::get(id) {
|
||||
Some(node) => node,
|
||||
None => continue,
|
||||
};
|
||||
@@ -314,7 +320,7 @@ impl<T: Config> List<T> {
|
||||
// this node is a head or tail, so the bag needs to be updated
|
||||
let bag = bags
|
||||
.entry(node.bag_upper)
|
||||
.or_insert_with(|| Bag::<T>::get_or_make(node.bag_upper));
|
||||
.or_insert_with(|| Bag::<T, I>::get_or_make(node.bag_upper));
|
||||
// node.bag_upper must be correct, therefore this bag will contain this node.
|
||||
bag.remove_node_unchecked(&node);
|
||||
}
|
||||
@@ -341,17 +347,17 @@ impl<T: Config> List<T> {
|
||||
/// [`self.insert`]. However, given large quantities of nodes to move, it may be more efficient
|
||||
/// to call [`self.remove_many`] followed by [`self.insert_many`].
|
||||
pub(crate) fn update_position_for(
|
||||
node: Node<T>,
|
||||
new_weight: VoteWeight,
|
||||
) -> Option<(VoteWeight, VoteWeight)> {
|
||||
node.is_misplaced(new_weight).then(move || {
|
||||
node: Node<T, I>,
|
||||
new_score: T::Score,
|
||||
) -> Option<(T::Score, T::Score)> {
|
||||
node.is_misplaced(new_score).then(move || {
|
||||
let old_bag_upper = node.bag_upper;
|
||||
|
||||
if !node.is_terminal() {
|
||||
// this node is not a head or a tail, so we can just cut it out of the list. update
|
||||
// and put the prev and next of this node, we do `node.put` inside `insert_note`.
|
||||
node.excise();
|
||||
} else if let Some(mut bag) = Bag::<T>::get(node.bag_upper) {
|
||||
} else if let Some(mut bag) = Bag::<T, I>::get(node.bag_upper) {
|
||||
// this is a head or tail, so the bag must be updated.
|
||||
bag.remove_node_unchecked(&node);
|
||||
bag.put();
|
||||
@@ -365,8 +371,8 @@ impl<T: Config> List<T> {
|
||||
}
|
||||
|
||||
// put the node into the appropriate new bag.
|
||||
let new_bag_upper = notional_bag_for::<T>(new_weight);
|
||||
let mut bag = Bag::<T>::get_or_make(new_bag_upper);
|
||||
let new_bag_upper = notional_bag_for::<T, I>(new_score);
|
||||
let mut bag = Bag::<T, I>::get_or_make(new_bag_upper);
|
||||
// prev, next, and bag_upper of the node are updated inside `insert_node`, also
|
||||
// `node.put` is in there.
|
||||
bag.insert_node_unchecked(node);
|
||||
@@ -377,23 +383,22 @@ impl<T: Config> List<T> {
|
||||
}
|
||||
|
||||
/// Put `heavier_id` to the position directly in front of `lighter_id`. Both ids must be in the
|
||||
/// same bag and the `weight_of` `lighter_id` must be less than that of `heavier_id`.
|
||||
/// same bag and the `score_of` `lighter_id` must be less than that of `heavier_id`.
|
||||
pub(crate) fn put_in_front_of(
|
||||
lighter_id: &T::AccountId,
|
||||
heavier_id: &T::AccountId,
|
||||
) -> Result<(), crate::pallet::Error<T>> {
|
||||
) -> Result<(), crate::pallet::Error<T, I>> {
|
||||
use crate::pallet;
|
||||
use frame_support::ensure;
|
||||
|
||||
let lighter_node = Node::<T>::get(&lighter_id).ok_or(pallet::Error::IdNotFound)?;
|
||||
let heavier_node = Node::<T>::get(&heavier_id).ok_or(pallet::Error::IdNotFound)?;
|
||||
let lighter_node = Node::<T, I>::get(&lighter_id).ok_or(pallet::Error::IdNotFound)?;
|
||||
let heavier_node = Node::<T, I>::get(&heavier_id).ok_or(pallet::Error::IdNotFound)?;
|
||||
|
||||
ensure!(lighter_node.bag_upper == heavier_node.bag_upper, pallet::Error::NotInSameBag);
|
||||
|
||||
// this is the most expensive check, so we do it last.
|
||||
ensure!(
|
||||
T::VoteWeightProvider::vote_weight(&heavier_id) >
|
||||
T::VoteWeightProvider::vote_weight(&lighter_id),
|
||||
T::ScoreProvider::score(&heavier_id) > T::ScoreProvider::score(&lighter_id),
|
||||
pallet::Error::NotHeavier
|
||||
);
|
||||
|
||||
@@ -403,7 +408,7 @@ impl<T: Config> List<T> {
|
||||
|
||||
// re-fetch `lighter_node` from storage since it may have been updated when `heavier_node`
|
||||
// was removed.
|
||||
let lighter_node = Node::<T>::get(&lighter_id).ok_or_else(|| {
|
||||
let lighter_node = Node::<T, I>::get(&lighter_id).ok_or_else(|| {
|
||||
debug_assert!(false, "id that should exist cannot be found");
|
||||
crate::log!(warn, "id that should exist cannot be found");
|
||||
pallet::Error::IdNotFound
|
||||
@@ -422,7 +427,7 @@ impl<T: Config> List<T> {
|
||||
/// - this is a naive function in that it does not check if `node` belongs to the same bag as
|
||||
/// `at`. It is expected that the call site will check preconditions.
|
||||
/// - this will panic if `at.bag_upper` is not a bag that already exists in storage.
|
||||
fn insert_at_unchecked(mut at: Node<T>, mut node: Node<T>) {
|
||||
fn insert_at_unchecked(mut at: Node<T, I>, mut node: Node<T, I>) {
|
||||
// connect `node` to its new `prev`.
|
||||
node.prev = at.prev.clone();
|
||||
if let Some(mut prev) = at.prev() {
|
||||
@@ -439,7 +444,7 @@ impl<T: Config> List<T> {
|
||||
// since `node` is always in front of `at` we know that 1) there is always at least 2
|
||||
// nodes in the bag, and 2) only `node` could be the head and only `at` could be the
|
||||
// tail.
|
||||
let mut bag = Bag::<T>::get(at.bag_upper)
|
||||
let mut bag = Bag::<T, I>::get(at.bag_upper)
|
||||
.expect("given nodes must always have a valid bag. qed.");
|
||||
|
||||
if node.prev == None {
|
||||
@@ -473,8 +478,8 @@ impl<T: Config> List<T> {
|
||||
);
|
||||
|
||||
let iter_count = Self::iter().count() as u32;
|
||||
let stored_count = crate::ListNodes::<T>::count();
|
||||
let nodes_count = crate::ListNodes::<T>::iter().count() as u32;
|
||||
let stored_count = crate::ListNodes::<T, I>::count();
|
||||
let nodes_count = crate::ListNodes::<T, I>::iter().count() as u32;
|
||||
ensure!(iter_count == stored_count, "iter_count != stored_count");
|
||||
ensure!(stored_count == nodes_count, "stored_count != nodes_count");
|
||||
|
||||
@@ -482,14 +487,15 @@ impl<T: Config> List<T> {
|
||||
|
||||
let active_bags = {
|
||||
let thresholds = T::BagThresholds::get().iter().copied();
|
||||
let thresholds: Vec<u64> = if thresholds.clone().last() == Some(VoteWeight::MAX) {
|
||||
// in the event that they included it, we don't need to make any changes
|
||||
thresholds.collect()
|
||||
} else {
|
||||
// otherwise, insert it here.
|
||||
thresholds.chain(iter::once(VoteWeight::MAX)).collect()
|
||||
};
|
||||
thresholds.into_iter().filter_map(|t| Bag::<T>::get(t))
|
||||
let thresholds: Vec<T::Score> =
|
||||
if thresholds.clone().last() == Some(T::Score::max_value()) {
|
||||
// in the event that they included it, we don't need to make any changes
|
||||
thresholds.collect()
|
||||
} else {
|
||||
// otherwise, insert it here.
|
||||
thresholds.chain(iter::once(T::Score::max_value())).collect()
|
||||
};
|
||||
thresholds.into_iter().filter_map(|t| Bag::<T, I>::get(t))
|
||||
};
|
||||
|
||||
let _ = active_bags.clone().map(|b| b.sanity_check()).collect::<Result<_, _>>()?;
|
||||
@@ -502,7 +508,7 @@ impl<T: Config> List<T> {
|
||||
|
||||
// check that all nodes are sane. We check the `ListNodes` storage item directly in case we
|
||||
// have some "stale" nodes that are not in a bag.
|
||||
for (_id, node) in crate::ListNodes::<T>::iter() {
|
||||
for (_id, node) in crate::ListNodes::<T, I>::iter() {
|
||||
node.sanity_check()?
|
||||
}
|
||||
|
||||
@@ -517,21 +523,24 @@ impl<T: Config> List<T> {
|
||||
/// Returns the nodes of all non-empty bags. For testing and benchmarks.
|
||||
#[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_bags() -> Vec<(VoteWeight, Vec<T::AccountId>)> {
|
||||
pub(crate) fn get_bags() -> Vec<(T::Score, Vec<T::AccountId>)> {
|
||||
use frame_support::traits::Get as _;
|
||||
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let iter = thresholds.iter().copied();
|
||||
let iter: Box<dyn Iterator<Item = u64>> = if thresholds.last() == Some(&VoteWeight::MAX) {
|
||||
let iter: Box<dyn Iterator<Item = T::Score>> = if thresholds.last() ==
|
||||
Some(&T::Score::max_value())
|
||||
{
|
||||
// in the event that they included it, we can just pass the iterator through unchanged.
|
||||
Box::new(iter)
|
||||
} else {
|
||||
// otherwise, insert it here.
|
||||
Box::new(iter.chain(sp_std::iter::once(VoteWeight::MAX)))
|
||||
Box::new(iter.chain(sp_std::iter::once(T::Score::max_value())))
|
||||
};
|
||||
|
||||
iter.filter_map(|t| {
|
||||
Bag::<T>::get(t).map(|bag| (t, bag.iter().map(|n| n.id().clone()).collect::<Vec<_>>()))
|
||||
Bag::<T, I>::get(t)
|
||||
.map(|bag| (t, bag.iter().map(|n| n.id().clone()).collect::<Vec<_>>()))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
@@ -546,37 +555,39 @@ impl<T: Config> List<T> {
|
||||
/// appearing within the ids set.
|
||||
#[derive(DefaultNoBound, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
#[scale_info(skip_type_params(T, I))]
|
||||
#[cfg_attr(feature = "std", derive(frame_support::DebugNoBound, Clone, PartialEq))]
|
||||
pub struct Bag<T: Config> {
|
||||
pub struct Bag<T: Config<I>, I: 'static = ()> {
|
||||
head: Option<T::AccountId>,
|
||||
tail: Option<T::AccountId>,
|
||||
|
||||
#[codec(skip)]
|
||||
bag_upper: VoteWeight,
|
||||
bag_upper: T::Score,
|
||||
#[codec(skip)]
|
||||
_phantom: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<T: Config> Bag<T> {
|
||||
impl<T: Config<I>, I: 'static> Bag<T, I> {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(
|
||||
head: Option<T::AccountId>,
|
||||
tail: Option<T::AccountId>,
|
||||
bag_upper: VoteWeight,
|
||||
bag_upper: T::Score,
|
||||
) -> Self {
|
||||
Self { head, tail, bag_upper }
|
||||
Self { head, tail, bag_upper, _phantom: PhantomData }
|
||||
}
|
||||
|
||||
/// Get a bag by its upper vote weight.
|
||||
pub(crate) fn get(bag_upper: VoteWeight) -> Option<Bag<T>> {
|
||||
crate::ListBags::<T>::try_get(bag_upper).ok().map(|mut bag| {
|
||||
/// Get a bag by its upper score.
|
||||
pub(crate) fn get(bag_upper: T::Score) -> Option<Bag<T, I>> {
|
||||
crate::ListBags::<T, I>::try_get(bag_upper).ok().map(|mut bag| {
|
||||
bag.bag_upper = bag_upper;
|
||||
bag
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a bag by its upper vote weight or make it, appropriately initialized. Does not check if
|
||||
/// Get a bag by its upper score or make it, appropriately initialized. Does not check if
|
||||
/// if `bag_upper` is a valid threshold.
|
||||
fn get_or_make(bag_upper: VoteWeight) -> Bag<T> {
|
||||
fn get_or_make(bag_upper: T::Score) -> Bag<T, I> {
|
||||
Self::get(bag_upper).unwrap_or(Bag { bag_upper, ..Default::default() })
|
||||
}
|
||||
|
||||
@@ -588,24 +599,24 @@ impl<T: Config> Bag<T> {
|
||||
/// Put the bag back into storage.
|
||||
fn put(self) {
|
||||
if self.is_empty() {
|
||||
crate::ListBags::<T>::remove(self.bag_upper);
|
||||
crate::ListBags::<T, I>::remove(self.bag_upper);
|
||||
} else {
|
||||
crate::ListBags::<T>::insert(self.bag_upper, self);
|
||||
crate::ListBags::<T, I>::insert(self.bag_upper, self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the head node in this bag.
|
||||
fn head(&self) -> Option<Node<T>> {
|
||||
fn head(&self) -> Option<Node<T, I>> {
|
||||
self.head.as_ref().and_then(|id| Node::get(id))
|
||||
}
|
||||
|
||||
/// Get the tail node in this bag.
|
||||
fn tail(&self) -> Option<Node<T>> {
|
||||
fn tail(&self) -> Option<Node<T, I>> {
|
||||
self.tail.as_ref().and_then(|id| Node::get(id))
|
||||
}
|
||||
|
||||
/// Iterate over the nodes in this bag.
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = Node<T>> {
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = Node<T, I>> {
|
||||
sp_std::iter::successors(self.head(), |prev| prev.next())
|
||||
}
|
||||
|
||||
@@ -620,7 +631,13 @@ impl<T: Config> Bag<T> {
|
||||
// insert_node will overwrite `prev`, `next` and `bag_upper` to the proper values. As long
|
||||
// as this bag is the correct one, we're good. All calls to this must come after getting the
|
||||
// correct [`notional_bag_for`].
|
||||
self.insert_node_unchecked(Node::<T> { id, prev: None, next: None, bag_upper: 0 });
|
||||
self.insert_node_unchecked(Node::<T, I> {
|
||||
id,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper: Zero::zero(),
|
||||
_phantom: PhantomData,
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a node into this bag.
|
||||
@@ -630,7 +647,7 @@ impl<T: Config> Bag<T> {
|
||||
///
|
||||
/// Storage note: this modifies storage, but only for the node. You still need to call
|
||||
/// `self.put()` after use.
|
||||
fn insert_node_unchecked(&mut self, mut node: Node<T>) {
|
||||
fn insert_node_unchecked(&mut self, mut node: Node<T, I>) {
|
||||
if let Some(tail) = &self.tail {
|
||||
if *tail == node.id {
|
||||
// this should never happen, but this check prevents one path to a worst case
|
||||
@@ -674,7 +691,7 @@ impl<T: Config> Bag<T> {
|
||||
///
|
||||
/// Storage note: this modifies storage, but only for adjacent nodes. You still need to call
|
||||
/// `self.put()` and `ListNodes::remove(id)` to update storage for the bag and `node`.
|
||||
fn remove_node_unchecked(&mut self, node: &Node<T>) {
|
||||
fn remove_node_unchecked(&mut self, node: &Node<T, I>) {
|
||||
// reassign neighboring nodes.
|
||||
node.excise();
|
||||
|
||||
@@ -735,7 +752,7 @@ impl<T: Config> Bag<T> {
|
||||
/// Iterate over the nodes in this bag (public for tests).
|
||||
#[cfg(feature = "std")]
|
||||
#[allow(dead_code)]
|
||||
pub fn std_iter(&self) -> impl Iterator<Item = Node<T>> {
|
||||
pub fn std_iter(&self) -> impl Iterator<Item = Node<T, I>> {
|
||||
sp_std::iter::successors(self.head(), |prev| prev.next())
|
||||
}
|
||||
|
||||
@@ -749,24 +766,26 @@ impl<T: Config> Bag<T> {
|
||||
/// A Node is the fundamental element comprising the doubly-linked list described by `Bag`.
|
||||
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
#[scale_info(skip_type_params(T, I))]
|
||||
#[cfg_attr(feature = "std", derive(frame_support::DebugNoBound, Clone, PartialEq))]
|
||||
pub struct Node<T: Config> {
|
||||
pub struct Node<T: Config<I>, I: 'static = ()> {
|
||||
id: T::AccountId,
|
||||
prev: Option<T::AccountId>,
|
||||
next: Option<T::AccountId>,
|
||||
bag_upper: VoteWeight,
|
||||
bag_upper: T::Score,
|
||||
#[codec(skip)]
|
||||
_phantom: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<T: Config> Node<T> {
|
||||
impl<T: Config<I>, I: 'static> Node<T, I> {
|
||||
/// Get a node by id.
|
||||
pub fn get(id: &T::AccountId) -> Option<Node<T>> {
|
||||
crate::ListNodes::<T>::try_get(id).ok()
|
||||
pub fn get(id: &T::AccountId) -> Option<Node<T, I>> {
|
||||
crate::ListNodes::<T, I>::try_get(id).ok()
|
||||
}
|
||||
|
||||
/// Put the node back into storage.
|
||||
fn put(self) {
|
||||
crate::ListNodes::<T>::insert(self.id.clone(), self);
|
||||
crate::ListNodes::<T, I>::insert(self.id.clone(), self);
|
||||
}
|
||||
|
||||
/// Update neighboring nodes to point to reach other.
|
||||
@@ -790,22 +809,22 @@ impl<T: Config> Node<T> {
|
||||
///
|
||||
/// It is naive because it does not check if the node has first been removed from its bag.
|
||||
fn remove_from_storage_unchecked(&self) {
|
||||
crate::ListNodes::<T>::remove(&self.id)
|
||||
crate::ListNodes::<T, I>::remove(&self.id)
|
||||
}
|
||||
|
||||
/// Get the previous node in the bag.
|
||||
fn prev(&self) -> Option<Node<T>> {
|
||||
fn prev(&self) -> Option<Node<T, I>> {
|
||||
self.prev.as_ref().and_then(|id| Node::get(id))
|
||||
}
|
||||
|
||||
/// Get the next node in the bag.
|
||||
fn next(&self) -> Option<Node<T>> {
|
||||
fn next(&self) -> Option<Node<T, I>> {
|
||||
self.next.as_ref().and_then(|id| Node::get(id))
|
||||
}
|
||||
|
||||
/// `true` when this voter is in the wrong bag.
|
||||
pub fn is_misplaced(&self, current_weight: VoteWeight) -> bool {
|
||||
notional_bag_for::<T>(current_weight) != self.bag_upper
|
||||
pub fn is_misplaced(&self, current_score: T::Score) -> bool {
|
||||
notional_bag_for::<T, I>(current_score) != self.bag_upper
|
||||
}
|
||||
|
||||
/// `true` when this voter is a bag head or tail.
|
||||
@@ -828,13 +847,13 @@ impl<T: Config> Node<T> {
|
||||
/// The bag this nodes belongs to (public for benchmarks).
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
#[allow(dead_code)]
|
||||
pub fn bag_upper(&self) -> VoteWeight {
|
||||
pub fn bag_upper(&self) -> T::Score {
|
||||
self.bag_upper
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn sanity_check(&self) -> Result<(), &'static str> {
|
||||
let expected_bag = Bag::<T>::get(self.bag_upper).ok_or("bag not found for node")?;
|
||||
let expected_bag = Bag::<T, I>::get(self.bag_upper).ok_or("bag not found for node")?;
|
||||
|
||||
let id = self.id();
|
||||
|
||||
|
||||
@@ -20,14 +20,20 @@ use crate::{
|
||||
mock::{test_utils::*, *},
|
||||
ListBags, ListNodes,
|
||||
};
|
||||
use frame_election_provider_support::SortedListProvider;
|
||||
use frame_election_provider_support::{SortedListProvider, VoteWeight};
|
||||
use frame_support::{assert_ok, assert_storage_noop};
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// syntactic sugar to create a raw node
|
||||
let node = |id, prev, next, bag_upper| Node::<Runtime> { id, prev, next, bag_upper };
|
||||
let node = |id, prev, next, bag_upper| Node::<Runtime> {
|
||||
id,
|
||||
prev,
|
||||
next,
|
||||
bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
assert_eq!(ListNodes::<Runtime>::count(), 4);
|
||||
assert_eq!(ListNodes::<Runtime>::iter().count(), 4);
|
||||
@@ -38,11 +44,11 @@ fn basic_setup_works() {
|
||||
// the state of the bags is as expected
|
||||
assert_eq!(
|
||||
ListBags::<Runtime>::get(10).unwrap(),
|
||||
Bag::<Runtime> { head: Some(1), tail: Some(1), bag_upper: 0 }
|
||||
Bag::<Runtime> { head: Some(1), tail: Some(1), bag_upper: 0, _phantom: PhantomData }
|
||||
);
|
||||
assert_eq!(
|
||||
ListBags::<Runtime>::get(1_000).unwrap(),
|
||||
Bag::<Runtime> { head: Some(2), tail: Some(4), bag_upper: 0 }
|
||||
Bag::<Runtime> { head: Some(2), tail: Some(4), bag_upper: 0, _phantom: PhantomData }
|
||||
);
|
||||
|
||||
assert_eq!(ListNodes::<Runtime>::get(2).unwrap(), node(2, None, Some(3), 1_000));
|
||||
@@ -65,24 +71,24 @@ fn basic_setup_works() {
|
||||
#[test]
|
||||
fn notional_bag_for_works() {
|
||||
// under a threshold gives the next threshold.
|
||||
assert_eq!(notional_bag_for::<Runtime>(0), 10);
|
||||
assert_eq!(notional_bag_for::<Runtime>(9), 10);
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(0), 10);
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(9), 10);
|
||||
|
||||
// at a threshold gives that threshold.
|
||||
assert_eq!(notional_bag_for::<Runtime>(10), 10);
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(10), 10);
|
||||
|
||||
// above the threshold, gives the next threshold.
|
||||
assert_eq!(notional_bag_for::<Runtime>(11), 20);
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(11), 20);
|
||||
|
||||
let max_explicit_threshold = *<Runtime as Config>::BagThresholds::get().last().unwrap();
|
||||
assert_eq!(max_explicit_threshold, 10_000);
|
||||
|
||||
// if the max explicit threshold is less than VoteWeight::MAX,
|
||||
// if the max explicit threshold is less than T::Value::max_value(),
|
||||
assert!(VoteWeight::MAX > max_explicit_threshold);
|
||||
|
||||
// then anything above it will belong to the VoteWeight::MAX bag.
|
||||
assert_eq!(notional_bag_for::<Runtime>(max_explicit_threshold), max_explicit_threshold);
|
||||
assert_eq!(notional_bag_for::<Runtime>(max_explicit_threshold + 1), VoteWeight::MAX);
|
||||
// then anything above it will belong to the T::Value::max_value() bag.
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(max_explicit_threshold), max_explicit_threshold);
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(max_explicit_threshold + 1), VoteWeight::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -388,14 +394,26 @@ mod list {
|
||||
#[should_panic = "given nodes must always have a valid bag. qed."]
|
||||
fn put_in_front_of_panics_if_bag_not_found() {
|
||||
ExtBuilder::default().skip_genesis_ids().build_and_execute_no_post_check(|| {
|
||||
let node_10_no_bag = Node::<Runtime> { id: 10, prev: None, next: None, bag_upper: 15 };
|
||||
let node_11_no_bag = Node::<Runtime> { id: 11, prev: None, next: None, bag_upper: 15 };
|
||||
let node_10_no_bag = Node::<Runtime> {
|
||||
id: 10,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper: 15,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
let node_11_no_bag = Node::<Runtime> {
|
||||
id: 11,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper: 15,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// given
|
||||
ListNodes::<Runtime>::insert(10, node_10_no_bag);
|
||||
ListNodes::<Runtime>::insert(11, node_11_no_bag);
|
||||
StakingMock::set_vote_weight_of(&10, 14);
|
||||
StakingMock::set_vote_weight_of(&11, 15);
|
||||
StakingMock::set_score_of(&10, 14);
|
||||
StakingMock::set_score_of(&11, 15);
|
||||
assert!(!ListBags::<Runtime>::contains_key(15));
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![]);
|
||||
|
||||
@@ -414,8 +432,13 @@ mod list {
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
|
||||
let node_42 =
|
||||
Node::<Runtime> { id: 42, prev: Some(1), next: Some(2), bag_upper: 1_000 };
|
||||
let node_42 = Node::<Runtime> {
|
||||
id: 42,
|
||||
prev: Some(1),
|
||||
next: Some(2),
|
||||
bag_upper: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_1 = crate::ListNodes::<Runtime>::get(&1).unwrap();
|
||||
@@ -438,7 +461,13 @@ mod list {
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
|
||||
let node_42 = Node::<Runtime> { id: 42, prev: Some(4), next: None, bag_upper: 1_000 };
|
||||
let node_42 = Node::<Runtime> {
|
||||
id: 42,
|
||||
prev: Some(4),
|
||||
next: None,
|
||||
bag_upper: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_2 = crate::ListNodes::<Runtime>::get(&2).unwrap();
|
||||
@@ -461,7 +490,13 @@ mod list {
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
|
||||
let node_42 = Node::<Runtime> { id: 42, prev: None, next: Some(2), bag_upper: 1_000 };
|
||||
let node_42 = Node::<Runtime> {
|
||||
id: 42,
|
||||
prev: None,
|
||||
next: Some(2),
|
||||
bag_upper: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_3 = crate::ListNodes::<Runtime>::get(&3).unwrap();
|
||||
@@ -484,8 +519,13 @@ mod list {
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
|
||||
let node_42 =
|
||||
Node::<Runtime> { id: 42, prev: Some(42), next: Some(42), bag_upper: 1_000 };
|
||||
let node_42 = Node::<Runtime> {
|
||||
id: 42,
|
||||
prev: Some(42),
|
||||
next: Some(42),
|
||||
bag_upper: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_4 = crate::ListNodes::<Runtime>::get(&4).unwrap();
|
||||
@@ -512,7 +552,7 @@ mod bags {
|
||||
let bag = Bag::<Runtime>::get(bag_upper).unwrap();
|
||||
let bag_ids = bag.iter().map(|n| *n.id()).collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(bag, Bag::<Runtime> { head, tail, bag_upper });
|
||||
assert_eq!(bag, Bag::<Runtime> { head, tail, bag_upper, _phantom: PhantomData });
|
||||
assert_eq!(bag_ids, ids);
|
||||
};
|
||||
|
||||
@@ -543,7 +583,13 @@ mod bags {
|
||||
#[test]
|
||||
fn insert_node_sets_proper_bag() {
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
let node = |id, bag_upper| Node::<Runtime> { id, prev: None, next: None, bag_upper };
|
||||
let node = |id, bag_upper| Node::<Runtime> {
|
||||
id,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
@@ -552,7 +598,7 @@ mod bags {
|
||||
|
||||
assert_eq!(
|
||||
ListNodes::<Runtime>::get(&42).unwrap(),
|
||||
Node { bag_upper: 10, prev: Some(1), next: None, id: 42 }
|
||||
Node { bag_upper: 10, prev: Some(1), next: None, id: 42, _phantom: PhantomData }
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -560,7 +606,13 @@ mod bags {
|
||||
#[test]
|
||||
fn insert_node_happy_paths_works() {
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
let node = |id, bag_upper| Node::<Runtime> { id, prev: None, next: None, bag_upper };
|
||||
let node = |id, bag_upper| Node::<Runtime> {
|
||||
id,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// when inserting into a bag with 1 node
|
||||
let mut bag_10 = Bag::<Runtime>::get(10).unwrap();
|
||||
@@ -581,15 +633,26 @@ mod bags {
|
||||
assert_eq!(bag_as_ids(&bag_20), vec![62]);
|
||||
|
||||
// when inserting a node pointing to the accounts not in the bag
|
||||
let node_61 =
|
||||
Node::<Runtime> { id: 61, prev: Some(21), next: Some(101), bag_upper: 20 };
|
||||
let node_61 = Node::<Runtime> {
|
||||
id: 61,
|
||||
prev: Some(21),
|
||||
next: Some(101),
|
||||
bag_upper: 20,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
bag_20.insert_node_unchecked(node_61);
|
||||
// then ids are in order
|
||||
assert_eq!(bag_as_ids(&bag_20), vec![62, 61]);
|
||||
// and when the node is re-fetched all the info is correct
|
||||
assert_eq!(
|
||||
Node::<Runtime>::get(&61).unwrap(),
|
||||
Node::<Runtime> { id: 61, prev: Some(62), next: None, bag_upper: 20 }
|
||||
Node::<Runtime> {
|
||||
id: 61,
|
||||
prev: Some(62),
|
||||
next: None,
|
||||
bag_upper: 20,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
);
|
||||
|
||||
// state of all bags is as expected
|
||||
@@ -604,7 +667,13 @@ mod bags {
|
||||
// Document improper ways `insert_node` may be getting used.
|
||||
#[test]
|
||||
fn insert_node_bad_paths_documented() {
|
||||
let node = |id, prev, next, bag_upper| Node::<Runtime> { id, prev, next, bag_upper };
|
||||
let node = |id, prev, next, bag_upper| Node::<Runtime> {
|
||||
id,
|
||||
prev,
|
||||
next,
|
||||
bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
// when inserting a node with both prev & next pointing at an account in an incorrect
|
||||
// bag.
|
||||
@@ -657,7 +726,10 @@ mod bags {
|
||||
);
|
||||
// ^^^ despite being the bags head, it has a prev
|
||||
|
||||
assert_eq!(bag_1000, Bag { head: Some(2), tail: Some(2), bag_upper: 1_000 })
|
||||
assert_eq!(
|
||||
bag_1000,
|
||||
Bag { head: Some(2), tail: Some(2), bag_upper: 1_000, _phantom: PhantomData }
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -669,7 +741,13 @@ mod bags {
|
||||
)]
|
||||
fn insert_node_duplicate_tail_panics_with_debug_assert() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let node = |id, prev, next, bag_upper| Node::<Runtime> { id, prev, next, bag_upper };
|
||||
let node = |id, prev, next, bag_upper| Node::<Runtime> {
|
||||
id,
|
||||
prev,
|
||||
next,
|
||||
bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])],);
|
||||
@@ -801,6 +879,7 @@ mod bags {
|
||||
prev: None,
|
||||
next: Some(3),
|
||||
bag_upper: 10, // should be 1_000
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
|
||||
|
||||
@@ -27,19 +27,21 @@ pub type AccountId = u32;
|
||||
pub type Balance = u32;
|
||||
|
||||
parameter_types! {
|
||||
// Set the vote weight for any id who's weight has _not_ been set with `set_vote_weight_of`.
|
||||
// Set the vote weight for any id who's weight has _not_ been set with `set_score_of`.
|
||||
pub static NextVoteWeight: VoteWeight = 0;
|
||||
pub static NextVoteWeightMap: HashMap<AccountId, VoteWeight> = Default::default();
|
||||
}
|
||||
|
||||
pub struct StakingMock;
|
||||
impl frame_election_provider_support::VoteWeightProvider<AccountId> for StakingMock {
|
||||
fn vote_weight(id: &AccountId) -> VoteWeight {
|
||||
impl frame_election_provider_support::ScoreProvider<AccountId> for StakingMock {
|
||||
type Score = VoteWeight;
|
||||
|
||||
fn score(id: &AccountId) -> Self::Score {
|
||||
*NextVoteWeightMap::get().get(id).unwrap_or(&NextVoteWeight::get())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
fn set_vote_weight_of(id: &AccountId, weight: VoteWeight) {
|
||||
fn set_score_of(id: &AccountId, weight: Self::Score) {
|
||||
NEXT_VOTE_WEIGHT_MAP.with(|m| m.borrow_mut().insert(id.clone(), weight));
|
||||
}
|
||||
}
|
||||
@@ -79,7 +81,8 @@ impl bags_list::Config for Runtime {
|
||||
type Event = Event;
|
||||
type WeightInfo = ();
|
||||
type BagThresholds = BagThresholds;
|
||||
type VoteWeightProvider = StakingMock;
|
||||
type ScoreProvider = StakingMock;
|
||||
type Score = VoteWeight;
|
||||
}
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
|
||||
@@ -134,7 +137,7 @@ impl ExtBuilder {
|
||||
ext.execute_with(|| {
|
||||
for (id, weight) in ids_with_weight {
|
||||
frame_support::assert_ok!(List::<Runtime>::insert(*id, *weight));
|
||||
StakingMock::set_vote_weight_of(id, *weight);
|
||||
StakingMock::set_score_of(id, *weight);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
use frame_support::{assert_noop, assert_ok, assert_storage_noop, traits::IntegrityTest};
|
||||
|
||||
use super::*;
|
||||
use frame_election_provider_support::SortedListProvider;
|
||||
use frame_election_provider_support::{SortedListProvider, VoteWeight};
|
||||
use list::Bag;
|
||||
use mock::{test_utils::*, *};
|
||||
|
||||
@@ -35,7 +35,7 @@ mod pallet {
|
||||
);
|
||||
|
||||
// when increasing vote weight to the level of non-existent bag
|
||||
StakingMock::set_vote_weight_of(&42, 2_000);
|
||||
StakingMock::set_score_of(&42, 2_000);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 42));
|
||||
|
||||
// then a new bag is created and the id moves into it
|
||||
@@ -45,7 +45,7 @@ mod pallet {
|
||||
);
|
||||
|
||||
// when decreasing weight within the range of the current bag
|
||||
StakingMock::set_vote_weight_of(&42, 1_001);
|
||||
StakingMock::set_score_of(&42, 1_001);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 42));
|
||||
|
||||
// then the id does not move
|
||||
@@ -55,7 +55,7 @@ mod pallet {
|
||||
);
|
||||
|
||||
// when reducing weight to the level of a non-existent bag
|
||||
StakingMock::set_vote_weight_of(&42, 30);
|
||||
StakingMock::set_score_of(&42, 30);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 42));
|
||||
|
||||
// then a new bag is created and the id moves into it
|
||||
@@ -65,7 +65,7 @@ mod pallet {
|
||||
);
|
||||
|
||||
// when increasing weight to the level of a pre-existing bag
|
||||
StakingMock::set_vote_weight_of(&42, 500);
|
||||
StakingMock::set_score_of(&42, 500);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 42));
|
||||
|
||||
// then the id moves into that bag
|
||||
@@ -85,7 +85,7 @@ mod pallet {
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// when
|
||||
StakingMock::set_vote_weight_of(&4, 10);
|
||||
StakingMock::set_score_of(&4, 10);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 4));
|
||||
|
||||
// then
|
||||
@@ -93,7 +93,7 @@ mod pallet {
|
||||
assert_eq!(Bag::<Runtime>::get(1_000).unwrap(), Bag::new(Some(2), Some(3), 1_000));
|
||||
|
||||
// when
|
||||
StakingMock::set_vote_weight_of(&3, 10);
|
||||
StakingMock::set_score_of(&3, 10);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 3));
|
||||
|
||||
// then
|
||||
@@ -104,7 +104,7 @@ mod pallet {
|
||||
assert_eq!(get_list_as_ids(), vec![2u32, 1, 4, 3]);
|
||||
|
||||
// when
|
||||
StakingMock::set_vote_weight_of(&2, 10);
|
||||
StakingMock::set_score_of(&2, 10);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 2));
|
||||
|
||||
// then
|
||||
@@ -119,7 +119,7 @@ mod pallet {
|
||||
fn rebag_head_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// when
|
||||
StakingMock::set_vote_weight_of(&2, 10);
|
||||
StakingMock::set_score_of(&2, 10);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 2));
|
||||
|
||||
// then
|
||||
@@ -127,7 +127,7 @@ mod pallet {
|
||||
assert_eq!(Bag::<Runtime>::get(1_000).unwrap(), Bag::new(Some(3), Some(4), 1_000));
|
||||
|
||||
// when
|
||||
StakingMock::set_vote_weight_of(&3, 10);
|
||||
StakingMock::set_score_of(&3, 10);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 3));
|
||||
|
||||
// then
|
||||
@@ -135,7 +135,7 @@ mod pallet {
|
||||
assert_eq!(Bag::<Runtime>::get(1_000).unwrap(), Bag::new(Some(4), Some(4), 1_000));
|
||||
|
||||
// when
|
||||
StakingMock::set_vote_weight_of(&4, 10);
|
||||
StakingMock::set_score_of(&4, 10);
|
||||
assert_ok!(BagsList::rebag(Origin::signed(0), 4));
|
||||
|
||||
// then
|
||||
@@ -241,7 +241,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&3, 999);
|
||||
StakingMock::set_score_of(&3, 999);
|
||||
|
||||
// when
|
||||
assert_ok!(BagsList::put_in_front_of(Origin::signed(4), 3));
|
||||
@@ -262,7 +262,7 @@ mod pallet {
|
||||
vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5, 6])]
|
||||
);
|
||||
|
||||
StakingMock::set_vote_weight_of(&5, 999);
|
||||
StakingMock::set_score_of(&5, 999);
|
||||
|
||||
// when
|
||||
assert_ok!(BagsList::put_in_front_of(Origin::signed(3), 5));
|
||||
@@ -281,7 +281,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&2, 999);
|
||||
StakingMock::set_score_of(&2, 999);
|
||||
|
||||
// when
|
||||
assert_ok!(BagsList::put_in_front_of(Origin::signed(3), 2));
|
||||
@@ -297,7 +297,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&3, 999);
|
||||
StakingMock::set_score_of(&3, 999);
|
||||
|
||||
// when
|
||||
assert_ok!(BagsList::put_in_front_of(Origin::signed(4), 3));
|
||||
@@ -313,7 +313,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&2, 999);
|
||||
StakingMock::set_score_of(&2, 999);
|
||||
|
||||
// when
|
||||
assert_ok!(BagsList::put_in_front_of(Origin::signed(5), 2));
|
||||
@@ -329,7 +329,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&4, 999);
|
||||
StakingMock::set_score_of(&4, 999);
|
||||
|
||||
// when
|
||||
BagsList::put_in_front_of(Origin::signed(2), 4).unwrap();
|
||||
@@ -359,7 +359,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&4, 999);
|
||||
StakingMock::set_score_of(&4, 999);
|
||||
|
||||
// when
|
||||
BagsList::put_in_front_of(Origin::signed(2), 4).unwrap();
|
||||
@@ -375,7 +375,7 @@ mod pallet {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
StakingMock::set_vote_weight_of(&3, 999);
|
||||
StakingMock::set_score_of(&3, 999);
|
||||
|
||||
// then
|
||||
assert_noop!(
|
||||
|
||||
Reference in New Issue
Block a user