diff --git a/pallets/runtime/tests/src/staking/mod.rs b/pallets/runtime/tests/src/staking/mod.rs index bc01ad7c59..b88f596431 100644 --- a/pallets/runtime/tests/src/staking/mod.rs +++ b/pallets/runtime/tests/src/staking/mod.rs @@ -5819,3 +5819,66 @@ fn test_bond_too_small() { assert_ok!(both(90, 91, MinimumBond::get() + 1)); }); } + +#[test] +fn chill_from_governance() { + ExtBuilder::default() + .validator_count(8) + .minimum_validator_count(1) + .build() + .execute_with(|| { + // 50 stash and 51 controller (corrected) + bond(50, 51, 500000); + let entity_id = Identity::get_identity(&50).unwrap(); + + // Add a new validator successfully. + bond_validator_with_intended_count(50, 51, 500000, Some(2)); + + assert_permissioned_identity_prefs!(entity_id, 2, 1); + + // Add other stash and controller to the same did. + add_secondary_key(50, 60); + add_secondary_key(50, 61); + + // Validate one more validator from the same entity. + // 60 stash and 61 controller. + bond_validator(60, 61, 500000); + assert_permissioned_identity_prefs!(entity_id, 2, 2); + + // Removes 50 and 60 from being validators + assert_ok!(Staking::chill_from_governance( + Origin::signed(2000), + entity_id, + vec![50, 60] + )); + + // No longer permissioned identity + assert_noop!( + Staking::chill_from_governance(Origin::signed(2000), entity_id, vec![50, 60]), + Error::::NotExists + ); + + // 70 stash and 71 controller + bond(70, 71, 500000); + let entity_id_2 = Identity::get_identity(&70).unwrap(); + + // Add a new validator successfully. + bond_validator_with_intended_count(70, 71, 500000, Some(2)); + + // Add other stash and controller to the same did. + add_secondary_key(70, 80); + add_secondary_key(70, 81); + + // Check keys that aren't joined with identity gives error + assert_noop!( + Staking::chill_from_governance(Origin::signed(2000), entity_id_2, vec![90, 95]), + Error::::NotStash + ); + + // Check key that is not GC gives error + assert_noop!( + Staking::chill_from_governance(Origin::signed(20), entity_id_2, vec![90, 95]), + BadOrigin + ); + }); +} diff --git a/pallets/staking/src/benchmarking.rs b/pallets/staking/src/benchmarking.rs index 0b6d99136c..39a47f7459 100644 --- a/pallets/staking/src/benchmarking.rs +++ b/pallets/staking/src/benchmarking.rs @@ -28,11 +28,15 @@ use polymesh_common_utilities::{ TestUtilsFn, }; use sp_runtime::traits::One; +use polymesh_primitives::{ + Permissions, +}; const SEED: u32 = 0; const MAX_SPANS: u32 = 100; const MAX_VALIDATORS: u32 = 1000; const MAX_SLASHES: u32 = 1000; const INIT_BALANCE: u32 = 10_000_000; +const MAX_STASHES: u32 = 100; macro_rules! whitelist_account { ($acc:expr) => { @@ -750,6 +754,50 @@ benchmarks! { verify { assert_eq!(Staking::::validator_count(), 12); } + + chill_from_governance { + let s in 1 .. MAX_STASHES; + let validator = create_funded_user::("caller", 2, 10000); + let validator_did = validator.did(); + let mut signatories = Vec::with_capacity(s as usize); + + // Increases the maximum number of validators + emulate_validator_setup::(1, 1000, Perbill::from_percent(60)); + assert_eq!(Staking::::validator_count(), 1000); + + // Add validator + Staking::::add_permissioned_validator(RawOrigin::Root.into(), validator_did, Some(MAX_STASHES)) + .expect("Failed to add permissioned validator"); + whitelist_account!(validator); + + for x in 0 .. s { + // Create stash key + let key = create_funded_user_without_did::("stash", x, 10000); + // Add key to signatories vec + signatories.push(key.account.clone()); + // Add key as secondary key to validator_did + Identity::::unsafe_join_identity(validator_did, Permissions::default(), key.account.clone()); + // Use key to bond + Staking::::bond(key.origin().into(), key.lookup(), 2_000_000u32.into(), RewardDestination::Staked).expect("Bond failed."); + // Create ValidatorPrefs + let validator_prefs = ValidatorPrefs { + commission: Perbill::from_percent(50), + ..Default::default() + }; + + whitelist_account!(key); + // Use stash key to validate + Staking::::validate(key.origin().into(), validator_prefs).expect("Validate fails"); + // Checks that the stash key is in validators storage + assert_eq!(>::contains_key(&key.account), true); + } + }: _(RawOrigin::Root, validator_did, signatories.clone()) + verify { + for key in signatories { + // Checks that the stash key has been removed from storage + assert_eq!(>::contains_key(&key), false); + } + } } #[cfg(test)] diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index df8162fdf6..d494fb0623 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -2656,6 +2656,27 @@ decl_module! { .map(|p| p.intended_count = new_intended_count) }) } + + + /// GC forcefully chills a validator. + /// Effects will be felt at the beginning of the next era. + /// And, it can be only called when [`EraElectionStatus`] is `Closed`. + /// + /// # Arguments + /// * origin which must be a GC. + /// * identity must be permissioned to run operator/validator nodes. + /// * stash_keys contains the secondary keys of the permissioned identity + /// + /// # Errors + /// * `BadOrigin` The origin was not a GC member. + /// * `CallNotAllowed` The call is not allowed at the given time due to restrictions of election period. + /// * `NotExists` Permissioned validator doesn't exist. + /// * `NotStash` Not a stash account for the permissioned identity. + #[weight = ::WeightInfo::chill_from_governance(stash_keys.len() as u32)] + pub fn chill_from_governance(origin, identity: IdentityId, stash_keys: Vec) -> DispatchResult { + Self::base_chill_from_governance(origin, identity, stash_keys) + } + } } @@ -3700,6 +3721,31 @@ impl Module { * T::ExpectedBlockTime::get().saturated_into::() } + fn base_chill_from_governance(origin: T::Origin, identity: IdentityId, stash_keys: Vec) -> DispatchResult { + // Checks that the era election status is closed. + ensure!(Self::era_election_status().is_closed(), Error::::CallNotAllowed); + // Required origin for removing a validator. + T::RequiredRemoveOrigin::ensure_origin(origin)?; + // Checks that the identity is allowed to run operator/validator nodes. + ensure!(Self::permissioned_identity(&identity).is_some(), Error::::NotExists); + + for key in &stash_keys { + let key_did = Identity::::get_identity(&key); + // Checks if the stash key identity is the same as the identity given. + ensure!(key_did == Some(identity), Error::::NotStash); + // Checks if the key is a validator if not returns an error. + ensure!(>::contains_key(&key), Error::::NotExists); + } + + for key in stash_keys { + Self::chill_stash(&key); + } + + // Change identity status to be Non-Permissioned + PermissionedIdentity::remove(&identity); + Ok(()) + } + #[cfg(feature = "runtime-benchmarks")] pub fn add_era_stakers(current_era: EraIndex, controller: T::AccountId, exposure: Exposure>) { >::insert(¤t_era, &controller, &exposure); diff --git a/pallets/staking/src/offchain_election.rs b/pallets/staking/src/offchain_election.rs index 202a72b809..752e401c79 100644 --- a/pallets/staking/src/offchain_election.rs +++ b/pallets/staking/src/offchain_election.rs @@ -555,6 +555,10 @@ mod test { fn submit_solution_better(v: u32, n: u32, a: u32, w: u32) -> Weight { (0 * v + 0 * n + 1000 * a + 0 * w) as Weight } + + fn chill_from_governance(s: u32) -> Weight { + unimplemented!() + } } #[test] diff --git a/pallets/staking/src/testing_utils.rs b/pallets/staking/src/testing_utils.rs index c66c0ac96d..e041204e99 100644 --- a/pallets/staking/src/testing_utils.rs +++ b/pallets/staking/src/testing_utils.rs @@ -463,3 +463,18 @@ pub fn create_assignments_for_offchain( Ok((winners, assignments)) } + +/// Grab a funded user with the given balance without did. +pub fn create_funded_user_without_did>>( + string: &'static str, + n: u32, + balance: u32, +) -> User { + let user = UserBuilder::::default() + .balance(balance) + .seed(n) + .build(string); + // ensure T::CurrencyToVote will work correctly. + T::Currency::issue(balance.into()); + user +} diff --git a/pallets/staking/src/weights.rs b/pallets/staking/src/weights.rs index 90ff125f6c..9ae60c60cc 100644 --- a/pallets/staking/src/weights.rs +++ b/pallets/staking/src/weights.rs @@ -35,4 +35,5 @@ pub trait WeightInfo { fn update_permissioned_validator_intended_count() -> Weight; fn increase_validator_count() -> Weight; fn scale_validator_count() -> Weight; + fn chill_from_governance(s: u32) -> Weight; } diff --git a/pallets/weights/src/pallet_staking.rs b/pallets/weights/src/pallet_staking.rs index 59f90f2dfe..fbcd1b20ad 100644 --- a/pallets/weights/src/pallet_staking.rs +++ b/pallets/weights/src/pallet_staking.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for pallet_staking //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-06-25, STEPS: `100`, REPEAT: 5, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-11-08, STEPS: `100`, REPEAT: 5, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 512 // Executed Command: @@ -469,4 +469,20 @@ impl pallet_staking::WeightInfo for WeightInfo { .saturating_add(DbWeight::get().reads(1 as Weight)) .saturating_add(DbWeight::get().writes(1 as Weight)) } + + // Storage: Staking EraElectionStatus (r:1 w:0) + // Storage: Staking PermissionedIdentity (r:1 w:1) + // Storage: Identity KeyRecords (r:1 w:0) + // Storage: Identity IsDidFrozen (r:1 w:0) + // Storage: Staking Validators (r:1 w:1) + // Storage: Staking Nominators (r:0 w:1) + fn chill_from_governance(s: u32) -> Weight { + (5_655_000 as Weight) + // Standard Error: 108_000 + .saturating_add((15_307_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(DbWeight::get().reads(3 as Weight)) + .saturating_add(DbWeight::get().reads((2 as Weight).saturating_mul(s as Weight))) + .saturating_add(DbWeight::get().writes(1 as Weight)) + .saturating_add(DbWeight::get().writes((2 as Weight).saturating_mul(s as Weight))) + } }