Skip to content

Commit

Permalink
[Manual] Marking offline collators not producing blocks during X roun…
Browse files Browse the repository at this point in the history
…ds (#2259)

* begin manual mark-offline

* remove CandidateLastActive

* fix precompile staking

* comments and fmt

* fix do_go_offline() weight

* make it compile

* apply some suggestions

* test case MaxOfflineRounds < RewardPaymentDelay

* fix test

* add killswitch for marking offline feature

* fix mock config in parachain staking precompile

* add benchmark for notify_inactive_collator extrinsic

* refactor benchmark

* fix call to select_top_candidates in benchmark

* minor changes in benchmark

* add killswitch as a storage item

* fmt

* remove ConstBool

* enable killswitch in benchmark

* add enable_marking_offline extrinsic

* optmize collators_len read and benchmark

* Add locally-generated benchmark for notify_inactive_collator

* Hook up newly added benchmark to pallet call

* add extra check for current round

* minor changes in tests

* add test

* modify rounds_to_check syntax

---------

Co-authored-by: Stephen Shelton <steve@brewcraft.org>
  • Loading branch information
Agusrodri and notlesh authored Oct 6, 2023
1 parent a612425 commit 2a4c274
Show file tree
Hide file tree
Showing 11 changed files with 669 additions and 25 deletions.
69 changes: 67 additions & 2 deletions pallets/parachain-staking/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
//! Benchmarking
use crate::{
AwardedPts, BalanceOf, BottomDelegations, Call, CandidateBondLessRequest, Config,
DelegationAction, Pallet, ParachainBondConfig, ParachainBondInfo, Points, Range, RewardPayment,
Round, ScheduledRequest, Staked, TopDelegations,
DelegationAction, EnableMarkingOffline, Pallet, ParachainBondConfig, ParachainBondInfo, Points,
Range, RewardPayment, Round, ScheduledRequest, Staked, TopDelegations,
};
use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite};
use frame_support::traits::{Currency, Get, OnFinalize, OnInitialize};
Expand Down Expand Up @@ -2192,6 +2192,71 @@ benchmarks! {
verify {
assert_eq!(T::Currency::free_balance(&collator), original_free_balance + 50u32.into());
}

notify_inactive_collator {
use crate::{AtStake, CollatorSnapshot, AwardedPts};

// Blocks per-round must be greater than TotalSelected
Pallet::<T>::set_blocks_per_round(RawOrigin::Root.into(), 101u32)?;
Pallet::<T>::set_total_selected(RawOrigin::Root.into(), 100u32)?;

let mut candidate_count = 1u32;
let mut seed = USER_SEED;

// Create collators up to MaxCandidates
for i in 0..(T::MaxCandidates::get() - 3) {
seed += i;
let collator = create_funded_collator::<T>(
"collator",
seed,
min_candidate_stk::<T>() * 1_000_000u32.into(),
true,
candidate_count
)?;
candidate_count += 1;
}

// Create two collators more: the one that will be marked as inactive
// and the one that will act as the caller of the extrinsic.
seed += 1;
let inactive_collator: T::AccountId = create_funded_collator::<T>(
"collator",
seed,
min_candidate_stk::<T>() * 1_000_000u32.into(),
true,
candidate_count
)?;
candidate_count += 1;

seed += 1;
let caller: T::AccountId = create_funded_collator::<T>(
"collator",
seed,
min_candidate_stk::<T>() * 1_000_000u32.into(),
true,
candidate_count
)?;

// Roll to round 2 and call to select_top_candidates.
// We do this to be able to have more than 66% of TotalSelected.
roll_to_and_author::<T>(2, caller.clone());
Pallet::<T>::select_top_candidates(2);

// Manually change these values for inactive_collator,
// so that it can be marked as inactive.
<AtStake<T>>::insert(1, &inactive_collator, CollatorSnapshot::default());
<AwardedPts<T>>::insert(1, &inactive_collator, 0);

<AtStake<T>>::insert(2, &inactive_collator, CollatorSnapshot::default());
<AwardedPts<T>>::insert(2, &inactive_collator, 0);

// Enable killswitch
<EnableMarkingOffline<T>>::set(true);

}: _(RawOrigin::Signed(caller), inactive_collator.clone())
verify {
assert!(!Pallet::<T>::candidate_info(&inactive_collator).expect("must exist").is_active());
}
}

