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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
+92
View File
@@ -0,0 +1,92 @@
[package]
name = "pezpallet-bags-list"
version = "27.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "FRAME pallet bags list"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
# parity
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
# primitives
pezsp-runtime = { workspace = true }
# FRAME
pezframe-election-provider-support = { workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
# third party
aquamarine = { workspace = true }
docify = { workspace = true }
log = { workspace = true }
# Optional imports for benchmarking
pezframe-benchmarking = { optional = true, workspace = true }
pezpallet-balances = { optional = true, workspace = true }
pezsp-core = { optional = true, workspace = true }
pezsp-io = { optional = true, workspace = true }
pezsp-tracing = { optional = true, workspace = true }
[dev-dependencies]
pezframe-benchmarking = { workspace = true, default-features = true }
pezframe-election-provider-support = { workspace = true, default-features = true }
pezpallet-balances = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
bizinikiwi-test-utils = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezframe-benchmarking?/std",
"pezframe-election-provider-support/std",
"pezframe-support/std",
"pezframe-system/std",
"log/std",
"pezpallet-balances?/std",
"scale-info/std",
"pezsp-core?/std",
"pezsp-io?/std",
"pezsp-runtime/std",
"pezsp-tracing?/std",
]
runtime-benchmarks = [
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-election-provider-support/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezsp-core",
"pezsp-io",
"pezsp-io?/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-tracing",
]
fuzz = [
"pezframe-election-provider-support/fuzz",
"pezpallet-balances",
"pezsp-core",
"pezsp-io",
"pezsp-tracing",
]
try-runtime = [
"pezframe-election-provider-support/try-runtime",
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-balances?/try-runtime",
"pezsp-runtime/try-runtime",
]
@@ -0,0 +1,2 @@
hfuzz_target
hfuzz_workspace
@@ -0,0 +1,32 @@
[package]
name = "pezpallet-bags-list-fuzzer"
version = "4.0.0-dev"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "Fuzzer for FRAME pallet bags list"
publish = false
[lints]
workspace = true
[[bin]]
name = "bags-list"
path = "src/main.rs"
[dependencies]
pezframe-election-provider-support = { features = [
"fuzz",
], workspace = true, default-features = true }
honggfuzz = { workspace = true }
pezpallet-bags-list = { features = [
"fuzz",
], workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezframe-election-provider-support/runtime-benchmarks",
"pezpallet-bags-list/runtime-benchmarks",
]
@@ -0,0 +1,94 @@
// 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.
//! # Running
//! Running this fuzzer can be done with `cargo hfuzz run bags-list`. `honggfuzz` CLI options can
//! be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
//!
//! # Debugging a panic
//! Once a panic is found, it can be debugged with
//! `cargo hfuzz run-debug fixed_point hfuzz_workspace/bags_list/*.fuzz`.
//!
//! # More information
//! More information about `honggfuzz` can be found
//! [here](https://docs.rs/honggfuzz/).
use pezframe_election_provider_support::{SortedListProvider, VoteWeight};
use honggfuzz::fuzz;
use pezpallet_bags_list::mock::{AccountId, BagsList, ExtBuilder};
const ID_RANGE: AccountId = 25_000;
/// Actions of a `SortedListProvider` that we fuzz.
enum Action {
Insert,
Update,
Remove,
}
impl From<u32> for Action {
fn from(v: u32) -> Self {
let num_variants = Self::Remove as u32 + 1;
match v % num_variants {
_x if _x == Action::Insert as u32 => Action::Insert,
_x if _x == Action::Update as u32 => Action::Update,
_x if _x == Action::Remove as u32 => Action::Remove,
_ => unreachable!(),
}
}
}
fn main() {
ExtBuilder::default().build_and_execute(|| loop {
fuzz!(|data: (AccountId, VoteWeight, u32)| {
let (account_id_seed, vote_weight, action_seed) = data;
let id = account_id_seed % ID_RANGE;
let action = Action::from(action_seed);
match action {
Action::Insert => {
if BagsList::on_insert(id, vote_weight).is_err() {
// this was a duplicate id, which is ok. We can just update it.
BagsList::on_update(&id, vote_weight).unwrap();
}
assert!(BagsList::contains(&id));
},
Action::Update => {
let already_contains = BagsList::contains(&id);
if already_contains {
BagsList::on_update(&id, vote_weight).unwrap();
assert!(BagsList::contains(&id));
} else {
BagsList::on_update(&id, vote_weight).unwrap_err();
}
},
Action::Remove => {
let already_contains = BagsList::contains(&id);
if already_contains {
BagsList::on_remove(&id).unwrap();
} else {
BagsList::on_remove(&id).unwrap_err();
}
assert!(!BagsList::contains(&id));
},
}
assert!(BagsList::do_try_state().is_ok());
})
});
}
@@ -0,0 +1,47 @@
[package]
name = "pezpallet-bags-list-remote-tests"
version = "4.0.0-dev"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "FRAME pallet bags list remote test"
publish = false
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
# frame
pezframe-election-provider-support = { workspace = true, default-features = true }
pezframe-support = { workspace = true, default-features = true }
pezframe-system = { workspace = true, default-features = true }
pezpallet-bags-list = { features = [
"fuzz",
], workspace = true, default-features = true }
pezpallet-staking = { workspace = true, default-features = true }
# core
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# utils
remote-externalities = { workspace = true, default-features = true }
# others
log = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezframe-election-provider-support/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-bags-list/runtime-benchmarks",
"pezpallet-staking/runtime-benchmarks",
"remote-externalities/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,160 @@
// 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.
//! Utilities for remote-testing pezpallet-bags-list.
use pezframe_election_provider_support::ScoreProvider;
use pezpallet_bags_list::Instance1;
/// A common log target to use.
pub const LOG_TARGET: &str = "runtime::bags-list::remote-tests";
pub mod migration;
pub mod snapshot;
pub mod try_state;
/// A wrapper for a runtime that the functions of this crate expect.
///
/// For example, this can be the `Runtime` type of the Pezkuwi runtime.
pub trait RuntimeT<I: 'static>:
pezpallet_staking::Config + pezpallet_bags_list::Config<I> + pezframe_system::Config
{
}
impl<
I: 'static,
T: pezpallet_staking::Config + pezpallet_bags_list::Config<I> + pezframe_system::Config,
> RuntimeT<I> for T
{
}
fn percent(portion: u32, total: u32) -> f64 {
(portion as f64 / total as f64) * 100f64
}
/// Display the number of nodes in each bag, while identifying those that need a rebag.
pub fn display_and_check_bags<Runtime: RuntimeT<Instance1>>(
currency_unit: u64,
currency_name: &'static str,
) {
use pezframe_election_provider_support::SortedListProvider;
use pezframe_support::traits::Get;
let min_nominator_bond = <pezpallet_staking::MinNominatorBond<Runtime>>::get();
log::info!(target: LOG_TARGET, "min nominator bond is {:?}", min_nominator_bond);
let voter_list_count = <Runtime as pezpallet_staking::Config>::VoterList::count();
// go through every bag to track the total number of voters within bags and log some info about
// how voters are distributed within the bags.
let mut seen_in_bags = 0;
let mut rebaggable = 0;
let mut active_bags = 0;
for vote_weight_thresh in <Runtime as pezpallet_bags_list::Config<Instance1>>::BagThresholds::get()
{
let vote_weight_thresh_u64: u64 = (*vote_weight_thresh)
.try_into()
.map_err(|_| "runtime must configure score to at most u64 to use this test")
.unwrap();
// threshold in terms of UNITS (e.g. KSM, HEZ etc)
let vote_weight_thresh_as_unit = vote_weight_thresh_u64 as f64 / currency_unit as f64;
let pretty_thresh = format!("Threshold: {}. {}", vote_weight_thresh_as_unit, currency_name);
let bag = match pezpallet_bags_list::Pallet::<Runtime, Instance1>::list_bags_get(
*vote_weight_thresh,
) {
Some(bag) => bag,
None => {
log::info!(target: LOG_TARGET, "{} NO VOTERS.", pretty_thresh);
continue;
},
};
active_bags += 1;
for id in bag.std_iter().map(|node| node.std_id().clone()) {
let vote_weight =
<Runtime as pezpallet_bags_list::Config<Instance1>>::ScoreProvider::score(&id)
.unwrap();
let vote_weight_thresh_u64: u64 = (*vote_weight_thresh)
.try_into()
.map_err(|_| "runtime must configure score to at most u64 to use this test")
.unwrap();
let vote_weight_as_balance: pezpallet_staking::BalanceOf<Runtime> =
vote_weight_thresh_u64.try_into().map_err(|_| "can't convert").unwrap();
if vote_weight_as_balance < min_nominator_bond {
log::trace!(
target: LOG_TARGET,
"⚠️ {} Account found below min bond: {:?}.",
pretty_thresh,
id
);
}
let node = pezpallet_bags_list::Node::<Runtime, Instance1>::get(&id)
.expect("node in bag must exist.");
if node.is_misplaced(vote_weight) {
rebaggable += 1;
let notional_bag = pezpallet_bags_list::notional_bag_for::<Runtime, _>(vote_weight);
let notional_bag_as_u64: u64 = notional_bag
.try_into()
.map_err(|_| "runtime must configure score to at most u64 to use this test")
.unwrap();
log::trace!(
target: LOG_TARGET,
"Account {:?} can be rebagged from {:?} to {:?}",
id,
vote_weight_thresh_as_unit,
notional_bag_as_u64 as f64 / currency_unit as f64
);
}
}
// update our overall counter
let voters_in_bag = bag.std_iter().count() as u32;
seen_in_bags += voters_in_bag;
// percentage of all nominators
let percent_of_voters = percent(voters_in_bag, voter_list_count);
log::info!(
target: LOG_TARGET,
"{} Nominators: {} [%{:.3}]",
pretty_thresh,
voters_in_bag,
percent_of_voters,
);
}
if seen_in_bags != voter_list_count {
log::error!(
target: LOG_TARGET,
"bags list population ({}) not on par whoever is voter_list ({})",
seen_in_bags,
voter_list_count,
)
}
log::info!(
target: LOG_TARGET,
"a total of {} nodes are in {} active bags [{} total bags], {} of which can be rebagged.",
voter_list_count,
active_bags,
<Runtime as pezpallet_bags_list::Config<Instance1>>::BagThresholds::get().len(),
rebaggable,
);
}
@@ -0,0 +1,67 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Bizinikiwi.
// 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.
//! Test to check the migration of the voter bag.
use crate::{RuntimeT, LOG_TARGET};
use pezframe_support::traits::PalletInfoAccess;
use pezpallet_staking::Nominators;
use remote_externalities::{Builder, Mode, OnlineConfig};
use pezsp_runtime::{traits::Block as BlockT, DeserializeOwned};
/// Test voter bags migration. `currency_unit` is the number of planks per the the runtimes `UNITS`
/// (i.e. number of decimal places per HEZ, KSM etc)
pub async fn execute<Runtime, Block>(
currency_unit: u64,
currency_name: &'static str,
ws_url: String,
) where
Runtime: RuntimeT<pezpallet_bags_list::Instance1>,
Block: BlockT + DeserializeOwned,
Block::Header: DeserializeOwned,
{
let mut ext = Builder::<Block>::new()
.mode(Mode::Online(OnlineConfig {
transport: ws_url.to_string().into(),
pallets: vec![pezpallet_staking::Pallet::<Runtime>::name().to_string()],
..Default::default()
}))
.build()
.await
.unwrap();
ext.execute_with(|| {
// get the nominator & validator count prior to migrating; these should be invariant.
let pre_migrate_nominator_count = <Nominators<Runtime>>::iter().count() as u32;
log::info!(target: LOG_TARGET, "Nominator count: {}", pre_migrate_nominator_count);
use pezframe_election_provider_support::SortedListProvider;
// run the actual migration
let moved = <Runtime as pezpallet_staking::Config>::VoterList::unsafe_regenerate(
pezpallet_staking::Nominators::<Runtime>::iter().map(|(n, _)| n),
Box::new(|x| Some(pezpallet_staking::Pallet::<Runtime>::weight_of(x))),
);
log::info!(target: LOG_TARGET, "Moved {} nominators", moved);
let voter_list_len = <Runtime as pezpallet_staking::Config>::VoterList::iter().count() as u32;
let voter_list_count = <Runtime as pezpallet_staking::Config>::VoterList::count();
// and confirm it is equal to the length of the `VoterList`.
assert_eq!(pre_migrate_nominator_count, voter_list_len);
assert_eq!(pre_migrate_nominator_count, voter_list_count);
crate::display_and_check_bags::<Runtime>(currency_unit, currency_name);
});
}
@@ -0,0 +1,104 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Bizinikiwi.
// 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.
//! Test to execute the snapshot using the voter bag.
use pezframe_election_provider_support::{
bounds::{CountBound, DataProviderBounds},
SortedListProvider,
};
use pezframe_support::traits::PalletInfoAccess;
use remote_externalities::{Builder, Mode, OnlineConfig};
use pezsp_runtime::{
traits::{Block as BlockT, Zero},
DeserializeOwned,
};
/// Execute create a snapshot from pezpallet-staking.
pub async fn execute<Runtime, Block>(voter_limit: Option<usize>, currency_unit: u64, ws_url: String)
where
Runtime: crate::RuntimeT<pezpallet_bags_list::Instance1>,
Block: BlockT + DeserializeOwned,
Block::Header: DeserializeOwned,
{
use pezframe_support::storage::generator::StorageMap;
let mut ext = Builder::<Block>::new()
.mode(Mode::Online(OnlineConfig {
transport: ws_url.to_string().into(),
// NOTE: we don't scrape pezpallet-staking, this kinda ensures that the source of the data
// is bags-list.
pallets: vec![pezpallet_bags_list::Pallet::<Runtime, pezpallet_bags_list::Instance1>::name()
.to_string()],
at: None,
hashed_prefixes: vec![
<pezpallet_staking::Bonded<Runtime>>::prefix_hash().to_vec(),
<pezpallet_staking::Ledger<Runtime>>::prefix_hash().to_vec(),
<pezpallet_staking::Validators<Runtime>>::map_storage_final_prefix(),
<pezpallet_staking::Nominators<Runtime>>::map_storage_final_prefix(),
],
hashed_keys: vec![
<pezpallet_staking::Validators<Runtime>>::counter_storage_final_key().to_vec(),
<pezpallet_staking::Nominators<Runtime>>::counter_storage_final_key().to_vec(),
],
..Default::default()
}))
.build()
.await
.unwrap();
ext.execute_with(|| {
use pezframe_election_provider_support::ElectionDataProvider;
log::info!(
target: crate::LOG_TARGET,
"{} nodes in bags list.",
<Runtime as pezpallet_staking::Config>::VoterList::count(),
);
let bounds = match voter_limit {
None => DataProviderBounds::default(),
Some(v) => DataProviderBounds { count: Some(CountBound(v as u32)), size: None },
};
// single page voter snapshot, thus page index == 0.
let voters =
<pezpallet_staking::Pallet<Runtime> as ElectionDataProvider>::electing_voters(bounds, Zero::zero())
.unwrap();
let mut voters_nominator_only = voters
.iter()
.filter(|(v, _, _)| pezpallet_staking::Nominators::<Runtime>::contains_key(v))
.cloned()
.collect::<Vec<_>>();
voters_nominator_only.sort_by_key(|(_, w, _)| *w);
let currency_unit = currency_unit as f64;
let min_voter = voters_nominator_only
.first()
.map(|(x, y, _)| (x.clone(), *y as f64 / currency_unit));
let max_voter = voters_nominator_only
.last()
.map(|(x, y, _)| (x.clone(), *y as f64 / currency_unit));
log::info!(
target: crate::LOG_TARGET,
"a snapshot with limit {:?} has been created, {} voters are taken. min nominator: {:?}, max: {:?}",
voter_limit,
voters.len(),
min_voter,
max_voter
);
});
}
@@ -0,0 +1,60 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Bizinikiwi.
// 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.
//! Test to execute the sanity-check of the voter bag.
use pezframe_support::{
storage::generator::StorageMap,
traits::{Get, PalletInfoAccess},
};
use remote_externalities::{Builder, Mode, OnlineConfig};
use pezsp_runtime::{traits::Block as BlockT, DeserializeOwned};
/// Execute the sanity check of the bags-list.
pub async fn execute<Runtime, Block>(
currency_unit: u64,
currency_name: &'static str,
ws_url: String,
) where
Runtime: crate::RuntimeT<pezpallet_bags_list::Instance1>,
Block: BlockT + DeserializeOwned,
Block::Header: DeserializeOwned,
{
let mut ext = Builder::<Block>::new()
.mode(Mode::Online(OnlineConfig {
transport: ws_url.to_string().into(),
pallets: vec![pezpallet_bags_list::Pallet::<Runtime, pezpallet_bags_list::Instance1>::name()
.to_string()],
hashed_prefixes: vec![
<pezpallet_staking::Bonded<Runtime>>::prefix_hash().to_vec(),
<pezpallet_staking::Ledger<Runtime>>::prefix_hash().to_vec(),
],
..Default::default()
}))
.build()
.await
.unwrap();
ext.execute_with(|| {
pezsp_core::crypto::set_default_ss58_version(Runtime::SS58Prefix::get().try_into().unwrap());
pezpallet_bags_list::Pallet::<Runtime, pezpallet_bags_list::Instance1>::do_try_state().unwrap();
log::info!(target: crate::LOG_TARGET, "executed bags-list sanity check with no errors.");
crate::display_and_check_bags::<Runtime>(currency_unit, currency_name);
});
}
@@ -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
);
}
+799
View File
@@ -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(())
}
}
+152
View File
@@ -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))
}
}