Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Add Ability to Add/Remove Invulnerable Collators #2596

Merged
merged 23 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pallets/collator-selection/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
authors = ["Anonymous"]
description = "Simple staking pallet with a fixed stake."
authors = ["Parity Technologies <admin@parity.io>"]
description = "Simple pallet to select collators for a parachain."
edition = "2021"
homepage = "https://substrate.io"
license = "Apache-2.0"
Expand Down
48 changes: 46 additions & 2 deletions pallets/collator-selection/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,61 @@ benchmarks! {
where_clause { where T: pallet_authorship::Config + session::Config }

set_invulnerables {
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let b in 1 .. T::MaxInvulnerables::get();
let new_invulnerables = register_validators::<T>(b);
let mut sorted_new_invulnerables = new_invulnerables.clone();
sorted_new_invulnerables.sort();
}: {
assert_ok!(
// call the function with the unsorted list
<CollatorSelection<T>>::set_invulnerables(origin, new_invulnerables.clone())
);
}
verify {
// assert that it comes out sorted
assert_last_event::<T>(Event::NewInvulnerables{invulnerables: sorted_new_invulnerables}.into());
}

add_invulnerable {
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
// we're going to add one, so need one less than max set as invulnerables to start
let b in 1 .. T::MaxInvulnerables::get() - 1;
let mut invulnerables = register_validators::<T>(b);
invulnerables.sort();
let invulnerables: frame_support::BoundedVec<_, T::MaxInvulnerables> = frame_support::BoundedVec::try_from(invulnerables).unwrap();
<Invulnerables<T>>::put(invulnerables);

// now let's set up a new one to add
let (new, keys) = validator::<T>(b + 1);
<session::Pallet<T>>::set_keys(RawOrigin::Signed(new.clone()).into(), keys, Vec::new()).unwrap();
}: {
assert_ok!(
<CollatorSelection<T>>::set_invulnerables(origin, new_invulnerables.clone())
<CollatorSelection<T>>::add_invulnerable(origin, new.clone())
);
}
verify {
assert_last_event::<T>(Event::InvulnerableAdded{account_id: new}.into());
}

remove_invulnerable {
let origin =
T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let b in 1 .. T::MaxInvulnerables::get();
let mut invulnerables = register_validators::<T>(b);
invulnerables.sort();
let invulnerables: frame_support::BoundedVec<_, T::MaxInvulnerables> = frame_support::BoundedVec::try_from(invulnerables).unwrap();
<Invulnerables<T>>::put(invulnerables);
let to_remove = <Invulnerables<T>>::get().first().unwrap().clone();
}: {
assert_ok!(
<CollatorSelection<T>>::remove_invulnerable(origin, to_remove.clone())
);
}
verify {
assert_last_event::<T>(Event::NewInvulnerables{invulnerables: new_invulnerables}.into());
assert_last_event::<T>(Event::InvulnerableRemoved{account_id: to_remove}.into());
}

set_desired_candidates {
Expand Down
108 changes: 92 additions & 16 deletions pallets/collator-selection/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod migration;
pub mod weights;

const LOG_TARGET: &str = "runtime::collator-selection";

#[frame_support::pallet]
pub mod pallet {
pub use crate::weights::WeightInfo;
Expand All @@ -95,6 +98,9 @@ pub mod pallet {
use sp_runtime::traits::Convert;
use sp_staking::SessionIndex;

/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);

type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;

Expand Down Expand Up @@ -165,9 +171,10 @@ pub mod pallet {
}

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);

/// The invulnerable, fixed collators.
/// The invulnerable, fixed collators. This list must be sorted.
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
#[pallet::storage]
#[pallet::getter(fn invulnerables)]
pub type Invulnerables<T: Config> =
Expand Down Expand Up @@ -230,14 +237,16 @@ pub mod pallet {
"duplicate invulnerables in genesis."
);

let bounded_invulnerables =
let mut bounded_invulnerables =
BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())
.expect("genesis invulnerables are more than T::MaxInvulnerables");
assert!(
T::MaxCandidates::get() >= self.desired_candidates,
"genesis desired_candidates are more than T::MaxCandidates",
);

bounded_invulnerables.sort();

<DesiredCandidates<T>>::put(self.desired_candidates);
<CandidacyBond<T>>::put(self.candidacy_bond);
<Invulnerables<T>>::put(bounded_invulnerables);
Expand All @@ -247,35 +256,41 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// New Invulnerables were set.
NewInvulnerables { invulnerables: Vec<T::AccountId> },
/// A new Invulnerable was added.
InvulnerableAdded { account_id: T::AccountId },
/// An Invulnverable was removed.
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
InvulnerableRemoved { account_id: T::AccountId },
/// The number of desired candidates was set.
NewDesiredCandidates { desired_candidates: u32 },
/// The candidacy bond was set.
NewCandidacyBond { bond_amount: BalanceOf<T> },
/// A new candidate joined.
CandidateAdded { account_id: T::AccountId, deposit: BalanceOf<T> },
/// A candidate was removed.
CandidateRemoved { account_id: T::AccountId },
}

// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// Too many candidates
/// The pallet has too many candidates.
TooManyCandidates,
/// Too few candidates
/// Leaving would result in too few candidates.
TooFewCandidates,
/// Unknown error
Unknown,
/// Permission issue
Permission,
/// User is already a candidate
/// Account is already a candidate.
AlreadyCandidate,
/// User is not a candidate
/// Account is not a candidate.
NotCandidate,
/// Too many invulnerables
/// There are too many Invulnerables.
TooManyInvulnerables,
/// User is already an Invulnerable
/// Account is already an Invulnerable.
AlreadyInvulnerable,
/// Account has no associated validator ID
/// Account is not an Invulnerable.
NotInvulnerable,
/// Account has no associated validator ID.
NoAssociatedValidatorId,
/// Validator ID is not yet registered
/// Validator ID is not yet registered.
ValidatorNotRegistered,
}

Expand All @@ -292,7 +307,7 @@ pub mod pallet {
new: Vec<T::AccountId>,
) -> DispatchResultWithPostInfo {
T::UpdateOrigin::ensure_origin(origin)?;
let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)
let mut bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)
.map_err(|_| Error::<T>::TooManyInvulnerables)?;

// check if the invulnerables have associated validator keys before they are set
Expand All @@ -305,6 +320,9 @@ pub mod pallet {
);
}

// Invulnerables must be sorted for removal.
bounded_invulnerables.sort();
Ank4n marked this conversation as resolved.
Show resolved Hide resolved

<Invulnerables<T>>::put(&bounded_invulnerables);
Self::deposit_event(Event::NewInvulnerables {
invulnerables: bounded_invulnerables.to_vec(),
Expand Down Expand Up @@ -406,6 +424,64 @@ pub mod pallet {

Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
}

/// Add a new account `who` to the list of `Invulnerables` collators.
///
/// The origin for this call must be the `UpdateOrigin`.
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::add_invulnerable(T::MaxInvulnerables::get() - 1))]
pub fn add_invulnerable(
origin: OriginFor<T>,
who: T::AccountId,
) -> DispatchResultWithPostInfo {
T::UpdateOrigin::ensure_origin(origin)?;
let invulnerables = Self::invulnerables().to_vec();

// ensure `who` is not already invulnerable
ensure!(!invulnerables.contains(&who), Error::<T>::AlreadyInvulnerable);

// ensure `who` has registred a validator key
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
let validator_key = T::ValidatorIdOf::convert(who.clone())
.ok_or(Error::<T>::NoAssociatedValidatorId)?;
ensure!(
T::ValidatorRegistration::is_registered(&validator_key),
Error::<T>::ValidatorNotRegistered
);

<Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
invulnerables
.try_push(who.clone())
.map_err(|_| Error::<T>::TooManyInvulnerables)?;
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
invulnerables.sort();
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
})?;

Self::deposit_event(Event::InvulnerableAdded { account_id: who });
Ok(().into())
}

