min collator check (#498)

* min collator check

* change statemint/mine min candidates

* Ci pass

* Update pallets/collator-selection/src/lib.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Update pallets/collator-selection/src/lib.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Apply suggestions from code review

* build fixes

* add error messages to errors

* added validator register check

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
JesseAbram
2021-06-22 18:59:14 +02:00
committed by GitHub
parent 69ca715506
commit 5aca3b54d8
7 changed files with 144 additions and 6 deletions
+40 -4
View File
@@ -41,6 +41,9 @@
//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve //! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve
//! manner. //! manner.
//! //!
//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates
//! fall below MinCandidates. This is for potential disaster recovery scenarios.
//!
//! ### Rewards //! ### Rewards
//! //!
//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the //! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the
@@ -76,7 +79,7 @@ pub mod pallet {
pallet_prelude::*, pallet_prelude::*,
inherent::Vec, inherent::Vec,
traits::{ traits::{
Currency, ReservableCurrency, EnsureOrigin, ExistenceRequirement::KeepAlive, Currency, ReservableCurrency, EnsureOrigin, ExistenceRequirement::KeepAlive, ValidatorRegistration
}, },
PalletId, PalletId,
}; };
@@ -89,6 +92,7 @@ pub mod pallet {
}, },
weights::DispatchClass, weights::DispatchClass,
}; };
use sp_runtime::traits::Convert;
use core::ops::Div; use core::ops::Div;
use pallet_session::SessionManager; use pallet_session::SessionManager;
use sp_staking::SessionIndex; use sp_staking::SessionIndex;
@@ -127,6 +131,12 @@ pub mod pallet {
/// This does not take into account the invulnerables. /// This does not take into account the invulnerables.
type MaxCandidates: Get<u32>; type MaxCandidates: Get<u32>;
/// Minimum number of candidates that we should have. This is used for disaster recovery.
///
/// This does not take into account the invulnerables.
type MinCandidates: Get<u32>;
/// Maximum number of invulnerables. /// Maximum number of invulnerables.
/// ///
/// Used only for benchmarking. /// Used only for benchmarking.
@@ -135,6 +145,18 @@ pub mod pallet {
// Will be kicked if block is not produced in threshold. // Will be kicked if block is not produced in threshold.
type KickThreshold: Get<Self::BlockNumber>; type KickThreshold: Get<Self::BlockNumber>;
/// A stable ID for a validator.
type ValidatorId: Member + Parameter;
/// A conversion from account ID to validator ID.
///
/// Its cost must be at most one storage read.
type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;
/// Validate a user is registered
type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;
/// The weight information of this pallet. /// The weight information of this pallet.
type WeightInfo: WeightInfo; type WeightInfo: WeightInfo;
} }
@@ -238,13 +260,24 @@ pub mod pallet {
// Errors inform users that something went wrong. // Errors inform users that something went wrong.
#[pallet::error] #[pallet::error]
pub enum Error<T> { pub enum Error<T> {
/// Too many candidates
TooManyCandidates, TooManyCandidates,
/// Too few candidates
TooFewCandidates,
/// Unknown error
Unknown, Unknown,
/// Permission issue
Permission, Permission,
/// User is already a candidate
AlreadyCandidate, AlreadyCandidate,
/// User is not a candidate
NotCandidate, NotCandidate,
/// User is already an Invulnerable
AlreadyInvulnerable, AlreadyInvulnerable,
InvalidProof, /// Account has no associated validator ID
NoAssociatedValidatorId,
/// Validator ID is not yet registered
ValidatorNotRegistered
} }
#[pallet::hooks] #[pallet::hooks]
@@ -300,6 +333,9 @@ pub mod pallet {
ensure!((length as u32) < Self::desired_candidates(), Error::<T>::TooManyCandidates); ensure!((length as u32) < Self::desired_candidates(), Error::<T>::TooManyCandidates);
ensure!(!Self::invulnerables().contains(&who), Error::<T>::AlreadyInvulnerable); ensure!(!Self::invulnerables().contains(&who), Error::<T>::AlreadyInvulnerable);
let validator_key = T::ValidatorIdOf::convert(who.clone()).ok_or(Error::<T>::NoAssociatedValidatorId)?;
ensure!(T::ValidatorRegistration::is_registered(&validator_key), Error::<T>::ValidatorNotRegistered);
let deposit = Self::candidacy_bond(); let deposit = Self::candidacy_bond();
// First authored block is current block plus kick threshold to handle session delay // First authored block is current block plus kick threshold to handle session delay
let incoming = CandidateInfo { who: who.clone(), deposit }; let incoming = CandidateInfo { who: who.clone(), deposit };
@@ -323,7 +359,7 @@ pub mod pallet {
#[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]
pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo { pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?; let who = ensure_signed(origin)?;
ensure!(Self::candidates().len() as u32 > T::MinCandidates::get(), Error::<T>::TooFewCandidates);
let current_count = Self::try_remove_candidate(&who)?; let current_count = Self::try_remove_candidate(&who)?;
Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
@@ -365,7 +401,7 @@ pub mod pallet {
let new_candidates = candidates.into_iter().filter_map(|c| { let new_candidates = candidates.into_iter().filter_map(|c| {
let last_block = <LastAuthoredBlock<T>>::get(c.who.clone()); let last_block = <LastAuthoredBlock<T>>::get(c.who.clone());
let since_last = now.saturating_sub(last_block); let since_last = now.saturating_sub(last_block);
if since_last < kick_threshold { if since_last < kick_threshold || Self::candidates().len() as u32 <= T::MinCandidates::get() {
Some(c.who) Some(c.who)
} else { } else {
let outcome = Self::try_remove_candidate(&c.who); let outcome = Self::try_remove_candidate(&c.who);
+17 -1
View File
@@ -18,7 +18,7 @@ use crate as collator_selection;
use sp_core::H256; use sp_core::H256;
use frame_support::{ use frame_support::{
parameter_types, ord_parameter_types, parameter_types, ord_parameter_types,
traits::{FindAuthor, GenesisBuild}, traits::{FindAuthor, GenesisBuild, ValidatorRegistration},
PalletId PalletId
}; };
use sp_runtime::{ use sp_runtime::{
@@ -188,6 +188,18 @@ parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake"); pub const PotId: PalletId = PalletId(*b"PotStake");
pub const MaxCandidates: u32 = 20; pub const MaxCandidates: u32 = 20;
pub const MaxInvulnerables: u32 = 20; pub const MaxInvulnerables: u32 = 20;
pub const MinCandidates: u32 = 1;
}
pub struct IsRegistered;
impl ValidatorRegistration<u64> for IsRegistered {
fn is_registered(id: &u64) -> bool {
if *id == 7u64 {
false
} else {
true
}
}
} }
impl Config for Test { impl Config for Test {
@@ -196,8 +208,12 @@ impl Config for Test {
type UpdateOrigin = EnsureSignedBy<RootAccount, u64>; type UpdateOrigin = EnsureSignedBy<RootAccount, u64>;
type PotId = PotId; type PotId = PotId;
type MaxCandidates = MaxCandidates; type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables; type MaxInvulnerables = MaxInvulnerables;
type KickThreshold = Period; type KickThreshold = Period;
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = IdentityCollator;
type ValidatorRegistration = IsRegistered;
type WeightInfo = (); type WeightInfo = ();
} }
@@ -106,6 +106,21 @@ fn cannot_register_candidate_if_too_many() {
}) })
} }
#[test]
fn cannot_unregister_candidate_if_too_few() {
new_test_ext().execute_with(|| {
// reset desired candidates:
<crate::DesiredCandidates<Test>>::put(1);
assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
// can not remove too few
assert_noop!(
CollatorSelection::leave_intent(Origin::signed(4)),
Error::<Test>::TooFewCandidates,
);
})
}
#[test] #[test]
fn cannot_register_as_candidate_if_invulnerable() { fn cannot_register_as_candidate_if_invulnerable() {
new_test_ext().execute_with(|| { new_test_ext().execute_with(|| {
@@ -119,6 +134,17 @@ fn cannot_register_as_candidate_if_invulnerable() {
}) })
} }
#[test]
fn cannot_register_as_candidate_if_keys_not_registered() {
new_test_ext().execute_with(|| {
// can't 7 because keys not registered.
assert_noop!(
CollatorSelection::register_as_candidate(Origin::signed(7)),
Error::<Test>::ValidatorNotRegistered
);
})
}
#[test] #[test]
fn cannot_register_dupe_candidate() { fn cannot_register_dupe_candidate() {
new_test_ext().execute_with(|| { new_test_ext().execute_with(|| {
@@ -184,6 +210,10 @@ fn leave_intent() {
assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
assert_eq!(Balances::free_balance(3), 90); assert_eq!(Balances::free_balance(3), 90);
// register too so can leave above min candidates
assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(5)));
assert_eq!(Balances::free_balance(5), 90);
// cannot leave if not candidate. // cannot leave if not candidate.
assert_noop!( assert_noop!(
CollatorSelection::leave_intent(Origin::signed(4)), CollatorSelection::leave_intent(Origin::signed(4)),
@@ -318,6 +348,34 @@ fn kick_mechanism() {
}); });
} }
#[test]
fn should_not_kick_mechanism_too_few() {
new_test_ext().execute_with(|| {
// add a new collator
assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(5)));
initialize_to_block(10);
assert_eq!(CollatorSelection::candidates().len(), 2);
initialize_to_block(20);
assert_eq!(SessionChangeBlock::get(), 20);
// 4 authored this block, 5 gets to stay too few 3 was kicked
assert_eq!(CollatorSelection::candidates().len(), 1);
// 3 will be kicked after 1 session delay
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 5]);
let collator = CandidateInfo {
who: 5,
deposit: 10,
};
assert_eq!(CollatorSelection::candidates(), vec![collator]);
assert_eq!(CollatorSelection::last_authored_block(4), 20);
initialize_to_block(30);
// 3 gets kicked after 1 session delay
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 5]);
// kicked collator gets funds back
assert_eq!(Balances::free_balance(3), 100);
});
}
#[test] #[test]
#[should_panic = "duplicate invulnerables in genesis."] #[should_panic = "duplicate invulnerables in genesis."]
@@ -638,6 +638,7 @@ impl pallet_aura::Config for Runtime {
parameter_types! { parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake"); pub const PotId: PalletId = PalletId(*b"PotStake");
pub const MaxCandidates: u32 = 1000; pub const MaxCandidates: u32 = 1000;
pub const MinCandidates: u32 = 5;
pub const SessionLength: BlockNumber = 6 * HOURS; pub const SessionLength: BlockNumber = 6 * HOURS;
pub const MaxInvulnerables: u32 = 100; pub const MaxInvulnerables: u32 = 100;
} }
@@ -655,9 +656,13 @@ impl pallet_collator_selection::Config for Runtime {
type UpdateOrigin = CollatorSelectionUpdateOrigin; type UpdateOrigin = CollatorSelectionUpdateOrigin;
type PotId = PotId; type PotId = PotId;
type MaxCandidates = MaxCandidates; type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables; type MaxInvulnerables = MaxInvulnerables;
// should be a multiple of session or things will get inconsistent // should be a multiple of session or things will get inconsistent
type KickThreshold = Period; type KickThreshold = Period;
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = pallet_collator_selection::IdentityCollator;
type ValidatorRegistration = Session;
type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>; type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>;
} }
@@ -65,7 +65,7 @@ where
mod tests { mod tests {
use super::*; use super::*;
use frame_support::traits::FindAuthor; use frame_support::traits::FindAuthor;
use frame_support::{parameter_types, PalletId}; use frame_support::{parameter_types, PalletId, traits::ValidatorRegistration};
use frame_system::{limits, EnsureRoot}; use frame_system::{limits, EnsureRoot};
use polkadot_primitives::v1::AccountId; use polkadot_primitives::v1::AccountId;
use sp_core::H256; use sp_core::H256;
@@ -74,6 +74,7 @@ mod tests {
traits::{BlakeTwo256, IdentityLookup}, traits::{BlakeTwo256, IdentityLookup},
Perbill, Perbill,
}; };
use pallet_collator_selection::IdentityCollator;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>; type Block = frame_system::mocking::MockBlock<Test>;
@@ -145,10 +146,18 @@ mod tests {
} }
} }
pub struct IsRegistered;
impl ValidatorRegistration<AccountId> for IsRegistered {
fn is_registered(_id: &AccountId) -> bool {
true
}
}
parameter_types! { parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake"); pub const PotId: PalletId = PalletId(*b"PotStake");
pub const MaxCandidates: u32 = 20; pub const MaxCandidates: u32 = 20;
pub const MaxInvulnerables: u32 = 20; pub const MaxInvulnerables: u32 = 20;
pub const MinCandidates: u32 = 1;
} }
impl pallet_collator_selection::Config for Test { impl pallet_collator_selection::Config for Test {
@@ -157,7 +166,11 @@ mod tests {
type UpdateOrigin = EnsureRoot<AccountId>; type UpdateOrigin = EnsureRoot<AccountId>;
type PotId = PotId; type PotId = PotId;
type MaxCandidates = MaxCandidates; type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables; type MaxInvulnerables = MaxInvulnerables;
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = IdentityCollator;
type ValidatorRegistration = IsRegistered;
type KickThreshold = (); type KickThreshold = ();
type WeightInfo = (); type WeightInfo = ();
} }
@@ -579,6 +579,7 @@ impl pallet_aura::Config for Runtime {
parameter_types! { parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake"); pub const PotId: PalletId = PalletId(*b"PotStake");
pub const MaxCandidates: u32 = 1000; pub const MaxCandidates: u32 = 1000;
pub const MinCandidates: u32 = 5;
pub const SessionLength: BlockNumber = 6 * HOURS; pub const SessionLength: BlockNumber = 6 * HOURS;
pub const MaxInvulnerables: u32 = 100; pub const MaxInvulnerables: u32 = 100;
} }
@@ -596,9 +597,13 @@ impl pallet_collator_selection::Config for Runtime {
type UpdateOrigin = CollatorSelectionUpdateOrigin; type UpdateOrigin = CollatorSelectionUpdateOrigin;
type PotId = PotId; type PotId = PotId;
type MaxCandidates = MaxCandidates; type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables; type MaxInvulnerables = MaxInvulnerables;
// should be a multiple of session or things will get inconsistent // should be a multiple of session or things will get inconsistent
type KickThreshold = Period; type KickThreshold = Period;
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = pallet_collator_selection::IdentityCollator;
type ValidatorRegistration = Session;
type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>; type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>;
} }
@@ -576,6 +576,7 @@ impl pallet_aura::Config for Runtime {
parameter_types! { parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake"); pub const PotId: PalletId = PalletId(*b"PotStake");
pub const MaxCandidates: u32 = 1000; pub const MaxCandidates: u32 = 1000;
pub const MinCandidates: u32 = 1;
pub const SessionLength: BlockNumber = 6 * HOURS; pub const SessionLength: BlockNumber = 6 * HOURS;
pub const MaxInvulnerables: u32 = 100; pub const MaxInvulnerables: u32 = 100;
} }
@@ -586,9 +587,13 @@ impl pallet_collator_selection::Config for Runtime {
type UpdateOrigin = EnsureRoot<AccountId>; type UpdateOrigin = EnsureRoot<AccountId>;
type PotId = PotId; type PotId = PotId;
type MaxCandidates = MaxCandidates; type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables; type MaxInvulnerables = MaxInvulnerables;
// should be a multiple of session or things will get inconsistent // should be a multiple of session or things will get inconsistent
type KickThreshold = Period; type KickThreshold = Period;
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = pallet_collator_selection::IdentityCollator;
type ValidatorRegistration = Session;
type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>; type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>;
} }