feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Benchmarks for the bags list pallet.
|
||||
|
||||
use super::*;
|
||||
use crate::list::List;
|
||||
use alloc::{vec, vec::Vec};
|
||||
use pezframe_benchmarking::v1::{
|
||||
account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller,
|
||||
};
|
||||
use pezframe_election_provider_support::ScoreProvider;
|
||||
use pezframe_support::{assert_ok, traits::Get};
|
||||
use pezframe_system::RawOrigin as SystemOrigin;
|
||||
use pezsp_runtime::traits::One;
|
||||
|
||||
benchmarks_instance_pallet! {
|
||||
// iteration of any number of items should only touch that many nodes and bags.
|
||||
#[extra]
|
||||
iter {
|
||||
let n = 100;
|
||||
|
||||
// clear any pre-existing storage.
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
// add n nodes, half to the first bag and half to the second bag.
|
||||
let bag_thresh = T::BagThresholds::get()[0];
|
||||
let second_bag_thresh = T::BagThresholds::get()[1];
|
||||
|
||||
|
||||
for i in 0..n/2 {
|
||||
let node: T::AccountId = account("node", i, 0);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh - One::one()));
|
||||
}
|
||||
for i in 0..n/2 {
|
||||
let node: T::AccountId = account("node", i, 1);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh + One::one()));
|
||||
}
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags().into_iter().map(|(bag, nodes)| (bag, nodes.len())).collect::<Vec<_>>(),
|
||||
vec![
|
||||
(bag_thresh, (n / 2) as usize),
|
||||
(second_bag_thresh, (n / 2) as usize),
|
||||
]
|
||||
);
|
||||
}: {
|
||||
let voters = <Pallet<T, _> as SortedListProvider<T::AccountId>>::iter();
|
||||
let len = voters.collect::<Vec<_>>().len();
|
||||
assert_eq!(len as u32, n,"len is {}, expected {}", len, n);
|
||||
}
|
||||
|
||||
// iteration of any number of items should only touch that many nodes and bags.
|
||||
#[extra]
|
||||
iter_take {
|
||||
let n = 100;
|
||||
|
||||
// clear any pre-existing storage.
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
// add n nodes, half to the first bag and half to the second bag.
|
||||
let bag_thresh = T::BagThresholds::get()[0];
|
||||
let second_bag_thresh = T::BagThresholds::get()[1];
|
||||
|
||||
|
||||
for i in 0..n/2 {
|
||||
let node: T::AccountId = account("node", i, 0);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh - One::one()));
|
||||
}
|
||||
for i in 0..n/2 {
|
||||
let node: T::AccountId = account("node", i, 1);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh + One::one()));
|
||||
}
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags().into_iter().map(|(bag, nodes)| (bag, nodes.len())).collect::<Vec<_>>(),
|
||||
vec![
|
||||
(bag_thresh, (n / 2) as usize),
|
||||
(second_bag_thresh, (n / 2) as usize),
|
||||
]
|
||||
);
|
||||
}: {
|
||||
// this should only go into one of the bags
|
||||
let voters = <Pallet<T, _> as SortedListProvider<T::AccountId>>::iter().take(n as usize / 4 );
|
||||
let len = voters.collect::<Vec<_>>().len();
|
||||
assert_eq!(len as u32, n / 4,"len is {}, expected {}", len, n / 4);
|
||||
}
|
||||
|
||||
#[extra]
|
||||
iter_next {
|
||||
let n = 100;
|
||||
|
||||
// clear any pre-existing storage.
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
// add n nodes, half to the first bag and half to the second bag.
|
||||
let bag_thresh = T::BagThresholds::get()[0];
|
||||
let second_bag_thresh = T::BagThresholds::get()[1];
|
||||
|
||||
|
||||
for i in 0..n/2 {
|
||||
let node: T::AccountId = account("node", i, 0);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh - One::one()));
|
||||
}
|
||||
for i in 0..n/2 {
|
||||
let node: T::AccountId = account("node", i, 1);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh + One::one()));
|
||||
}
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags().into_iter().map(|(bag, nodes)| (bag, nodes.len())).collect::<Vec<_>>(),
|
||||
vec![
|
||||
(bag_thresh, (n / 2) as usize),
|
||||
(second_bag_thresh, (n / 2) as usize),
|
||||
]
|
||||
);
|
||||
}: {
|
||||
// this should only go into one of the bags
|
||||
let mut iter_var = <Pallet<T, _> as SortedListProvider<T::AccountId>>::iter();
|
||||
let mut voters = Vec::<T::AccountId>::with_capacity((n/4) as usize);
|
||||
for _ in 0..(n/4) {
|
||||
let next = iter_var.next().unwrap();
|
||||
voters.push(next);
|
||||
}
|
||||
|
||||
let len = voters.len();
|
||||
assert_eq!(len as u32, n / 4,"len is {}, expected {}", len, n / 4);
|
||||
}
|
||||
|
||||
#[extra]
|
||||
iter_from {
|
||||
let n = 100;
|
||||
|
||||
// clear any pre-existing storage.
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
// populate the first 4 bags with n/4 nodes each
|
||||
let bag_thresh = T::BagThresholds::get()[0];
|
||||
|
||||
for i in 0..n/4 {
|
||||
let node: T::AccountId = account("node", i, 0);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh - One::one()));
|
||||
}
|
||||
for i in 0..n/4 {
|
||||
let node: T::AccountId = account("node", i, 1);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh + One::one()));
|
||||
}
|
||||
|
||||
let bag_thresh = T::BagThresholds::get()[2];
|
||||
|
||||
for i in 0..n/4 {
|
||||
let node: T::AccountId = account("node", i, 2);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh - One::one()));
|
||||
}
|
||||
|
||||
for i in 0..n/4 {
|
||||
let node: T::AccountId = account("node", i, 3);
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), bag_thresh + One::one()));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags().into_iter().map(|(bag, nodes)| (bag, nodes.len())).collect::<Vec<_>>(),
|
||||
vec![
|
||||
(T::BagThresholds::get()[0], (n / 4) as usize),
|
||||
(T::BagThresholds::get()[1], (n / 4) as usize),
|
||||
(T::BagThresholds::get()[2], (n / 4) as usize),
|
||||
(T::BagThresholds::get()[3], (n / 4) as usize),
|
||||
]
|
||||
);
|
||||
|
||||
// iter from someone in the 3rd bag, so this should touch ~75 nodes and 3 bags
|
||||
let from: T::AccountId = account("node", 0, 2);
|
||||
}: {
|
||||
let voters = <Pallet<T, _> as SortedListProvider<T::AccountId>>::iter_from(&from).unwrap();
|
||||
let len = voters.collect::<Vec<_>>().len();
|
||||
assert_eq!(len as u32, 74,"len is {}, expected {}", len, 74);
|
||||
}
|
||||
|
||||
|
||||
rebag_non_terminal {
|
||||
// An expensive case for rebag-ing (rebag a non-terminal node):
|
||||
//
|
||||
// - The node to be rebagged, _R_, should exist as a non-terminal node in a bag with at
|
||||
// least 2 other nodes. Thus _R_ will have both its `prev` and `next` nodes updated when
|
||||
// it is removed. (3 W/R)
|
||||
// - The destination bag is not empty, thus we need to update the `next` pointer of the last
|
||||
// node in the destination in addition to the work we do otherwise. (2 W/R)
|
||||
|
||||
// clear any pre-existing storage.
|
||||
// NOTE: safe to call outside block production
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
// define our origin and destination thresholds.
|
||||
let origin_bag_thresh = T::BagThresholds::get()[0];
|
||||
let dest_bag_thresh = T::BagThresholds::get()[1];
|
||||
|
||||
// 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));
|
||||
|
||||
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));
|
||||
|
||||
let origin_tail: T::AccountId = account("origin_tail", 0, 0);
|
||||
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));
|
||||
|
||||
let origin_middle_lookup = T::Lookup::unlookup(origin_middle.clone());
|
||||
|
||||
// the bags are in the expected state after initial setup.
|
||||
assert_eq!(
|
||||
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()])
|
||||
]
|
||||
);
|
||||
|
||||
let caller = whitelisted_caller();
|
||||
// update the weight of `origin_middle` to guarantee it will be rebagged into the destination.
|
||||
T::ScoreProvider::set_score_of(&origin_middle, dest_bag_thresh);
|
||||
}: rebag(SystemOrigin::Signed(caller), origin_middle_lookup.clone())
|
||||
verify {
|
||||
// check the bags have updated as expected.
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(
|
||||
origin_bag_thresh,
|
||||
vec![origin_head, origin_tail],
|
||||
),
|
||||
(
|
||||
dest_bag_thresh,
|
||||
vec![dest_head, origin_middle],
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
rebag_terminal {
|
||||
// An expensive case for rebag-ing (rebag a terminal node):
|
||||
//
|
||||
// - The node to be rebagged, _R_, is a terminal node; so _R_, the node pointing to _R_ and
|
||||
// the origin bag itself will need to be updated. (3 W/R)
|
||||
// - The destination bag is not empty, thus we need to update the `next` pointer of the last
|
||||
// node in the destination in addition to the work we do otherwise. (2 W/R)
|
||||
|
||||
// clear any pre-existing storage.
|
||||
// NOTE: safe to call outside block production
|
||||
List::<T, I>::unsafe_clear();
|
||||
|
||||
// define our origin and destination thresholds.
|
||||
let origin_bag_thresh = T::BagThresholds::get()[0];
|
||||
let dest_bag_thresh = T::BagThresholds::get()[1];
|
||||
|
||||
// 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));
|
||||
|
||||
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));
|
||||
|
||||
// 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));
|
||||
|
||||
let origin_tail_lookup = T::Lookup::unlookup(origin_tail.clone());
|
||||
|
||||
// the bags are in the expected state after initial setup.
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(origin_bag_thresh, vec![origin_head.clone(), origin_tail.clone()]),
|
||||
(dest_bag_thresh, vec![dest_head.clone()])
|
||||
]
|
||||
);
|
||||
|
||||
let caller = whitelisted_caller();
|
||||
// update the weight of `origin_tail` to guarantee it will be rebagged into the destination.
|
||||
T::ScoreProvider::set_score_of(&origin_tail, dest_bag_thresh);
|
||||
}: rebag(SystemOrigin::Signed(caller), origin_tail_lookup.clone())
|
||||
verify {
|
||||
// check the bags have updated as expected.
|
||||
assert_eq!(
|
||||
List::<T, _>::get_bags(),
|
||||
vec![
|
||||
(origin_bag_thresh, vec![origin_head.clone()]),
|
||||
(dest_bag_thresh, vec![dest_head.clone(), origin_tail])
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
put_in_front_of {
|
||||
// The most expensive case for `put_in_front_of`:
|
||||
//
|
||||
// - both heavier's `prev` and `next` are nodes that will need to be read and written.
|
||||
// - `lighter` is the bag's `head`, so the bag will need to be read and written.
|
||||
|
||||
// clear any pre-existing storage.
|
||||
// NOTE: safe to call outside block production
|
||||
List::<T, I>::unsafe_clear();
|
||||
|
||||
let bag_thresh = T::BagThresholds::get()[0];
|
||||
|
||||
// insert the nodes in order
|
||||
let lighter: T::AccountId = account("lighter", 0, 0);
|
||||
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));
|
||||
|
||||
let heavier: T::AccountId = account("heavier", 0, 0);
|
||||
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));
|
||||
|
||||
T::ScoreProvider::set_score_of(&lighter, bag_thresh - One::one());
|
||||
T::ScoreProvider::set_score_of(&heavier, bag_thresh);
|
||||
|
||||
let lighter_lookup = T::Lookup::unlookup(lighter.clone());
|
||||
|
||||
assert_eq!(
|
||||
List::<T, _>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
|
||||
vec![lighter.clone(), heavier_prev.clone(), heavier.clone(), heavier_next.clone()]
|
||||
);
|
||||
|
||||
whitelist_account!(heavier);
|
||||
}: _(SystemOrigin::Signed(heavier.clone()), lighter_lookup.clone())
|
||||
verify {
|
||||
assert_eq!(
|
||||
List::<T, _>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
|
||||
vec![heavier, lighter, heavier_prev, heavier_next]
|
||||
)
|
||||
}
|
||||
|
||||
on_idle {
|
||||
// This benchmark generates weights for `on_idle` based on runtime configuration.
|
||||
// The main input is the runtime's `MaxAutoRebagPerBlock` type, which defines how many
|
||||
// nodes can be rebagged per block.
|
||||
// This benchmark simulates a scenario with both pending rebag processing
|
||||
// and fragmented rebag scenario.
|
||||
|
||||
List::<T, _>::unsafe_clear();
|
||||
|
||||
let bag_thresh = T::BagThresholds::get();
|
||||
let low = bag_thresh[0];
|
||||
let mid = bag_thresh[1];
|
||||
let high = bag_thresh[2];
|
||||
|
||||
let rebag_budget = <T as Config<I>>::MaxAutoRebagPerBlock::get();
|
||||
|
||||
// Adjust counts to ensure exact budget usage
|
||||
let pending_count = rebag_budget / 3; // Smaller portion for pending
|
||||
let regular_count = rebag_budget + 5;
|
||||
|
||||
// Insert regular nodes with varying scores
|
||||
for i in 0..regular_count {
|
||||
let node: T::AccountId = account("regular_node", i, 0);
|
||||
let score = match i % 3 {
|
||||
0 => low - One::one(),
|
||||
1 => mid - One::one(),
|
||||
_ => high - One::one(),
|
||||
};
|
||||
assert_ok!(List::<T, _>::insert(node.clone(), score));
|
||||
}
|
||||
|
||||
// Corrupt some nodes to simulate edge cases
|
||||
for i in (0..regular_count).step_by(4) {
|
||||
let node: T::AccountId = account("regular_node", i, 0);
|
||||
let _ = List::<T, _>::remove(&node); // orphan nodes
|
||||
}
|
||||
|
||||
// Lock the list and simulate pending rebag insertions
|
||||
<Pallet<T, I>>::lock();
|
||||
|
||||
// Create pending rebag entries (mix of valid and corrupted)
|
||||
for i in 0..pending_count {
|
||||
let pending_node: T::AccountId = account("pending_node", i, 0);
|
||||
let pending_score = match i % 3 {
|
||||
0 => mid,
|
||||
1 => high,
|
||||
_ => high + high,
|
||||
};
|
||||
|
||||
// Set score first for most nodes, but skip some to simulate cleanup scenarios
|
||||
if i % 7 != 0 {
|
||||
T::ScoreProvider::set_score_of(&pending_node, pending_score);
|
||||
}
|
||||
|
||||
let _ = <Pallet<T, I> as SortedListProvider<T::AccountId>>::on_insert(
|
||||
pending_node, pending_score
|
||||
);
|
||||
}
|
||||
|
||||
<Pallet<T, I>>::unlock();
|
||||
|
||||
// Now set new scores that will move nodes into higher bags
|
||||
for i in 0..regular_count {
|
||||
let node: T::AccountId = account("regular_node", i, 0);
|
||||
let new_score = match i % 3 {
|
||||
0 => mid,
|
||||
1 => high,
|
||||
_ => high + high, // force into a new top bag
|
||||
};
|
||||
T::ScoreProvider::set_score_of(&node, new_score);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
PendingRebag::<T, I>::count(),
|
||||
pending_count,
|
||||
"Expected exactly {} pending rebag entries",
|
||||
pending_count
|
||||
);
|
||||
// Ensure we have at least three bags populated before rebag
|
||||
assert!(List::<T, _>::get_bags().len() >= 2);
|
||||
}
|
||||
: {
|
||||
use pezframe_support::traits::Hooks;
|
||||
<Pallet<T, I> as Hooks<_>>::on_idle(Default::default(), Weight::MAX);
|
||||
}
|
||||
verify {
|
||||
// Verify all pending rebag entries were processed.
|
||||
// This should always be true since pending_count = rebag_budget / 3 < rebag_budget,
|
||||
// and pending accounts are processed first so all pending entries fit within the budget.
|
||||
assert_eq!(PendingRebag::<T, I>::count(), 0, "All pending rebag entries should be processed");
|
||||
|
||||
// Count how many nodes ended up in higher bags
|
||||
let total_rebagged: usize = List::<T, _>::get_bags()
|
||||
.iter()
|
||||
.filter(|(b, _)| *b > T::BagThresholds::get()[0])
|
||||
.map(|(_, nodes)| nodes.len())
|
||||
.sum();
|
||||
|
||||
let expected = <T as Config<I>>::MaxAutoRebagPerBlock::get() as usize;
|
||||
assert_eq!(total_rebagged, expected, "Expected exactly {:?} rebagged nodes, found {:?}", expected, total_rebagged);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
mock::ExtBuilder::default().skip_genesis_ids().build(),
|
||||
mock::Runtime
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,799 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! > Made with *Bizinikiwi*, for *Pezkuwi*.
|
||||
//!
|
||||
//! [![github]](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/pezframe/bags-list) -
|
||||
//! [![pezkuwi]](https://pezkuwichain.io)
|
||||
//!
|
||||
//! [pezkuwi]:
|
||||
//! https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white
|
||||
//! [github]:
|
||||
//! https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
|
||||
//!
|
||||
//! # Bags-List Pallet
|
||||
//!
|
||||
//! An onchain implementation of a semi-sorted linked list, with permissionless sorting and update
|
||||
//! operations.
|
||||
//!
|
||||
//! ## Pallet API
|
||||
//!
|
||||
//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
|
||||
//! including its configuration trait, dispatchables, storage items, events and errors.
|
||||
//!
|
||||
//! This pallet provides an implementation of
|
||||
//! [`pezframe_election_provider_support::SortedListProvider`] and it can typically be used by another
|
||||
//! pallet via this API.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! This pallet splits `AccountId`s into different bags. Within a bag, these `AccountId`s are stored
|
||||
//! as nodes in a linked-list manner. This pallet then provides iteration over all bags, which
|
||||
//! basically allows an infinitely large list of items to be kept in a sorted manner.
|
||||
//!
|
||||
//! Each bags has a upper and lower range of scores, denoted by [`Config::BagThresholds`]. All nodes
|
||||
//! within a bag must be within the range of the bag. If not, the permissionless [`Pallet::rebag`]
|
||||
//! can be used to move any node to the right bag.
|
||||
//!
|
||||
//! Once a `rebag` happens, the order within a node is still not enforced. To move a node to the
|
||||
//! optimal position in a bag, the [`Pallet::put_in_front_of`] or [`Pallet::put_in_front_of_other`]
|
||||
//! can be used.
|
||||
//!
|
||||
//! Additional reading, about how this pallet is used in the context of Pezkuwi's staking system:
|
||||
//! <https://pezkuwichain.io/blog/staking-update-september-2021/#bags-list-in-depth>
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! See [`example`] for a diagram of `rebag` and `put_in_front_of` operations.
|
||||
//!
|
||||
//! ## Low Level / Implementation Details
|
||||
//!
|
||||
//! The data structure exposed by this pallet aims to be optimized for:
|
||||
//!
|
||||
//! - insertions and removals.
|
||||
//! - iteration over the top* N items by score, where the precise ordering of items doesn't
|
||||
//! particularly matter.
|
||||
//!
|
||||
//! ### Further Details
|
||||
//!
|
||||
//! - 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.
|
||||
//! - items within a bag are iterated in order of insertion. Thus removing an item and re-inserting
|
||||
//! 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 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)]
|
||||
|
||||
extern crate alloc;
|
||||
#[cfg(doc)]
|
||||
#[cfg_attr(doc, aquamarine::aquamarine)]
|
||||
///
|
||||
/// In this example, assuming each node has an equal id and score (eg. node 21 has a score of 21),
|
||||
/// the node 22 can be moved from bag 1 to bag 0 with the `rebag` operation.
|
||||
///
|
||||
/// Once the whole list is iterated, assuming the above above rebag happens, the order of iteration
|
||||
/// would be: `25, 21, 22, 12, 22, 5, 7, 3`.
|
||||
///
|
||||
/// Moreover, in bag2, node 7 can be moved to the front of node 5 with the `put_in_front_of`, as it
|
||||
/// has a higher score.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// graph LR
|
||||
/// Bag0 --> Bag1 --> Bag2
|
||||
///
|
||||
/// subgraph Bag0[Bag 0: 21-30 HEZ]
|
||||
/// direction LR
|
||||
/// 25 --> 21 --> 22X[22]
|
||||
/// end
|
||||
///
|
||||
/// subgraph Bag1[Bag 1: 11-20 HEZ]
|
||||
/// direction LR
|
||||
/// 12 --> 22
|
||||
/// end
|
||||
///
|
||||
/// subgraph Bag2[Bag 2: 0-10 HEZ]
|
||||
/// direction LR
|
||||
/// 5 --> 7 --> 3
|
||||
/// end
|
||||
///
|
||||
/// style 22X stroke-dasharray: 5 5,opacity:50%
|
||||
/// ```
|
||||
///
|
||||
/// The equivalent of this in code would be:
|
||||
#[doc = docify::embed!("src/tests.rs", examples_work)]
|
||||
pub mod example {}
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use codec::FullCodec;
|
||||
use pezframe_election_provider_support::{ScoreProvider, SortedListProvider};
|
||||
use pezframe_support::{
|
||||
traits::Get,
|
||||
weights::{Weight, WeightMeter},
|
||||
};
|
||||
use pezframe_system::ensure_signed;
|
||||
use pezsp_runtime::traits::{AtLeast32BitUnsigned, Bounded, StaticLookup};
|
||||
|
||||
#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
mod benchmarks;
|
||||
|
||||
pub mod list;
|
||||
pub mod migrations;
|
||||
#[cfg(any(test, feature = "fuzz"))]
|
||||
pub mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod weights;
|
||||
|
||||
pub use list::{notional_bag_for, Bag, List, ListError, Node};
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
pub(crate) const LOG_TARGET: &str = "runtime::bags-list";
|
||||
|
||||
// syntactic sugar for logging.
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
|
||||
log::$level!(
|
||||
target: crate::LOG_TARGET,
|
||||
concat!("[{:?}] 👜 [{}]", $patter),
|
||||
<pezframe_system::Pallet<T>>::block_number(),
|
||||
<crate::Pallet::<T, I> as pezframe_support::traits::PalletInfoAccess>::name()
|
||||
$(, $values)*
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
type AccountIdLookupOf<T> = <<T as pezframe_system::Config>::Lookup as StaticLookup>::Source;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T, I = ()>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config<I: 'static = ()>: pezframe_system::Config {
|
||||
/// The overarching event type.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self, I>>
|
||||
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: weights::WeightInfo;
|
||||
|
||||
/// 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 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 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 `Score::MAX`; that value does not need to be
|
||||
/// specified within the bag. For any two threshold lists, if one ends with
|
||||
/// `Score::MAX`, the other one does not, and they are otherwise equal, the two
|
||||
/// lists will behave identically.
|
||||
///
|
||||
/// # Calculation
|
||||
///
|
||||
/// It is recommended to generate the set of thresholds in a geometric series, such that
|
||||
/// there exists some constant ratio such that `threshold[k + 1] == (threshold[k] *
|
||||
/// constant_ratio).max(threshold[k] + 1)` for all `k`.
|
||||
///
|
||||
/// The helpers in the `/utils/pezframe/generate-bags` module can simplify this calculation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// - If `BagThresholds::get().is_empty()`, then all ids are put into the same bag, and
|
||||
/// iteration is strictly in insertion order.
|
||||
/// - If `BagThresholds::get().len() == 64`, and the thresholds are determined according to
|
||||
/// 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 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 [Self::Score]>;
|
||||
|
||||
/// Maximum number of accounts that may be re-bagged automatically in `on_idle`.
|
||||
///
|
||||
/// A value of `0` (obtained by configuring `type MaxAutoRebagPerBlock = ();`) disables
|
||||
/// the feature.
|
||||
#[pallet::constant]
|
||||
type MaxAutoRebagPerBlock: Get<u32>;
|
||||
|
||||
/// The type used to dictate a node position relative to other nodes.
|
||||
type Score: Clone
|
||||
+ Default
|
||||
+ PartialEq
|
||||
+ Eq
|
||||
+ Ord
|
||||
+ PartialOrd
|
||||
+ core::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 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 type ListBags<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Twox64Concat, T::Score, list::Bag<T, I>>;
|
||||
|
||||
/// Pointer that remembers the next node that will be auto-rebagged.
|
||||
/// When `None`, the next scan will start from the list head again.
|
||||
#[pallet::storage]
|
||||
pub type NextNodeAutoRebagged<T: Config<I>, I: 'static = ()> =
|
||||
StorageValue<_, T::AccountId, OptionQuery>;
|
||||
|
||||
/// Lock all updates to this pallet.
|
||||
///
|
||||
/// If any nodes needs updating, removal or addition due to a temporary lock, the
|
||||
/// [`Call::rebag`] can be used.
|
||||
#[pallet::storage]
|
||||
pub type Lock<T: Config<I>, I: 'static = ()> = StorageValue<_, (), OptionQuery>;
|
||||
|
||||
/// Accounts that failed to be inserted into the bags-list due to locking.
|
||||
/// These accounts will be processed with priority in `on_idle` or via `rebag` extrinsic.
|
||||
///
|
||||
/// Note: This storage is intentionally unbounded. The following factors make bounding
|
||||
/// unnecessary:
|
||||
/// 1. The storage usage is temporary - accounts are processed and removed in `on_idle`
|
||||
/// 2. The pallet is only locked during snapshot generation, which is weight-limited
|
||||
/// 3. Processing happens at multiple accounts per block, clearing even large backlogs quickly
|
||||
/// 4. An artificial limit could be exhausted by an attacker, preventing legitimate
|
||||
/// auto-rebagging from putting accounts in the correct position
|
||||
///
|
||||
/// We don't store the score here - it's always fetched from `ScoreProvider` when processing,
|
||||
/// ensuring we use the most up-to-date score (accounts may have been slashed, rewarded, etc.
|
||||
/// while waiting in the queue).
|
||||
#[pallet::storage]
|
||||
pub type PendingRebag<T: Config<I>, I: 'static = ()> =
|
||||
CountedStorageMap<_, Twox64Concat, T::AccountId, ()>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
/// Moved an account from one bag to another.
|
||||
Rebagged { who: T::AccountId, from: T::Score, to: T::Score },
|
||||
/// Updated the score of some account to the given amount.
|
||||
ScoreUpdated { who: T::AccountId, new_score: T::Score },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
/// A error in the list interface implementation.
|
||||
List(ListError),
|
||||
/// Could not update a node, because the pallet is locked.
|
||||
Locked,
|
||||
}
|
||||
|
||||
impl<T, I> From<ListError> for Error<T, I> {
|
||||
fn from(t: ListError) -> Self {
|
||||
Error::<T, I>::List(t)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::view_functions]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Get the current `score` of a given account.
|
||||
///
|
||||
/// Returns `(current, real_score)`, the former being the current score that this pallet is
|
||||
/// aware of, which may or may not be up to date, and the latter being the real score, as
|
||||
/// provided by
|
||||
// [`Config::ScoreProvider`].
|
||||
///
|
||||
/// If the two differ, it means this node is eligible for [`Call::rebag`].
|
||||
pub fn scores(who: T::AccountId) -> (Option<T::Score>, Option<T::Score>) {
|
||||
(ListNodes::<T, I>::get(&who).map(|node| node.score), T::ScoreProvider::score(&who))
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Declare that some `dislocated` account has, through rewards or penalties, sufficiently
|
||||
/// 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.
|
||||
///
|
||||
/// Will always update the stored score of `dislocated` to the correct score, based on
|
||||
/// `ScoreProvider`.
|
||||
///
|
||||
/// If `dislocated` does not exists, it returns an error.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::rebag_non_terminal().max(T::WeightInfo::rebag_terminal()))]
|
||||
pub fn rebag(origin: OriginFor<T>, dislocated: AccountIdLookupOf<T>) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
let dislocated = T::Lookup::lookup(dislocated)?;
|
||||
Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
|
||||
|
||||
Self::rebag_internal(&dislocated).map_err::<DispatchError, _>(Into::into)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move the caller's Id directly in front of `lighter`.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ and can only be called by the Id of
|
||||
/// the account going in front of `lighter`. Fee is payed by the origin under all
|
||||
/// circumstances.
|
||||
///
|
||||
/// Only works if:
|
||||
///
|
||||
/// - both nodes are within the same bag,
|
||||
/// - and `origin` has a greater `Score` than `lighter`.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::put_in_front_of())]
|
||||
pub fn put_in_front_of(
|
||||
origin: OriginFor<T>,
|
||||
lighter: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
let heavier = ensure_signed(origin)?;
|
||||
let lighter = T::Lookup::lookup(lighter)?;
|
||||
Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
|
||||
List::<T, I>::put_in_front_of(&lighter, &heavier)
|
||||
.map_err::<Error<T, I>, _>(Into::into)
|
||||
.map_err::<DispatchError, _>(Into::into)
|
||||
}
|
||||
|
||||
/// Same as [`Pallet::put_in_front_of`], but it can be called by anyone.
|
||||
///
|
||||
/// Fee is paid by the origin under all circumstances.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::put_in_front_of())]
|
||||
pub fn put_in_front_of_other(
|
||||
origin: OriginFor<T>,
|
||||
heavier: AccountIdLookupOf<T>,
|
||||
lighter: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
let lighter = T::Lookup::lookup(lighter)?;
|
||||
let heavier = T::Lookup::lookup(heavier)?;
|
||||
Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
|
||||
List::<T, I>::put_in_front_of(&lighter, &heavier)
|
||||
.map_err::<Error<T, I>, _>(Into::into)
|
||||
.map_err::<DispatchError, _>(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
|
||||
fn integrity_test() {
|
||||
// to ensure they are strictly increasing, this also implies that duplicates are
|
||||
// detected.
|
||||
assert!(
|
||||
T::BagThresholds::get().windows(2).all(|window| window[1] > window[0]),
|
||||
"thresholds must strictly increase, and have no duplicates",
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn try_state(_: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
|
||||
<Self as SortedListProvider<T::AccountId>>::try_state()
|
||||
}
|
||||
|
||||
/// Called during the idle phase of block execution.
|
||||
/// Automatically performs a limited number of `rebag` operations each block,
|
||||
/// incrementally correcting the position of accounts within the bags-list.
|
||||
///
|
||||
/// Processes accounts in the following priority order:
|
||||
/// 1. Pending accounts that failed to be inserted due to locking
|
||||
/// 2. Regular accounts that need rebagging
|
||||
///
|
||||
/// Guarantees processing as many nodes as possible without failing on errors.
|
||||
/// It stores a persistent cursor to continue across blocks.
|
||||
fn on_idle(_n: BlockNumberFor<T>, limit: Weight) -> Weight {
|
||||
let mut meter = WeightMeter::with_limit(limit);
|
||||
// This weight assumes worst-case usage of `MaxAutoRebagPerBlock`.
|
||||
// Changing the runtime value requires re-running the benchmarks.
|
||||
if meter.try_consume(T::WeightInfo::on_idle()).is_err() {
|
||||
log!(debug, "Not enough Weight for on_idle. Skipping rebugging.");
|
||||
return Weight::zero();
|
||||
}
|
||||
|
||||
let rebag_budget = T::MaxAutoRebagPerBlock::get();
|
||||
if rebag_budget == 0 {
|
||||
log!(debug, "Auto-rebag skipped: rebag_budget=0");
|
||||
return meter.consumed();
|
||||
}
|
||||
|
||||
let total_nodes = ListNodes::<T, I>::count();
|
||||
let pending_count = PendingRebag::<T, I>::count();
|
||||
|
||||
if total_nodes == 0 && pending_count == 0 {
|
||||
log!(debug, "Auto-rebag skipped: total_nodes=0 and pending_count=0");
|
||||
return meter.consumed();
|
||||
}
|
||||
|
||||
if Self::ensure_unlocked().is_err() {
|
||||
log!(debug, "Auto-rebag skipped: pallet is locked");
|
||||
return meter.consumed();
|
||||
}
|
||||
|
||||
log!(
|
||||
debug,
|
||||
"Starting auto-rebag. Budget: {} accounts/block, total_nodes={}, pending_count={}.",
|
||||
rebag_budget,
|
||||
total_nodes,
|
||||
pending_count
|
||||
);
|
||||
|
||||
let cursor = NextNodeAutoRebagged::<T, I>::get();
|
||||
let regular_iter = match cursor {
|
||||
Some(ref last) => {
|
||||
log!(debug, "Next node from previous block: {:?}", last);
|
||||
|
||||
// Build an iterator that yields `last` first, then everything *after* it.
|
||||
let tail = Self::iter_from(last).unwrap_or_else(|_| Self::iter());
|
||||
let head_and_tail = core::iter::once(last.clone()).chain(tail);
|
||||
Box::new(head_and_tail) as Box<dyn Iterator<Item = T::AccountId>>
|
||||
},
|
||||
None => {
|
||||
log!(debug, "No NextNodeAutoRebagged found. Starting from head of the list");
|
||||
Self::iter()
|
||||
},
|
||||
};
|
||||
|
||||
// Chain PendingRebag accounts with regular ListNodes.
|
||||
// PendingRebag comes first for priority processing
|
||||
let combined_iter = PendingRebag::<T, I>::iter_keys().chain(regular_iter);
|
||||
|
||||
let accounts: Vec<_> = combined_iter.take((rebag_budget + 1) as usize).collect();
|
||||
|
||||
// Safe split: if we reached (or passed) the tail of the list, we don't want to panic.
|
||||
let (to_process, next_cursor) = if accounts.len() <= rebag_budget as usize {
|
||||
// This guarantees we either get the next account to process
|
||||
// or gracefully receive None.
|
||||
(accounts.as_slice(), &[][..])
|
||||
} else {
|
||||
accounts.split_at(rebag_budget as usize)
|
||||
};
|
||||
|
||||
let mut processed = 0u32;
|
||||
let mut successful_rebags = 0u32;
|
||||
let mut failed_rebags = 0u32;
|
||||
let mut pending_processed = 0u32;
|
||||
|
||||
for account in to_process {
|
||||
let pending_value =
|
||||
if PendingRebag::<T, I>::contains_key(&account) { 1 } else { 0 };
|
||||
|
||||
match Self::rebag_internal(&account) {
|
||||
Err(Error::<T, I>::Locked) => {
|
||||
defensive!("Pallet became locked during auto-rebag, stopping");
|
||||
break;
|
||||
},
|
||||
Err(e) => {
|
||||
log!(warn, "Error during rebagging: {:?}", e);
|
||||
failed_rebags += 1;
|
||||
},
|
||||
Ok(Some((from, to))) => {
|
||||
log!(debug, "Rebagged {:?}: moved from {:?} to {:?}", account, from, to);
|
||||
successful_rebags += 1;
|
||||
pending_processed += pending_value;
|
||||
},
|
||||
Ok(None) => {
|
||||
log!(debug, "Rebagging not needed for {:?}", account);
|
||||
pending_processed += pending_value;
|
||||
},
|
||||
}
|
||||
|
||||
processed += 1;
|
||||
if processed == rebag_budget {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor - only track regular ListNodes accounts, not PendingRebag
|
||||
let next_regular_account =
|
||||
next_cursor.iter().find(|account| !PendingRebag::<T, I>::contains_key(account));
|
||||
|
||||
match next_regular_account {
|
||||
// Defensive check: prevents re-processing the same node multiple times within a
|
||||
// single block. This situation should not occur during normal execution, but
|
||||
// can happen in test environments or if `on_idle()` is invoked more than once
|
||||
// per block (e.g. via custom test harnesses or manual calls).
|
||||
Some(next) if to_process.contains(next) => {
|
||||
NextNodeAutoRebagged::<T, I>::kill();
|
||||
defensive!("Loop detected: {:?} already processed — cursor killed", next);
|
||||
},
|
||||
// Normal case: save the next regular node as a cursor for the following block.
|
||||
Some(next) => {
|
||||
NextNodeAutoRebagged::<T, I>::put(next);
|
||||
log!(debug, "Saved next node to be processed in rebag cursor: {:?}", next);
|
||||
},
|
||||
// End of regular list reached: no cursor needed.
|
||||
// This happens when either:
|
||||
// 1. We've processed all regular accounts in the list, OR
|
||||
// 2. We've collected fewer than budget+1 accounts (meaning the iterator was
|
||||
// exhausted)
|
||||
// Since pending accounts are processed first and not tracked in the cursor,
|
||||
// this simply means there are no more regular accounts to process.
|
||||
None => {
|
||||
NextNodeAutoRebagged::<T, I>::kill();
|
||||
log!(debug, "End of regular list reached — cursor killed");
|
||||
},
|
||||
}
|
||||
|
||||
let weight_used = meter.consumed();
|
||||
log!(
|
||||
debug,
|
||||
"Auto-rebag finished: processed={}, successful_rebags={}, errors={}, pending_processed={}, weight_used={:?}",
|
||||
processed,
|
||||
successful_rebags,
|
||||
failed_rebags,
|
||||
pending_processed,
|
||||
weight_used
|
||||
);
|
||||
|
||||
weight_used
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
pub fn do_try_state() -> Result<(), TryRuntimeError> {
|
||||
List::<T, I>::do_try_state()
|
||||
}
|
||||
}
|
||||
|
||||
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 `Ok(Some((from, to)))`.
|
||||
pub fn do_rebag(
|
||||
account: &T::AccountId,
|
||||
new_score: T::Score,
|
||||
) -> Result<Option<(T::Score, T::Score)>, ListError> {
|
||||
// If no voter at that node, don't do anything. the caller just wasted the fee to call this.
|
||||
let node = list::Node::<T, I>::get(&account).ok_or(ListError::NodeNotFound)?;
|
||||
if node.score != new_score {
|
||||
Self::deposit_event(Event::<T, I>::ScoreUpdated { who: account.clone(), new_score });
|
||||
}
|
||||
let maybe_movement = List::update_position_for(node, new_score);
|
||||
if let Some((from, to)) = maybe_movement {
|
||||
Self::deposit_event(Event::<T, I>::Rebagged { who: account.clone(), from, to });
|
||||
};
|
||||
Ok(maybe_movement)
|
||||
}
|
||||
|
||||
fn ensure_unlocked() -> Result<(), ListError> {
|
||||
match Lock::<T, I>::get() {
|
||||
None => Ok(()),
|
||||
Some(()) => Err(ListError::Locked),
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent to `ListBags::get`, but public. Useful for tests in outside of this crate.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn list_bags_get(score: T::Score) -> Option<list::Bag<T, I>> {
|
||||
ListBags::get(score)
|
||||
}
|
||||
|
||||
/// Perform the internal rebagging logic for an account based on its updated score.
|
||||
/// This function does not handle origin checks or higher-level dispatch logic.
|
||||
///
|
||||
/// Returns `Ok(Some((from, to)))` if rebagging occurred, or `Ok(None)` if nothing changed.
|
||||
fn rebag_internal(account: &T::AccountId) -> Result<Option<(T::Score, T::Score)>, Error<T, I>> {
|
||||
// Ensure the pallet is not locked
|
||||
Self::ensure_unlocked().map_err(|_| Error::<T, I>::Locked)?;
|
||||
|
||||
PendingRebag::<T, I>::remove(account);
|
||||
|
||||
// Check if the account exists and retrieve its current score
|
||||
let existed = ListNodes::<T, I>::contains_key(account);
|
||||
let maybe_score = T::ScoreProvider::score(account);
|
||||
|
||||
match (existed, maybe_score) {
|
||||
(true, Some(current_score)) => {
|
||||
// The account exists and has a valid score, so try to rebag
|
||||
log!(debug, "Attempting to rebag node {:?}", account);
|
||||
Pallet::<T, I>::do_rebag(account, current_score)
|
||||
.map_err::<Error<T, I>, _>(Into::into)
|
||||
},
|
||||
(false, Some(current_score)) => {
|
||||
// The account doesn't exist, but it has a valid score - insert it
|
||||
log!(debug, "Inserting node {:?} with score {:?}", account, current_score);
|
||||
List::<T, I>::insert(account.clone(), current_score)
|
||||
.map_err::<Error<T, I>, _>(Into::into)?;
|
||||
Ok(None)
|
||||
},
|
||||
(true, None) => {
|
||||
// The account exists but no longer has a valid score, so remove it
|
||||
log!(debug, "Removing node {:?}", account);
|
||||
List::<T, I>::remove(account).map_err::<Error<T, I>, _>(Into::into)?;
|
||||
Ok(None)
|
||||
},
|
||||
(false, None) => {
|
||||
// The account doesn't exist and has no valid score - do nothing
|
||||
Err(Error::<T, I>::List(ListError::NodeNotFound))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I> {
|
||||
type Error = ListError;
|
||||
type Score = T::Score;
|
||||
|
||||
fn range() -> (Self::Score, Self::Score) {
|
||||
use pezframe_support::traits::Get;
|
||||
(
|
||||
T::BagThresholds::get().first().cloned().unwrap_or_default(),
|
||||
T::BagThresholds::get().last().cloned().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
|
||||
Box::new(List::<T, I>::iter().map(|n| n.id().clone()))
|
||||
}
|
||||
|
||||
fn lock() {
|
||||
Lock::<T, I>::put(())
|
||||
}
|
||||
|
||||
fn unlock() {
|
||||
Lock::<T, I>::kill()
|
||||
}
|
||||
|
||||
fn iter_from(
|
||||
start: &T::AccountId,
|
||||
) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
|
||||
let iter = List::<T, I>::iter_from(start)?;
|
||||
Ok(Box::new(iter.map(|n| n.id().clone())))
|
||||
}
|
||||
|
||||
fn count() -> u32 {
|
||||
ListNodes::<T, I>::count()
|
||||
}
|
||||
|
||||
fn contains(id: &T::AccountId) -> bool {
|
||||
List::<T, I>::contains(id)
|
||||
}
|
||||
|
||||
fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
|
||||
Pallet::<T, I>::ensure_unlocked().inspect_err(|_| {
|
||||
// Pallet is locked - store in PendingRebag for later processing
|
||||
// Only queue if auto-rebagging is enabled
|
||||
if T::MaxAutoRebagPerBlock::get() > 0u32 {
|
||||
PendingRebag::<T, I>::insert(&id, ());
|
||||
}
|
||||
})?;
|
||||
List::<T, I>::insert(id, score)
|
||||
}
|
||||
|
||||
fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> {
|
||||
Pallet::<T, I>::ensure_unlocked()?;
|
||||
Pallet::<T, I>::do_rebag(id, new_score).map(|_| ())
|
||||
}
|
||||
|
||||
fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
|
||||
List::<T, I>::get_score(id)
|
||||
}
|
||||
|
||||
fn on_remove(id: &T::AccountId) -> Result<(), ListError> {
|
||||
Pallet::<T, I>::ensure_unlocked()?;
|
||||
List::<T, I>::remove(id)
|
||||
}
|
||||
|
||||
fn unsafe_regenerate(
|
||||
all: impl IntoIterator<Item = T::AccountId>,
|
||||
score_of: Box<dyn Fn(&T::AccountId) -> Option<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, I>::unsafe_regenerate(all, score_of)
|
||||
}
|
||||
|
||||
fn unsafe_clear() {
|
||||
// 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, I>::unsafe_clear()
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn try_state() -> Result<(), TryRuntimeError> {
|
||||
Self::do_try_state()
|
||||
}
|
||||
|
||||
pezframe_election_provider_support::runtime_benchmarks_enabled! {
|
||||
fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score {
|
||||
use pezframe_support::traits::Get as _;
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let node = list::Node::<T, I>::get(who).unwrap();
|
||||
let current_bag_idx = thresholds
|
||||
.iter()
|
||||
.chain(core::iter::once(&T::Score::max_value()))
|
||||
.position(|w| w == &node.bag_upper)
|
||||
.unwrap();
|
||||
|
||||
if is_increase {
|
||||
let next_threshold_idx = current_bag_idx + 1;
|
||||
assert!(thresholds.len() > next_threshold_idx);
|
||||
thresholds[next_threshold_idx]
|
||||
} else {
|
||||
assert!(current_bag_idx != 0);
|
||||
let prev_threshold_idx = current_bag_idx - 1;
|
||||
thresholds[prev_threshold_idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> ScoreProvider<T::AccountId> for Pallet<T, I> {
|
||||
type Score = <Pallet<T, I> as SortedListProvider<T::AccountId>>::Score;
|
||||
|
||||
fn score(id: &T::AccountId) -> Option<T::Score> {
|
||||
Node::<T, I>::get(id).map(|node| node.score())
|
||||
}
|
||||
|
||||
pezframe_election_provider_support::runtime_benchmarks_or_std_enabled! {
|
||||
fn set_score_of(id: &T::AccountId, new_score: T::Score) {
|
||||
ListNodes::<T, I>::mutate(id, |maybe_node| {
|
||||
if let Some(node) = maybe_node.as_mut() {
|
||||
node.score = new_score;
|
||||
} else {
|
||||
panic!("trying to mutate {:?} which does not exists", id);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,972 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementation of a "bags list": a semi-sorted list where ordering granularity is dictated by
|
||||
//! configurable thresholds that delineate the boundaries of bags. It uses a pattern of composite
|
||||
//! data structures, where multiple storage items are masked by one outer API. See
|
||||
//! [`crate::ListNodes`], [`crate::ListBags`] for more information.
|
||||
//!
|
||||
//! The outer API of this module is the [`List`] struct. It wraps all acceptable operations on top
|
||||
//! of the aggregate linked list. All operations with the bags list should happen through this
|
||||
//! interface.
|
||||
|
||||
use crate::Config;
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
|
||||
};
|
||||
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
|
||||
use core::{iter, marker::PhantomData};
|
||||
use pezframe_election_provider_support::ScoreProvider;
|
||||
use pezframe_support::{
|
||||
defensive, ensure,
|
||||
traits::{Defensive, DefensiveOption, Get},
|
||||
CloneNoBound, DefaultNoBound, EqNoBound, PalletError, PartialEqNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_runtime::traits::{Bounded, Zero};
|
||||
|
||||
#[cfg(any(
|
||||
test,
|
||||
feature = "try-runtime",
|
||||
feature = "fuzz",
|
||||
feature = "std",
|
||||
feature = "runtime-benchmarks"
|
||||
))]
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Encode,
|
||||
Decode,
|
||||
DecodeWithMemTracking,
|
||||
MaxEncodedLen,
|
||||
TypeInfo,
|
||||
PalletError,
|
||||
)]
|
||||
pub enum ListError {
|
||||
/// A duplicate id has been detected.
|
||||
Duplicate,
|
||||
/// An Id does not have a greater score than another Id.
|
||||
NotHeavier,
|
||||
/// Attempted to place node in front of a node in another bag.
|
||||
NotInSameBag,
|
||||
/// Given node id was not found.
|
||||
NodeNotFound,
|
||||
/// The List is locked, therefore updates cannot happen now.
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// 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 `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| score > threshold);
|
||||
thresholds.get(idx).copied().unwrap_or_else(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 `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 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 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<I>, I: 'static = ()>(PhantomData<(T, I)>);
|
||||
|
||||
impl<T: Config<I>, I: 'static> List<T, I> {
|
||||
/// Remove all data associated with the list from storage.
|
||||
///
|
||||
/// ## WARNING
|
||||
///
|
||||
/// 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() {
|
||||
#[allow(deprecated)]
|
||||
crate::ListBags::<T, I>::remove_all(None);
|
||||
#[allow(deprecated)]
|
||||
crate::ListNodes::<T, I>::remove_all();
|
||||
}
|
||||
|
||||
/// Regenerate all of the data from the given ids.
|
||||
///
|
||||
/// WARNING: this is expensive and should only ever be performed when the list needs to be
|
||||
/// generated from scratch. Care needs to be taken to ensure
|
||||
///
|
||||
/// This may or may not need to be called at genesis as well, based on the configuration of the
|
||||
/// pallet using this `List`.
|
||||
///
|
||||
/// Returns the number of ids migrated.
|
||||
pub fn unsafe_regenerate(
|
||||
all: impl IntoIterator<Item = T::AccountId>,
|
||||
score_of: Box<dyn Fn(&T::AccountId) -> Option<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, score_of)
|
||||
}
|
||||
|
||||
/// Migrate the list from one set of thresholds to another.
|
||||
///
|
||||
/// This should only be called as part of an intentional migration; it's fairly expensive.
|
||||
///
|
||||
/// Returns the number of accounts affected.
|
||||
///
|
||||
/// Preconditions:
|
||||
///
|
||||
/// - `old_thresholds` is the previous list of thresholds.
|
||||
/// - All `bag_upper` currently in storage are members of `old_thresholds`.
|
||||
/// - `T::BagThresholds` has already been updated and is the new set of thresholds.
|
||||
///
|
||||
/// Postconditions:
|
||||
///
|
||||
/// - All `bag_upper` currently in storage are members of `T::BagThresholds`.
|
||||
/// - No id is changed unless required to by the difference between the old threshold list and
|
||||
/// the new.
|
||||
/// - 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: &[T::Score]) -> u32 {
|
||||
let new_thresholds = T::BagThresholds::get();
|
||||
if new_thresholds == old_thresholds {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we can't check all preconditions, but we can check one
|
||||
debug_assert!(
|
||||
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, I>::iter()
|
||||
.all(|(_, node)| old_thresholds.contains(&node.bag_upper)),
|
||||
"not all `node.bag_upper` currently in storage are members of `old_thresholds`",
|
||||
);
|
||||
|
||||
let old_set: BTreeSet<_> = old_thresholds.iter().copied().collect();
|
||||
let new_set: BTreeSet<_> = new_thresholds.iter().copied().collect();
|
||||
|
||||
// accounts that need to be rebagged
|
||||
let mut affected_accounts = BTreeSet::new();
|
||||
// track affected old bags to make sure we only iterate them once
|
||||
let mut affected_old_bags = BTreeSet::new();
|
||||
|
||||
let new_bags = new_set.difference(&old_set).copied();
|
||||
// a new bag means that all accounts previously using the old bag's threshold must now
|
||||
// be rebagged
|
||||
for inserted_bag in new_bags {
|
||||
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_else(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
|
||||
// no point iterating through bag 10 twice.
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bag) = Bag::<T, I>::get(affected_bag) {
|
||||
affected_accounts.extend(bag.iter().map(|node| node.id));
|
||||
}
|
||||
}
|
||||
|
||||
let removed_bags = old_set.difference(&new_set).copied();
|
||||
// a removed bag means that all members of that bag must be rebagged
|
||||
for removed_bag in removed_bags.clone() {
|
||||
if !affected_old_bags.insert(removed_bag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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 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(), score_of);
|
||||
debug_assert_eq!(_inserted, num_affected);
|
||||
|
||||
// we couldn't previously remove the old bags because both insertion and removal assume that
|
||||
// it's always safe to add a bag if it's not present. Now that that's sorted, we can get rid
|
||||
// of them.
|
||||
//
|
||||
// it's pretty cheap to iterate this again, because both sets are in-memory and require no
|
||||
// lookups.
|
||||
for removed_bag in removed_bags {
|
||||
debug_assert!(
|
||||
!crate::ListNodes::<T, I>::iter().any(|(_, node)| node.bag_upper == removed_bag),
|
||||
"no id should be present in a removed bag",
|
||||
);
|
||||
crate::ListBags::<T, I>::remove(removed_bag);
|
||||
}
|
||||
|
||||
num_affected
|
||||
}
|
||||
|
||||
/// Returns `true` if the list contains `id`, otherwise returns `false`.
|
||||
pub(crate) fn contains(id: &T::AccountId) -> bool {
|
||||
crate::ListNodes::<T, I>::contains_key(id)
|
||||
}
|
||||
|
||||
/// Get the score of the given node,
|
||||
pub fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
|
||||
Node::<T, I>::get(id).map(|node| node.score()).ok_or(ListError::NodeNotFound)
|
||||
}
|
||||
|
||||
/// 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)`, or call `.next()` one by one.
|
||||
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.
|
||||
//
|
||||
// It's important to retain the ability to omit the final bound because it makes tests much
|
||||
// easier; they can just configure `type BagThresholds = ()`.
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let iter = thresholds.iter().copied();
|
||||
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(T::Score::max_value())).rev())
|
||||
};
|
||||
|
||||
iter.filter_map(Bag::get).flat_map(|bag| bag.iter())
|
||||
}
|
||||
|
||||
/// Same as `iter`, but we start from a specific node.
|
||||
///
|
||||
/// All items after this node are returned, excluding `start` itself.
|
||||
pub(crate) fn iter_from(
|
||||
start: &T::AccountId,
|
||||
) -> Result<impl Iterator<Item = Node<T, I>>, ListError> {
|
||||
// We chain two iterators:
|
||||
// 1. from the given `start` till the end of the bag
|
||||
// 2. all the bags that come after `start`'s bag.
|
||||
|
||||
let start_node = Node::<T, I>::get(start).ok_or(ListError::NodeNotFound)?;
|
||||
let start_node_upper = start_node.bag_upper;
|
||||
let start_bag = core::iter::successors(start_node.next(), |prev| prev.next());
|
||||
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let idx = thresholds.partition_point(|&threshold| start_node_upper > threshold);
|
||||
let leftover_bags = thresholds
|
||||
.into_iter()
|
||||
.take(idx)
|
||||
.copied()
|
||||
.rev()
|
||||
.filter_map(Bag::get)
|
||||
.flat_map(|bag| bag.iter());
|
||||
|
||||
crate::log!(
|
||||
debug,
|
||||
"starting to iterate from {:?}, who's bag is {:?}, and there are {:?} leftover bags",
|
||||
&start,
|
||||
start_node_upper,
|
||||
idx
|
||||
);
|
||||
Ok(start_bag.chain(leftover_bags))
|
||||
}
|
||||
|
||||
/// Insert several ids into the appropriate bags in the list. Continues with insertions
|
||||
/// if duplicates are detected.
|
||||
///
|
||||
/// Returns the final count of number of ids inserted.
|
||||
fn insert_many(
|
||||
ids: impl IntoIterator<Item = T::AccountId>,
|
||||
score_of: impl Fn(&T::AccountId) -> Option<T::Score>,
|
||||
) -> u32 {
|
||||
let mut count = 0;
|
||||
ids.into_iter().for_each(|v| {
|
||||
if let Some(score) = score_of(&v) {
|
||||
if Self::insert(v, score).is_ok() {
|
||||
count += 1;
|
||||
}
|
||||
} else {
|
||||
// nada
|
||||
}
|
||||
});
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// 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, score: T::Score) -> Result<(), ListError> {
|
||||
if Self::contains(&id) {
|
||||
return Err(ListError::Duplicate);
|
||||
}
|
||||
|
||||
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(), score);
|
||||
|
||||
// new inserts are always the tail, so we must write the bag.
|
||||
bag.put();
|
||||
|
||||
crate::log!(
|
||||
trace,
|
||||
"inserted {:?} with score {:?} into bag {:?}, new count is {}",
|
||||
id,
|
||||
score,
|
||||
bag_score,
|
||||
crate::ListNodes::<T, I>::count(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an id from the list, returning an error if `id` does not exists.
|
||||
pub(crate) fn remove(id: &T::AccountId) -> Result<(), ListError> {
|
||||
if !Self::contains(id) {
|
||||
return Err(ListError::NodeNotFound);
|
||||
}
|
||||
let _ = Self::remove_many(core::iter::once(id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove many ids from the list.
|
||||
///
|
||||
/// This is more efficient than repeated calls to `Self::remove`.
|
||||
///
|
||||
/// Returns the final count of number of ids removed.
|
||||
fn remove_many<'a>(ids: impl IntoIterator<Item = &'a T::AccountId>) -> u32 {
|
||||
let mut bags = BTreeMap::new();
|
||||
let mut count = 0;
|
||||
|
||||
for id in ids.into_iter() {
|
||||
let node = match Node::<T, I>::get(id) {
|
||||
Some(node) => node,
|
||||
None => continue,
|
||||
};
|
||||
count += 1;
|
||||
|
||||
if !node.is_terminal() {
|
||||
// this node is not a head or a tail and thus the bag does not need to be updated
|
||||
node.excise()
|
||||
} else {
|
||||
// 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, 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);
|
||||
}
|
||||
|
||||
// now get rid of the node itself
|
||||
node.remove_from_storage_unchecked()
|
||||
}
|
||||
|
||||
for (_, bag) in bags {
|
||||
bag.put();
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// Update a node's position in the list.
|
||||
///
|
||||
/// If the node was in the correct bag, no effect. If the node was in the incorrect bag, they
|
||||
/// are moved into the correct bag.
|
||||
///
|
||||
/// Returns `Some((old_idx, new_idx))` if the node moved, otherwise `None`. In both cases, the
|
||||
/// node's score is written to the `score` field. Thus, this is not a noop, even if `None`.
|
||||
///
|
||||
/// This operation is somewhat more efficient than simply calling [`self.remove`] followed by
|
||||
/// [`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(
|
||||
mut node: Node<T, I>,
|
||||
new_score: T::Score,
|
||||
) -> Option<(T::Score, T::Score)> {
|
||||
node.score = new_score;
|
||||
if node.is_misplaced(new_score) {
|
||||
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, I>::get(node.bag_upper) {
|
||||
// this is a head or tail, so the bag must be updated.
|
||||
bag.remove_node_unchecked(&node);
|
||||
bag.put();
|
||||
} else {
|
||||
pezframe_support::defensive!(
|
||||
"Node did not have a bag; BagsList is in an inconsistent state"
|
||||
);
|
||||
}
|
||||
|
||||
// put the node into the appropriate new bag.
|
||||
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);
|
||||
bag.put();
|
||||
|
||||
Some((old_bag_upper, new_bag_upper))
|
||||
} else {
|
||||
// just write the new score.
|
||||
node.put();
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `heavier_id` to the position directly in front of `lighter_id`. Both ids must be in the
|
||||
/// 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<(), ListError> {
|
||||
let lighter_node = Node::<T, I>::get(&lighter_id).ok_or(ListError::NodeNotFound)?;
|
||||
let heavier_node = Node::<T, I>::get(&heavier_id).ok_or(ListError::NodeNotFound)?;
|
||||
|
||||
ensure!(lighter_node.bag_upper == heavier_node.bag_upper, ListError::NotInSameBag);
|
||||
|
||||
// this is the most expensive check, so we do it last.
|
||||
ensure!(
|
||||
T::ScoreProvider::score(&heavier_id) > T::ScoreProvider::score(&lighter_id),
|
||||
ListError::NotHeavier
|
||||
);
|
||||
|
||||
// remove the heavier node from this list. Note that this removes the node from storage and
|
||||
// decrements the node counter.
|
||||
let _ =
|
||||
Self::remove(&heavier_id).defensive_proof("both nodes have been checked to exist; qed");
|
||||
|
||||
// re-fetch `lighter_node` from storage since it may have been updated when `heavier_node`
|
||||
// was removed.
|
||||
let lighter_node =
|
||||
Node::<T, I>::get(lighter_id).defensive_ok_or_else(|| ListError::NodeNotFound)?;
|
||||
|
||||
// insert `heavier_node` directly in front of `lighter_node`. This will update both nodes
|
||||
// in storage and update the node counter.
|
||||
Self::insert_at_unchecked(lighter_node, heavier_node);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert `node` directly in front of `at`.
|
||||
///
|
||||
/// WARNINGS:
|
||||
/// - 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, I>, mut node: Node<T, I>) {
|
||||
// connect `node` to its new `prev`.
|
||||
node.prev = at.prev.clone();
|
||||
if let Some(mut prev) = at.prev() {
|
||||
prev.next = Some(node.id().clone());
|
||||
prev.put()
|
||||
}
|
||||
|
||||
// connect `node` and `at`.
|
||||
node.next = Some(at.id().clone());
|
||||
at.prev = Some(node.id().clone());
|
||||
|
||||
if node.is_terminal() {
|
||||
// `node` is the new head, so we make sure the bag is updated. Note,
|
||||
// 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, I>::get(at.bag_upper)
|
||||
.expect("given nodes must always have a valid bag. qed.");
|
||||
|
||||
if node.prev == None {
|
||||
bag.head = Some(node.id().clone())
|
||||
}
|
||||
|
||||
bag.put()
|
||||
};
|
||||
|
||||
// write the updated nodes to storage.
|
||||
at.put();
|
||||
node.put();
|
||||
}
|
||||
|
||||
/// Check the internal state of the list.
|
||||
///
|
||||
/// This should be called from the call-site, whenever one of the mutating apis (e.g. `insert`)
|
||||
/// is being used, after all other staking data (such as counter) has been updated. It checks:
|
||||
///
|
||||
/// * there are no duplicate ids,
|
||||
/// * length of this list is in sync with `ListNodes::count()`,
|
||||
/// * and sanity-checks all bags and nodes. This will cascade down all the checks and makes sure
|
||||
/// all bags and nodes are checked per *any* update to `List`.
|
||||
#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
|
||||
pub(crate) fn do_try_state() -> Result<(), TryRuntimeError> {
|
||||
let mut seen_in_list = BTreeSet::new();
|
||||
ensure!(
|
||||
Self::iter().map(|node| node.id).all(|id| seen_in_list.insert(id)),
|
||||
"duplicate identified"
|
||||
);
|
||||
|
||||
let iter_count = Self::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");
|
||||
|
||||
crate::log!(trace, "count of nodes: {}", stored_count);
|
||||
|
||||
let active_bags = {
|
||||
let thresholds = T::BagThresholds::get().iter().copied();
|
||||
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))
|
||||
};
|
||||
|
||||
// build map of bags and the corresponding nodes to avoid multiple lookups
|
||||
let mut bags_map = BTreeMap::<T::Score, Vec<T::AccountId>>::new();
|
||||
|
||||
active_bags.clone().try_for_each(|b| {
|
||||
bags_map.insert(
|
||||
b.bag_upper,
|
||||
b.iter().map(|n: Node<T, I>| n.id().clone()).collect::<Vec<_>>(),
|
||||
);
|
||||
b.do_try_state()
|
||||
})?;
|
||||
|
||||
let nodes_in_bags_count =
|
||||
active_bags.clone().fold(0u32, |acc, cur| acc + cur.iter().count() as u32);
|
||||
ensure!(nodes_count == nodes_in_bags_count, "stored_count != nodes_in_bags_count");
|
||||
|
||||
crate::log!(trace, "count of active bags {}", active_bags.count());
|
||||
|
||||
// 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, I>::iter() {
|
||||
// check that the node is in the correct bag
|
||||
let expected_bag = bags_map
|
||||
.get(&node.bag_upper)
|
||||
.ok_or("bag not found for the node in active bags")?;
|
||||
pezframe_support::ensure!(expected_bag.contains(node.id()), "node not found in the bag");
|
||||
|
||||
// verify node state
|
||||
node.do_try_state()?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<(T::Score, Vec<T::AccountId>)> {
|
||||
use pezframe_support::traits::Get as _;
|
||||
|
||||
let thresholds = T::BagThresholds::get();
|
||||
let iter = thresholds.iter().copied();
|
||||
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(core::iter::once(T::Score::max_value())))
|
||||
};
|
||||
|
||||
iter.filter_map(|t| {
|
||||
Bag::<T, I>::get(t)
|
||||
.map(|bag| (t, bag.iter().map(|n| n.id().clone()).collect::<Vec<_>>()))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Bag is a doubly-linked list of ids, where each id is mapped to a [`Node`].
|
||||
///
|
||||
/// Note that we maintain both head and tail pointers. While it would be possible to get away with
|
||||
/// maintaining only a head pointer and cons-ing elements onto the front of the list, it's more
|
||||
/// desirable to ensure that there is some element of first-come, first-serve to the list's
|
||||
/// iteration so that there's no incentive to churn ids positioning to improve the chances of
|
||||
/// appearing within the ids set.
|
||||
#[derive(
|
||||
DefaultNoBound,
|
||||
Encode,
|
||||
Decode,
|
||||
MaxEncodedLen,
|
||||
TypeInfo,
|
||||
RuntimeDebugNoBound,
|
||||
CloneNoBound,
|
||||
PartialEqNoBound,
|
||||
EqNoBound,
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T, I))]
|
||||
pub struct Bag<T: Config<I>, I: 'static = ()> {
|
||||
pub head: Option<T::AccountId>,
|
||||
pub tail: Option<T::AccountId>,
|
||||
|
||||
#[codec(skip)]
|
||||
pub bag_upper: T::Score,
|
||||
#[codec(skip)]
|
||||
pub _phantom: PhantomData<I>,
|
||||
}
|
||||
|
||||
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: T::Score,
|
||||
) -> Self {
|
||||
Self { head, tail, bag_upper, _phantom: PhantomData }
|
||||
}
|
||||
|
||||
/// 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 score or make it, appropriately initialized. Does not check if
|
||||
/// if `bag_upper` is a valid threshold.
|
||||
fn get_or_make(bag_upper: T::Score) -> Bag<T, I> {
|
||||
Self::get(bag_upper).unwrap_or(Bag { bag_upper, ..Default::default() })
|
||||
}
|
||||
|
||||
/// `True` if self is empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.head.is_none() && self.tail.is_none()
|
||||
}
|
||||
|
||||
/// Put the bag back into storage.
|
||||
fn put(self) {
|
||||
if self.is_empty() {
|
||||
crate::ListBags::<T, I>::remove(self.bag_upper);
|
||||
} else {
|
||||
crate::ListBags::<T, I>::insert(self.bag_upper, self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the head node in this bag.
|
||||
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, 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, I>> {
|
||||
core::iter::successors(self.head(), |prev| prev.next())
|
||||
}
|
||||
|
||||
/// Insert a new id into this bag.
|
||||
///
|
||||
/// This is private on purpose because it's naive: it doesn't check whether this is the
|
||||
/// appropriate bag for this id at all. Generally, use [`List::insert`] instead.
|
||||
///
|
||||
/// Storage note: this modifies storage, but only for the nodes. You still need to call
|
||||
/// `self.put()` after use.
|
||||
fn insert_unchecked(&mut self, id: T::AccountId, score: T::Score) {
|
||||
// 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, I> {
|
||||
id,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper: Zero::zero(),
|
||||
score,
|
||||
_phantom: PhantomData,
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a node into this bag.
|
||||
///
|
||||
/// This is private on purpose because it's naive; it doesn't check whether this is the
|
||||
/// appropriate bag for this node at all. Generally, use [`List::insert`] instead.
|
||||
///
|
||||
/// 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, 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
|
||||
// infinite loop.
|
||||
defensive!("system logic error: inserting a node who has the id of tail");
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
// re-set the `bag_upper`. Regardless of whatever the node had previously, now it is going
|
||||
// to be `self.bag_upper`.
|
||||
node.bag_upper = self.bag_upper;
|
||||
|
||||
let id = node.id.clone();
|
||||
// update this node now, treating it as the new tail.
|
||||
node.prev = self.tail.clone();
|
||||
node.next = None;
|
||||
node.put();
|
||||
|
||||
// update the previous tail.
|
||||
if let Some(mut old_tail) = self.tail() {
|
||||
old_tail.next = Some(id.clone());
|
||||
old_tail.put();
|
||||
}
|
||||
self.tail = Some(id.clone());
|
||||
|
||||
// ensure head exist. This is only set when the length of the bag is just 1, i.e. if this is
|
||||
// the first insertion into the bag. In this case, both head and tail should point to the
|
||||
// same node.
|
||||
if self.head.is_none() {
|
||||
self.head = Some(id);
|
||||
debug_assert!(self.iter().count() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a node from this bag.
|
||||
///
|
||||
/// This is private on purpose because it doesn't check whether this bag contains the node in
|
||||
/// the first place. Generally, use [`List::remove`] instead, similar to `insert_unchecked`.
|
||||
///
|
||||
/// 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, I>) {
|
||||
// reassign neighboring nodes.
|
||||
node.excise();
|
||||
|
||||
// clear the bag head/tail pointers as necessary.
|
||||
if self.tail.as_ref() == Some(&node.id) {
|
||||
self.tail = node.prev.clone();
|
||||
}
|
||||
if self.head.as_ref() == Some(&node.id) {
|
||||
self.head = node.next.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the internal state of the bag.
|
||||
///
|
||||
/// Should be called by the call-site, after any mutating operation on a bag. The call site of
|
||||
/// this struct is always `List`.
|
||||
///
|
||||
/// * Ensures head has no prev.
|
||||
/// * Ensures tail has no next.
|
||||
/// * Ensures there are no loops, traversal from head to tail is correct.
|
||||
#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
|
||||
fn do_try_state(&self) -> Result<(), TryRuntimeError> {
|
||||
pezframe_support::ensure!(
|
||||
self.head()
|
||||
.map(|head| head.prev().is_none())
|
||||
// if there is no head, then there must not be a tail, meaning that the bag is
|
||||
// empty.
|
||||
.unwrap_or_else(|| self.tail.is_none()),
|
||||
"head has a prev"
|
||||
);
|
||||
|
||||
pezframe_support::ensure!(
|
||||
self.tail()
|
||||
.map(|tail| tail.next().is_none())
|
||||
// if there is no tail, then there must not be a head, meaning that the bag is
|
||||
// empty.
|
||||
.unwrap_or_else(|| self.head.is_none()),
|
||||
"tail has a next"
|
||||
);
|
||||
|
||||
let mut seen_in_bag = BTreeSet::new();
|
||||
pezframe_support::ensure!(
|
||||
self.iter()
|
||||
.map(|node| node.id)
|
||||
// each voter is only seen once, thus there is no cycle within a bag
|
||||
.all(|voter| seen_in_bag.insert(voter)),
|
||||
"duplicate found in bag"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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, I>> {
|
||||
core::iter::successors(self.head(), |prev| prev.next())
|
||||
}
|
||||
}
|
||||
|
||||
/// A Node is the fundamental element comprising the doubly-linked list described by `Bag`.
|
||||
#[derive(
|
||||
Encode,
|
||||
Decode,
|
||||
MaxEncodedLen,
|
||||
TypeInfo,
|
||||
CloneNoBound,
|
||||
PartialEqNoBound,
|
||||
EqNoBound,
|
||||
RuntimeDebugNoBound,
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T, I))]
|
||||
pub struct Node<T: Config<I>, I: 'static = ()> {
|
||||
pub id: T::AccountId,
|
||||
pub prev: Option<T::AccountId>,
|
||||
pub next: Option<T::AccountId>,
|
||||
pub bag_upper: T::Score,
|
||||
pub score: T::Score,
|
||||
#[codec(skip)]
|
||||
pub _phantom: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Node<T, I> {
|
||||
/// Get a node by id.
|
||||
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, I>::insert(self.id.clone(), self);
|
||||
}
|
||||
|
||||
/// Update neighboring nodes to point to reach other.
|
||||
///
|
||||
/// Only updates storage for adjacent nodes, but not `self`; so the user may need to call
|
||||
/// `self.put`.
|
||||
fn excise(&self) {
|
||||
// Update previous node.
|
||||
if let Some(mut prev) = self.prev() {
|
||||
prev.next = self.next.clone();
|
||||
prev.put();
|
||||
}
|
||||
// Update next self.
|
||||
if let Some(mut next) = self.next() {
|
||||
next.prev = self.prev.clone();
|
||||
next.put();
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a naive function that removes a node from the `ListNodes` storage item.
|
||||
///
|
||||
/// 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, I>::remove(&self.id)
|
||||
}
|
||||
|
||||
/// Get the previous node in the bag.
|
||||
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, 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_score: T::Score) -> bool {
|
||||
notional_bag_for::<T, I>(current_score) != self.bag_upper
|
||||
}
|
||||
|
||||
/// `true` when this voter is a bag head or tail.
|
||||
fn is_terminal(&self) -> bool {
|
||||
self.prev.is_none() || self.next.is_none()
|
||||
}
|
||||
|
||||
/// Get the underlying voter.
|
||||
pub(crate) fn id(&self) -> &T::AccountId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Get the current vote weight of the node.
|
||||
pub(crate) fn score(&self) -> T::Score {
|
||||
self.score
|
||||
}
|
||||
|
||||
/// Get the underlying voter (public fo tests).
|
||||
#[cfg(feature = "std")]
|
||||
#[allow(dead_code)]
|
||||
pub fn std_id(&self) -> &T::AccountId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", feature = "fuzz", test))]
|
||||
pub fn set_score(&mut self, s: T::Score) {
|
||||
self.score = s
|
||||
}
|
||||
|
||||
/// The bag this nodes belongs to (public for benchmarks).
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
#[allow(dead_code)]
|
||||
pub fn bag_upper(&self) -> T::Score {
|
||||
self.bag_upper
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
|
||||
fn do_try_state(&self) -> Result<(), TryRuntimeError> {
|
||||
let expected_bag = Bag::<T, I>::get(self.bag_upper).ok_or("bag not found for node")?;
|
||||
let id = self.id();
|
||||
|
||||
let non_terminal_check = !self.is_terminal() &&
|
||||
expected_bag.head.as_ref() != Some(id) &&
|
||||
expected_bag.tail.as_ref() != Some(id);
|
||||
let terminal_check =
|
||||
expected_bag.head.as_ref() == Some(id) || expected_bag.tail.as_ref() == Some(id);
|
||||
pezframe_support::ensure!(
|
||||
non_terminal_check || terminal_check,
|
||||
"a terminal node is neither its bag head or tail"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,967 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
mock::{test_utils::*, *},
|
||||
ListBags, ListNodes,
|
||||
};
|
||||
use pezframe_election_provider_support::{SortedListProvider, VoteWeight};
|
||||
use pezframe_support::{assert_ok, assert_storage_noop};
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
fn node(
|
||||
id: AccountId,
|
||||
prev: Option<AccountId>,
|
||||
next: Option<AccountId>,
|
||||
bag_upper: VoteWeight,
|
||||
) -> Node<Runtime> {
|
||||
Node::<Runtime> { id, prev, next, bag_upper, score: bag_upper, _phantom: PhantomData }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
assert_eq!(ListNodes::<Runtime>::count(), 4);
|
||||
assert_eq!(ListNodes::<Runtime>::iter().count(), 4);
|
||||
assert_eq!(ListBags::<Runtime>::iter().count(), 2);
|
||||
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// 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, _phantom: PhantomData }
|
||||
);
|
||||
assert_eq!(
|
||||
ListBags::<Runtime>::get(1_000).unwrap(),
|
||||
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));
|
||||
assert_eq!(ListNodes::<Runtime>::get(3).unwrap(), node(3, Some(2), Some(4), 1_000));
|
||||
assert_eq!(ListNodes::<Runtime>::get(4).unwrap(), node(4, Some(3), None, 1_000));
|
||||
assert_eq!(ListNodes::<Runtime>::get(1).unwrap(), node(1, None, None, 10));
|
||||
|
||||
// non-existent id does not have a storage footprint
|
||||
assert_eq!(ListNodes::<Runtime>::get(42), None);
|
||||
|
||||
// iteration of the bags would yield:
|
||||
assert_eq!(
|
||||
List::<Runtime>::iter().map(|n| *n.id()).collect::<Vec<_>>(),
|
||||
vec![2, 3, 4, 1],
|
||||
// ^^ note the order of insertion in genesis!
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
// at a threshold gives that threshold.
|
||||
assert_eq!(notional_bag_for::<Runtime, _>(10), 10);
|
||||
|
||||
// above the threshold, gives the next threshold.
|
||||
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 T::Score::max_value(),
|
||||
assert!(VoteWeight::MAX > max_explicit_threshold);
|
||||
|
||||
// then anything above it will belong to the T::Score::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]
|
||||
fn remove_last_node_in_bags_cleans_bag() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// bump 1 to a bigger bag
|
||||
List::<Runtime>::remove(&1).unwrap();
|
||||
assert_ok!(List::<Runtime>::insert(1, 10_000));
|
||||
|
||||
// then the bag with bound 10 is wiped from storage.
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(1_000, vec![2, 3, 4]), (10_000, vec![1])]);
|
||||
|
||||
// and can be recreated again as needed.
|
||||
assert_ok!(List::<Runtime>::insert(77, 10));
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![77]), (1_000, vec![2, 3, 4]), (10_000, vec![1])]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_works() {
|
||||
ExtBuilder::default()
|
||||
.add_ids(vec![(710, 15), (711, 16), (712, 2_000)])
|
||||
.build_and_execute(|| {
|
||||
// given
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![
|
||||
(10, vec![1]),
|
||||
(20, vec![710, 711]),
|
||||
(1_000, vec![2, 3, 4]),
|
||||
(2_000, vec![712])
|
||||
]
|
||||
);
|
||||
let old_thresholds = <Runtime as Config>::BagThresholds::get();
|
||||
assert_eq!(old_thresholds, vec![10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]);
|
||||
|
||||
// when the new thresholds adds `15` and removes `2_000`
|
||||
const NEW_THRESHOLDS: &'static [VoteWeight] =
|
||||
&[10, 15, 20, 30, 40, 50, 60, 1_000, 10_000];
|
||||
BagThresholds::set(NEW_THRESHOLDS);
|
||||
// and we call
|
||||
List::<Runtime>::migrate(old_thresholds);
|
||||
assert_eq!(List::<Runtime>::do_try_state(), Ok(()));
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![
|
||||
(10, vec![1]),
|
||||
(15, vec![710]), // nodes in range 11 ..= 15 move from bag 20 to bag 15
|
||||
(20, vec![711]),
|
||||
(1_000, vec![2, 3, 4]),
|
||||
// nodes in range 1_001 ..= 2_000 move from bag 2_000 to bag 10_000
|
||||
(10_000, vec![712]),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
mod list {
|
||||
use pezframe_support::assert_noop;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn iteration_is_semi_sorted() {
|
||||
ExtBuilder::default()
|
||||
.add_ids(vec![(5, 2_000), (6, 2_000)])
|
||||
.build_and_execute(|| {
|
||||
// given
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1]), (1_000, vec![2, 3, 4]), (2_000, vec![5, 6])]
|
||||
);
|
||||
assert_eq!(
|
||||
get_list_as_ids(),
|
||||
vec![
|
||||
5, 6, // best bag
|
||||
2, 3, 4, // middle bag
|
||||
1, // last bag.
|
||||
]
|
||||
);
|
||||
|
||||
// when adding an id that has a higher score than pre-existing ids in the bag
|
||||
assert_ok!(List::<Runtime>::insert(7, 10));
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
get_list_as_ids(),
|
||||
vec![
|
||||
5, 6, // best bag
|
||||
2, 3, 4, // middle bag
|
||||
1, 7, // last bag; new id is last.
|
||||
]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
/// we can `take` x ids, even if that quantity ends midway through a list.
|
||||
#[test]
|
||||
fn take_works() {
|
||||
ExtBuilder::default()
|
||||
.add_ids(vec![(5, 2_000), (6, 2_000)])
|
||||
.build_and_execute(|| {
|
||||
// given
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1]), (1_000, vec![2, 3, 4]), (2_000, vec![5, 6])]
|
||||
);
|
||||
|
||||
// when
|
||||
let iteration =
|
||||
List::<Runtime>::iter().map(|node| *node.id()).take(4).collect::<Vec<_>>();
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
iteration,
|
||||
vec![
|
||||
5, 6, // best bag, fully iterated
|
||||
2, 3, // middle bag, partially iterated
|
||||
]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// when inserting into an existing bag
|
||||
assert_ok!(List::<Runtime>::insert(5, 1_000));
|
||||
|
||||
// then
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5])]);
|
||||
assert_eq!(get_list_as_ids(), vec![2, 3, 4, 5, 1]);
|
||||
|
||||
// when inserting into a non-existent bag
|
||||
assert_ok!(List::<Runtime>::insert(6, 1_001));
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5]), (2_000, vec![6])]
|
||||
);
|
||||
assert_eq!(get_list_as_ids(), vec![6, 2, 3, 4, 5, 1]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_errors_with_duplicate_id() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given
|
||||
assert!(get_list_as_ids().contains(&3));
|
||||
|
||||
// then
|
||||
assert_noop!(List::<Runtime>::insert(3, 20), ListError::Duplicate);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_works() {
|
||||
use crate::{ListBags, ListNodes};
|
||||
let ensure_left = |id, counter| {
|
||||
assert!(!ListNodes::<Runtime>::contains_key(id));
|
||||
assert_eq!(ListNodes::<Runtime>::count(), counter);
|
||||
assert_eq!(ListNodes::<Runtime>::iter().count() as u32, counter);
|
||||
};
|
||||
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// removing a non-existent id is a noop
|
||||
assert!(!ListNodes::<Runtime>::contains_key(42));
|
||||
assert_noop!(List::<Runtime>::remove(&42), ListError::NodeNotFound);
|
||||
|
||||
// when removing a node from a bag with multiple nodes:
|
||||
List::<Runtime>::remove(&2).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(get_list_as_ids(), vec![3, 4, 1]);
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![3, 4])]);
|
||||
ensure_left(2, 3);
|
||||
|
||||
// when removing a node from a bag with only one node:
|
||||
List::<Runtime>::remove(&1).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(get_list_as_ids(), vec![3, 4]);
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(1_000, vec![3, 4])]);
|
||||
ensure_left(1, 2);
|
||||
// bag 10 is removed
|
||||
assert!(!ListBags::<Runtime>::contains_key(10));
|
||||
|
||||
// remove remaining ids to make sure storage cleans up as expected
|
||||
List::<Runtime>::remove(&3).unwrap();
|
||||
ensure_left(3, 1);
|
||||
assert_eq!(get_list_as_ids(), vec![4]);
|
||||
|
||||
List::<Runtime>::remove(&4).unwrap();
|
||||
ensure_left(4, 0);
|
||||
assert_eq!(get_list_as_ids(), Vec::<AccountId>::new());
|
||||
|
||||
// bags are deleted via removals
|
||||
assert_eq!(ListBags::<Runtime>::iter().count(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_many_is_noop_with_non_existent_ids() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let non_existent_ids = vec![&42, &666, &13];
|
||||
|
||||
// when account ids don' exist in the list
|
||||
assert!(non_existent_ids.iter().all(|id| !BagsList::contains(id)));
|
||||
|
||||
// then removing them is a noop
|
||||
assert_storage_noop!(List::<Runtime>::remove_many(non_existent_ids));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_position_for_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given a correctly placed account 1 at bag 10.
|
||||
let node = Node::<Runtime>::get(&1).unwrap();
|
||||
assert_eq!(node.score, 10);
|
||||
assert!(!node.is_misplaced(10));
|
||||
|
||||
// .. it is invalid with score 20
|
||||
assert!(node.is_misplaced(20));
|
||||
|
||||
// move it to bag 20.
|
||||
assert_eq!(List::<Runtime>::update_position_for(node.clone(), 20), Some((10, 20)));
|
||||
assert_eq!(Node::<Runtime>::get(&1).unwrap().score, 20);
|
||||
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(20, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// get the new updated node; try and update the position with no change in score.
|
||||
let node = Node::<Runtime>::get(&1).unwrap();
|
||||
assert_storage_noop!(assert_eq!(
|
||||
List::<Runtime>::update_position_for(node.clone(), 20),
|
||||
None
|
||||
));
|
||||
|
||||
// then move it to bag 1_000 by giving it score 500.
|
||||
assert_eq!(List::<Runtime>::update_position_for(node.clone(), 500), Some((20, 1_000)));
|
||||
assert_eq!(Node::<Runtime>::get(&1).unwrap().score, 500);
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(1_000, vec![2, 3, 4, 1])]);
|
||||
|
||||
// moving within that bag again is a noop
|
||||
let node = Node::<Runtime>::get(&1).unwrap();
|
||||
assert_eq!(List::<Runtime>::update_position_for(node.clone(), 750), None);
|
||||
assert_eq!(Node::<Runtime>::get(&1).unwrap().score, 750);
|
||||
assert_eq!(List::<Runtime>::update_position_for(node.clone(), 1_000), None,);
|
||||
assert_eq!(Node::<Runtime>::get(&1).unwrap().score, 1_000);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_state_works() {
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
assert_ok!(List::<Runtime>::do_try_state());
|
||||
});
|
||||
|
||||
// make sure there are no duplicates.
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
Bag::<Runtime>::get(10).unwrap().insert_unchecked(2, 10);
|
||||
assert_eq!(
|
||||
List::<Runtime>::do_try_state(),
|
||||
TryRuntimeError::Other("duplicate identified").into()
|
||||
);
|
||||
});
|
||||
|
||||
// ensure count is in sync with `ListNodes::count()`.
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
assert_eq!(crate::ListNodes::<Runtime>::count(), 4);
|
||||
// we do some wacky stuff here to get access to the counter, since it is (reasonably)
|
||||
// not exposed as mutable in any sense.
|
||||
#[pezframe_support::storage_alias]
|
||||
type CounterForListNodes<T: Config> =
|
||||
StorageValue<crate::Pallet<T>, u32, pezframe_support::pezpallet_prelude::ValueQuery>;
|
||||
CounterForListNodes::<Runtime>::mutate(|counter| *counter += 1);
|
||||
assert_eq!(crate::ListNodes::<Runtime>::count(), 5);
|
||||
|
||||
assert_eq!(
|
||||
List::<Runtime>::do_try_state(),
|
||||
TryRuntimeError::Other("iter_count != stored_count").into()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
assert!(GENESIS_IDS.iter().all(|(id, _)| List::<Runtime>::contains(id)));
|
||||
|
||||
let non_existent_ids = vec![&42, &666, &13];
|
||||
assert!(non_existent_ids.iter().all(|id| !List::<Runtime>::contains(id)));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
score: 15,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
let node_11_no_bag = Node::<Runtime> {
|
||||
id: 11,
|
||||
prev: None,
|
||||
next: None,
|
||||
bag_upper: 15,
|
||||
score: 15,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// given
|
||||
ListNodes::<Runtime>::insert(10, node_10_no_bag);
|
||||
ListNodes::<Runtime>::insert(11, node_11_no_bag);
|
||||
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![]);
|
||||
|
||||
// then .. this panics
|
||||
let _ = List::<Runtime>::put_in_front_of(&10, &11);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_unchecked_at_is_only_node() {
|
||||
// Note that this `insert_at_unchecked` test should fail post checks because node 42 does
|
||||
// not get re-assigned the correct bag upper. This is because `insert_at_unchecked` assumes
|
||||
// both nodes are already in the same bag with the correct bag upper.
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
// given
|
||||
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,
|
||||
score: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_1 = crate::ListNodes::<Runtime>::get(&1).unwrap();
|
||||
|
||||
// when
|
||||
List::<Runtime>::insert_at_unchecked(node_1, node_42);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![42, 1]), (1_000, vec![2, 3, 4])]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_unchecked_at_is_head() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given
|
||||
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,
|
||||
score: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_2 = crate::ListNodes::<Runtime>::get(&2).unwrap();
|
||||
|
||||
// when
|
||||
List::<Runtime>::insert_at_unchecked(node_2, node_42);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1]), (1_000, vec![42, 2, 3, 4])]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_unchecked_at_is_non_terminal() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given
|
||||
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,
|
||||
score: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_3 = crate::ListNodes::<Runtime>::get(&3).unwrap();
|
||||
|
||||
// when
|
||||
List::<Runtime>::insert_at_unchecked(node_3, node_42);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1]), (1_000, vec![2, 42, 3, 4])]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_unchecked_at_is_tail() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given
|
||||
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,
|
||||
score: 1_000,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
assert!(!crate::ListNodes::<Runtime>::contains_key(42));
|
||||
|
||||
let node_4 = crate::ListNodes::<Runtime>::get(&4).unwrap();
|
||||
|
||||
// when
|
||||
List::<Runtime>::insert_at_unchecked(node_4, node_42);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1]), (1_000, vec![2, 3, 42, 4])]
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mod bags {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn get_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let check_bag = |bag_upper, head, tail, ids| {
|
||||
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, _phantom: PhantomData });
|
||||
assert_eq!(bag_ids, ids);
|
||||
};
|
||||
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
// we can fetch them
|
||||
check_bag(10, Some(1), Some(1), vec![1]);
|
||||
check_bag(1_000, Some(2), Some(4), vec![2, 3, 4]);
|
||||
|
||||
// and all other bag thresholds don't get bags.
|
||||
<Runtime as Config>::BagThresholds::get()
|
||||
.iter()
|
||||
.chain(iter::once(&VoteWeight::MAX))
|
||||
.filter(|bag_upper| !vec![10, 1_000].contains(bag_upper))
|
||||
.for_each(|bag_upper| {
|
||||
assert_storage_noop!(assert_eq!(Bag::<Runtime>::get(*bag_upper), None));
|
||||
assert!(!ListBags::<Runtime>::contains_key(*bag_upper));
|
||||
});
|
||||
|
||||
// when we make a pre-existing bag empty
|
||||
List::<Runtime>::remove(&1).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(Bag::<Runtime>::get(10), None)
|
||||
});
|
||||
}
|
||||
|
||||
#[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,
|
||||
score: bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);
|
||||
|
||||
let mut bag_10 = Bag::<Runtime>::get(10).unwrap();
|
||||
bag_10.insert_node_unchecked(node(42, 5));
|
||||
|
||||
assert_eq!(
|
||||
ListNodes::<Runtime>::get(&42).unwrap(),
|
||||
Node {
|
||||
bag_upper: 10,
|
||||
score: 5,
|
||||
prev: Some(1),
|
||||
next: None,
|
||||
id: 42,
|
||||
_phantom: PhantomData
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[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,
|
||||
score: bag_upper,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// when inserting into a bag with 1 node
|
||||
let mut bag_10 = Bag::<Runtime>::get(10).unwrap();
|
||||
bag_10.insert_node_unchecked(node(42, bag_10.bag_upper));
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_10), vec![1, 42]);
|
||||
|
||||
// when inserting into a bag with 3 nodes
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
bag_1000.insert_node_unchecked(node(52, bag_1000.bag_upper));
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4, 52]);
|
||||
|
||||
// when inserting into a new bag
|
||||
let mut bag_20 = Bag::<Runtime>::get_or_make(20);
|
||||
bag_20.insert_node_unchecked(node(62, bag_20.bag_upper));
|
||||
// then
|
||||
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,
|
||||
score: 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,
|
||||
score: 20,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
);
|
||||
|
||||
// state of all bags is as expected
|
||||
bag_20.put(); // need to put this newly created bag so its in the storage map
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![1, 42]), (20, vec![62, 61]), (1_000, vec![2, 3, 4, 52])]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Document improper ways `insert_node` may be getting used.
|
||||
#[test]
|
||||
fn insert_node_bad_paths_documented() {
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
// when inserting a node with both prev & next pointing at an account in an incorrect
|
||||
// bag.
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
bag_1000.insert_node_unchecked(node(42, Some(1), Some(1), 500));
|
||||
|
||||
// then the proper prev and next is set.
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4, 42]);
|
||||
|
||||
// and when the node is re-fetched all the info is correct
|
||||
assert_eq!(
|
||||
Node::<Runtime>::get(&42).unwrap(),
|
||||
Node::<Runtime> {
|
||||
id: 42,
|
||||
prev: Some(4),
|
||||
next: None,
|
||||
bag_upper: bag_1000.bag_upper,
|
||||
score: 500,
|
||||
_phantom: PhantomData
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
// given 3 is in bag_1000 (and not a tail node)
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4]);
|
||||
|
||||
// when inserting a node with duplicate id 3
|
||||
bag_1000.insert_node_unchecked(node(3, None, None, bag_1000.bag_upper));
|
||||
|
||||
// then all the nodes after the duplicate are lost (because it is set as the tail)
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3]);
|
||||
// also in the full iteration, 2 and 3 are from bag_1000 and 1 is from bag_10.
|
||||
assert_eq!(get_list_as_ids(), vec![2, 3, 1]);
|
||||
|
||||
// and the last accessible node has an **incorrect** prev pointer.
|
||||
assert_eq!(
|
||||
Node::<Runtime>::get(&3).unwrap(),
|
||||
node(3, Some(4), None, bag_1000.bag_upper)
|
||||
);
|
||||
});
|
||||
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
// when inserting a duplicate id of the head
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4]);
|
||||
bag_1000.insert_node_unchecked(node(2, None, None, 0));
|
||||
|
||||
// then all nodes after the head are lost
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2]);
|
||||
|
||||
// and the re-fetched node has bad pointers
|
||||
assert_eq!(
|
||||
Node::<Runtime>::get(&2).unwrap(),
|
||||
Node::<Runtime> {
|
||||
id: 2,
|
||||
prev: Some(4),
|
||||
next: None,
|
||||
bag_upper: bag_1000.bag_upper,
|
||||
score: 0,
|
||||
_phantom: PhantomData
|
||||
},
|
||||
);
|
||||
// ^^^ despite being the bags head, it has a prev
|
||||
|
||||
assert_eq!(
|
||||
bag_1000,
|
||||
Bag { head: Some(2), tail: Some(2), bag_upper: 1_000, _phantom: PhantomData }
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Panics in case of duplicate tail insert (which would result in an infinite loop).
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
should_panic = "system logic error: inserting a node who has the id of tail"
|
||||
)]
|
||||
fn insert_node_duplicate_tail_panics_with_debug_assert() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// given
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])],);
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
|
||||
// when inserting a duplicate id that is already the tail
|
||||
assert_eq!(bag_1000.tail, Some(4));
|
||||
assert_eq!(bag_1000.iter().count(), 3);
|
||||
bag_1000.insert_node_unchecked(node(4, None, None, bag_1000.bag_upper)); // panics in debug
|
||||
assert_eq!(bag_1000.iter().count(), 3); // in release we expect it to silently ignore the
|
||||
// request.
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_node_happy_paths_works() {
|
||||
ExtBuilder::default()
|
||||
.add_ids(vec![
|
||||
(11, 10),
|
||||
(12, 10),
|
||||
(13, 1_000),
|
||||
(14, 1_000),
|
||||
(15, 2_000),
|
||||
(16, 2_000),
|
||||
(17, 2_000),
|
||||
(18, 2_000),
|
||||
(19, 2_000),
|
||||
])
|
||||
.build_and_execute_no_post_check(|| {
|
||||
let mut bag_10 = Bag::<Runtime>::get(10).unwrap();
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
let mut bag_2000 = Bag::<Runtime>::get(2_000).unwrap();
|
||||
|
||||
// given
|
||||
assert_eq!(bag_as_ids(&bag_10), vec![1, 11, 12]);
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4, 13, 14]);
|
||||
assert_eq!(bag_as_ids(&bag_2000), vec![15, 16, 17, 18, 19]);
|
||||
|
||||
// when removing a node that is not pointing at the head or tail
|
||||
let node_4 = Node::<Runtime>::get(&4).unwrap();
|
||||
let node_4_pre_remove = node_4.clone();
|
||||
bag_1000.remove_node_unchecked(&node_4);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 13, 14]);
|
||||
assert_ok!(bag_1000.do_try_state());
|
||||
// and the node isn't mutated when its removed
|
||||
assert_eq!(node_4, node_4_pre_remove);
|
||||
|
||||
// when removing a head that is not pointing at the tail
|
||||
let node_2 = Node::<Runtime>::get(&2).unwrap();
|
||||
bag_1000.remove_node_unchecked(&node_2);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![3, 13, 14]);
|
||||
assert_ok!(bag_1000.do_try_state());
|
||||
|
||||
// when removing a tail that is not pointing at the head
|
||||
let node_14 = Node::<Runtime>::get(&14).unwrap();
|
||||
bag_1000.remove_node_unchecked(&node_14);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![3, 13]);
|
||||
assert_ok!(bag_1000.do_try_state());
|
||||
|
||||
// when removing a tail that is pointing at the head
|
||||
let node_13 = Node::<Runtime>::get(&13).unwrap();
|
||||
bag_1000.remove_node_unchecked(&node_13);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_1000), vec![3]);
|
||||
assert_ok!(bag_1000.do_try_state());
|
||||
|
||||
// when removing a node that is both the head & tail
|
||||
let node_3 = Node::<Runtime>::get(&3).unwrap();
|
||||
bag_1000.remove_node_unchecked(&node_3);
|
||||
bag_1000.put(); // put into storage so `get` returns the updated bag
|
||||
|
||||
// then
|
||||
assert_eq!(Bag::<Runtime>::get(1_000), None);
|
||||
|
||||
// when removing a node that is pointing at both the head & tail
|
||||
let node_11 = Node::<Runtime>::get(&11).unwrap();
|
||||
bag_10.remove_node_unchecked(&node_11);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_10), vec![1, 12]);
|
||||
assert_ok!(bag_10.do_try_state());
|
||||
|
||||
// when removing a head that is pointing at the tail
|
||||
let node_1 = Node::<Runtime>::get(&1).unwrap();
|
||||
bag_10.remove_node_unchecked(&node_1);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_10), vec![12]);
|
||||
assert_ok!(bag_10.do_try_state());
|
||||
// and since we updated the bag's head/tail, we need to write this storage so we
|
||||
// can correctly `get` it again in later checks
|
||||
bag_10.put();
|
||||
|
||||
// when removing a node that is pointing at the head but not the tail
|
||||
let node_16 = Node::<Runtime>::get(&16).unwrap();
|
||||
bag_2000.remove_node_unchecked(&node_16);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_2000), vec![15, 17, 18, 19]);
|
||||
assert_ok!(bag_2000.do_try_state());
|
||||
|
||||
// when removing a node that is pointing at tail, but not head
|
||||
let node_18 = Node::<Runtime>::get(&18).unwrap();
|
||||
bag_2000.remove_node_unchecked(&node_18);
|
||||
|
||||
// then
|
||||
assert_eq!(bag_as_ids(&bag_2000), vec![15, 17, 19]);
|
||||
assert_ok!(bag_2000.do_try_state());
|
||||
|
||||
// finally, when reading from storage, the state of all bags is as expected
|
||||
assert_eq!(
|
||||
List::<Runtime>::get_bags(),
|
||||
vec![(10, vec![12]), (2_000, vec![15, 17, 19])]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_node_bad_paths_documented() {
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
let bad_upper_node_2 = Node::<Runtime> {
|
||||
id: 2,
|
||||
prev: None,
|
||||
next: Some(3),
|
||||
bag_upper: 10, // should be 1_000
|
||||
score: 10,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
let mut bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
|
||||
// when removing a node that is in the bag but has the wrong upper
|
||||
bag_1000.remove_node_unchecked(&bad_upper_node_2);
|
||||
bag_1000.put();
|
||||
|
||||
// then the node is no longer in any bags
|
||||
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![3, 4])]);
|
||||
// .. and the bag it was removed from
|
||||
let bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
// is sane
|
||||
assert_ok!(bag_1000.do_try_state());
|
||||
// and has the correct head and tail.
|
||||
assert_eq!(bag_1000.head, Some(3));
|
||||
assert_eq!(bag_1000.tail, Some(4));
|
||||
});
|
||||
|
||||
// Removing a node that is in another bag, will mess up that other bag.
|
||||
ExtBuilder::default().build_and_execute_no_post_check(|| {
|
||||
// given a tail node is in bag 1_000
|
||||
let node_4 = Node::<Runtime>::get(&4).unwrap();
|
||||
|
||||
// when we remove it from bag 10
|
||||
let mut bag_10 = Bag::<Runtime>::get(10).unwrap();
|
||||
bag_10.remove_node_unchecked(&node_4);
|
||||
bag_10.put();
|
||||
|
||||
// then bag remove was called on is ok,
|
||||
let bag_10 = Bag::<Runtime>::get(10).unwrap();
|
||||
assert_eq!(bag_10.tail, Some(1));
|
||||
assert_eq!(bag_10.head, Some(1));
|
||||
|
||||
// but the bag that the node belonged to is in an invalid state
|
||||
let bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
|
||||
// because it still has the removed node as its tail.
|
||||
assert_eq!(bag_1000.tail, Some(4));
|
||||
assert_eq!(bag_1000.head, Some(2));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod node {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_misplaced_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let node = Node::<Runtime>::get(&1).unwrap();
|
||||
|
||||
// given
|
||||
assert_eq!(node.bag_upper, 10);
|
||||
|
||||
// then within bag 10 its not misplaced,
|
||||
assert!(!node.is_misplaced(0));
|
||||
assert!(!node.is_misplaced(9));
|
||||
assert!(!node.is_misplaced(10));
|
||||
|
||||
// and out of bag 10 it is misplaced
|
||||
assert!(node.is_misplaced(11));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! The migrations of this pallet.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_election_provider_support::ScoreProvider;
|
||||
use pezframe_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezframe_support::ensure;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// A struct that does not migration, but only checks that the counter prefix exists and is correct.
|
||||
pub struct CheckCounterPrefix<T: crate::Config<I>, I: 'static>(core::marker::PhantomData<(T, I)>);
|
||||
impl<T: crate::Config<I>, I: 'static> OnRuntimeUpgrade for CheckCounterPrefix<T, I> {
|
||||
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
|
||||
pezframe_support::weights::Weight::zero()
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
// The old explicit storage item.
|
||||
#[pezframe_support::storage_alias]
|
||||
type CounterForListNodes<T: crate::Config<I>, I: 'static> =
|
||||
StorageValue<crate::Pallet<T, I>, u32>;
|
||||
|
||||
// ensure that a value exists in the counter struct.
|
||||
ensure!(
|
||||
crate::ListNodes::<T, I>::count() == CounterForListNodes::<T, I>::get().unwrap(),
|
||||
"wrong list node counter"
|
||||
);
|
||||
|
||||
crate::log!(
|
||||
info,
|
||||
"checked bags-list prefix to be correct and have {} nodes",
|
||||
crate::ListNodes::<T, I>::count()
|
||||
);
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
mod old {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct PreScoreNode<T: crate::Config<I>, I: 'static = ()> {
|
||||
pub id: T::AccountId,
|
||||
pub prev: Option<T::AccountId>,
|
||||
pub next: Option<T::AccountId>,
|
||||
pub bag_upper: T::Score,
|
||||
#[codec(skip)]
|
||||
pub _phantom: PhantomData<I>,
|
||||
}
|
||||
|
||||
#[pezframe_support::storage_alias]
|
||||
pub type ListNodes<T: crate::Config<I>, I: 'static> = StorageMap<
|
||||
crate::Pallet<T, I>,
|
||||
Twox64Concat,
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
PreScoreNode<T, I>,
|
||||
>;
|
||||
|
||||
#[pezframe_support::storage_alias]
|
||||
pub type CounterForListNodes<T: crate::Config<I>, I: 'static> =
|
||||
StorageValue<crate::Pallet<T, I>, u32, ValueQuery>;
|
||||
}
|
||||
|
||||
/// A struct that migrates all bags lists to contain a score value.
|
||||
pub struct AddScore<T: crate::Config<I>, I: 'static = ()>(core::marker::PhantomData<(T, I)>);
|
||||
impl<T: crate::Config<I>, I: 'static> OnRuntimeUpgrade for AddScore<T, I> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
// The list node data should be corrupt at this point, so this is zero.
|
||||
ensure!(crate::ListNodes::<T, I>::iter().count() == 0, "list node data is not corrupt");
|
||||
// We can use the helper `old::ListNode` to get the existing data.
|
||||
let iter_node_count: u32 = old::ListNodes::<T, I>::iter().count() as u32;
|
||||
let tracked_node_count: u32 = old::CounterForListNodes::<T, I>::get();
|
||||
crate::log!(info, "number of nodes before: {:?} {:?}", iter_node_count, tracked_node_count);
|
||||
ensure!(iter_node_count == tracked_node_count, "Node count is wrong.");
|
||||
Ok(iter_node_count.encode())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
|
||||
for (_key, node) in old::ListNodes::<T, I>::iter() {
|
||||
let score = T::ScoreProvider::score(&node.id);
|
||||
|
||||
let new_node = crate::Node {
|
||||
id: node.id.clone(),
|
||||
prev: node.prev,
|
||||
next: node.next,
|
||||
bag_upper: node.bag_upper,
|
||||
score: score.unwrap_or_default(),
|
||||
_phantom: node._phantom,
|
||||
};
|
||||
|
||||
crate::ListNodes::<T, I>::insert(node.id, new_node);
|
||||
}
|
||||
|
||||
return pezframe_support::weights::Weight::MAX;
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(node_count_before: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let node_count_before: u32 = Decode::decode(&mut node_count_before.as_slice())
|
||||
.expect("the state parameter should be something that was generated by pre_upgrade");
|
||||
// Now the list node data is not corrupt anymore.
|
||||
let iter_node_count_after: u32 = crate::ListNodes::<T, I>::iter().count() as u32;
|
||||
let tracked_node_count_after: u32 = crate::ListNodes::<T, I>::count();
|
||||
crate::log!(
|
||||
info,
|
||||
"number of nodes after: {:?} {:?}",
|
||||
iter_node_count_after,
|
||||
tracked_node_count_after,
|
||||
);
|
||||
ensure!(iter_node_count_after == node_count_before, "Not all nodes were migrated.");
|
||||
ensure!(tracked_node_count_after == iter_node_count_after, "Node count is wrong.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Mock runtime for pezpallet-bags-lists tests.
|
||||
|
||||
use super::*;
|
||||
use crate::{self as bags_list};
|
||||
use pezframe_election_provider_support::VoteWeight;
|
||||
use pezframe_support::{derive_impl, parameter_types};
|
||||
use pezsp_runtime::BuildStorage;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type AccountId = <Runtime as pezframe_system::Config>::AccountId;
|
||||
pub type Balance = u32;
|
||||
|
||||
parameter_types! {
|
||||
// Set the vote weight for any id who's weight has _not_ been set with `set_score_of`.
|
||||
pub static NextVoteWeightMap: HashMap<AccountId, VoteWeight> = Default::default();
|
||||
}
|
||||
|
||||
pub struct StakingMock;
|
||||
impl ScoreProvider<AccountId> for StakingMock {
|
||||
type Score = VoteWeight;
|
||||
|
||||
fn score(id: &AccountId) -> Option<Self::Score> {
|
||||
NextVoteWeightMap::get().get(id).cloned()
|
||||
}
|
||||
|
||||
pezframe_election_provider_support::runtime_benchmarks_or_std_enabled! {
|
||||
fn set_score_of(id: &AccountId, weight: Self::Score) {
|
||||
NEXT_VOTE_WEIGHT_MAP.with(|m| m.borrow_mut().insert(*id, weight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Runtime {
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static BagThresholds: &'static [VoteWeight] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000];
|
||||
pub static AutoRebagNumber: u32 = 10;
|
||||
}
|
||||
|
||||
impl bags_list::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type ScoreProvider = StakingMock;
|
||||
type BagThresholds = BagThresholds;
|
||||
type MaxAutoRebagPerBlock = AutoRebagNumber;
|
||||
type Score = VoteWeight;
|
||||
}
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Runtime>;
|
||||
pezframe_support::construct_runtime!(
|
||||
pub enum Runtime {
|
||||
System: pezframe_system,
|
||||
BagsList: bags_list,
|
||||
}
|
||||
);
|
||||
|
||||
/// Default AccountIds and their weights.
|
||||
pub(crate) const GENESIS_IDS: [(AccountId, VoteWeight); 4] =
|
||||
[(1, 10), (2, 1_000), (3, 1_000), (4, 1_000)];
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtBuilder {
|
||||
ids: Vec<(AccountId, VoteWeight)>,
|
||||
skip_genesis_ids: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", feature = "fuzz", test))]
|
||||
impl ExtBuilder {
|
||||
/// Skip adding the default genesis ids to the list.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn skip_genesis_ids(mut self) -> Self {
|
||||
self.skip_genesis_ids = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add some AccountIds to insert into `List`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn add_ids(mut self, ids: Vec<(AccountId, VoteWeight)>) -> Self {
|
||||
self.ids = ids;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> pezsp_io::TestExternalities {
|
||||
pezsp_tracing::try_init_simple();
|
||||
let storage = pezframe_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
|
||||
|
||||
let ids_with_weight: Vec<_> = if self.skip_genesis_ids {
|
||||
self.ids.iter().collect()
|
||||
} else {
|
||||
GENESIS_IDS.iter().chain(self.ids.iter()).collect()
|
||||
};
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::from(storage);
|
||||
ext.execute_with(|| {
|
||||
for (id, weight) in ids_with_weight {
|
||||
pezframe_support::assert_ok!(List::<Runtime>::insert(*id, *weight));
|
||||
StakingMock::set_score_of(id, *weight);
|
||||
}
|
||||
});
|
||||
|
||||
ext
|
||||
}
|
||||
|
||||
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
|
||||
self.build().execute_with(|| {
|
||||
test();
|
||||
List::<Runtime>::do_try_state().expect("do_try_state post condition failed")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_and_execute_no_post_check(self, test: impl FnOnce() -> ()) {
|
||||
self.build().execute_with(test)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_utils {
|
||||
use super::*;
|
||||
use list::Bag;
|
||||
|
||||
/// Returns the ordered ids within the given bag.
|
||||
pub(crate) fn bag_as_ids(bag: &Bag<Runtime>) -> Vec<AccountId> {
|
||||
bag.iter().map(|n| *n.id()).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Returns the ordered ids from the list.
|
||||
pub(crate) fn get_list_as_ids() -> Vec<AccountId> {
|
||||
List::<Runtime>::iter().map(|n| *n.id()).collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,267 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Autogenerated weights for `pezpallet_bags_list`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-09-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `9907067fc6f8`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --extrinsic=*
|
||||
// --runtime=target/production/wbuild/kitchensink-runtime/kitchensink_runtime.wasm
|
||||
// --pallet=pezpallet_bags_list
|
||||
// --header=/__w/pezkuwi-sdk/pezkuwi-sdk/bizinikiwi/HEADER-APACHE2
|
||||
// --output=/__w/pezkuwi-sdk/pezkuwi-sdk/bizinikiwi/pezframe/bags-list/src/weights.rs
|
||||
// --wasm-execution=compiled
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --heap-pages=4096
|
||||
// --template=bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
// --no-storage-info
|
||||
// --no-min-squares
|
||||
// --no-median-slopes
|
||||
// --exclude-pallets=pezpallet_xcm,pezpallet_xcm_benchmarks::fungible,pezpallet_xcm_benchmarks::generic,pezpallet_nomination_pools,pezpallet_remark,pezpallet_transaction_storage
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_bags_list`.
|
||||
pub trait WeightInfo {
|
||||
fn rebag_non_terminal() -> Weight;
|
||||
fn rebag_terminal() -> Weight;
|
||||
fn put_in_front_of() -> Weight;
|
||||
fn on_idle() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_bags_list` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::PendingRebag` (r:1 w:0)
|
||||
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:4 w:4)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:1 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:1 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:1 w:1)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
fn rebag_non_terminal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1818`
|
||||
// Estimated: `11506`
|
||||
// Minimum execution time: 75_224_000 picoseconds.
|
||||
Weight::from_parts(77_682_000, 11506)
|
||||
.saturating_add(T::DbWeight::get().reads(9_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::PendingRebag` (r:1 w:0)
|
||||
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:3 w:3)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:1 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:1 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:2 w:2)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
fn rebag_terminal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1712`
|
||||
// Estimated: `8877`
|
||||
// Minimum execution time: 73_169_000 picoseconds.
|
||||
Weight::from_parts(75_025_000, 8877)
|
||||
.saturating_add(T::DbWeight::get().reads(9_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:4 w:4)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:2 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:2 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::CounterForListNodes` (r:1 w:1)
|
||||
/// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:1 w:1)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
fn put_in_front_of() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `2024`
|
||||
// Estimated: `11506`
|
||||
// Minimum execution time: 85_000_000 picoseconds.
|
||||
Weight::from_parts(88_005_000, 11506)
|
||||
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(6_u64))
|
||||
}
|
||||
/// Storage: `VoterList::CounterForListNodes` (r:1 w:1)
|
||||
/// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::CounterForPendingRebag` (r:1 w:1)
|
||||
/// Proof: `VoterList::CounterForPendingRebag` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::NextNodeAutoRebagged` (r:1 w:1)
|
||||
/// Proof: `VoterList::NextNodeAutoRebagged` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::PendingRebag` (r:12 w:3)
|
||||
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:199 w:3)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:11 w:10)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:10 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:9 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
fn on_idle() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `4933`
|
||||
// Estimated: `509833`
|
||||
// Minimum execution time: 801_938_000 picoseconds.
|
||||
Weight::from_parts(824_412_000, 509833)
|
||||
.saturating_add(T::DbWeight::get().reads(245_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(19_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::PendingRebag` (r:1 w:0)
|
||||
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:4 w:4)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:1 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:1 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:1 w:1)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
fn rebag_non_terminal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1818`
|
||||
// Estimated: `11506`
|
||||
// Minimum execution time: 75_224_000 picoseconds.
|
||||
Weight::from_parts(77_682_000, 11506)
|
||||
.saturating_add(RocksDbWeight::get().reads(9_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::PendingRebag` (r:1 w:0)
|
||||
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:3 w:3)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:1 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:1 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:2 w:2)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
fn rebag_terminal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1712`
|
||||
// Estimated: `8877`
|
||||
// Minimum execution time: 73_169_000 picoseconds.
|
||||
Weight::from_parts(75_025_000, 8877)
|
||||
.saturating_add(RocksDbWeight::get().reads(9_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:4 w:4)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:2 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:2 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::CounterForListNodes` (r:1 w:1)
|
||||
/// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:1 w:1)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
fn put_in_front_of() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `2024`
|
||||
// Estimated: `11506`
|
||||
// Minimum execution time: 85_000_000 picoseconds.
|
||||
Weight::from_parts(88_005_000, 11506)
|
||||
.saturating_add(RocksDbWeight::get().reads(11_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||
}
|
||||
/// Storage: `VoterList::CounterForListNodes` (r:1 w:1)
|
||||
/// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::CounterForPendingRebag` (r:1 w:1)
|
||||
/// Proof: `VoterList::CounterForPendingRebag` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::Lock` (r:1 w:0)
|
||||
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::NextNodeAutoRebagged` (r:1 w:1)
|
||||
/// Proof: `VoterList::NextNodeAutoRebagged` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::PendingRebag` (r:12 w:3)
|
||||
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListBags` (r:199 w:3)
|
||||
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
|
||||
/// Storage: `VoterList::ListNodes` (r:11 w:10)
|
||||
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Bonded` (r:10 w:0)
|
||||
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Staking::Ledger` (r:9 w:0)
|
||||
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
|
||||
fn on_idle() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `4933`
|
||||
// Estimated: `509833`
|
||||
// Minimum execution time: 801_938_000 picoseconds.
|
||||
Weight::from_parts(824_412_000, 509833)
|
||||
.saturating_add(RocksDbWeight::get().reads(245_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(19_u64))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user