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:
Zeke Mostov
2022-03-09 16:28:28 +00:00
committed by GitHub
parent 64f6664691
commit f8e0e41e15
14 changed files with 428 additions and 283 deletions
@@ -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
);
}
}
+24 -23
View File
@@ -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]
)
}
+82 -64
View File
@@ -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();
+127 -108
View File
@@ -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();
+111 -32
View File
@@ -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();
+9 -6
View File
@@ -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);
}
});
+19 -19
View File
@@ -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!(