#[cfg(test)]
Expand Down
104 changes: 103 additions & 1 deletion pallets/parachain-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ pub mod pallet {
/// Minimum number of blocks per round
#[pallet::constant]
type MinBlocksPerRound: Get<u32>;
/// If a collator doesn't produce any block on this number of rounds, it is notified as inactive.
/// This value must be less than or equal to RewardPaymentDelay.
#[pallet::constant]
type MaxOfflineRounds: Get<u32>;
/// Number of rounds that candidates remain bonded before exit request is executable
#[pallet::constant]
type LeaveCandidatesDelay: Get<RoundIndex>;
Expand Down Expand Up @@ -170,6 +174,10 @@ pub mod pallet {
/// Handler to distribute a collator's reward.
/// To use the default implementation of minting rewards, specify the type `()`.
type PayoutCollatorReward: PayoutCollatorReward<Self>;
/// Handler to notify the runtime when a collator is inactive.
/// The default behavior is to mark the collator as offline.
/// If you need to use the default implementation, specify the type `()`.
type OnInactiveCollator: OnInactiveCollator<Self>;
/// Handler to notify the runtime when a new round begin.
/// If you don't need it, you can specify the type `()`.
type OnNewRound: OnNewRound;
Expand Down Expand Up @@ -227,12 +235,16 @@ pub mod pallet {
TooLowDelegationCountToAutoCompound,
TooLowCandidateAutoCompoundingDelegationCountToAutoCompound,
TooLowCandidateAutoCompoundingDelegationCountToDelegate,
TooLowCollatorCountToNotifyAsInactive,
CannotBeNotifiedAsInactive,
TooLowCandidateAutoCompoundingDelegationCountToLeaveCandidates,
TooLowCandidateCountWeightHint,
TooLowCandidateCountWeightHintGoOffline,
CandidateLimitReached,
CannotSetAboveMaxCandidates,
RemovedCall,
MarkingOfflineNotEnabled,
CurrentRoundTooLow,
}

#[pallet::event]
Expand Down Expand Up @@ -608,7 +620,7 @@ pub mod pallet {
Twox64Concat,
T::AccountId,
CollatorSnapshot<T::AccountId, BalanceOf<T>>,
ValueQuery,
OptionQuery,
>;

#[pallet::storage]
Expand Down Expand Up @@ -645,6 +657,11 @@ pub mod pallet {
ValueQuery,
>;

#[pallet::storage]
#[pallet::getter(fn marking_offline)]
/// Killswitch to enable/disable marking offline feature.
pub type EnableMarkingOffline<T: Config> = StorageValue<_, bool, ValueQuery>;

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
/// Initialize balance and register all as collators: `(collator AccountId, balance Amount)`
Expand Down Expand Up @@ -1375,6 +1392,91 @@ pub mod pallet {

Ok(().into())
}

/// Notify a collator is inactive during MaxOfflineRounds
#[pallet::call_index(29)]
#[pallet::weight(<T as Config>::WeightInfo::notify_inactive_collator())]
pub fn notify_inactive_collator(
origin: OriginFor<T>,
collator: T::AccountId,
) -> DispatchResult {
ensure!(
<EnableMarkingOffline<T>>::get(),
<Error<T>>::MarkingOfflineNotEnabled
);
ensure_signed(origin)?;

let mut collators_len = 0usize;
let max_collators = <TotalSelected<T>>::get();

if let Some(len) = <SelectedCandidates<T>>::decode_len() {
collators_len = len;
};

// Check collators length is not below or eq to 66% of max_collators.
// We use saturating logic here with (2/3)
// as it is dangerous to use floating point numbers directly.
ensure!(
collators_len * 3 > (max_collators * 2) as usize,
<Error<T>>::TooLowCollatorCountToNotifyAsInactive
);

let round_info = <Round<T>>::get();
let max_offline_rounds = T::MaxOfflineRounds::get();

ensure!(
round_info.current > max_offline_rounds,
<Error<T>>::CurrentRoundTooLow
);

// Have rounds_to_check = [8,9]
// in case we are in round 10 for instance
// with MaxOfflineRounds = 2
let first_round_to_check = round_info.current.saturating_sub(max_offline_rounds);
let rounds_to_check = first_round_to_check..round_info.current;

// If this counter is eq to max_offline_rounds,
// the collator should be notified as inactive
let mut inactive_counter: RoundIndex = 0u32;

// Iter rounds to check
//
// - The collator has AtStake associated and their AwardedPts are zero
//
// If the previous condition is met in all rounds of rounds_to_check,
// the collator is notified as inactive
for r in rounds_to_check {
let stake = <AtStake<T>>::get(r, &collator);
let pts = <AwardedPts<T>>::get(r, &collator);

if stake.is_some() && pts.is_zero() {
inactive_counter = inactive_counter.saturating_add(1);
}
}

if inactive_counter == max_offline_rounds {
let _ = T::OnInactiveCollator::on_inactive_collator(
collator.clone(),
round_info.current.saturating_sub(1),
);
} else {
return Err(<Error<T>>::CannotBeNotifiedAsInactive.into());
}

Ok(().into())
}

/// Enable/Disable marking offline feature
#[pallet::call_index(30)]
#[pallet::weight(
Weight::from_parts(3_000_000u64, 4_000u64)
.saturating_add(T::DbWeight::get().writes(1u64))
)]
pub fn enable_marking_offline(origin: OriginFor<T>, value: bool) -> DispatchResult {
ensure_root(origin)?;
<EnableMarkingOffline<T>>::set(value);
Ok(())
}
}

/// Represents a payout made via `pay_one_collator_reward`.
Expand Down
9 changes: 9 additions & 0 deletions pallets/parachain-staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
pallet, AwardedPts, Config, Event as ParachainStakingEvent, InflationInfo, Points, Range,
COLLATOR_LOCK_ID, DELEGATOR_LOCK_ID,
};
use block_author::BlockAuthor as BlockAuthorMap;
use frame_support::{
construct_runtime, parameter_types,
traits::{Everything, GenesisBuild, LockIdentifier, OnFinalize, OnInitialize},
Expand Down Expand Up @@ -111,6 +112,7 @@ const GENESIS_PARACHAIN_BOND_RESERVE_PERCENT: Percent = Percent::from_percent(30
const GENESIS_NUM_SELECTED_CANDIDATES: u32 = 5;
parameter_types! {
pub const MinBlocksPerRound: u32 = 3;
pub const MaxOfflineRounds: u32 = 1;
pub const LeaveCandidatesDelay: u32 = 2;
pub const CandidateBondLessDelay: u32 = 2;
pub const LeaveDelegatorsDelay: u32 = 2;
Expand All @@ -130,6 +132,7 @@ impl Config for Test {
type Currency = Balances;
type MonetaryGovernanceOrigin = frame_system::EnsureRoot<AccountId>;
type MinBlocksPerRound = MinBlocksPerRound;
type MaxOfflineRounds = MaxOfflineRounds;
type LeaveCandidatesDelay = LeaveCandidatesDelay;
type CandidateBondLessDelay = CandidateBondLessDelay;
type LeaveDelegatorsDelay = LeaveDelegatorsDelay;
Expand All @@ -145,6 +148,7 @@ impl Config for Test {
type BlockAuthor = BlockAuthor;
type OnCollatorPayout = ();
type PayoutCollatorReward = ();
type OnInactiveCollator = ();
type OnNewRound = ();
type WeightInfo = ();
type MaxCandidates = MaxCandidates;
Expand Down Expand Up @@ -515,6 +519,11 @@ pub(crate) fn set_author(round: BlockNumber, acc: u64, pts: u32) {
<AwardedPts<Test>>::mutate(round, acc, |p| *p += pts);
}

// Allows to change the block author (default is always 0)
pub(crate) fn set_block_author(acc: u64) {
<BlockAuthorMap<Test>>::set(acc);
}

/// fn to query the lock amount
pub(crate) fn query_lock_amount(account_id: u64, id: LockIdentifier) -> Option<Balance> {
for lock in Balances::locks(&account_id) {
Expand Down
Loading

0 comments on commit 2a4c274

Please sign in to comment.