/// Remove an account `who` from the list of `Invulnerables` collators. `Invulnerables` must
/// be sorted.
///
/// The origin for this call must be the `UpdateOrigin`.
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxInvulnerables::get()))]
pub fn remove_invulnerable(
origin: OriginFor<T>,
who: T::AccountId,
) -> DispatchResultWithPostInfo {
T::UpdateOrigin::ensure_origin(origin)?;

<Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
let pos =
invulnerables.binary_search(&who).map_err(|_| Error::<T>::NotInvulnerable)?;
invulnerables.remove(pos);
Ok(())
})?;

Self::deposit_event(Event::InvulnerableRemoved { account_id: who });
Ok(().into())
}
}

impl<T: Config> Pallet<T> {
Expand Down
103 changes: 103 additions & 0 deletions pallets/collator-selection/src/migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright Parity Technologies (UK) Ltd.
// This file is part of Polkadot.

// Polkadot 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.

// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.

//! A module that is responsible for migration of storage for Collator Selection.

use super::*;
use frame_support::{log, traits::OnRuntimeUpgrade};

/// Version 1 Migration
/// This migration ensures that any existing `Invulnerables` storage lists are sorted.
pub mod v1 {
use super::*;
#[cfg(feature = "try-runtime")]
use frame_support::inherent::Vec;
use frame_support::{pallet_prelude::*, weights::Weight};
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved

pub struct MigrateToV1<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
fn on_runtime_upgrade() -> Weight {
let current_version = Pallet::<T>::current_storage_version();
let onchain_version = Pallet::<T>::on_chain_storage_version();
if onchain_version == 0 && current_version == 1 {
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
let invulnerables_len = Invulnerables::<T>::get().to_vec().len();
<Invulnerables<T>>::mutate(|invulnerables| {
invulnerables.sort();
});

current_version.put::<Pallet<T>>();
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
log::info!(
target: LOG_TARGET,
"Sorted {} Invulnerables, upgraded storage to version {:?}",
invulnerables_len,
current_version
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
);
// Similar complexity to `set_invulnerables` (put storage value)
// Plus 1 read for length, 1 read for `onchain_version`, 1 write to put version
T::WeightInfo::set_invulnerables(invulnerables_len as u32)
.saturating_add(T::DbWeight::get().reads_writes(2, 1))
} else {
log::info!(
target: LOG_TARGET,
"Migration did not execute. This probably should be removed"
);
T::DbWeight::get().reads(1)
}
}

#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, &'static str> {
frame_support::ensure!(
Pallet::<T>::on_chain_storage_version() == 0,
"must upgrade linearly"
);
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
let number_of_invulnerables = Invulnerables::<T>::get().to_vec().len();
Ok((number_of_invulnerables as u32).encode())
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(number_of_invulnerables: Vec<u8>) -> Result<(), &'static str> {
let stored_invulnerables = Invulnerables::<T>::get().to_vec();
let mut sorted_invulnerables = stored_invulnerables.clone();
sorted_invulnerables.sort();
assert_eq!(
stored_invulnerables, sorted_invulnerables,
"after migration, the stored invulnerables should be sorted"
);

let number_of_invulnerables: u32 = Decode::decode(
&mut number_of_invulnerables.as_slice(),
)
.expect("the state parameter should be something that was generated by pre_upgrade");
let stored_invulnerables_len = stored_invulnerables.len() as u32;
assert_eq!(
number_of_invulnerables, stored_invulnerables_len,
"after migration, there should be the same number of invulnerables"
);

let current_version = Pallet::<T>::current_storage_version();
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
let onchain_version = Pallet::<T>::on_chain_storage_version();
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved

frame_support::ensure!(current_version == 1, "must_upgrade");
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(
current_version, onchain_version,
"after migration, the current_version and onchain_version should be the same"
);
joepetrowski marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}
}
}
2 changes: 1 addition & 1 deletion pallets/collator-selection/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ impl Config for Test {
pub fn new_test_ext() -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
let invulnerables = vec![1, 2];
let invulnerables = vec![2, 1]; // unsorted

let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)];
let keys = balances
Expand Down
Loading