difftreelog
feat(configuration) benchmarks
in: master
15 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5846,6 +5846,7 @@
version = "0.1.2"
dependencies = [
"fp-evm",
+ "frame-benchmarking",
"frame-support",
"frame-system",
"parity-scale-codec 3.2.1",
Makefilediffbeforeafterboth--- a/Makefile
+++ b/Makefile
@@ -89,6 +89,10 @@
bench-evm-migration:
make _bench PALLET=evm-migration
+.PHONY: bench-configuration
+bench-configuration:
+ make _bench PALLET=configuration
+
.PHONY: bench-common
bench-common:
make _bench PALLET=common
@@ -143,4 +147,4 @@
.PHONY: bench
# Disabled: bench-scheduler, bench-rmrk-core, bench-rmrk-equip
-bench: bench-evm-migration bench-unique bench-structure bench-fungible bench-refungible bench-nonfungible bench-foreign-assets bench-collator-selection bench-identity
+bench: bench-evm-migration bench-unique bench-structure bench-fungible bench-refungible bench-nonfungible bench-configuration bench-foreign-assets bench-collator-selection bench-identity
pallets/collator-selection/src/benchmarking.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/benchmarking.rs
+++ b/pallets/collator-selection/src/benchmarking.rs
@@ -52,9 +52,6 @@
};
use sp_std::prelude::*;
-/*pub type BalanceOf<T> =
-<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;*/
-
const SEED: u32 = 0;
// TODO: remove if this is given in substrate commit.
@@ -116,14 +113,24 @@
validators.into_iter().map(|(who, _)| who).collect()
}
+fn register_invulnerables<T: Config + configuration::Config>(count: u32) {
+ let candidates = (0..count)
+ .map(|c| account("candidate", c, SEED))
+ .collect::<Vec<_>>();
+
+ for who in candidates {
+ <CollatorSelection<T>>::add_invulnerable(T::UpdateOrigin::successful_origin(), who).unwrap();
+ }
+}
+
fn register_candidates<T: Config + configuration::Config>(count: u32) {
let candidates = (0..count)
.map(|c| account("candidate", c, SEED))
.collect::<Vec<_>>();
- assert!(
+ /*assert!(
<LicenseBond<T>>::get() > 0u32.into(),
"Bond cannot be zero!"
- );
+ );*/
for who in candidates {
T::Currency::make_free_balance_be(&who, <LicenseBond<T>>::get() * 2u32.into());
@@ -132,16 +139,45 @@
}
}
+fn get_licenses<T: Config + configuration::Config>(count: u32) {
+ let candidates = (0..count)
+ .map(|c| account("candidate", c, SEED))
+ .collect::<Vec<_>>();
+ /*assert!(
+ <LicenseBond<T>>::get() > 0u32.into(),
+ "Bond cannot be zero!"
+ );*/
+
+ for who in candidates {
+ T::Currency::make_free_balance_be(&who, <LicenseBond<T>>::get() * 2u32.into());
+ <CollatorSelection<T>>::get_license(RawOrigin::Signed(who.clone()).into()).unwrap();
+ }
+}
+
benchmarks! {
where_clause { where T: pallet_authorship::Config + session::Config + configuration::Config }
add_invulnerable {
- let b in 1 .. T::MaxCollators::get();
- let new_invulnerable = register_validators::<T>(b)[0].clone();
- let origin = T::UpdateOrigin::successful_origin();
+ let b in 1 .. T::MaxCollators::get() - 3;
+ register_validators::<T>(b);
+ register_invulnerables::<T>(b);
+
+ // log::info!("{} {}", <Invulnerables<T>>::get().len(), b);
+
+ let new_invulnerable: T::AccountId = whitelisted_caller();
+ let bond: BalanceOf<T> = T::Currency::minimum_balance() * 2u32.into();
+ T::Currency::make_free_balance_be(&new_invulnerable, bond.clone());
+
+ <session::Pallet<T>>::set_keys(
+ RawOrigin::Signed(new_invulnerable.clone()).into(),
+ keys::<T>(b + 1),
+ Vec::new()
+ ).unwrap();
+
+ let root_origin = T::UpdateOrigin::successful_origin();
}: {
assert_ok!(
- <CollatorSelection<T>>::add_invulnerable(origin, new_invulnerable.clone())
+ <CollatorSelection<T>>::add_invulnerable(root_origin, new_invulnerable.clone())
);
}
verify {
@@ -150,52 +186,28 @@
remove_invulnerable {
let b in 1 .. T::MaxCollators::get();
- let new_invulnerable = register_validators::<T>(b)[0].clone();
- let origin = T::UpdateOrigin::successful_origin();
- assert_ok!(
- <CollatorSelection<T>>::add_invulnerable(origin.clone(), new_invulnerable.clone())
- );
- }: {
- assert_ok!(
- <CollatorSelection<T>>::remove_invulnerable(origin, new_invulnerable.clone())
- );
- }
- verify {
- assert_last_event::<T>(Event::InvulnerableRemoved{invulnerable: new_invulnerable}.into());
- }
+ register_validators::<T>(b);
+ register_invulnerables::<T>(b);
- /*set_desired_collators {
- let max: u32 = 999;
- let origin = T::UpdateOrigin::successful_origin();
+ let root_origin = T::UpdateOrigin::successful_origin();
+ let leaving = <Invulnerables<T>>::get().last().unwrap().clone();
+ whitelist!(leaving);
}: {
assert_ok!(
- <CollatorSelection<T>>::set_desired_collators(origin, max.clone())
+ <CollatorSelection<T>>::remove_invulnerable(root_origin, leaving.clone())
);
}
verify {
- assert_last_event::<T>(Event::NewDesiredCollators{desired_collators: max}.into());
+ assert_last_event::<T>(Event::InvulnerableRemoved{invulnerable: leaving}.into());
}
- set_license_bond {
- let bond_amount: BalanceOf<T> = T::Currency::minimum_balance() * 10u32.into();
- let origin = T::UpdateOrigin::successful_origin();
- }: {
- assert_ok!(
- <CollatorSelection<T>>::set_license_bond(origin, bond_amount.clone())
- );
- }
- verify {
- assert_last_event::<T>(Event::NewLicenseBond{bond_amount}.into());
- }*/
-
get_license {
let c in 1 .. T::MaxCollators::get();
<LicenseBond<T>>::put(T::Currency::minimum_balance());
- <DesiredCollators<T>>::put(c + 1);
register_validators::<T>(c);
- register_candidates::<T>(c);
+ get_licenses::<T>(c);
let caller: T::AccountId = whitelisted_caller();
let bond: BalanceOf<T> = T::Currency::minimum_balance() * 2u32.into();
@@ -215,10 +227,10 @@
// worst case is when we have all the max-candidate slots filled except one, and we fill that
// one.
onboard {
- let c in 1 .. T::MaxCollators::get();
+ let c in 1 .. 5;
<LicenseBond<T>>::put(T::Currency::minimum_balance());
- <DesiredCollators<T>>::put(c + 1);
+ <DesiredCollators<T>>::put(c + 2);
register_validators::<T>(c);
register_candidates::<T>(c);
@@ -247,7 +259,7 @@
offboard {
let c in 1 .. T::MaxCollators::get();
<LicenseBond<T>>::put(T::Currency::minimum_balance());
- <DesiredCollators<T>>::put(c);
+ <DesiredCollators<T>>::put(c + 2);
register_validators::<T>(c);
register_candidates::<T>(c);
pallets/collator-selection/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// Original license:18// Copyright (C) 2021 Parity Technologies (UK) Ltd.19// SPDX-License-Identifier: Apache-2.02021// Licensed under the Apache License, Version 2.0 (the "License");22// you may not use this file except in compliance with the License.23// You may obtain a copy of the License at24//25// http://www.apache.org/licenses/LICENSE-2.026//27// Unless required by applicable law or agreed to in writing, software28// distributed under the License is distributed on an "AS IS" BASIS,29// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.30// See the License for the specific language governing permissions and31// limitations under the License.3233// todo:collator documentation34//! Collator Selection pallet.35//!36//! A pallet to manage collators in a parachain.37//!38//! ## Overview39//!40//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a41//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT42//! safety assumptions of the chosen set.43//!44//! ## Terminology45//!46//! - Collator: A parachain block producer.47//! - Bond: An amount of `Balance` _reserved_ for candidate registration.48//! - Invulnerable: An account guaranteed to be in the collator set.49//!50//! ## Implementation51//!52//! The final `Collators` are aggregated from two individual lists:53//!54//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be55//! collators.56//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as57//! a final collator.58//!59//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve60//! manner.61//!62//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates63//! fall below MinCandidates. This is for potential disaster recovery scenarios.64//!65//! ### Rewards66//!67//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the68//! collator who authored it receives:69//!70//! - Half the value of the Pot.71//! - Half the value of the transaction fees within the block. The other half of the transaction72//! fees are deposited into the Pot.73//!74//! To initiate rewards an ED needs to be transferred to the pot address.75//!76//! Note: Eventually the Pot distribution may be modified as discussed in77//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7879#![cfg_attr(not(feature = "std"), no_std)]8081pub use pallet::*;8283#[cfg(test)]84mod mock;8586#[cfg(test)]87mod tests;8889#[cfg(feature = "runtime-benchmarks")]90mod benchmarking;91pub mod weights;9293#[frame_support::pallet]94pub mod pallet {95 pub use crate::weights::WeightInfo;96 use core::ops::Div;97 use frame_support::{98 dispatch::{DispatchClass, DispatchResultWithPostInfo},99 inherent::Vec,100 pallet_prelude::*,101 sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},102 traits::{103 Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,104 ValidatorRegistration,105 },106 BoundedVec, PalletId,107 };108 use frame_system::pallet_prelude::*;109 use pallet_session::SessionManager;110 use sp_runtime::{Perbill, traits::Convert};111 use pallet_configuration::{112 CollatorSelectionDesiredCollatorsOverride as DesiredCollators,113 CollatorSelectionLicenseBondOverride as LicenseBond,114 CollatorSelectionKickThresholdOverride as KickThreshold, BalanceOf,115 };116 use sp_staking::SessionIndex;117118 /// A convertor from collators id. Since this pallet does not have stash/controller, this is119 /// just identity.120 pub struct IdentityCollator;121 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {122 fn convert(t: T) -> Option<T> {123 Some(t)124 }125 }126127 /// Configure the pallet by specifying the parameters and types on which it depends.128 #[pallet::config]129 pub trait Config: frame_system::Config + pallet_configuration::Config {130 /// Overarching event type.131 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;132133 /// Origin that can dictate updating parameters of this pallet.134 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;135136 /// Account Identifier that holds the chain's treasury.137 type TreasuryAccountId: Get<Self::AccountId>;138139 /// Account Identifier from which the internal Pot is generated.140 type PotId: Get<PalletId>;141142 /// Maximum number of candidates and invulnerables that we should have. This is enforced in code.143 type MaxCollators: Get<u32>;144145 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.146 type SlashRatio: Get<Perbill>;147148 /// A stable ID for a validator.149 type ValidatorId: Member + Parameter;150151 /// A conversion from account ID to validator ID.152 ///153 /// Its cost must be at most one storage read.154 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;155156 /// Validate a user is registered157 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;158159 /// The weight information of this pallet.160 type WeightInfo: WeightInfo;161 }162163 #[pallet::pallet]164 #[pallet::generate_store(pub(super) trait Store)]165 pub struct Pallet<T>(_);166167 /// The invulnerable, fixed collators.168 #[pallet::storage]169 #[pallet::getter(fn invulnerables)]170 pub type Invulnerables<T: Config> =171 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;172173 /// The (community) collation license holders.174 #[pallet::storage]175 #[pallet::getter(fn license_deposit_of)]176 pub type LicenseDepositOf<T: Config> =177 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;178179 /// The (community, limited) collation candidates.180 #[pallet::storage]181 #[pallet::getter(fn candidates)]182 pub type Candidates<T: Config> =183 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;184185 /// Last block authored by collator.186 #[pallet::storage]187 #[pallet::getter(fn last_authored_block)]188 pub type LastAuthoredBlock<T: Config> =189 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;190191 #[pallet::genesis_config]192 pub struct GenesisConfig<T: Config> {193 pub invulnerables: Vec<T::AccountId>,194 }195196 #[cfg(feature = "std")]197 impl<T: Config> Default for GenesisConfig<T> {198 fn default() -> Self {199 Self {200 invulnerables: Default::default(),201 }202 }203 }204205 #[pallet::genesis_build]206 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {207 fn build(&self) {208 let duplicate_invulnerables = self209 .invulnerables210 .iter()211 .collect::<std::collections::BTreeSet<_>>();212 assert!(213 duplicate_invulnerables.len() == self.invulnerables.len(),214 "duplicate invulnerables in genesis."215 );216217 let bounded_invulnerables =218 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())219 .expect("genesis invulnerables are more than T::MaxCollators");220221 <Invulnerables<T>>::put(bounded_invulnerables);222 }223 }224225 #[pallet::event]226 #[pallet::generate_deposit(pub(super) fn deposit_event)]227 pub enum Event<T: Config> {228 InvulnerableAdded {229 invulnerable: T::AccountId,230 },231 InvulnerableRemoved {232 invulnerable: T::AccountId,233 },234 LicenseObtained {235 account_id: T::AccountId,236 deposit: BalanceOf<T>,237 },238 LicenseReleased {239 account_id: T::AccountId,240 deposit_returned: BalanceOf<T>,241 },242 CandidateAdded {243 account_id: T::AccountId,244 },245 CandidateRemoved {246 account_id: T::AccountId,247 },248 }249250 // Errors inform users that something went wrong.251 #[pallet::error]252 pub enum Error<T> {253 /// Too many candidates254 TooManyCandidates,255 /// Unknown error256 Unknown,257 /// Permission issue258 Permission,259 /// User already holds license to collate260 AlreadyHoldingLicense,261 /// User does not hold a license to collate262 NoLicense,263 /// User is already a candidate264 AlreadyCandidate,265 /// User is not a candidate266 NotCandidate,267 /// Too many invulnerables268 TooManyInvulnerables,269 /// Too few invulnerables270 TooFewInvulnerables,271 /// User is already an Invulnerable272 AlreadyInvulnerable,273 /// User is not an Invulnerable274 NotInvulnerable,275 /// Account has no associated validator ID276 NoAssociatedValidatorId,277 /// Validator ID is not yet registered278 ValidatorNotRegistered,279 }280281 #[pallet::hooks]282 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}283284 #[pallet::call]285 impl<T: Config> Pallet<T> {286 /// Add a collator to the list of invulnerable (fixed) collators.287 #[pallet::call_index(0)]288 #[pallet::weight(T::WeightInfo::add_invulnerable(T::MaxCollators::get()))]289 pub fn add_invulnerable(290 origin: OriginFor<T>,291 new: T::AccountId,292 ) -> DispatchResultWithPostInfo {293 T::UpdateOrigin::ensure_origin(origin)?;294295 // check if the new invulnerable has associated validator keys before it is added296 let validator_key = T::ValidatorIdOf::convert(new.clone())297 .ok_or(Error::<T>::NoAssociatedValidatorId)?;298 ensure!(299 T::ValidatorRegistration::is_registered(&validator_key),300 Error::<T>::ValidatorNotRegistered301 );302 if Self::invulnerables().contains(&new) {303 return Ok(().into());304 }305306 <Invulnerables<T>>::try_append(new.clone())307 .map_err(|_| Error::<T>::TooManyInvulnerables)?;308309 // try to offboard the new invulnerable if it was a collator candidate before310 let _ = Self::try_remove_candidate(&new);311312 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });313 Ok(().into())314 }315316 /// Remove a collator from the list of invulnerable (fixed) collators.317 #[pallet::call_index(1)]318 #[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]319 pub fn remove_invulnerable(320 origin: OriginFor<T>,321 who: T::AccountId,322 ) -> DispatchResultWithPostInfo {323 T::UpdateOrigin::ensure_origin(origin)?;324325 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {326 if invulnerables.len() <= 1 {327 return Err(Error::<T>::TooFewInvulnerables.into());328 }329330 let index = invulnerables331 .into_iter()332 .position(|r| *r == who)333 .ok_or(Error::<T>::NotInvulnerable)?;334 invulnerables.remove(index);335 Ok(())336 })?;337 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });338 Ok(().into())339 }340341 /// Purchase a license on block collation for this account.342 /// It does not make it a collator candidate, use `onboard` afterward. The account must343 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.344 ///345 /// This call is not available to `Invulnerable` collators.346 #[pallet::call_index(2)]347 #[pallet::weight(T::WeightInfo::get_license(T::MaxCollators::get()))]348 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {349 // register_as_candidate350 let who = ensure_signed(origin)?;351352 if LicenseDepositOf::<T>::contains_key(&who) {353 return Err(Error::<T>::AlreadyHoldingLicense.into());354 }355356 let validator_key = T::ValidatorIdOf::convert(who.clone())357 .ok_or(Error::<T>::NoAssociatedValidatorId)?;358 ensure!(359 T::ValidatorRegistration::is_registered(&validator_key),360 Error::<T>::ValidatorNotRegistered361 );362363 let deposit = <LicenseBond<T>>::get();364365 T::Currency::reserve(&who, deposit)?;366 LicenseDepositOf::<T>::insert(who.clone(), deposit);367368 Self::deposit_event(Event::LicenseObtained {369 account_id: who,370 deposit,371 });372 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())373 }374375 /// Register this account as a candidate for collators for next sessions.376 /// The account must already hold a license, and cannot offboard immediately during a session.377 ///378 /// This call is not available to `Invulnerable` collators.379 #[pallet::call_index(3)]380 #[pallet::weight(T::WeightInfo::onboard(T::MaxCollators::get()))]381 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {382 // register_as_candidate383 let who = ensure_signed(origin)?;384385 // ensure the user obtained the license.386 ensure!(387 LicenseDepositOf::<T>::contains_key(&who),388 Error::<T>::NoLicense389 );390 // ensure we are below limit.391 let length = <Candidates<T>>::decode_len().unwrap_or_default()392 + <Invulnerables<T>>::decode_len().unwrap_or_default();393 ensure!(394 (length as u32) < <DesiredCollators<T>>::get(),395 Error::<T>::TooManyCandidates396 );397 ensure!(398 !Self::invulnerables().contains(&who),399 Error::<T>::AlreadyInvulnerable400 );401402 let current_count =403 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {404 if candidates.iter().any(|candidate| *candidate == who) {405 Err(Error::<T>::AlreadyCandidate)?406 } else {407 candidates408 .try_push(who.clone())409 .map_err(|_| Error::<T>::TooManyCandidates)?;410 // First authored block is current block plus kick threshold to handle session delay411 <LastAuthoredBlock<T>>::insert(412 who.clone(),413 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),414 );415 Ok(candidates.len())416 }417 })?;418419 Self::deposit_event(Event::CandidateAdded { account_id: who });420 Ok(Some(T::WeightInfo::onboard(current_count as u32)).into())421 }422423 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on424 /// session change. The license to `onboard` later at any other time will remain.425 #[pallet::call_index(4)]426 #[pallet::weight(T::WeightInfo::offboard(T::MaxCollators::get()))]427 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {428 // leave_intent429 let who = ensure_signed(origin)?;430 let current_count = Self::try_remove_candidate(&who)?;431432 Ok(Some(T::WeightInfo::offboard(current_count as u32)).into())433 }434435 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.436 ///437 /// This call is not available to `Invulnerable` collators.438 #[pallet::call_index(5)]439 #[pallet::weight(T::WeightInfo::release_license(T::MaxCollators::get()))]440 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {441 // leave_intent442 let who = ensure_signed(origin)?;443444 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;445446 Ok(Some(T::WeightInfo::release_license(current_count as u32)).into())447 }448449 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.450 /// Note that the collator can only leave on session change.451 /// The `LicenseBond` will be unreserved and returned immediately.452 ///453 /// This call is, of course, not applicable to `Invulnerable` collators.454 #[pallet::call_index(6)]455 #[pallet::weight(T::WeightInfo::force_release_license(T::MaxCollators::get()))]456 pub fn force_release_license(457 origin: OriginFor<T>,458 who: T::AccountId,459 ) -> DispatchResultWithPostInfo {460 // leave_intent461 T::UpdateOrigin::ensure_origin(origin)?;462463 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;464465 Ok(Some(T::WeightInfo::force_release_license(current_count as u32)).into())466 }467 }468469 impl<T: Config> Pallet<T> {470 /// Get a unique, inaccessible account id from the `PotId`.471 pub fn account_id() -> T::AccountId {472 T::PotId::get().into_account_truncating()473 }474475 /// Removes a candidate and their license, optionally slashed and optionally ignoring,476 /// whether or not they actually are a candidate.477 fn try_remove_candidate_and_release_license(478 who: &T::AccountId,479 should_slash: bool,480 ignore_if_not_candidate: bool,481 ) -> Result<usize, DispatchError> {482 let current_count = Self::try_remove_candidate(who);483 let current_count = if ignore_if_not_candidate484 && current_count == Err(Error::<T>::NotCandidate.into())485 {486 <Candidates<T>>::decode_len().unwrap_or_default()487 } else {488 current_count?489 };490 Self::try_release_license(who, should_slash)?;491 Ok(current_count)492 }493494 /// Removes a candidate from the collator pool for the next session if they exist.495 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {496 let current_count =497 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {498 let index = candidates499 .iter()500 .position(|candidate| *candidate == *who)501 .ok_or(Error::<T>::NotCandidate)?;502 candidates.remove(index);503 <LastAuthoredBlock<T>>::remove(who.clone());504 Ok(candidates.len())505 })?;506 Self::deposit_event(Event::CandidateRemoved {507 account_id: who.clone(),508 });509 Ok(current_count)510 }511512 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.513 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {514 let mut deposit_returned = BalanceOf::<T>::default();515 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {516 if let Some(deposit) = deposit.take() {517 if should_slash {518 let slashed = T::SlashRatio::get() * deposit;519 let remaining = deposit - slashed;520521 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);522 //T::Currency::unreserve(who, remaining);523 deposit_returned = remaining;524525 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);526 } else {527 //T::Currency::unreserve(who, deposit);528 deposit_returned = deposit;529 }530531 T::Currency::unreserve(who, deposit_returned);532 Ok(())533 } else {534 Err(Error::<T>::NoLicense.into())535 }536 })?;537 Self::deposit_event(Event::LicenseReleased {538 account_id: who.clone(),539 deposit_returned,540 });541 Ok(())542 }543544 /// Assemble the current set of candidates and invulnerables into the next collator set.545 ///546 /// This is done on the fly, as frequent as we are told to do so, as the session manager.547 pub fn assemble_collators(548 candidates: BoundedVec<T::AccountId, T::MaxCollators>,549 ) -> Vec<T::AccountId> {550 let mut collators = Self::invulnerables().to_vec();551 collators.extend(candidates);552 collators553 }554555 /// Kicks out candidates that did not produce a block in the kick threshold556 /// and **confiscates** their deposits to the treasury.557 pub fn kick_stale_candidates(558 candidates: BoundedVec<T::AccountId, T::MaxCollators>,559 ) -> BoundedVec<T::AccountId, T::MaxCollators> {560 let now = frame_system::Pallet::<T>::block_number();561 let kick_threshold = <KickThreshold<T>>::get();562 candidates563 .into_iter()564 .filter_map(|c| {565 let last_block = <LastAuthoredBlock<T>>::get(c.clone());566 let since_last = now.saturating_sub(last_block);567 if since_last < kick_threshold {568 Some(c)569 } else {570 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);571 if let Err(why) = outcome {572 log::warn!("Failed to kick collator and release license {:?}", why);573 debug_assert!(false, "failed to kick collator and release license {why:?}");574 }575 None576 }577 })578 .collect::<Vec<_>>()579 .try_into()580 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")581 }582 }583584 /// Keep track of number of authored blocks per authority, uncles are counted as well since585 /// they're a valid proof of being online.586 impl<T: Config + pallet_authorship::Config>587 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>588 {589 fn note_author(author: T::AccountId) {590 let pot = Self::account_id();591 // assumes an ED will be sent to pot.592 let reward = T::Currency::free_balance(&pot)593 .checked_sub(&T::Currency::minimum_balance())594 .unwrap_or_else(Zero::zero)595 .div(2u32.into());596 // `reward` is half of pot account minus ED, this should never fail.597 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);598 debug_assert!(_success.is_ok());599 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());600601 frame_system::Pallet::<T>::register_extra_weight_unchecked(602 T::WeightInfo::note_author(),603 DispatchClass::Mandatory,604 );605 }606607 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {608 //TODO can we ignore this?609 }610 }611612 /// Play the role of the session manager.613 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {614 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {615 log::info!(616 "assembling new collators for new session {} at #{:?}",617 index,618 <frame_system::Pallet<T>>::block_number(),619 );620621 let candidates = Self::candidates();622 let candidates_len_before = candidates.len();623 let active_candidates = Self::kick_stale_candidates(candidates);624 let removed = candidates_len_before - active_candidates.len();625 let result = Self::assemble_collators(active_candidates);626627 frame_system::Pallet::<T>::register_extra_weight_unchecked(628 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),629 DispatchClass::Mandatory,630 );631 Some(result)632 }633 fn start_session(_: SessionIndex) {634 // we don't care.635 }636 fn end_session(_: SessionIndex) {637 // we don't care.638 }639 }640}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// Original license:18// Copyright (C) 2021 Parity Technologies (UK) Ltd.19// SPDX-License-Identifier: Apache-2.02021// Licensed under the Apache License, Version 2.0 (the "License");22// you may not use this file except in compliance with the License.23// You may obtain a copy of the License at24//25// http://www.apache.org/licenses/LICENSE-2.026//27// Unless required by applicable law or agreed to in writing, software28// distributed under the License is distributed on an "AS IS" BASIS,29// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.30// See the License for the specific language governing permissions and31// limitations under the License.3233// todo:collator documentation34//! Collator Selection pallet.35//!36//! A pallet to manage collators in a parachain.37//!38//! ## Overview39//!40//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a41//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT42//! safety assumptions of the chosen set.43//!44//! ## Terminology45//!46//! - Collator: A parachain block producer.47//! - Bond: An amount of `Balance` _reserved_ for candidate registration.48//! - Invulnerable: An account guaranteed to be in the collator set.49//!50//! ## Implementation51//!52//! The final `Collators` are aggregated from two individual lists:53//!54//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be55//! collators.56//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as57//! a final collator.58//!59//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve60//! manner.61//!62//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates63//! fall below MinCandidates. This is for potential disaster recovery scenarios.64//!65//! ### Rewards66//!67//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the68//! collator who authored it receives:69//!70//! - Half the value of the Pot.71//! - Half the value of the transaction fees within the block. The other half of the transaction72//! fees are deposited into the Pot.73//!74//! To initiate rewards an ED needs to be transferred to the pot address.75//!76//! Note: Eventually the Pot distribution may be modified as discussed in77//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7879#![cfg_attr(not(feature = "std"), no_std)]8081pub use pallet::*;8283#[cfg(test)]84mod mock;8586#[cfg(test)]87mod tests;8889#[cfg(feature = "runtime-benchmarks")]90mod benchmarking;91pub mod weights;9293#[frame_support::pallet]94pub mod pallet {95 pub use crate::weights::WeightInfo;96 use core::ops::Div;97 use frame_support::{98 dispatch::{DispatchClass, DispatchResultWithPostInfo},99 inherent::Vec,100 pallet_prelude::*,101 sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},102 traits::{103 Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,104 ValidatorRegistration,105 },106 BoundedVec, PalletId,107 };108 use frame_system::pallet_prelude::*;109 use pallet_session::SessionManager;110 use sp_runtime::{Perbill, traits::Convert};111 use pallet_configuration::{112 CollatorSelectionDesiredCollatorsOverride as DesiredCollators,113 CollatorSelectionLicenseBondOverride as LicenseBond,114 CollatorSelectionKickThresholdOverride as KickThreshold, BalanceOf,115 };116 use sp_staking::SessionIndex;117118 /// A convertor from collators id. Since this pallet does not have stash/controller, this is119 /// just identity.120 pub struct IdentityCollator;121 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {122 fn convert(t: T) -> Option<T> {123 Some(t)124 }125 }126127 /// Configure the pallet by specifying the parameters and types on which it depends.128 #[pallet::config]129 pub trait Config: frame_system::Config + pallet_configuration::Config {130 /// Overarching event type.131 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;132133 /// Origin that can dictate updating parameters of this pallet.134 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;135136 /// Account Identifier that holds the chain's treasury.137 type TreasuryAccountId: Get<Self::AccountId>;138139 /// Account Identifier from which the internal Pot is generated.140 type PotId: Get<PalletId>;141142 /// Maximum number of candidates and invulnerables that we should have. This is enforced in code.143 type MaxCollators: Get<u32>;144145 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.146 type SlashRatio: Get<Perbill>;147148 /// A stable ID for a validator.149 type ValidatorId: Member + Parameter;150151 /// A conversion from account ID to validator ID.152 ///153 /// Its cost must be at most one storage read.154 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;155156 /// Validate a user is registered157 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;158159 /// The weight information of this pallet.160 type WeightInfo: WeightInfo;161 }162163 #[pallet::pallet]164 #[pallet::generate_store(pub(super) trait Store)]165 pub struct Pallet<T>(_);166167 /// The invulnerable, fixed collators.168 #[pallet::storage]169 #[pallet::getter(fn invulnerables)]170 pub type Invulnerables<T: Config> =171 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;172173 /// The (community) collation license holders.174 #[pallet::storage]175 #[pallet::getter(fn license_deposit_of)]176 pub type LicenseDepositOf<T: Config> =177 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;178179 /// The (community, limited) collation candidates.180 #[pallet::storage]181 #[pallet::getter(fn candidates)]182 pub type Candidates<T: Config> =183 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;184185 /// Last block authored by collator.186 #[pallet::storage]187 #[pallet::getter(fn last_authored_block)]188 pub type LastAuthoredBlock<T: Config> =189 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;190191 #[pallet::genesis_config]192 pub struct GenesisConfig<T: Config> {193 pub invulnerables: Vec<T::AccountId>,194 }195196 #[cfg(feature = "std")]197 impl<T: Config> Default for GenesisConfig<T> {198 fn default() -> Self {199 Self {200 invulnerables: Default::default(),201 }202 }203 }204205 #[pallet::genesis_build]206 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {207 fn build(&self) {208 let duplicate_invulnerables = self209 .invulnerables210 .iter()211 .collect::<std::collections::BTreeSet<_>>();212 assert!(213 duplicate_invulnerables.len() == self.invulnerables.len(),214 "duplicate invulnerables in genesis."215 );216217 let bounded_invulnerables =218 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())219 .expect("genesis invulnerables are more than T::MaxCollators");220221 <Invulnerables<T>>::put(bounded_invulnerables);222 }223 }224225 #[pallet::event]226 #[pallet::generate_deposit(pub(super) fn deposit_event)]227 pub enum Event<T: Config> {228 InvulnerableAdded {229 invulnerable: T::AccountId,230 },231 InvulnerableRemoved {232 invulnerable: T::AccountId,233 },234 LicenseObtained {235 account_id: T::AccountId,236 deposit: BalanceOf<T>,237 },238 LicenseReleased {239 account_id: T::AccountId,240 deposit_returned: BalanceOf<T>,241 },242 CandidateAdded {243 account_id: T::AccountId,244 },245 CandidateRemoved {246 account_id: T::AccountId,247 },248 }249250 // Errors inform users that something went wrong.251 #[pallet::error]252 pub enum Error<T> {253 /// Too many candidates254 TooManyCandidates,255 /// Unknown error256 Unknown,257 /// Permission issue258 Permission,259 /// User already holds license to collate260 AlreadyHoldingLicense,261 /// User does not hold a license to collate262 NoLicense,263 /// User is already a candidate264 AlreadyCandidate,265 /// User is not a candidate266 NotCandidate,267 /// Too many invulnerables268 TooManyInvulnerables,269 /// Too few invulnerables270 TooFewInvulnerables,271 /// User is already an Invulnerable272 AlreadyInvulnerable,273 /// User is not an Invulnerable274 NotInvulnerable,275 /// Account has no associated validator ID276 NoAssociatedValidatorId,277 /// Validator ID is not yet registered278 ValidatorNotRegistered,279 }280281 #[pallet::hooks]282 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}283284 #[pallet::call]285 impl<T: Config> Pallet<T> {286 /// Add a collator to the list of invulnerable (fixed) collators.287 #[pallet::call_index(0)]288 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]289 pub fn add_invulnerable(290 origin: OriginFor<T>,291 new: T::AccountId,292 ) -> DispatchResultWithPostInfo {293 T::UpdateOrigin::ensure_origin(origin)?;294295 // check if the new invulnerable has associated validator keys before it is added296 let validator_key = T::ValidatorIdOf::convert(new.clone())297 .ok_or(Error::<T>::NoAssociatedValidatorId)?;298 ensure!(299 T::ValidatorRegistration::is_registered(&validator_key),300 Error::<T>::ValidatorNotRegistered301 );302 if Self::invulnerables().contains(&new) {303 return Ok(().into());304 }305306 <Invulnerables<T>>::try_append(new.clone())307 .map_err(|_| Error::<T>::TooManyInvulnerables)?;308309 // try to offboard the new invulnerable if it was a collator candidate before310 let _ = Self::try_remove_candidate(&new);311312 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });313 Ok(().into())314 }315316 /// Remove a collator from the list of invulnerable (fixed) collators.317 #[pallet::call_index(1)]318 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]319 pub fn remove_invulnerable(320 origin: OriginFor<T>,321 who: T::AccountId,322 ) -> DispatchResultWithPostInfo {323 T::UpdateOrigin::ensure_origin(origin)?;324325 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {326 if invulnerables.len() <= 1 {327 return Err(Error::<T>::TooFewInvulnerables.into());328 }329330 let index = invulnerables331 .into_iter()332 .position(|r| *r == who)333 .ok_or(Error::<T>::NotInvulnerable)?;334 invulnerables.remove(index);335 Ok(())336 })?;337 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });338 Ok(().into())339 }340341 /// Purchase a license on block collation for this account.342 /// It does not make it a collator candidate, use `onboard` afterward. The account must343 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.344 ///345 /// This call is not available to `Invulnerable` collators.346 #[pallet::call_index(2)]347 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]348 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {349 // register_as_candidate350 let who = ensure_signed(origin)?;351352 if LicenseDepositOf::<T>::contains_key(&who) {353 return Err(Error::<T>::AlreadyHoldingLicense.into());354 }355356 let validator_key = T::ValidatorIdOf::convert(who.clone())357 .ok_or(Error::<T>::NoAssociatedValidatorId)?;358 ensure!(359 T::ValidatorRegistration::is_registered(&validator_key),360 Error::<T>::ValidatorNotRegistered361 );362363 let deposit = <LicenseBond<T>>::get();364365 T::Currency::reserve(&who, deposit)?;366 LicenseDepositOf::<T>::insert(who.clone(), deposit);367368 Self::deposit_event(Event::LicenseObtained {369 account_id: who,370 deposit,371 });372 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())373 }374375 /// Register this account as a candidate for collators for next sessions.376 /// The account must already hold a license, and cannot offboard immediately during a session.377 ///378 /// This call is not available to `Invulnerable` collators.379 #[pallet::call_index(3)]380 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]381 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {382 // register_as_candidate383 let who = ensure_signed(origin)?;384385 // ensure the user obtained the license.386 ensure!(387 LicenseDepositOf::<T>::contains_key(&who),388 Error::<T>::NoLicense389 );390 // ensure we are below limit.391 let length = <Candidates<T>>::decode_len().unwrap_or_default()392 + <Invulnerables<T>>::decode_len().unwrap_or_default();393 ensure!(394 (length as u32) < <DesiredCollators<T>>::get(),395 Error::<T>::TooManyCandidates396 );397 ensure!(398 !Self::invulnerables().contains(&who),399 Error::<T>::AlreadyInvulnerable400 );401402 let current_count =403 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {404 if candidates.iter().any(|candidate| *candidate == who) {405 Err(Error::<T>::AlreadyCandidate)?406 } else {407 candidates408 .try_push(who.clone())409 .map_err(|_| Error::<T>::TooManyCandidates)?;410 // First authored block is current block plus kick threshold to handle session delay411 <LastAuthoredBlock<T>>::insert(412 who.clone(),413 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),414 );415 Ok(candidates.len())416 }417 })?;418419 Self::deposit_event(Event::CandidateAdded { account_id: who });420 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())421 }422423 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on424 /// session change. The license to `onboard` later at any other time will remain.425 #[pallet::call_index(4)]426 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]427 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {428 // leave_intent429 let who = ensure_signed(origin)?;430 let current_count = Self::try_remove_candidate(&who)?;431432 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())433 }434435 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.436 ///437 /// This call is not available to `Invulnerable` collators.438 #[pallet::call_index(5)]439 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]440 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {441 // leave_intent442 let who = ensure_signed(origin)?;443444 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;445446 Ok(Some(<T as Config>::WeightInfo::release_license(447 current_count as u32,448 ))449 .into())450 }451452 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.453 /// Note that the collator can only leave on session change.454 /// The `LicenseBond` will be unreserved and returned immediately.455 ///456 /// This call is, of course, not applicable to `Invulnerable` collators.457 #[pallet::call_index(6)]458 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]459 pub fn force_release_license(460 origin: OriginFor<T>,461 who: T::AccountId,462 ) -> DispatchResultWithPostInfo {463 // leave_intent464 T::UpdateOrigin::ensure_origin(origin)?;465466 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;467468 Ok(Some(<T as Config>::WeightInfo::force_release_license(469 current_count as u32,470 ))471 .into())472 }473 }474475 impl<T: Config> Pallet<T> {476 /// Get a unique, inaccessible account id from the `PotId`.477 pub fn account_id() -> T::AccountId {478 T::PotId::get().into_account_truncating()479 }480481 /// Removes a candidate and their license, optionally slashed and optionally ignoring,482 /// whether or not they actually are a candidate.483 fn try_remove_candidate_and_release_license(484 who: &T::AccountId,485 should_slash: bool,486 ignore_if_not_candidate: bool,487 ) -> Result<usize, DispatchError> {488 let current_count = Self::try_remove_candidate(who);489 let current_count = if ignore_if_not_candidate490 && current_count == Err(Error::<T>::NotCandidate.into())491 {492 <Candidates<T>>::decode_len().unwrap_or_default()493 } else {494 current_count?495 };496 Self::try_release_license(who, should_slash)?;497 Ok(current_count)498 }499500 /// Removes a candidate from the collator pool for the next session if they exist.501 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {502 let current_count =503 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {504 let index = candidates505 .iter()506 .position(|candidate| *candidate == *who)507 .ok_or(Error::<T>::NotCandidate)?;508 candidates.remove(index);509 <LastAuthoredBlock<T>>::remove(who.clone());510 Ok(candidates.len())511 })?;512 Self::deposit_event(Event::CandidateRemoved {513 account_id: who.clone(),514 });515 Ok(current_count)516 }517518 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.519 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {520 let mut deposit_returned = BalanceOf::<T>::default();521 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {522 if let Some(deposit) = deposit.take() {523 if should_slash {524 let slashed = T::SlashRatio::get() * deposit;525 let remaining = deposit - slashed;526527 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);528 //T::Currency::unreserve(who, remaining);529 deposit_returned = remaining;530531 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);532 } else {533 //T::Currency::unreserve(who, deposit);534 deposit_returned = deposit;535 }536537 T::Currency::unreserve(who, deposit_returned);538 Ok(())539 } else {540 Err(Error::<T>::NoLicense.into())541 }542 })?;543 Self::deposit_event(Event::LicenseReleased {544 account_id: who.clone(),545 deposit_returned,546 });547 Ok(())548 }549550 /// Assemble the current set of candidates and invulnerables into the next collator set.551 ///552 /// This is done on the fly, as frequent as we are told to do so, as the session manager.553 pub fn assemble_collators(554 candidates: BoundedVec<T::AccountId, T::MaxCollators>,555 ) -> Vec<T::AccountId> {556 let mut collators = Self::invulnerables().to_vec();557 collators.extend(candidates);558 collators559 }560561 /// Kicks out candidates that did not produce a block in the kick threshold562 /// and **confiscates** their deposits to the treasury.563 pub fn kick_stale_candidates(564 candidates: BoundedVec<T::AccountId, T::MaxCollators>,565 ) -> BoundedVec<T::AccountId, T::MaxCollators> {566 let now = frame_system::Pallet::<T>::block_number();567 let kick_threshold = <KickThreshold<T>>::get();568 candidates569 .into_iter()570 .filter_map(|c| {571 let last_block = <LastAuthoredBlock<T>>::get(c.clone());572 let since_last = now.saturating_sub(last_block);573 if since_last < kick_threshold {574 Some(c)575 } else {576 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);577 if let Err(why) = outcome {578 log::warn!("Failed to kick collator and release license {:?}", why);579 debug_assert!(false, "failed to kick collator and release license {why:?}");580 }581 None582 }583 })584 .collect::<Vec<_>>()585 .try_into()586 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")587 }588 }589590 /// Keep track of number of authored blocks per authority, uncles are counted as well since591 /// they're a valid proof of being online.592 impl<T: Config + pallet_authorship::Config>593 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>594 {595 fn note_author(author: T::AccountId) {596 let pot = Self::account_id();597 // assumes an ED will be sent to pot.598 let reward = T::Currency::free_balance(&pot)599 .checked_sub(&T::Currency::minimum_balance())600 .unwrap_or_else(Zero::zero)601 .div(2u32.into());602 // `reward` is half of pot account minus ED, this should never fail.603 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);604 debug_assert!(_success.is_ok());605 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());606607 frame_system::Pallet::<T>::register_extra_weight_unchecked(608 <T as Config>::WeightInfo::note_author(),609 DispatchClass::Mandatory,610 );611 }612613 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {614 //TODO can we ignore this?615 }616 }617618 /// Play the role of the session manager.619 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {620 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {621 log::info!(622 "assembling new collators for new session {} at #{:?}",623 index,624 <frame_system::Pallet<T>>::block_number(),625 );626627 let candidates = Self::candidates();628 let candidates_len_before = candidates.len();629 let active_candidates = Self::kick_stale_candidates(candidates);630 let removed = candidates_len_before - active_candidates.len();631 let result = Self::assemble_collators(active_candidates);632633 frame_system::Pallet::<T>::register_extra_weight_unchecked(634 <T as Config>::WeightInfo::new_session(635 candidates_len_before as u32,636 removed as u32,637 ),638 DispatchClass::Mandatory,639 );640 Some(result)641 }642 fn start_session(_: SessionIndex) {643 // we don't care.644 }645 fn end_session(_: SessionIndex) {646 // we don't care.647 }648 }649}pallets/collator-selection/src/mock.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/mock.rs
+++ b/pallets/collator-selection/src/mock.rs
@@ -225,6 +225,7 @@
type MaxXcmAllowedLocations = MaxXcmAllowedLocations;
type AppPromotionDailyRate = AppPromotionDailyRate;
type DayRelayBlocks = DayRelayBlocks;
+ type WeightInfo = ();
}
ord_parameter_types! {
pallets/configuration/Cargo.tomldiffbeforeafterboth--- a/pallets/configuration/Cargo.toml
+++ b/pallets/configuration/Cargo.toml
@@ -12,6 +12,7 @@
] }
frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
+frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.36" }
@@ -22,10 +23,12 @@
[features]
default = ["std"]
+runtime-benchmarks = ["frame-benchmarking"]
std = [
"parity-scale-codec/std",
"frame-support/std",
"frame-system/std",
+ "frame-benchmarking/std",
"sp-runtime/std",
"sp-std/std",
"sp-core/std",
pallets/configuration/src/benchmarking.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/configuration/src/benchmarking.rs
@@ -0,0 +1,100 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+//! Benchmarking setup for pallet-configuration
+
+use super::*;
+use frame_benchmarking::benchmarks;
+use frame_system::{EventRecord, RawOrigin};
+use frame_support::{assert_ok, BoundedVec, traits::Currency};
+use xcm::v1::MultiLocation;
+
+fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
+ let events = frame_system::Pallet::<T>::events();
+ let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
+ // compare to the last event record
+ let EventRecord { event, .. } = &events[events.len() - 1];
+ assert_eq!(event, &system_event);
+}
+
+benchmarks! {
+ where_clause { where T: Config }
+
+ set_weight_to_fee_coefficient_override {
+ let coeff: u64 = 999;
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_weight_to_fee_coefficient_override(RawOrigin::Root.into(), Some(coeff))
+ );
+ }
+
+ set_min_gas_price_override {
+ let coeff: u64 = 999;
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_min_gas_price_override(RawOrigin::Root.into(), Some(coeff))
+ );
+ }
+
+ set_xcm_allowed_locations {
+ let locations: BoundedVec<MultiLocation, T::MaxXcmAllowedLocations> = Default::default();
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_xcm_allowed_locations(RawOrigin::Root.into(), Some(locations))
+ );
+ }
+
+ set_app_promotion_configuration_override {
+ let configuration: AppPromotionConfiguration<T::BlockNumber> = Default::default();
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_app_promotion_configuration_override(RawOrigin::Root.into(), configuration)
+ );
+ }
+
+ set_collator_selection_desired_collators {
+ let max: u32 = 999;
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_collator_selection_desired_collators(RawOrigin::Root.into(), Some(max.clone()))
+ );
+ }
+ verify {
+ assert_last_event::<T>(Event::NewDesiredCollators{desired_collators: Some(max)}.into());
+ }
+
+ set_collator_selection_license_bond {
+ let bond_cost: Option<BalanceOf<T>> = Some(T::Currency::minimum_balance() * 10u32.into());
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_collator_selection_license_bond(RawOrigin::Root.into(), bond_cost.clone())
+ );
+ }
+ verify {
+ assert_last_event::<T>(Event::NewCollatorLicenseBond{bond_cost}.into());
+ }
+
+ set_collator_selection_kick_threshold {
+ let threshold: Option<T::BlockNumber> = Some(900u32.into());
+ }: {
+ assert_ok!(
+ <Pallet<T>>::set_collator_selection_kick_threshold(RawOrigin::Root.into(), threshold.clone())
+ );
+ }
+ verify {
+ assert_last_event::<T>(Event::NewCollatorKickThreshold{length_in_blocks: threshold}.into());
+ }
+}
pallets/configuration/src/lib.rsdiffbeforeafterboth--- a/pallets/configuration/src/lib.rs
+++ b/pallets/configuration/src/lib.rs
@@ -34,6 +34,10 @@
pub use pallet::*;
use sp_core::U256;
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+pub mod weights;
+
#[pallet]
mod pallet {
use super::*;
@@ -45,6 +49,7 @@
use frame_system::{pallet_prelude::OriginFor, ensure_root, Config as SystemConfig};
use xcm::v1::MultiLocation;
+ pub use crate::weights::WeightInfo;
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
@@ -74,6 +79,9 @@
type DefaultCollatorSelectionLicenseBond: Get<BalanceOf<Self>>;
#[pallet::constant]
type DefaultCollatorSelectionKickThreshold: Get<Self::BlockNumber>;
+
+ /// The weight information of this pallet.
+ type WeightInfo: WeightInfo;
}
#[pallet::event]
@@ -140,7 +148,7 @@
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_weight_to_fee_coefficient_override())]
pub fn set_weight_to_fee_coefficient_override(
origin: OriginFor<T>,
coeff: Option<u64>,
@@ -155,7 +163,7 @@
}
#[pallet::call_index(1)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_min_gas_price_override())]
pub fn set_min_gas_price_override(
origin: OriginFor<T>,
coeff: Option<u64>,
@@ -170,7 +178,7 @@
}
#[pallet::call_index(2)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_xcm_allowed_locations())]
pub fn set_xcm_allowed_locations(
origin: OriginFor<T>,
locations: Option<BoundedVec<MultiLocation, T::MaxXcmAllowedLocations>>,
@@ -181,7 +189,7 @@
}
#[pallet::call_index(3)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_app_promotion_configuration_override())]
pub fn set_app_promotion_configuration_override(
origin: OriginFor<T>,
mut configuration: AppPromotionConfiguration<T::BlockNumber>,
@@ -202,7 +210,7 @@
}
#[pallet::call_index(4)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_collator_selection_desired_collators())]
pub fn set_collator_selection_desired_collators(
origin: OriginFor<T>,
max: Option<u32>,
@@ -224,7 +232,7 @@
}
#[pallet::call_index(5)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_collator_selection_license_bond())]
pub fn set_collator_selection_license_bond(
origin: OriginFor<T>,
amount: Option<BalanceOf<T>>,
@@ -240,7 +248,7 @@
}
#[pallet::call_index(6)]
- #[pallet::weight(T::DbWeight::get().writes(1))]
+ #[pallet::weight(T::WeightInfo::set_collator_selection_kick_threshold())]
pub fn set_collator_selection_kick_threshold(
origin: OriginFor<T>,
threshold: Option<T::BlockNumber>,
pallets/configuration/src/weights.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/configuration/src/weights.rs
@@ -0,0 +1,123 @@
+// Template adopted from https://github.com/paritytech/substrate/blob/master/.maintain/frame-weight-template.hbs
+
+//! Autogenerated weights for pallet_configuration
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2022-12-28, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
+
+// Executed Command:
+// target/release/unique-collator
+// benchmark
+// pallet
+// --pallet
+// pallet-configuration
+// --wasm-execution
+// compiled
+// --extrinsic
+// *
+// --template
+// .maintain/frame-weight-template.hbs
+// --steps=50
+// --repeat=80
+// --heap-pages=4096
+// --output=./pallets/configuration/src/weights.rs
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+#![allow(clippy::unnecessary_cast)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for pallet_configuration.
+pub trait WeightInfo {
+ fn set_weight_to_fee_coefficient_override() -> Weight;
+ fn set_min_gas_price_override() -> Weight;
+ fn set_xcm_allowed_locations() -> Weight;
+ fn set_app_promotion_configuration_override() -> Weight;
+ fn set_collator_selection_desired_collators() -> Weight;
+ fn set_collator_selection_license_bond() -> Weight;
+ fn set_collator_selection_kick_threshold() -> Weight;
+}
+
+/// Weights for pallet_configuration using the Substrate node and recommended hardware.
+pub struct SubstrateWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
+ // Storage: Configuration WeightToFeeCoefficientOverride (r:0 w:1)
+ fn set_weight_to_fee_coefficient_override() -> Weight {
+ Weight::from_ref_time(5_691_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration MinGasPriceOverride (r:0 w:1)
+ fn set_min_gas_price_override() -> Weight {
+ Weight::from_ref_time(5_521_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration XcmAllowedLocationsOverride (r:0 w:1)
+ fn set_xcm_allowed_locations() -> Weight {
+ Weight::from_ref_time(6_091_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration AppPromomotionConfigurationOverride (r:0 w:1)
+ fn set_app_promotion_configuration_override() -> Weight {
+ Weight::from_ref_time(6_241_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration CollatorSelectionDesiredCollatorsOverride (r:0 w:1)
+ fn set_collator_selection_desired_collators() -> Weight {
+ Weight::from_ref_time(25_298_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration CollatorSelectionLicenseBondOverride (r:0 w:1)
+ fn set_collator_selection_license_bond() -> Weight {
+ Weight::from_ref_time(18_675_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration CollatorSelectionKickThresholdOverride (r:0 w:1)
+ fn set_collator_selection_kick_threshold() -> Weight {
+ Weight::from_ref_time(18_044_000 as u64)
+ .saturating_add(T::DbWeight::get().writes(1 as u64))
+ }
+}
+
+// For backwards compatibility and tests
+impl WeightInfo for () {
+ // Storage: Configuration WeightToFeeCoefficientOverride (r:0 w:1)
+ fn set_weight_to_fee_coefficient_override() -> Weight {
+ Weight::from_ref_time(5_691_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration MinGasPriceOverride (r:0 w:1)
+ fn set_min_gas_price_override() -> Weight {
+ Weight::from_ref_time(5_521_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration XcmAllowedLocationsOverride (r:0 w:1)
+ fn set_xcm_allowed_locations() -> Weight {
+ Weight::from_ref_time(6_091_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration AppPromomotionConfigurationOverride (r:0 w:1)
+ fn set_app_promotion_configuration_override() -> Weight {
+ Weight::from_ref_time(6_241_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration CollatorSelectionDesiredCollatorsOverride (r:0 w:1)
+ fn set_collator_selection_desired_collators() -> Weight {
+ Weight::from_ref_time(25_298_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration CollatorSelectionLicenseBondOverride (r:0 w:1)
+ fn set_collator_selection_license_bond() -> Weight {
+ Weight::from_ref_time(18_675_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+ // Storage: Configuration CollatorSelectionKickThresholdOverride (r:0 w:1)
+ fn set_collator_selection_kick_threshold() -> Weight {
+ Weight::from_ref_time(18_044_000 as u64)
+ .saturating_add(RocksDbWeight::get().writes(1 as u64))
+ }
+}
runtime/common/config/pallets/mod.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -121,6 +121,7 @@
type MaxXcmAllowedLocations = ConstU32<16>;
type AppPromotionDailyRate = AppPromotionDailyRate;
type DayRelayBlocks = DayRelayBlocks;
+ type WeightInfo = pallet_configuration::weights::SubstrateWeight<Self>;
}
impl pallet_maintenance::Config for Runtime {
runtime/common/construct_runtime/mod.rsdiffbeforeafterboth--- a/runtime/common/construct_runtime/mod.rs
+++ b/runtime/common/construct_runtime/mod.rs
@@ -32,17 +32,17 @@
ParachainSystem: cumulus_pallet_parachain_system::{Pallet, Call, Config, Storage, Inherent, Event<T>, ValidateUnsigned} = 20,
ParachainInfo: parachain_info::{Pallet, Storage, Config} = 21,
- Aura: pallet_aura::{Pallet, Config<T>} = 22,
- AuraExt: cumulus_pallet_aura_ext::{Pallet, Config} = 23,
+ #[runtimes(opal)]
+ Authorship: pallet_authorship::{Pallet, Call, Storage} = 22,
#[runtimes(opal)]
- Authorship: pallet_authorship::{Pallet, Call, Storage} = 24,
+ CollatorSelection: pallet_collator_selection::{Pallet, Call, Storage, Event<T>, Config<T>} = 23,
#[runtimes(opal)]
- CollatorSelection: pallet_collator_selection::{Pallet, Call, Storage, Event<T>, Config<T>} = 25,
+ Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>} = 24,
- #[runtimes(opal)]
- Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>} = 26,
+ Aura: pallet_aura::{Pallet, Config<T>} = 25,
+ AuraExt: cumulus_pallet_aura_ext::{Pallet, Config} = 26,
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 30,
RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Pallet, Storage} = 31,
runtime/common/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/runtime_apis.rs
+++ b/runtime/common/runtime_apis.rs
@@ -686,6 +686,7 @@
list_benchmark!(list, extra, pallet_unique, Unique);
list_benchmark!(list, extra, pallet_structure, Structure);
list_benchmark!(list, extra, pallet_inflation, Inflation);
+ list_benchmark!(list, extra, pallet_configuration, Configuration);
#[cfg(feature = "app-promotion")]
list_benchmark!(list, extra, pallet_app_promotion, AppPromotion);
@@ -755,6 +756,7 @@
add_benchmark!(params, batches, pallet_unique, Unique);
add_benchmark!(params, batches, pallet_structure, Structure);
add_benchmark!(params, batches, pallet_inflation, Inflation);
+ add_benchmark!(params, batches, pallet_configuration, Configuration);
#[cfg(feature = "app-promotion")]
add_benchmark!(params, batches, pallet_app_promotion, AppPromotion);
runtime/opal/Cargo.tomldiffbeforeafterboth--- a/runtime/opal/Cargo.toml
+++ b/runtime/opal/Cargo.toml
@@ -28,6 +28,7 @@
'pallet-evm-coder-substrate/runtime-benchmarks',
'pallet-balances/runtime-benchmarks',
'pallet-timestamp/runtime-benchmarks',
+ 'pallet-configuration/runtime-benchmarks',
'pallet-common/runtime-benchmarks',
'pallet-structure/runtime-benchmarks',
'pallet-fungible/runtime-benchmarks',
runtime/quartz/Cargo.tomldiffbeforeafterboth--- a/runtime/quartz/Cargo.toml
+++ b/runtime/quartz/Cargo.toml
@@ -28,6 +28,7 @@
'pallet-evm-coder-substrate/runtime-benchmarks',
'pallet-balances/runtime-benchmarks',
'pallet-timestamp/runtime-benchmarks',
+ 'pallet-configuration/runtime-benchmarks',
'pallet-common/runtime-benchmarks',
'pallet-structure/runtime-benchmarks',
'pallet-fungible/runtime-benchmarks',
runtime/unique/Cargo.tomldiffbeforeafterboth--- a/runtime/unique/Cargo.toml
+++ b/runtime/unique/Cargo.toml
@@ -28,6 +28,7 @@
'pallet-evm-coder-substrate/runtime-benchmarks',
'pallet-balances/runtime-benchmarks',
'pallet-timestamp/runtime-benchmarks',
+ 'pallet-configuration/runtime-benchmarks',
'pallet-common/runtime-benchmarks',
'pallet-structure/runtime-benchmarks',
'pallet-fungible/runtime-benchmarks',