difftreelog
feat(collator-selection) interaction with configuration pallet
in: master
17 files changed
node/cli/src/chain_spec.rsdiffbeforeafterboth--- a/node/cli/src/chain_spec.rs
+++ b/node/cli/src/chain_spec.rs
@@ -24,7 +24,6 @@
use serde_json::map::Map;
use up_common::types::opaque::*;
-use up_common::constants::{GENESIS_LICENSE_BOND, SESSION_LENGTH};
#[cfg(feature = "unique-runtime")]
pub use unique_runtime as default_runtime;
@@ -196,9 +195,6 @@
.cloned()
.map(|(acc, _)| acc)
.collect(),
- desired_collators: 10,
- license_bond: GENESIS_LICENSE_BOND,
- kick_threshold: SESSION_LENGTH,
},
session: SessionConfig {
keys: $initial_invulnerables
node/cli/src/service.rsdiffbeforeafterboth--- a/node/cli/src/service.rs
+++ b/node/cli/src/service.rs
@@ -921,7 +921,7 @@
.import_notification_stream()
.map(|_| EngineCommand::SealNewBlock {
create_empty: true,
- finalize: false,
+ finalize: false, // todo:collator finalize true
parent_hash: None,
sender: None,
}),
@@ -932,7 +932,7 @@
dyn Stream<Item = EngineCommand<Hash>> + Send + Sync + Unpin,
> = Box::new(autoseal_interval.map(|_| EngineCommand::SealNewBlock {
create_empty: true,
- finalize: false,
+ finalize: false, // todo:collator finalize true
parent_hash: None,
sender: None,
}));
pallets/collator-selection/Cargo.tomldiffbeforeafterboth--- a/pallets/collator-selection/Cargo.toml
+++ b/pallets/collator-selection/Cargo.toml
@@ -25,6 +25,7 @@
frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
pallet-authorship = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
pallet-session = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
+pallet-configuration = { default-features = false, path = "../configuration" }
frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
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::*, Config as SystemConfig};109 use pallet_session::SessionManager;110 use sp_runtime::{111 Perbill,112 traits::{One, Convert},113 };114 use sp_staking::SessionIndex;115116 type BalanceOf<T> =117 <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;118119 /// A convertor from collators id. Since this pallet does not have stash/controller, this is120 /// just identity.121 pub struct IdentityCollator;122 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {123 fn convert(t: T) -> Option<T> {124 Some(t)125 }126 }127128 /// Configure the pallet by specifying the parameters and types on which it depends.129 #[pallet::config]130 pub trait Config: frame_system::Config {131 /// Overarching event type.132 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;133134 /// The currency mechanism.135 type Currency: ReservableCurrency<Self::AccountId>;136137 /// Origin that can dictate updating parameters of this pallet.138 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;139140 /// Account Identifier that holds the chain's treasury.141 type TreasuryAccountId: Get<Self::AccountId>;142143 /// Account Identifier from which the internal Pot is generated.144 type PotId: Get<PalletId>;145146 /// Maximum number of candidates and invulnerables that we should have. This is enforced in code.147 type MaxCollators: Get<u32>;148149 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.150 type SlashRatio: Get<Perbill>;151152 /// A stable ID for a validator.153 type ValidatorId: Member + Parameter;154155 /// A conversion from account ID to validator ID.156 ///157 /// Its cost must be at most one storage read.158 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;159160 /// Validate a user is registered161 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;162163 /// The weight information of this pallet.164 type WeightInfo: WeightInfo;165 }166167 #[pallet::pallet]168 #[pallet::generate_store(pub(super) trait Store)]169 pub struct Pallet<T>(_);170171 /// The invulnerable, fixed collators.172 #[pallet::storage]173 #[pallet::getter(fn invulnerables)]174 pub type Invulnerables<T: Config> =175 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;176177 /// The (community) collation license holders.178 #[pallet::storage]179 #[pallet::getter(fn licenses)]180 pub type Licenses<T: Config> =181 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;182183 /// The (community, limited) collation candidates.184 #[pallet::storage]185 #[pallet::getter(fn candidates)]186 pub type Candidates<T: Config> =187 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;188189 /// Collator will be kicked if it does not produce a block within the threshold (does not apply to invulnerables).190 ///191 /// Should be a multiple of session or things will get inconsistent. todo:collator reword?192 #[pallet::storage]193 #[pallet::getter(fn kick_threshold)]194 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;195196 /// Last block authored by collator.197 #[pallet::storage]198 #[pallet::getter(fn last_authored_block)]199 pub type LastAuthoredBlock<T: Config> =200 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;201202 /// Desired number of candidates.203 ///204 /// This should ideally always be less than [`Config::MaxCollators`] for weights to be correct.205 #[pallet::storage]206 #[pallet::getter(fn desired_collators)]207 pub type DesiredCollators<T> = StorageValue<_, u32, ValueQuery>;208209 /// Fixed amount to deposit to become a collator.210 ///211 /// When a collator calls `leave_intent` they immediately receive the deposit back.212 #[pallet::storage]213 #[pallet::getter(fn license_bond)]214 pub type LicenseBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;215216 #[pallet::genesis_config]217 pub struct GenesisConfig<T: Config> {218 pub invulnerables: Vec<T::AccountId>,219 pub license_bond: BalanceOf<T>,220 pub kick_threshold: T::BlockNumber,221 pub desired_collators: u32,222 }223224 #[cfg(feature = "std")]225 impl<T: Config> Default for GenesisConfig<T> {226 fn default() -> Self {227 Self {228 invulnerables: Default::default(),229 license_bond: Default::default(),230 kick_threshold: T::BlockNumber::one(),231 desired_collators: Default::default(),232 }233 }234 }235236 #[pallet::genesis_build]237 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {238 fn build(&self) {239 let duplicate_invulnerables = self240 .invulnerables241 .iter()242 .collect::<std::collections::BTreeSet<_>>();243 assert!(244 duplicate_invulnerables.len() == self.invulnerables.len(),245 "duplicate invulnerables in genesis."246 );247248 let bounded_invulnerables =249 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())250 .expect("genesis invulnerables are more than T::MaxCollators");251 assert!(252 T::MaxCollators::get() >= self.desired_collators,253 "genesis desired_collators are more than T::MaxCollators",254 );255256 <DesiredCollators<T>>::put(self.desired_collators);257 <LicenseBond<T>>::put(self.license_bond);258 <KickThreshold<T>>::put(self.kick_threshold);259 <Invulnerables<T>>::put(bounded_invulnerables);260 }261 }262263 #[pallet::event]264 #[pallet::generate_deposit(pub(super) fn deposit_event)]265 pub enum Event<T: Config> {266 NewDesiredCollators {267 desired_collators: u32,268 },269 NewLicenseBond {270 bond_amount: BalanceOf<T>,271 },272 NewKickThreshold {273 length_in_blocks: T::BlockNumber,274 },275 InvulnerableAdded {276 invulnerable: T::AccountId,277 },278 InvulnerableRemoved {279 invulnerable: T::AccountId,280 },281 LicenseObtained {282 account_id: T::AccountId,283 deposit: BalanceOf<T>,284 },285 LicenseForfeited {286 account_id: T::AccountId,287 deposit_returned: BalanceOf<T>,288 },289 CandidateAdded {290 account_id: T::AccountId,291 },292 CandidateRemoved {293 account_id: T::AccountId,294 },295 }296297 // Errors inform users that something went wrong.298 #[pallet::error]299 pub enum Error<T> {300 /// Too many candidates301 TooManyCandidates,302 /// Unknown error303 Unknown,304 /// Permission issue305 Permission,306 /// User already holds license to collate307 AlreadyHoldingLicense,308 /// User does not hold a license to collate309 NoLicense,310 /// User is already a candidate311 AlreadyCandidate,312 /// User is not a candidate313 NotCandidate,314 /// Too many invulnerables315 TooManyInvulnerables,316 /// Too few invulnerables317 TooFewInvulnerables,318 /// User is already an Invulnerable319 AlreadyInvulnerable,320 /// User is not an Invulnerable321 NotInvulnerable,322 /// Account has no associated validator ID323 NoAssociatedValidatorId,324 /// Validator ID is not yet registered325 ValidatorNotRegistered,326 }327328 #[pallet::hooks]329 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}330331 #[pallet::call]332 impl<T: Config> Pallet<T> {333 /// Add a collator to the list of invulnerable (fixed) collators.334 #[pallet::weight(T::WeightInfo::set_invulnerables(1u32))] // todo:collator weight335 pub fn add_invulnerable(336 origin: OriginFor<T>,337 new: T::AccountId,338 ) -> DispatchResultWithPostInfo {339 T::UpdateOrigin::ensure_origin(origin)?;340341 // check if the new invulnerable has associated validator keys before it is added342 let validator_key = T::ValidatorIdOf::convert(new.clone())343 .ok_or(Error::<T>::NoAssociatedValidatorId)?;344 ensure!(345 T::ValidatorRegistration::is_registered(&validator_key),346 Error::<T>::ValidatorNotRegistered347 );348 if Self::invulnerables().contains(&new) {349 return Ok(().into());350 }351352 <Invulnerables<T>>::try_append(new.clone())353 .map_err(|_| Error::<T>::TooManyInvulnerables)?;354355 // try to offboard the new invulnerable if it was a collator candidate before356 let _ = Self::try_remove_candidate(&new);357358 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });359 Ok(().into())360 }361362 /// Remove a collator from the list of invulnerable (fixed) collators.363 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] // todo:collator weight364 pub fn remove_invulnerable(365 origin: OriginFor<T>,366 who: T::AccountId,367 ) -> DispatchResultWithPostInfo {368 T::UpdateOrigin::ensure_origin(origin)?;369370 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {371 if invulnerables.len() <= 1 {372 return Err(Error::<T>::TooFewInvulnerables.into());373 }374375 let index = invulnerables376 .into_iter()377 .position(|r| *r == who)378 .ok_or(Error::<T>::NotInvulnerable)?;379 invulnerables.remove(index);380 Ok(())381 })?;382 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });383 Ok(().into())384 }385386 /// Set the ideal number of collators. If lowering this number,387 /// then the number of running collators could be higher than this figure.388 /// Aside from that edge case, there should be no other way to have more collators than the desired number.389 #[pallet::weight(T::WeightInfo::set_desired_collators())]390 pub fn set_desired_collators(origin: OriginFor<T>, max: u32) -> DispatchResultWithPostInfo {391 T::UpdateOrigin::ensure_origin(origin)?;392 // we trust origin calls, this is just a for more accurate benchmarking393 if max > T::MaxCollators::get() {394 log::warn!("max > T::MaxCollators; you might need to run benchmarks again");395 }396 <DesiredCollators<T>>::put(max);397 Self::deposit_event(Event::NewDesiredCollators {398 desired_collators: max,399 });400 Ok(().into())401 }402403 /// Set the candidacy bond amount.404 #[pallet::weight(T::WeightInfo::set_license_bond())]405 pub fn set_license_bond(406 origin: OriginFor<T>,407 bond: BalanceOf<T>,408 ) -> DispatchResultWithPostInfo {409 T::UpdateOrigin::ensure_origin(origin)?;410 <LicenseBond<T>>::put(bond);411 Self::deposit_event(Event::NewLicenseBond { bond_amount: bond });412 Ok(().into())413 }414415 /// Set the length of the kick threshold.416 /// Note that if the length is not a multiple of the session period, it might get inconsistent.417 #[pallet::weight(T::WeightInfo::set_license_bond())] // todo:collator weight418 pub fn set_kick_threshold(419 origin: OriginFor<T>,420 kick_threshold: T::BlockNumber,421 ) -> DispatchResultWithPostInfo {422 T::UpdateOrigin::ensure_origin(origin)?;423 // todo:collator insert something to guarantee consistency?424 <KickThreshold<T>>::put(kick_threshold);425 Self::deposit_event(Event::NewKickThreshold {426 length_in_blocks: kick_threshold,427 });428 Ok(().into())429 }430431 /// Purchase a license on block collation for this account.432 /// It does not make it a collator candidate, use `onboard` afterward. The account must433 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.434 ///435 /// This call is not available to `Invulnerable` collators.436 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] // todo:collator weight437 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {438 // register_as_candidate439 let who = ensure_signed(origin)?;440441 if Licenses::<T>::contains_key(&who) {442 return Err(Error::<T>::AlreadyHoldingLicense.into());443 }444445 let validator_key = T::ValidatorIdOf::convert(who.clone())446 .ok_or(Error::<T>::NoAssociatedValidatorId)?;447 ensure!(448 T::ValidatorRegistration::is_registered(&validator_key),449 Error::<T>::ValidatorNotRegistered450 );451452 let deposit = Self::license_bond();453454 T::Currency::reserve(&who, deposit)?;455 Licenses::<T>::insert(who.clone(), deposit);456457 Self::deposit_event(Event::LicenseObtained {458 account_id: who,459 deposit,460 });461 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())462 }463464 /// Register this account as a candidate for collators for next sessions.465 /// The account must already hold a license, and cannot offboard immediately during a session.466 ///467 /// This call is not available to `Invulnerable` collators.468 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] // todo:collator weight469 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {470 // register_as_candidate471 let who = ensure_signed(origin)?;472473 // ensure the user obtained the license.474 ensure!(Licenses::<T>::contains_key(&who), Error::<T>::NoLicense);475 // ensure we are below limit.476 let length = <Candidates<T>>::decode_len().unwrap_or_default()477 + <Invulnerables<T>>::decode_len().unwrap_or_default();478 ensure!(479 (length as u32) < Self::desired_collators(),480 Error::<T>::TooManyCandidates481 );482 ensure!(483 !Self::invulnerables().contains(&who),484 Error::<T>::AlreadyInvulnerable485 );486487 let current_count =488 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {489 if candidates.iter().any(|candidate| *candidate == who) {490 Err(Error::<T>::AlreadyCandidate)?491 } else {492 candidates493 .try_push(who.clone())494 .map_err(|_| Error::<T>::TooManyCandidates)?;495 // First authored block is current block plus kick threshold to handle session delay496 <LastAuthoredBlock<T>>::insert(497 who.clone(),498 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),499 );500 Ok(candidates.len())501 }502 })?;503504 Self::deposit_event(Event::CandidateAdded { account_id: who });505 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())506 }507508 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on509 /// session change. The license to `onboard` later at any other time will remain.510 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight511 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {512 // leave_intent513 let who = ensure_signed(origin)?;514 let current_count = Self::try_remove_candidate(&who)?;515516 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight517 }518519 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.520 ///521 /// This call is not available to `Invulnerable` collators.522 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight523 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {524 // leave_intent525 let who = ensure_signed(origin)?;526527 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;528529 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight530 }531532 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.533 /// Note that the collator can only leave on session change.534 /// The `LicenseBond` will be unreserved and returned immediately.535 ///536 /// This call is, of course, not applicable to `Invulnerable` collators.537 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight538 pub fn force_revoke_license(539 origin: OriginFor<T>,540 who: T::AccountId,541 ) -> DispatchResultWithPostInfo {542 // leave_intent543 T::UpdateOrigin::ensure_origin(origin)?;544545 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;546547 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight548 }549 }550551 impl<T: Config> Pallet<T> {552 /// Get a unique, inaccessible account id from the `PotId`.553 pub fn account_id() -> T::AccountId {554 T::PotId::get().into_account_truncating()555 }556557 /// Removes a candidate and their license, optionally slashed and optionally ignoring,558 /// whether or not they actually are a candidate.559 fn try_remove_candidate_and_release_license(560 who: &T::AccountId,561 should_slash: bool,562 ignore_if_not_candidate: bool,563 ) -> Result<usize, DispatchError> {564 let current_count = Self::try_remove_candidate(who);565 let current_count = if ignore_if_not_candidate566 && current_count == Err(Error::<T>::NotCandidate.into())567 {568 <Candidates<T>>::decode_len().unwrap_or_default()569 } else {570 current_count?571 };572 Self::try_release_license(who, should_slash)?;573 Ok(current_count)574 }575576 /// Removes a candidate from the collator pool for the next session if they exist.577 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {578 let current_count =579 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {580 let index = candidates581 .iter()582 .position(|candidate| *candidate == *who)583 .ok_or(Error::<T>::NotCandidate)?;584 candidates.remove(index);585 <LastAuthoredBlock<T>>::remove(who.clone());586 Ok(candidates.len())587 })?;588 Self::deposit_event(Event::CandidateRemoved {589 account_id: who.clone(),590 });591 Ok(current_count)592 }593594 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.595 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {596 let mut deposit_returned = BalanceOf::<T>::default();597 Licenses::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {598 if let Some(deposit) = deposit.take() {599 if should_slash {600 let slashed = T::SlashRatio::get() * deposit;601 let remaining = deposit - slashed;602603 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);604 //T::Currency::unreserve(who, remaining);605 deposit_returned = remaining;606607 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);608 } else {609 //T::Currency::unreserve(who, deposit);610 deposit_returned = deposit;611 }612613 T::Currency::unreserve(who, deposit_returned);614 Ok(())615 } else {616 Err(Error::<T>::NoLicense.into())617 }618 })?;619 Self::deposit_event(Event::LicenseForfeited {620 account_id: who.clone(),621 deposit_returned,622 });623 Ok(())624 }625626 /// Assemble the current set of candidates and invulnerables into the next collator set.627 ///628 /// This is done on the fly, as frequent as we are told to do so, as the session manager.629 pub fn assemble_collators(630 candidates: BoundedVec<T::AccountId, T::MaxCollators>,631 ) -> Vec<T::AccountId> {632 let mut collators = Self::invulnerables().to_vec();633 collators.extend(candidates);634 collators635 }636637 /// Kicks out candidates that did not produce a block in the kick threshold638 /// and **confiscates** their deposits to the treasury.639 pub fn kick_stale_candidates(640 candidates: BoundedVec<T::AccountId, T::MaxCollators>,641 ) -> BoundedVec<T::AccountId, T::MaxCollators> {642 let now = frame_system::Pallet::<T>::block_number();643 let kick_threshold = Self::kick_threshold();644 candidates645 .into_iter()646 .filter_map(|c| {647 let last_block = <LastAuthoredBlock<T>>::get(c.clone());648 let since_last = now.saturating_sub(last_block);649 if since_last < kick_threshold {650 Some(c)651 } else {652 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);653 if let Err(why) = outcome {654 log::warn!("Failed to kick collator and release license {:?}", why);655 debug_assert!(false, "failed to kick collator and release license {why:?}");656 }657 None658 }659 })660 .collect::<Vec<_>>()661 .try_into()662 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")663 }664 }665666 /// Keep track of number of authored blocks per authority, uncles are counted as well since667 /// they're a valid proof of being online.668 impl<T: Config + pallet_authorship::Config>669 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>670 {671 fn note_author(author: T::AccountId) {672 let pot = Self::account_id();673 // assumes an ED will be sent to pot.674 let reward = T::Currency::free_balance(&pot)675 .checked_sub(&T::Currency::minimum_balance())676 .unwrap_or_else(Zero::zero)677 .div(2u32.into());678 // `reward` is half of pot account minus ED, this should never fail.679 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);680 debug_assert!(_success.is_ok());681 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());682683 frame_system::Pallet::<T>::register_extra_weight_unchecked(684 T::WeightInfo::note_author(),685 DispatchClass::Mandatory,686 );687 }688689 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {690 //TODO can we ignore this?691 }692 }693694 /// Play the role of the session manager.695 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {696 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {697 log::info!(698 "assembling new collators for new session {} at #{:?}",699 index,700 <frame_system::Pallet<T>>::block_number(),701 );702703 let candidates = Self::candidates();704 let candidates_len_before = candidates.len();705 let active_candidates = Self::kick_stale_candidates(candidates);706 let removed = candidates_len_before - active_candidates.len();707 let result = Self::assemble_collators(active_candidates);708709 frame_system::Pallet::<T>::register_extra_weight_unchecked(710 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),711 DispatchClass::Mandatory,712 );713 Some(result)714 }715 fn start_session(_: SessionIndex) {716 // we don't care.717 }718 fn end_session(_: SessionIndex) {719 // we don't care.720 }721 }722}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");220 221 <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 LicenseForfeited {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::weight(T::WeightInfo::set_invulnerables(1u32))] // todo:collator weight288 pub fn add_invulnerable(289 origin: OriginFor<T>,290 new: T::AccountId,291 ) -> DispatchResultWithPostInfo {292 T::UpdateOrigin::ensure_origin(origin)?;293294 // check if the new invulnerable has associated validator keys before it is added295 let validator_key = T::ValidatorIdOf::convert(new.clone())296 .ok_or(Error::<T>::NoAssociatedValidatorId)?;297 ensure!(298 T::ValidatorRegistration::is_registered(&validator_key),299 Error::<T>::ValidatorNotRegistered300 );301 if Self::invulnerables().contains(&new) {302 return Ok(().into());303 }304305 <Invulnerables<T>>::try_append(new.clone())306 .map_err(|_| Error::<T>::TooManyInvulnerables)?;307308 // try to offboard the new invulnerable if it was a collator candidate before309 let _ = Self::try_remove_candidate(&new);310311 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });312 Ok(().into())313 }314315 /// Remove a collator from the list of invulnerable (fixed) collators.316 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] // todo:collator weight317 pub fn remove_invulnerable(318 origin: OriginFor<T>,319 who: T::AccountId,320 ) -> DispatchResultWithPostInfo {321 T::UpdateOrigin::ensure_origin(origin)?;322323 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {324 if invulnerables.len() <= 1 {325 return Err(Error::<T>::TooFewInvulnerables.into());326 }327328 let index = invulnerables329 .into_iter()330 .position(|r| *r == who)331 .ok_or(Error::<T>::NotInvulnerable)?;332 invulnerables.remove(index);333 Ok(())334 })?;335 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });336 Ok(().into())337 }338339 /// Purchase a license on block collation for this account.340 /// It does not make it a collator candidate, use `onboard` afterward. The account must341 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.342 ///343 /// This call is not available to `Invulnerable` collators.344 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] // todo:collator weight345 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {346 // register_as_candidate347 let who = ensure_signed(origin)?;348349 if LicenseDepositOf::<T>::contains_key(&who) {350 return Err(Error::<T>::AlreadyHoldingLicense.into());351 }352353 let validator_key = T::ValidatorIdOf::convert(who.clone())354 .ok_or(Error::<T>::NoAssociatedValidatorId)?;355 ensure!(356 T::ValidatorRegistration::is_registered(&validator_key),357 Error::<T>::ValidatorNotRegistered358 );359360 let deposit = <LicenseBond<T>>::get();361362 T::Currency::reserve(&who, deposit)?;363 LicenseDepositOf::<T>::insert(who.clone(), deposit);364365 Self::deposit_event(Event::LicenseObtained {366 account_id: who,367 deposit,368 });369 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())370 }371372 /// Register this account as a candidate for collators for next sessions.373 /// The account must already hold a license, and cannot offboard immediately during a session.374 ///375 /// This call is not available to `Invulnerable` collators.376 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] // todo:collator weight377 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {378 // register_as_candidate379 let who = ensure_signed(origin)?;380381 // ensure the user obtained the license.382 ensure!(383 LicenseDepositOf::<T>::contains_key(&who),384 Error::<T>::NoLicense385 );386 // ensure we are below limit.387 let length = <Candidates<T>>::decode_len().unwrap_or_default()388 + <Invulnerables<T>>::decode_len().unwrap_or_default();389 ensure!(390 (length as u32) < <DesiredCollators<T>>::get(),391 Error::<T>::TooManyCandidates392 );393 ensure!(394 !Self::invulnerables().contains(&who),395 Error::<T>::AlreadyInvulnerable396 );397398 let current_count =399 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {400 if candidates.iter().any(|candidate| *candidate == who) {401 Err(Error::<T>::AlreadyCandidate)?402 } else {403 candidates404 .try_push(who.clone())405 .map_err(|_| Error::<T>::TooManyCandidates)?;406 // First authored block is current block plus kick threshold to handle session delay407 <LastAuthoredBlock<T>>::insert(408 who.clone(),409 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),410 );411 Ok(candidates.len())412 }413 })?;414415 Self::deposit_event(Event::CandidateAdded { account_id: who });416 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())417 }418419 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on420 /// session change. The license to `onboard` later at any other time will remain.421 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight422 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {423 // leave_intent424 let who = ensure_signed(origin)?;425 let current_count = Self::try_remove_candidate(&who)?;426427 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight428 }429430 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.431 ///432 /// This call is not available to `Invulnerable` collators.433 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight434 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {435 // leave_intent436 let who = ensure_signed(origin)?;437438 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;439440 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight441 }442443 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.444 /// Note that the collator can only leave on session change.445 /// The `LicenseBond` will be unreserved and returned immediately.446 ///447 /// This call is, of course, not applicable to `Invulnerable` collators.448 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight449 pub fn force_revoke_license(450 origin: OriginFor<T>,451 who: T::AccountId,452 ) -> DispatchResultWithPostInfo {453 // leave_intent454 T::UpdateOrigin::ensure_origin(origin)?;455456 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;457458 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight459 }460 }461462 impl<T: Config> Pallet<T> {463 /// Get a unique, inaccessible account id from the `PotId`.464 pub fn account_id() -> T::AccountId {465 T::PotId::get().into_account_truncating()466 }467468 /// Removes a candidate and their license, optionally slashed and optionally ignoring,469 /// whether or not they actually are a candidate.470 fn try_remove_candidate_and_release_license(471 who: &T::AccountId,472 should_slash: bool,473 ignore_if_not_candidate: bool,474 ) -> Result<usize, DispatchError> {475 let current_count = Self::try_remove_candidate(who);476 let current_count = if ignore_if_not_candidate477 && current_count == Err(Error::<T>::NotCandidate.into())478 {479 <Candidates<T>>::decode_len().unwrap_or_default()480 } else {481 current_count?482 };483 Self::try_release_license(who, should_slash)?;484 Ok(current_count)485 }486487 /// Removes a candidate from the collator pool for the next session if they exist.488 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {489 let current_count =490 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {491 let index = candidates492 .iter()493 .position(|candidate| *candidate == *who)494 .ok_or(Error::<T>::NotCandidate)?;495 candidates.remove(index);496 <LastAuthoredBlock<T>>::remove(who.clone());497 Ok(candidates.len())498 })?;499 Self::deposit_event(Event::CandidateRemoved {500 account_id: who.clone(),501 });502 Ok(current_count)503 }504505 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.506 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {507 let mut deposit_returned = BalanceOf::<T>::default();508 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {509 if let Some(deposit) = deposit.take() {510 if should_slash {511 let slashed = T::SlashRatio::get() * deposit;512 let remaining = deposit - slashed;513514 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);515 //T::Currency::unreserve(who, remaining);516 deposit_returned = remaining;517518 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);519 } else {520 //T::Currency::unreserve(who, deposit);521 deposit_returned = deposit;522 }523524 T::Currency::unreserve(who, deposit_returned);525 Ok(())526 } else {527 Err(Error::<T>::NoLicense.into())528 }529 })?;530 Self::deposit_event(Event::LicenseForfeited {531 account_id: who.clone(),532 deposit_returned,533 });534 Ok(())535 }536537 /// Assemble the current set of candidates and invulnerables into the next collator set.538 ///539 /// This is done on the fly, as frequent as we are told to do so, as the session manager.540 pub fn assemble_collators(541 candidates: BoundedVec<T::AccountId, T::MaxCollators>,542 ) -> Vec<T::AccountId> {543 let mut collators = Self::invulnerables().to_vec();544 collators.extend(candidates);545 collators546 }547548 /// Kicks out candidates that did not produce a block in the kick threshold549 /// and **confiscates** their deposits to the treasury.550 pub fn kick_stale_candidates(551 candidates: BoundedVec<T::AccountId, T::MaxCollators>,552 ) -> BoundedVec<T::AccountId, T::MaxCollators> {553 let now = frame_system::Pallet::<T>::block_number();554 let kick_threshold = <KickThreshold<T>>::get();555 candidates556 .into_iter()557 .filter_map(|c| {558 let last_block = <LastAuthoredBlock<T>>::get(c.clone());559 let since_last = now.saturating_sub(last_block);560 if since_last < kick_threshold {561 Some(c)562 } else {563 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);564 if let Err(why) = outcome {565 log::warn!("Failed to kick collator and release license {:?}", why);566 debug_assert!(false, "failed to kick collator and release license {why:?}");567 }568 None569 }570 })571 .collect::<Vec<_>>()572 .try_into()573 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")574 }575 }576577 /// Keep track of number of authored blocks per authority, uncles are counted as well since578 /// they're a valid proof of being online.579 impl<T: Config + pallet_authorship::Config>580 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>581 {582 fn note_author(author: T::AccountId) {583 let pot = Self::account_id();584 // assumes an ED will be sent to pot.585 let reward = T::Currency::free_balance(&pot)586 .checked_sub(&T::Currency::minimum_balance())587 .unwrap_or_else(Zero::zero)588 .div(2u32.into());589 // `reward` is half of pot account minus ED, this should never fail.590 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);591 debug_assert!(_success.is_ok());592 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());593594 frame_system::Pallet::<T>::register_extra_weight_unchecked(595 T::WeightInfo::note_author(),596 DispatchClass::Mandatory,597 );598 }599600 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {601 //TODO can we ignore this?602 }603 }604605 /// Play the role of the session manager.606 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {607 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {608 log::info!(609 "assembling new collators for new session {} at #{:?}",610 index,611 <frame_system::Pallet<T>>::block_number(),612 );613614 let candidates = Self::candidates();615 let candidates_len_before = candidates.len();616 let active_candidates = Self::kick_stale_candidates(candidates);617 let removed = candidates_len_before - active_candidates.len();618 let result = Self::assemble_collators(active_candidates);619620 frame_system::Pallet::<T>::register_extra_weight_unchecked(621 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),622 DispatchClass::Mandatory,623 );624 Some(result)625 }626 fn start_session(_: SessionIndex) {627 // we don't care.628 }629 fn end_session(_: SessionIndex) {630 // we don't care.631 }632 }633}pallets/collator-selection/src/mock.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/mock.rs
+++ b/pallets/collator-selection/src/mock.rs
@@ -63,6 +63,7 @@
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
CollatorSelection: collator_selection::{Pallet, Call, Storage, Event<T>},
Authorship: pallet_authorship::{Pallet, Call, Storage, Inherent},
+ Configuration: pallet_configuration::{Pallet, Call, Storage, Event<T>},
}
);
@@ -200,13 +201,38 @@
type WeightInfo = ();
}
+parameter_types! {
+ pub const MaxCollators: u32 = 5;
+ pub const LicenseBond: u64 = 10;
+ pub const KickThreshold: u64 = 10;
+ // the following values do not matter and are meaningless, etc.
+ pub const DefaultWeightToFeeCoefficient: u32 = 100_000;
+ pub const DefaultMinGasPrice: u64 = 100_000;
+ pub const MaxXcmAllowedLocations: u32 = 16;
+ pub AppPromotionDailyRate: Perbill = Perbill::from_rational(5u32, 10_000);
+ pub const DayRelayBlocks: u32 = 1;
+}
+
+impl pallet_configuration::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type Currency = Balances;
+ type DefaultCollatorSelectionMaxCollators = MaxCollators;
+ type DefaultCollatorSelectionKickThreshold = KickThreshold;
+ type DefaultCollatorSelectionLicenseBond = LicenseBond;
+ // the following we don't care about
+ type DefaultWeightToFeeCoefficient = DefaultWeightToFeeCoefficient;
+ type DefaultMinGasPrice = DefaultMinGasPrice;
+ type MaxXcmAllowedLocations = MaxXcmAllowedLocations;
+ type AppPromotionDailyRate = AppPromotionDailyRate;
+ type DayRelayBlocks = DayRelayBlocks;
+}
+
ord_parameter_types! {
pub const RootAccount: u64 = 777;
}
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCollators: u32 = 20;
pub const MaxAuthorities: u32 = 100_000;
pub const SlashRatio: Perbill = Perbill::one();
}
@@ -224,7 +250,6 @@
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
- type Currency = Balances;
type UpdateOrigin = EnsureSignedBy<RootAccount, u64>;
type PotId = PotId;
type MaxCollators = MaxCollators;
@@ -257,9 +282,6 @@
})
.collect::<Vec<_>>();
let collator_selection = collator_selection::GenesisConfig::<Test> {
- desired_collators: 5,
- license_bond: 10,
- kick_threshold: 10,
invulnerables,
};
let session = pallet_session::GenesisConfig::<Test> { keys };
pallets/collator-selection/src/tests.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/tests.rs
+++ b/pallets/collator-selection/src/tests.rs
@@ -36,8 +36,14 @@
assert_noop, assert_ok,
traits::{Currency, GenesisBuild, OnInitialize},
};
+use frame_system::RawOrigin;
use pallet_balances::Error as BalancesError;
use sp_runtime::traits::BadOrigin;
+use pallet_configuration::{
+ CollatorSelectionDesiredCollatorsOverride as DesiredCollators,
+ CollatorSelectionKickThresholdOverride as KickThreshold,
+ CollatorSelectionLicenseBondOverride as LicenseBond,
+};
fn get_license_and_onboard(account_id: <Test as frame_system::Config>::AccountId) {
assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(
@@ -51,8 +57,8 @@
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
- assert_eq!(CollatorSelection::desired_collators(), 5);
- assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(<DesiredCollators<Test>>::get(), 5);
+ assert_eq!(<LicenseBond<Test>>::get(), 10);
assert!(CollatorSelection::candidates().is_empty());
assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
@@ -122,18 +128,21 @@
fn set_desired_collators_works() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::desired_collators(), 5);
+ assert_eq!(<DesiredCollators<Test>>::get(), 5);
// can set
- assert_ok!(CollatorSelection::set_desired_collators(
- RuntimeOrigin::signed(RootAccount::get()),
- 7
+ assert_ok!(Configuration::set_collator_selection_desired_collators(
+ RawOrigin::Root.into(),
+ Some(7)
));
- assert_eq!(CollatorSelection::desired_collators(), 7);
+ assert_eq!(<DesiredCollators<Test>>::get(), 7);
// rejects bad origin
assert_noop!(
- CollatorSelection::set_desired_collators(RuntimeOrigin::signed(1), 8),
+ Configuration::set_collator_selection_desired_collators(
+ RuntimeOrigin::signed(1),
+ Some(8)
+ ),
BadOrigin
);
});
@@ -143,18 +152,18 @@
fn set_license_bond() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(<LicenseBond<Test>>::get(), 10);
// can set
- assert_ok!(CollatorSelection::set_license_bond(
- RuntimeOrigin::signed(RootAccount::get()),
- 7
+ assert_ok!(Configuration::set_collator_selection_license_bond(
+ RawOrigin::Root.into(),
+ Some(7)
));
- assert_eq!(CollatorSelection::license_bond(), 7);
+ assert_eq!(<LicenseBond<Test>>::get(), 7);
// rejects bad origin.
assert_noop!(
- CollatorSelection::set_license_bond(RuntimeOrigin::signed(1), 8),
+ Configuration::set_collator_selection_license_bond(RuntimeOrigin::signed(1), Some(8)),
BadOrigin
);
});
@@ -179,7 +188,7 @@
fn cannot_onboard_candidate_if_too_many() {
new_test_ext().execute_with(|| {
// reset desired candidates
- <crate::DesiredCollators<Test>>::put(0);
+ <pallet_configuration::CollatorSelectionDesiredCollatorsOverride<Test>>::put(0);
// can still get a license.
assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(4)));
@@ -191,7 +200,7 @@
);
// reset desired candidates to invulnerables + 1
- <crate::DesiredCollators<Test>>::put(3);
+ <pallet_configuration::CollatorSelectionDesiredCollatorsOverride<Test>>::put(3);
assert_ok!(CollatorSelection::onboard(RuntimeOrigin::signed(4)));
// but no more.
@@ -236,7 +245,7 @@
new_test_ext().execute_with(|| {
// can add 3 as candidate
get_license_and_onboard(3);
- assert_eq!(CollatorSelection::licenses(3), 10);
+ assert_eq!(CollatorSelection::license_deposit_of(3), 10);
assert_eq!(CollatorSelection::candidates(), vec![3]);
assert_eq!(CollatorSelection::last_authored_block(3), 10);
assert_eq!(Balances::free_balance(3), 90);
@@ -257,8 +266,8 @@
fn becoming_candidate_works() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::desired_collators(), 5);
- assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(<DesiredCollators<Test>>::get(), 5);
+ assert_eq!(<LicenseBond<Test>>::get(), 10);
assert_eq!(CollatorSelection::candidates(), Vec::new());
assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
@@ -315,7 +324,7 @@
));
// should exclude from candidates, but not revoke the license
assert_eq!(CollatorSelection::candidates(), vec![]);
- assert_eq!(CollatorSelection::licenses(3), 10);
+ assert_eq!(CollatorSelection::license_deposit_of(3), 10);
assert_eq!(Balances::free_balance(3), 90);
});
}
@@ -503,7 +512,7 @@
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]);
assert_eq!(CollatorSelection::candidates(), vec![4]);
- assert_eq!(CollatorSelection::kick_threshold(), 10);
+ assert_eq!(<KickThreshold<Test>>::get(), 10);
assert_eq!(CollatorSelection::last_authored_block(4), 20);
initialize_to_block(30);
@@ -524,9 +533,6 @@
let invulnerables = vec![1, 1];
let collator_selection = collator_selection::GenesisConfig::<Test> {
- desired_collators: 5,
- license_bond: 10,
- kick_threshold: 10,
invulnerables,
};
// collator selection must be initialized before session.
pallets/configuration/src/lib.rsdiffbeforeafterboth--- a/pallets/configuration/src/lib.rs
+++ b/pallets/configuration/src/lib.rs
@@ -36,15 +36,24 @@
mod pallet {
use super::*;
use frame_support::{
- traits::Get,
- pallet_prelude::{StorageValue, ValueQuery, DispatchResult, OptionQuery},
- BoundedVec,
+ traits::{Get, ReservableCurrency, Currency},
+ pallet_prelude::{StorageValue, ValueQuery, DispatchResult, IsType, OptionQuery},
+ BoundedVec, log,
};
- use frame_system::{pallet_prelude::OriginFor, ensure_root};
+ use frame_system::{pallet_prelude::OriginFor, ensure_root, Config as SystemConfig};
use xcm::v1::MultiLocation;
+ pub type BalanceOf<T> =
+ <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
+
#[pallet::config]
pub trait Config: frame_system::Config {
+ /// Overarching event type.
+ type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+
+ /// The currency mechanism.
+ type Currency: ReservableCurrency<Self::AccountId>;
+
#[pallet::constant]
type DefaultWeightToFeeCoefficient: Get<u32>;
@@ -57,6 +66,27 @@
type AppPromotionDailyRate: Get<Perbill>;
#[pallet::constant]
type DayRelayBlocks: Get<Self::BlockNumber>;
+
+ #[pallet::constant]
+ type DefaultCollatorSelectionMaxCollators: Get<u32>;
+ #[pallet::constant]
+ type DefaultCollatorSelectionLicenseBond: Get<BalanceOf<Self>>;
+ #[pallet::constant]
+ type DefaultCollatorSelectionKickThreshold: Get<Self::BlockNumber>;
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event<T: Config> {
+ NewDesiredCollators {
+ desired_collators: Option<u32>,
+ },
+ NewCollatorLicenseBond {
+ bond_cost: Option<BalanceOf<T>>,
+ },
+ NewCollatorKickThreshold {
+ length_in_blocks: Option<T::BlockNumber>,
+ },
}
#[pallet::error]
@@ -85,6 +115,27 @@
pub type AppPromomotionConfigurationOverride<T: Config> =
StorageValue<Value = AppPromotionConfiguration<T::BlockNumber>, QueryKind = ValueQuery>;
+ #[pallet::storage]
+ pub type CollatorSelectionDesiredCollatorsOverride<T: Config> = StorageValue<
+ Value = u32,
+ QueryKind = ValueQuery,
+ OnEmpty = T::DefaultCollatorSelectionMaxCollators,
+ >;
+
+ #[pallet::storage]
+ pub type CollatorSelectionLicenseBondOverride<T: Config> = StorageValue<
+ Value = BalanceOf<T>,
+ QueryKind = ValueQuery,
+ OnEmpty = T::DefaultCollatorSelectionLicenseBond,
+ >;
+
+ #[pallet::storage]
+ pub type CollatorSelectionKickThresholdOverride<T: Config> = StorageValue<
+ Value = T::BlockNumber,
+ QueryKind = ValueQuery,
+ OnEmpty = T::DefaultCollatorSelectionKickThreshold,
+ >;
+
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(T::DbWeight::get().writes(1))]
@@ -144,6 +195,55 @@
Ok(())
}
+
+ #[pallet::weight(T::DbWeight::get().writes(1))]
+ pub fn set_collator_selection_desired_collators(
+ origin: OriginFor<T>,
+ max: Option<u32>,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ if let Some(max) = max {
+ // we trust origin calls, this is just a for more accurate benchmarking
+ if max > T::DefaultCollatorSelectionMaxCollators::get() {
+ log::warn!("max > T::DefaultCollatorSelectionMaxCollators; you might need to run benchmarks again");
+ }
+ <CollatorSelectionDesiredCollatorsOverride<T>>::set(max);
+ } else {
+ <CollatorSelectionDesiredCollatorsOverride<T>>::kill();
+ }
+ Self::deposit_event(Event::NewDesiredCollators { desired_collators: max });
+ Ok(())
+ }
+
+ #[pallet::weight(T::DbWeight::get().writes(1))]
+ pub fn set_collator_selection_license_bond(
+ origin: OriginFor<T>,
+ amount: Option<BalanceOf<T>>,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ if let Some(amount) = amount {
+ <CollatorSelectionLicenseBondOverride<T>>::set(amount);
+ } else {
+ <CollatorSelectionLicenseBondOverride<T>>::kill();
+ }
+ Self::deposit_event(Event::NewCollatorLicenseBond { bond_cost: amount });
+ Ok(())
+ }
+
+ #[pallet::weight(T::DbWeight::get().writes(1))]
+ pub fn set_collator_selection_kick_threshold(
+ origin: OriginFor<T>,
+ threshold: Option<T::BlockNumber>,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ if let Some(threshold) = threshold {
+ <CollatorSelectionKickThresholdOverride<T>>::set(threshold);
+ } else {
+ <CollatorSelectionKickThresholdOverride<T>>::kill();
+ }
+ Self::deposit_event(Event::NewCollatorKickThreshold { length_in_blocks: threshold });
+ Ok(())
+ }
}
#[pallet::pallet]
primitives/common/src/constants.rsdiffbeforeafterboth--- a/primitives/common/src/constants.rs
+++ b/primitives/common/src/constants.rs
@@ -46,6 +46,8 @@
pub const EXISTENTIAL_DEPOSIT: u128 = 0;
/// Amount of Balance reserved for candidate registration.
pub const GENESIS_LICENSE_BOND: u128 = 1_000_000_000_000 * UNIQUE;
+/// Amount of maximum collators for Collator Selection.
+pub const MAX_COLLATORS: u32 = 10;
/// How long a periodic session lasts in blocks.
pub const SESSION_LENGTH: BlockNumber = HOURS;
runtime/common/config/pallets/collator_selection.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/collator_selection.rs
+++ b/runtime/common/config/pallets/collator_selection.rs
@@ -17,14 +17,14 @@
use frame_support::{parameter_types, PalletId};
use frame_system::EnsureRoot;
use crate::{
- AccountId, BlockNumber, Runtime, RuntimeEvent, Balances, Aura, Session, SessionKeys,
- CollatorSelection, config::pallets::TreasuryAccountId,
+ AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeEvent, Aura, Session, SessionKeys,
+ CollatorSelection, Treasury,
+ config::pallets::{MaxCollators, SessionPeriod, TreasuryAccountId},
};
use sp_runtime::Perbill;
-use up_common::constants::*;
+use up_common::constants::{UNIQUE, MILLIUNIQUE};
parameter_types! {
- pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
pub const SessionOffset: BlockNumber = 0;
}
@@ -55,13 +55,11 @@
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCollators: u32 = 10;
pub const SlashRatio: Perbill = Perbill::from_percent(100);
}
impl pallet_collator_selection::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
- type Currency = Balances;
// We allow root only to execute privileged collator selection operations.
type UpdateOrigin = EnsureRoot<AccountId>;
type TreasuryAccountId = TreasuryAccountId;
runtime/common/config/pallets/mod.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -25,7 +25,7 @@
},
Runtime, RuntimeEvent, RuntimeCall, Balances,
};
-use frame_support::traits::{ConstU32, ConstU64};
+use frame_support::traits::{ConstU32, ConstU64, ConstU128};
use up_common::{
types::{AccountId, Balance, BlockNumber},
constants::*,
@@ -104,11 +104,20 @@
parameter_types! {
pub AppPromotionDailyRate: Perbill = Perbill::from_rational(5u32, 10_000);
+ pub const MaxCollators: u32 = MAX_COLLATORS;
+ pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
pub const DayRelayBlocks: BlockNumber = RELAY_DAYS;
}
+
impl pallet_configuration::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type Currency = Balances;
type DefaultWeightToFeeCoefficient = ConstU32<{ up_common::constants::WEIGHT_TO_FEE_COEFF }>;
type DefaultMinGasPrice = ConstU64<{ up_common::constants::MIN_GAS_PRICE }>;
+ type DefaultCollatorSelectionMaxCollators = MaxCollators;
+ type DefaultCollatorSelectionKickThreshold = SessionPeriod;
+ type DefaultCollatorSelectionLicenseBond =
+ ConstU128<{ up_common::constants::GENESIS_LICENSE_BOND }>;
type MaxXcmAllowedLocations = ConstU32<16>;
type AppPromotionDailyRate = AppPromotionDailyRate;
type DayRelayBlocks = DayRelayBlocks;
runtime/common/construct_runtime/mod.rsdiffbeforeafterboth--- a/runtime/common/construct_runtime/mod.rs
+++ b/runtime/common/construct_runtime/mod.rs
@@ -69,7 +69,7 @@
// #[runtimes(opal)]
// Scheduler: pallet_unique_scheduler_v2::{Pallet, Call, Storage, Event<T>} = 62,
- Configuration: pallet_configuration::{Pallet, Call, Storage} = 63,
+ Configuration: pallet_configuration::{Pallet, Call, Storage, Event<T>} = 63,
Charging: pallet_charge_transaction::{Pallet, Call, Storage } = 64,
// ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage} = 65,
runtime/common/maintenance.rsdiffbeforeafterboth--- a/runtime/common/maintenance.rs
+++ b/runtime/common/maintenance.rs
@@ -85,6 +85,12 @@
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
}
+ #[cfg(feature = "collator-selection")]
+ RuntimeCall::CollatorSelection(_)
+ | RuntimeCall::Authorship(_)
+ | RuntimeCall::Session(_)
+ | RuntimeCall::Identity(_) => Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
+
#[cfg(feature = "pallet-test-utils")]
RuntimeCall::TestUtils(_) => Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
@@ -110,7 +116,7 @@
) -> TransactionValidity {
if Maintenance::is_enabled() {
match call {
- RuntimeCall::EVM(_) | RuntimeCall::Ethereum(_) | RuntimeCall::EvmMigration(_) => {
+ RuntimeCall::EVM(_) | RuntimeCall::Ethereum(_) | RuntimeCall::DataManagement(_) => {
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
}
_ => Ok(ValidTransaction::default()),
runtime/common/mod.rsdiffbeforeafterboth--- a/runtime/common/mod.rs
+++ b/runtime/common/mod.rs
@@ -186,13 +186,9 @@
#[cfg(feature = "collator-selection")]
{
use frame_support::{BoundedVec, storage::migration};
- use sp_runtime::{
- traits::{OpaqueKeys, Saturating},
- RuntimeAppPublic,
- };
+ use sp_runtime::{traits::OpaqueKeys, RuntimeAppPublic};
use pallet_session::SessionManager;
- use up_common::constants::{GENESIS_LICENSE_BOND, SESSION_LENGTH};
- use crate::config::pallets::collator_selection::MaxCollators;
+ use crate::config::pallets::MaxCollators;
let mut weight = <Runtime as frame_system::Config>::DbWeight::get().reads(1);
@@ -241,9 +237,6 @@
.expect("Existing collators/invulnerables are more than MaxCollators");
<pallet_collator_selection::Invulnerables<Runtime>>::put(bounded_invulnerables);
- <pallet_collator_selection::KickThreshold<Runtime>>::put(SESSION_LENGTH);
- <pallet_collator_selection::DesiredCollators<Runtime>>::put(MaxCollators::get());
- <pallet_collator_selection::LicenseBond<Runtime>>::put(GENESIS_LICENSE_BOND);
let keys = invulnerables
.into_iter()
runtime/common/tests/mod.rsdiffbeforeafterboth--- a/runtime/common/tests/mod.rs
+++ b/runtime/common/tests/mod.rs
@@ -64,9 +64,6 @@
let cfg = GenesisConfig {
collator_selection: CollatorSelectionConfig {
- desired_collators: 2,
- license_bond: 10,
- kick_threshold: 10,
invulnerables,
},
session: SessionConfig { keys },
tests/src/collatorSelection.seqtest.tsdiffbeforeafterboth--- a/tests/src/collatorSelection.seqtest.ts
+++ b/tests/src/collatorSelection.seqtest.ts
@@ -17,8 +17,6 @@
import {IKeyringPair} from '@polkadot/types/types';
import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from './util';
-const MAX_INVULNERABLES = 10;
-
async function resetInvulnerables() {
await usingPlaygrounds(async (helper, privateKey) => {
const superuser = await privateKey('//Alice');
@@ -31,7 +29,7 @@
let nonce = await helper.chain.getNonce(alice.address);
// In case there are too many invulnerables already, remove some of them, leaving space for Alice and Bob.
- if (invulnerables.length + 2 >= MAX_INVULNERABLES) {
+ if (invulnerables.length + 2 >= helper.collatorSelection.maxCollators()) {
await Promise.all([
helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()], true, {nonce: nonce++}),
helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()], true, {nonce: nonce++}),
@@ -409,7 +407,7 @@
// 28 non-functioning collators, teehee.
const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;
- const invulnerablesUntilLimit = MAX_INVULNERABLES - invulnerablesLength;
+ const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength;
const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);
const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);
tests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -65,6 +65,7 @@
arrange: ArrangeGroup;
wait: WaitGroup;
admin: AdminGroup;
+ session: SessionGroup;
testUtils: TestUtilGroup;
constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
@@ -75,6 +76,7 @@
this.wait = new WaitGroup(this);
this.admin = new AdminGroup(this);
this.testUtils = new TestUtilGroup(this);
+ this.session = new SessionGroup(this);
}
async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
@@ -456,14 +458,14 @@
console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`
+ ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
- const expectedSessionIndex = await this.helper.session.getIndex() + sessionCount;
+ const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;
let currentSessionIndex = -1;
while (currentSessionIndex < expectedSessionIndex) {
// eslint-disable-next-line no-async-promise-executor
currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {
await this.newBlocks(1);
- const res = this.helper.session.getIndex();
+ const res = await (this.helper as DevUniqueHelper).session.getIndex();
resolve(res);
}), blockTimeout, 'The chain has stopped producing blocks!');
}
@@ -552,6 +554,36 @@
}
}
+class SessionGroup {
+ helper: ChainHelperBase;
+
+ constructor(helper: ChainHelperBase) {
+ this.helper = helper;
+ }
+
+ //todo:collator documentation
+ async getIndex(): Promise<number> {
+ return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();
+ }
+
+ newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {
+ return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);
+ }
+
+ setOwnKeys(signer: TSigner, key: string) {
+ return this.helper.executeExtrinsic(
+ signer,
+ 'api.tx.session.setKeys',
+ [key, '0x0'],
+ true,
+ );
+ }
+
+ setOwnKeysFromAddress(signer: TSigner) {
+ return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));
+ }
+}
+
class TestUtilGroup {
helper: DevUniqueHelper;
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -45,7 +45,6 @@
import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';
import type {Vec} from '@polkadot/types-codec';
import {FrameSystemEventRecord} from '@polkadot/types/lookup';
-import {DevUniqueHelper} from './unique.dev';
export class CrossAccountId implements ICrossAccountId {
Substrate?: TSubstrateAccount;
@@ -376,7 +375,6 @@
children: ChainHelperBase[];
address: AddressGroup;
chain: ChainGroup;
- session: SessionGroup;
constructor(logger?: ILogger, helperBase?: any) {
this.helperBase = helperBase;
@@ -392,7 +390,6 @@
this.children = [];
this.address = new AddressGroup(this);
this.chain = new ChainGroup(this);
- this.session = new SessionGroup(this);
}
clone(helperCls: ChainHelperBaseConstructor, options: {[key: string]: any} = {}) {
@@ -2642,30 +2639,6 @@
blocksNum,
options,
}) as T;
- }
-}
-
-class SessionGroup extends HelperGroup<ChainHelperBase> {
- //todo:collator documentation
- async getIndex(): Promise<number> {
- return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();
- }
-
- newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {
- return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);
- }
-
- setOwnKeys(signer: TSigner, key: string) {
- return this.helper.executeExtrinsic(
- signer,
- 'api.tx.session.setKeys',
- [key, '0x0'],
- true,
- );
- }
-
- setOwnKeysFromAddress(signer: TSigner) {
- return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));
}
}
@@ -2683,12 +2656,21 @@
return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
}
+ /** and also total max invulnerables */
+ maxCollators(): number {
+ return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);
+ }
+
+ async getDesiredCollators(): Promise<number> {
+ return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();
+ }
+
setLicenseBond(signer: TSigner, amount: bigint) {
- return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.setLicenseBond', [amount]);
+ return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);
}
async getLicenseBond(): Promise<bigint> {
- return (await this.helper.callRpc('api.query.collatorSelection.licenseBond')).toBigInt();
+ return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();
}
obtainLicense(signer: TSigner) {
@@ -2704,7 +2686,7 @@
}
async hasLicense(address: string): Promise<bigint> {
- return (await this.helper.callRpc('api.query.collatorSelection.licenses', [address])).toBigInt();
+ return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();
}
onboard(signer: TSigner) {