Skip to content

Commit

Permalink
Able to chill inactive validator
Browse files Browse the repository at this point in the history
  • Loading branch information
aurexav committed Jan 13, 2025
1 parent e051f3e commit 7d05f85
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 8 deletions.
4 changes: 4 additions & 0 deletions polkadot/runtime/westend/src/weights/pallet_staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,4 +826,8 @@ impl<T: frame_system::Config> pallet_staking::WeightInfo for WeightInfo<T> {
.saturating_add(T::DbWeight::get().reads(5))
.saturating_add(T::DbWeight::get().writes(4))
}
// TODO: benchmark.
fn chill_inactive_validator(l: u32, ) -> Weight {
0.into()
}
}
68 changes: 61 additions & 7 deletions substrate/frame/staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ mod benchmarks {

let origin_weight = MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>());

// setup a worst case list scenario. Note that we don't care about the setup of the
// Setup a worst case list scenario. Note that we don't care about the setup of the
// destination position because we are doing a removal from the list but no insert.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
Expand Down Expand Up @@ -461,7 +461,7 @@ mod benchmarks {

let origin_weight = MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>());

// setup a worst case list scenario. Note we don't care about the destination position,
// Setup a worst case list scenario. Note we don't care about the destination position,
// because we are just doing an insert into the origin position.
ListScenario::<T>::new(origin_weight, true)?;
let (stash, controller) = create_stash_controller_with_balance::<T>(
Expand Down Expand Up @@ -494,7 +494,7 @@ mod benchmarks {

let origin_weight = MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>());

// setup a worst case list scenario. Note that we don't care about the setup of the
// Setup a worst case list scenario. Note that we don't care about the setup of the
// destination position because we are doing a removal from the list but no insert.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
Expand Down Expand Up @@ -655,7 +655,7 @@ mod benchmarks {

let origin_weight = MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>());

// setup a worst case list scenario. Note that we don't care about the setup of the
// Setup a worst case list scenario. Note that we don't care about the setup of the
// destination position because we are doing a removal from the list but no insert.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
Expand Down Expand Up @@ -749,7 +749,7 @@ mod benchmarks {
// we use 100 to play friendly with the list threshold values in the mock
.max(100u32.into());

// setup a worst case list scenario.
// Setup a worst case list scenario.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let dest_weight = scenario.dest_weight;

Expand Down Expand Up @@ -793,7 +793,7 @@ mod benchmarks {

let origin_weight = MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>());

// setup a worst case list scenario. Note that we don't care about the setup of the
// Setup a worst case list scenario. Note that we don't care about the setup of the
// destination position because we are doing a removal from the list but no insert.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let controller = scenario.origin_controller1.clone();
Expand Down Expand Up @@ -1006,6 +1006,7 @@ mod benchmarks {
ConfigOp::Set(u32::MAX),
ConfigOp::Set(u32::MAX),
ConfigOp::Set(Percent::max_value()),
ConfigOp::Set(T::HistoryDepth::get()),
ConfigOp::Set(Perbill::max_value()),
ConfigOp::Set(Percent::max_value()),
);
Expand All @@ -1015,6 +1016,7 @@ mod benchmarks {
assert_eq!(MaxNominatorsCount::<T>::get(), Some(u32::MAX));
assert_eq!(MaxValidatorsCount::<T>::get(), Some(u32::MAX));
assert_eq!(ChillThreshold::<T>::get(), Some(Percent::from_percent(100)));
assert_eq!(ChillInactiveValidatorThreshold::<T>::get(), T::HistoryDepth::get());
assert_eq!(MinCommission::<T>::get(), Perbill::from_percent(100));
assert_eq!(MaxStakedRewards::<T>::get(), Some(Percent::from_percent(100)));
}
Expand All @@ -1031,6 +1033,7 @@ mod benchmarks {
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
);

assert!(!MinNominatorBond::<T>::exists());
Expand All @@ -1049,7 +1052,7 @@ mod benchmarks {

let origin_weight = MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>());

// setup a worst case list scenario. Note that we don't care about the setup of the
// Setup a worst case list scenario. Note that we don't care about the setup of the
// destination position because we are doing a removal from the list but no insert.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let stash = scenario.origin_stash1;
Expand All @@ -1062,6 +1065,7 @@ mod benchmarks {
ConfigOp::Set(0),
ConfigOp::Set(0),
ConfigOp::Set(Percent::from_percent(0)),
ConfigOp::Set(2),
ConfigOp::Set(Zero::zero()),
ConfigOp::Noop,
)?;
Expand Down Expand Up @@ -1133,6 +1137,56 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn chill_inactive_validator(
l: Linear<2, { ChillInactiveValidatorThreshold::<T>::get() }>,
) -> Result<(), BenchmarkError> {
let (stash, _) = create_validator_with_nominators::<T>(
0,
0,
false,
true,
RewardDestination::Staked,
)?;
assert!(T::VoterList::contains(&stash));

Staking::<T>::set_staking_configs(
RawOrigin::Root.into(),
ConfigOp::Set(BalanceOf::<T>::max_value()),
ConfigOp::Set(BalanceOf::<T>::max_value()),
ConfigOp::Set(0),
ConfigOp::Set(0),
ConfigOp::Set(Percent::from_percent(0)),
ConfigOp::Set(l),
ConfigOp::Set(Zero::zero()),
ConfigOp::Noop,
)?;

let caller = whitelisted_caller();
// Set the validator has been inactive for `l` eras.
let proof = (0..l)
.map(|era| {
ErasRewardPoints::<T>::insert(
era,
EraRewardPoints {
total: 0,
individual: BTreeMap::from_iter([(stash.clone(), 0)]),
},
);

era
})
.collect::<Vec<_>>();
let proof = BoundedVec::truncate_from(proof);

#[extrinsic_call]
_(RawOrigin::Signed(caller), stash.clone(), proof);

assert!(!T::VoterList::contains(&stash));

Ok(())
}

impl_benchmark_test_suite!(
Staking,
crate::mock::ExtBuilder::default().has_stakers(true),
Expand Down
89 changes: 89 additions & 0 deletions substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,15 @@ pub mod pallet {
#[pallet::storage]
pub(crate) type ChillThreshold<T: Config> = StorageValue<_, Percent, OptionQuery>;

/// The number of consecutive eras a validator can remain inactive before being subject to
/// chilling by anyone.
///
/// This must be equal or less than [`Config::HistoryDepth`] and greater than `2` for better
/// safety.
#[pallet::storage]
pub(crate) type ChillInactiveValidatorThreshold<T: Config> =
StorageValue<_, u32, ValueQuery, T::HistoryDepth>;

#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
Expand Down Expand Up @@ -867,6 +876,8 @@ pub mod pallet {
NotController,
/// Not a stash account.
NotStash,
/// Not a validator.
NotValidator,
/// Stash is already bonded.
AlreadyBonded,
/// Controller is already paired.
Expand Down Expand Up @@ -895,8 +906,13 @@ pub mod pallet {
NotSortedAndUnique,
/// Rewards for this era have already been claimed for this validator.
AlreadyClaimed,
// The threshold is greater than the [`Config::HistoryDepth`] or less than 2.
InvalidChillInactiveValidatorThreshold,
/// No nominators exist on this page.
InvalidPage,
/// Non-consecutive, unsorted, or insufficient era indexes were provided to chill an
/// inactive validator.
InvalidProof,
/// Incorrect previous history depth input provided.
IncorrectHistoryDepth,
/// Incorrect number of slashing spans provided.
Expand Down Expand Up @@ -1941,11 +1957,19 @@ pub mod pallet {
max_nominator_count: ConfigOp<u32>,
max_validator_count: ConfigOp<u32>,
chill_threshold: ConfigOp<Percent>,
chill_inactive_validator_threshold: ConfigOp<u32>,
min_commission: ConfigOp<Perbill>,
max_staked_rewards: ConfigOp<Percent>,
) -> DispatchResult {
ensure_root(origin)?;

if let ConfigOp::Set(threshold) = chill_inactive_validator_threshold {
ensure!(
threshold >= 2 && threshold <= T::HistoryDepth::get(),
Error::<T>::InvalidChillInactiveValidatorThreshold
);
}

macro_rules! config_op_exp {
($storage:ty, $op:ident) => {
match $op {
Expand All @@ -1961,10 +1985,13 @@ pub mod pallet {
config_op_exp!(MaxNominatorsCount<T>, max_nominator_count);
config_op_exp!(MaxValidatorsCount<T>, max_validator_count);
config_op_exp!(ChillThreshold<T>, chill_threshold);
config_op_exp!(ChillInactiveValidatorThreshold<T>, chill_inactive_validator_threshold);
config_op_exp!(MinCommission<T>, min_commission);
config_op_exp!(MaxStakedRewards<T>, max_staked_rewards);

Ok(())
}

/// Declare a `controller` to stop participating as either a validator or nominator.
///
/// Effects will be felt at the beginning of the next era.
Expand Down Expand Up @@ -2020,6 +2047,7 @@ pub mod pallet {
//
// Otherwise, if caller is the same as the controller, this is just like `chill`.

// Only if the key exists but the `OptionQuery`'s value is `None`.
if Nominators::<T>::contains_key(&stash) && Nominators::<T>::get(&stash).is_none() {
Self::chill_stash(&stash);
return Ok(())
Expand Down Expand Up @@ -2291,6 +2319,67 @@ pub mod pallet {
);
Ok(())
}

/// Chill an inactive validator.
///
/// - Anyone can call this function.
/// - The validator's stash account should be provided.
/// - The proof should be a list of consecutive sorted era indexes where the validator has
/// not produced any blocks.
/// - This will only be successful if the validator has not produced any blocks in
/// [`ChillInactiveValidatorThreshold`] consecutive eras.
#[pallet::call_index(30)]
#[pallet::weight(T::WeightInfo::chill_inactive_validator(proof.len() as _))]
pub fn chill_inactive_validator(
origin: OriginFor<T>,
stash: T::AccountId,
proof: BoundedVec<EraIndex, T::HistoryDepth>,
) -> DispatchResult {
ensure_signed(origin)?;

ensure!(Validators::<T>::contains_key(&stash), Error::<T>::NotValidator);

let threshold = ChillInactiveValidatorThreshold::<T>::get();

ensure!(proof.len() as EraIndex >= threshold, Error::<T>::InvalidProof);

// Check that the proof is sorted and consecutive.
for w in proof.windows(2) {
ensure!(w[1] == w[0] + 1, Error::<T>::InvalidProof);
}

let mut consecutive_inactives = 0;

for era in proof {
// Check if the ear exists.
if !ErasRewardPoints::<T>::contains_key(era) {
consecutive_inactives = 0;

continue;
}

// Check if the stash was a validator of the era.
let Some(&points) = ErasRewardPoints::<T>::get(era).individual.get(&stash) else {
consecutive_inactives = 0;

continue;
};

if points == 0 {
consecutive_inactives += 1;

if consecutive_inactives >= threshold {
Self::chill_stash(&stash);

return Ok(());
}
} else {
consecutive_inactives = 0;
}
}

Err(Error::<T>::InvalidProof)?
}
}
}

Expand Down
Loading

0 comments on commit 7d05f85

Please sign in to comment.