diff --git a/pallets/parachain-staking/src/benchmarks.rs b/pallets/parachain-staking/src/benchmarks.rs index 4b4ccf832bb..d3d966e784b 100644 --- a/pallets/parachain-staking/src/benchmarks.rs +++ b/pallets/parachain-staking/src/benchmarks.rs @@ -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}; @@ -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::::set_blocks_per_round(RawOrigin::Root.into(), 101u32)?; + Pallet::::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::( + "collator", + seed, + min_candidate_stk::() * 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::( + "collator", + seed, + min_candidate_stk::() * 1_000_000u32.into(), + true, + candidate_count + )?; + candidate_count += 1; + + seed += 1; + let caller: T::AccountId = create_funded_collator::( + "collator", + seed, + min_candidate_stk::() * 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::(2, caller.clone()); + Pallet::::select_top_candidates(2); + + // Manually change these values for inactive_collator, + // so that it can be marked as inactive. + >::insert(1, &inactive_collator, CollatorSnapshot::default()); + >::insert(1, &inactive_collator, 0); + + >::insert(2, &inactive_collator, CollatorSnapshot::default()); + >::insert(2, &inactive_collator, 0); + + // Enable killswitch + >::set(true); + + }: _(RawOrigin::Signed(caller), inactive_collator.clone()) + verify { + assert!(!Pallet::::candidate_info(&inactive_collator).expect("must exist").is_active()); + } } #[cfg(test)] diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index eff3cde70ea..dc3804ba9c2 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -126,6 +126,10 @@ pub mod pallet { /// Minimum number of blocks per round #[pallet::constant] type MinBlocksPerRound: Get; + /// 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; /// Number of rounds that candidates remain bonded before exit request is executable #[pallet::constant] type LeaveCandidatesDelay: Get; @@ -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; + /// 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; /// Handler to notify the runtime when a new round begin. /// If you don't need it, you can specify the type `()`. type OnNewRound: OnNewRound; @@ -227,12 +235,16 @@ pub mod pallet { TooLowDelegationCountToAutoCompound, TooLowCandidateAutoCompoundingDelegationCountToAutoCompound, TooLowCandidateAutoCompoundingDelegationCountToDelegate, + TooLowCollatorCountToNotifyAsInactive, + CannotBeNotifiedAsInactive, TooLowCandidateAutoCompoundingDelegationCountToLeaveCandidates, TooLowCandidateCountWeightHint, TooLowCandidateCountWeightHintGoOffline, CandidateLimitReached, CannotSetAboveMaxCandidates, RemovedCall, + MarkingOfflineNotEnabled, + CurrentRoundTooLow, } #[pallet::event] @@ -608,7 +620,7 @@ pub mod pallet { Twox64Concat, T::AccountId, CollatorSnapshot>, - ValueQuery, + OptionQuery, >; #[pallet::storage] @@ -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 = StorageValue<_, bool, ValueQuery>; + #[pallet::genesis_config] pub struct GenesisConfig { /// Initialize balance and register all as collators: `(collator AccountId, balance Amount)` @@ -1375,6 +1392,91 @@ pub mod pallet { Ok(().into()) } + + /// Notify a collator is inactive during MaxOfflineRounds + #[pallet::call_index(29)] + #[pallet::weight(::WeightInfo::notify_inactive_collator())] + pub fn notify_inactive_collator( + origin: OriginFor, + collator: T::AccountId, + ) -> DispatchResult { + ensure!( + >::get(), + >::MarkingOfflineNotEnabled + ); + ensure_signed(origin)?; + + let mut collators_len = 0usize; + let max_collators = >::get(); + + if let Some(len) = >::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, + >::TooLowCollatorCountToNotifyAsInactive + ); + + let round_info = >::get(); + let max_offline_rounds = T::MaxOfflineRounds::get(); + + ensure!( + round_info.current > max_offline_rounds, + >::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 = >::get(r, &collator); + let pts = >::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(>::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, value: bool) -> DispatchResult { + ensure_root(origin)?; + >::set(value); + Ok(()) + } } /// Represents a payout made via `pay_one_collator_reward`. diff --git a/pallets/parachain-staking/src/mock.rs b/pallets/parachain-staking/src/mock.rs index 1d543eb3aae..faab5217157 100644 --- a/pallets/parachain-staking/src/mock.rs +++ b/pallets/parachain-staking/src/mock.rs @@ -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}, @@ -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; @@ -130,6 +132,7 @@ impl Config for Test { type Currency = Balances; type MonetaryGovernanceOrigin = frame_system::EnsureRoot; type MinBlocksPerRound = MinBlocksPerRound; + type MaxOfflineRounds = MaxOfflineRounds; type LeaveCandidatesDelay = LeaveCandidatesDelay; type CandidateBondLessDelay = CandidateBondLessDelay; type LeaveDelegatorsDelay = LeaveDelegatorsDelay; @@ -145,6 +148,7 @@ impl Config for Test { type BlockAuthor = BlockAuthor; type OnCollatorPayout = (); type PayoutCollatorReward = (); + type OnInactiveCollator = (); type OnNewRound = (); type WeightInfo = (); type MaxCandidates = MaxCandidates; @@ -515,6 +519,11 @@ pub(crate) fn set_author(round: BlockNumber, acc: u64, pts: u32) { >::mutate(round, acc, |p| *p += pts); } +// Allows to change the block author (default is always 0) +pub(crate) fn set_block_author(acc: u64) { + >::set(acc); +} + /// fn to query the lock amount pub(crate) fn query_lock_amount(account_id: u64, id: LockIdentifier) -> Option { for lock in Balances::locks(&account_id) { diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index aa77091bd0f..82ebbe31f3b 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -25,15 +25,15 @@ use crate::auto_compound::{AutoCompoundConfig, AutoCompoundDelegations}; use crate::delegation_requests::{CancelledScheduledRequest, DelegationAction, ScheduledRequest}; use crate::mock::{ - roll_blocks, roll_to, roll_to_round_begin, roll_to_round_end, set_author, Balances, - BlockNumber, ExtBuilder, ParachainStaking, RuntimeOrigin, Test, + roll_blocks, roll_to, roll_to_round_begin, roll_to_round_end, set_author, set_block_author, + Balances, BlockNumber, ExtBuilder, ParachainStaking, RuntimeOrigin, Test, }; use crate::{ assert_events_emitted, assert_events_emitted_match, assert_events_eq, assert_no_events, - AtStake, Bond, CollatorStatus, DelegationScheduledRequests, DelegatorAdded, Error, Event, - Range, DELEGATOR_LOCK_ID, + AtStake, Bond, CollatorStatus, DelegationScheduledRequests, DelegatorAdded, + EnableMarkingOffline, Error, Event, Range, DELEGATOR_LOCK_ID, }; -use frame_support::{assert_err, assert_noop, assert_ok, BoundedVec}; +use frame_support::{assert_err, assert_noop, assert_ok, pallet_prelude::*, BoundedVec}; use sp_runtime::{traits::Zero, DispatchError, ModuleError, Perbill, Percent}; // ~~ ROOT ~~ @@ -979,6 +979,267 @@ fn insufficient_leave_candidates_weight_hint_fails() { }); } +#[test] +fn enable_marking_offline_works() { + ExtBuilder::default() + .with_balances(vec![(1, 20)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::enable_marking_offline( + RuntimeOrigin::root(), + true + )); + assert!(ParachainStaking::marking_offline()); + + // Set to false now + assert_ok!(ParachainStaking::enable_marking_offline( + RuntimeOrigin::root(), + false + )); + assert!(!ParachainStaking::marking_offline()); + }); +} + +#[test] +fn enable_marking_offline_fails_bad_origin() { + ExtBuilder::default() + .with_balances(vec![(1, 20)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::enable_marking_offline(RuntimeOrigin::signed(1), true), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn notify_inactive_collator_works() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + // Enable killswitch + >::set(true); + + // Round 2 + roll_to_round_begin(2); + + // Change block author + set_block_author(1); + + // Finalize the first block of round 2 + ParachainStaking::on_finalize(5); + + // We don't produce blocks on round 3 + roll_to_round_begin(3); + roll_blocks(1); + + // We don't produce blocks on round 4 + roll_to_round_begin(4); + roll_blocks(1); + + // Round 6 - notify the collator as inactive + roll_to_round_begin(6); + roll_blocks(1); + + assert_eq!(::MaxOfflineRounds::get(), 1); + assert_eq!(::RewardPaymentDelay::get(), 2); + + // Call 'notify_inactive_collator' extrinsic + assert_ok!(ParachainStaking::notify_inactive_collator( + RuntimeOrigin::signed(1), + 1 + )); + + // Check the collator was marked as offline as it hasn't produced blocks + assert_events_eq!(Event::CandidateWentOffline { candidate: 1 },); + }); +} + +#[test] +fn notify_inactive_collator_fails_too_low_collator_count() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20)]) + .build() + .execute_with(|| { + // Enable killswitch + >::set(true); + + // Round 4 + roll_to_round_begin(4); + roll_blocks(1); + + // Call 'notify_inactive_collator' extrinsic + assert_noop!( + ParachainStaking::notify_inactive_collator(RuntimeOrigin::signed(1), 1), + Error::::TooLowCollatorCountToNotifyAsInactive + ); + }); +} + +#[test] +fn notify_inactive_collator_fails_candidate_is_not_collator() { + ExtBuilder::default() + .with_balances(vec![(1, 80), (2, 80), (3, 80), (4, 80), (5, 80), (6, 20)]) + .with_candidates(vec![(1, 80), (2, 80), (3, 80), (4, 80), (5, 80)]) + .build() + .execute_with(|| { + // Enable killswitch + >::set(true); + + set_block_author(1); + + roll_to_round_begin(2); + assert_events_eq!( + Event::CollatorChosen { + round: 2, + collator_account: 1, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 2, + collator_account: 2, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 2, + collator_account: 3, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 2, + collator_account: 4, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 2, + collator_account: 5, + total_exposed_amount: 80, + }, + Event::NewRound { + starting_block: 5, + round: 2, + selected_collators_number: 5, + total_balance: 400, + }, + ); + roll_blocks(1); + + assert_ok!(ParachainStaking::join_candidates( + RuntimeOrigin::signed(6), + 10, + 100 + )); + + // Round 6 + roll_to_round_begin(6); + assert_events_eq!( + Event::CollatorChosen { + round: 6, + collator_account: 1, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 6, + collator_account: 2, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 6, + collator_account: 3, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 6, + collator_account: 4, + total_exposed_amount: 80, + }, + Event::CollatorChosen { + round: 6, + collator_account: 5, + total_exposed_amount: 80, + }, + Event::NewRound { + starting_block: 25, + round: 6, + selected_collators_number: 5, + total_balance: 400, + }, + ); + roll_blocks(1); + + // A candidate cannot be notified as inactive if it hasn't been selected + // to produce blocks + assert_noop!( + ParachainStaking::notify_inactive_collator(RuntimeOrigin::signed(1), 6), + Error::::CannotBeNotifiedAsInactive + ); + }); +} + +#[test] +fn notify_inactive_collator_fails_cannot_be_notified_as_inactive() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + // Enable killswitch + >::set(true); + + // Round 2 + roll_to_round_begin(2); + + // Change block author + set_block_author(1); + + // Finalize the first block of round 2 + ParachainStaking::on_finalize(5); + + // Round 3 + roll_to_round_begin(3); + roll_blocks(1); + + // Finalize a block of round 3 + ParachainStaking::on_finalize(11); + + // Round 4 + roll_to_round_begin(4); + roll_blocks(1); + + // Call 'notify_inactive_collator' extrinsic + assert_noop!( + ParachainStaking::notify_inactive_collator(RuntimeOrigin::signed(1), 1), + Error::::CannotBeNotifiedAsInactive + ); + }); +} + +#[test] +fn notify_inactive_collator_fails_round_too_low() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + // Enable killswitch + >::set(true); + + // Round 1 + roll_to_round_begin(1); + roll_blocks(1); + + // Call 'notify_inactive_collator' extrinsic + assert_noop!( + ParachainStaking::notify_inactive_collator(RuntimeOrigin::signed(1), 1), + Error::::CurrentRoundTooLow + ); + }); +} + #[test] fn sufficient_leave_candidates_weight_hint_succeeds() { ExtBuilder::default() @@ -6689,7 +6950,8 @@ fn test_delegator_scheduled_for_revoke_is_rewarded_for_previous_rounds_but_not_f rewards: 5, },); let collator_snapshot = - ParachainStaking::at_stake(ParachainStaking::round().current, 1); + ParachainStaking::at_stake(ParachainStaking::round().current, 1) + .unwrap_or_default(); assert_eq!( 1, collator_snapshot.delegations.len(), @@ -6747,7 +7009,8 @@ fn test_delegator_scheduled_for_revoke_is_rewarded_when_request_cancelled() { rewards: 5, },); let collator_snapshot = - ParachainStaking::at_stake(ParachainStaking::round().current, 1); + ParachainStaking::at_stake(ParachainStaking::round().current, 1) + .unwrap_or_default(); assert_eq!( 1, collator_snapshot.delegations.len(), @@ -6835,7 +7098,8 @@ fn test_delegator_scheduled_for_bond_decrease_is_rewarded_for_previous_rounds_bu }, ); let collator_snapshot = - ParachainStaking::at_stake(ParachainStaking::round().current, 1); + ParachainStaking::at_stake(ParachainStaking::round().current, 1) + .unwrap_or_default(); assert_eq!( 1, collator_snapshot.delegations.len(), @@ -6900,7 +7164,8 @@ fn test_delegator_scheduled_for_bond_decrease_is_rewarded_when_request_cancelled }, ); let collator_snapshot = - ParachainStaking::at_stake(ParachainStaking::round().current, 1); + ParachainStaking::at_stake(ParachainStaking::round().current, 1) + .unwrap_or_default(); assert_eq!( 1, collator_snapshot.delegations.len(), @@ -6992,7 +7257,8 @@ fn test_delegator_scheduled_for_leave_is_rewarded_for_previous_rounds_but_not_fo rewards: 5, },); let collator_snapshot = - ParachainStaking::at_stake(ParachainStaking::round().current, 1); + ParachainStaking::at_stake(ParachainStaking::round().current, 1) + .unwrap_or_default(); assert_eq!( 1, collator_snapshot.delegations.len(), @@ -7066,7 +7332,8 @@ fn test_delegator_scheduled_for_leave_is_rewarded_when_request_cancelled() { rewards: 5, },); let collator_snapshot = - ParachainStaking::at_stake(ParachainStaking::round().current, 1); + ParachainStaking::at_stake(ParachainStaking::round().current, 1) + .unwrap_or_default(); assert_eq!( 1, collator_snapshot.delegations.len(), diff --git a/pallets/parachain-staking/src/traits.rs b/pallets/parachain-staking/src/traits.rs index 2b30b04cc18..d6477f13df7 100644 --- a/pallets/parachain-staking/src/traits.rs +++ b/pallets/parachain-staking/src/traits.rs @@ -16,7 +16,9 @@ //! traits for parachain-staking -use frame_support::pallet_prelude::Weight; +use crate::weights::WeightInfo; +use frame_support::{dispatch::PostDispatchInfo, pallet_prelude::Weight}; +use sp_runtime::DispatchErrorWithPostInfo; pub trait OnCollatorPayout { fn on_collator_payout( @@ -64,3 +66,22 @@ impl PayoutCollatorReward for () { crate::Pallet::::mint_collator_reward(for_round, collator_id, amount) } } + +pub trait OnInactiveCollator { + fn on_inactive_collator( + collator_id: Runtime::AccountId, + round: crate::RoundIndex, + ) -> Result>; +} + +impl OnInactiveCollator for () { + fn on_inactive_collator( + collator_id: ::AccountId, + _round: crate::RoundIndex, + ) -> Result> { + crate::Pallet::::go_offline_inner(collator_id)?; + Ok(::WeightInfo::go_offline( + crate::MAX_CANDIDATES, + )) + } +} diff --git a/pallets/parachain-staking/src/weights.rs b/pallets/parachain-staking/src/weights.rs index 518dc22cb4d..7582113ead8 100644 --- a/pallets/parachain-staking/src/weights.rs +++ b/pallets/parachain-staking/src/weights.rs @@ -88,6 +88,7 @@ pub trait WeightInfo { fn delegate_with_auto_compound(x: u32, y: u32, z: u32, ) -> Weight; fn delegate_with_auto_compound_worst() -> Weight; fn mint_collator_reward() -> Weight; + fn notify_inactive_collator() -> Weight; } /// Weights for parachain_staking using the Substrate node and recommended hardware. @@ -871,6 +872,32 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: ParachainStaking EnableMarkingOffline (r:1 w:0) + /// Proof Skipped: ParachainStaking EnableMarkingOffline (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking TotalSelected (r:1 w:0) + /// Proof Skipped: ParachainStaking TotalSelected (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking SelectedCandidates (r:1 w:0) + /// Proof Skipped: ParachainStaking SelectedCandidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking AtStake (r:2 w:0) + /// Proof Skipped: ParachainStaking AtStake (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking AwardedPts (r:2 w:0) + /// Proof Skipped: ParachainStaking AwardedPts (max_values: None, max_size: None, mode: Measured) + /// Storage: MoonbeamOrbiters OrbiterPerRound (r:1 w:0) + /// Proof Skipped: MoonbeamOrbiters OrbiterPerRound (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking CandidateInfo (r:1 w:1) + /// Proof Skipped: ParachainStaking CandidateInfo (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking CandidatePool (r:1 w:1) + /// Proof Skipped: ParachainStaking CandidatePool (max_values: Some(1), max_size: None, mode: Measured) + fn notify_inactive_collator() -> Weight { + // Proof Size summary in bytes: + // Measured: `11494` + // Estimated: `17434` + // Minimum execution time: 41_130_000 picoseconds. + Weight::from_parts(41_130_000, 0) + .saturating_add(Weight::from_parts(0, 17434)) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } } // For backwards compatibility and tests @@ -1653,4 +1680,30 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } -} \ No newline at end of file + /// Storage: ParachainStaking EnableMarkingOffline (r:1 w:0) + /// Proof Skipped: ParachainStaking EnableMarkingOffline (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking TotalSelected (r:1 w:0) + /// Proof Skipped: ParachainStaking TotalSelected (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking SelectedCandidates (r:1 w:0) + /// Proof Skipped: ParachainStaking SelectedCandidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking AtStake (r:2 w:0) + /// Proof Skipped: ParachainStaking AtStake (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking AwardedPts (r:2 w:0) + /// Proof Skipped: ParachainStaking AwardedPts (max_values: None, max_size: None, mode: Measured) + /// Storage: MoonbeamOrbiters OrbiterPerRound (r:1 w:0) + /// Proof Skipped: MoonbeamOrbiters OrbiterPerRound (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking CandidateInfo (r:1 w:1) + /// Proof Skipped: ParachainStaking CandidateInfo (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking CandidatePool (r:1 w:1) + /// Proof Skipped: ParachainStaking CandidatePool (max_values: Some(1), max_size: None, mode: Measured) + fn notify_inactive_collator() -> Weight { + // Proof Size summary in bytes: + // Measured: `11494` + // Estimated: `17434` + // Minimum execution time: 41_130_000 picoseconds. + Weight::from_parts(41_130_000, 0) + .saturating_add(Weight::from_parts(0, 17434)) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } +} diff --git a/precompiles/parachain-staking/src/mock.rs b/precompiles/parachain-staking/src/mock.rs index 6b5bd05760e..cf06887203e 100644 --- a/precompiles/parachain-staking/src/mock.rs +++ b/precompiles/parachain-staking/src/mock.rs @@ -170,6 +170,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 = 2; pub const LeaveCandidatesDelay: u32 = 2; pub const CandidateBondLessDelay: u32 = 2; pub const LeaveDelegatorsDelay: u32 = 2; @@ -190,6 +191,7 @@ impl pallet_parachain_staking::Config for Runtime { type Currency = Balances; type MonetaryGovernanceOrigin = frame_system::EnsureRoot; type MinBlocksPerRound = MinBlocksPerRound; + type MaxOfflineRounds = MaxOfflineRounds; type LeaveCandidatesDelay = LeaveCandidatesDelay; type CandidateBondLessDelay = CandidateBondLessDelay; type LeaveDelegatorsDelay = LeaveDelegatorsDelay; @@ -205,6 +207,7 @@ impl pallet_parachain_staking::Config for Runtime { type BlockAuthor = BlockAuthor; type PayoutCollatorReward = (); type OnCollatorPayout = (); + type OnInactiveCollator = (); type OnNewRound = (); type WeightInfo = (); type MaxCandidates = MaxCandidates; diff --git a/runtime/common/src/weights/pallet_parachain_staking.rs b/runtime/common/src/weights/pallet_parachain_staking.rs index dfa4c055f27..49d1e726bd4 100644 --- a/runtime/common/src/weights/pallet_parachain_staking.rs +++ b/runtime/common/src/weights/pallet_parachain_staking.rs @@ -859,4 +859,30 @@ impl pallet_parachain_staking::WeightInfo for WeightInf .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: ParachainStaking EnableMarkingOffline (r:1 w:0) + /// Proof Skipped: ParachainStaking EnableMarkingOffline (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking TotalSelected (r:1 w:0) + /// Proof Skipped: ParachainStaking TotalSelected (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking SelectedCandidates (r:1 w:0) + /// Proof Skipped: ParachainStaking SelectedCandidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ParachainStaking AtStake (r:2 w:0) + /// Proof Skipped: ParachainStaking AtStake (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking AwardedPts (r:2 w:0) + /// Proof Skipped: ParachainStaking AwardedPts (max_values: None, max_size: None, mode: Measured) + /// Storage: MoonbeamOrbiters OrbiterPerRound (r:1 w:0) + /// Proof Skipped: MoonbeamOrbiters OrbiterPerRound (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking CandidateInfo (r:1 w:1) + /// Proof Skipped: ParachainStaking CandidateInfo (max_values: None, max_size: None, mode: Measured) + /// Storage: ParachainStaking CandidatePool (r:1 w:1) + /// Proof Skipped: ParachainStaking CandidatePool (max_values: Some(1), max_size: None, mode: Measured) + fn notify_inactive_collator() -> Weight { + // Proof Size summary in bytes: + // Measured: `11494` + // Estimated: `17434` + // Minimum execution time: 41_130_000 picoseconds. + Weight::from_parts(41_130_000, 0) + .saturating_add(Weight::from_parts(0, 17434)) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } } diff --git a/runtime/moonbase/src/lib.rs b/runtime/moonbase/src/lib.rs index 78f202013ee..7b552a6f2a3 100644 --- a/runtime/moonbase/src/lib.rs +++ b/runtime/moonbase/src/lib.rs @@ -38,7 +38,7 @@ use account::AccountId20; pub use frame_support::traits::Get; use frame_support::{ construct_runtime, - dispatch::{DispatchClass, GetDispatchInfo}, + dispatch::{DispatchClass, GetDispatchInfo, PostDispatchInfo}, ensure, pallet_prelude::DispatchResult, parameter_types, @@ -74,7 +74,7 @@ use pallet_evm::{ FeeCalculator, GasWeightMapping, IdentityAddressMapping, OnChargeEVMTransaction as OnChargeEVMTransactionT, Runner, }; -pub use pallet_parachain_staking::{InflationInfo, Range}; +pub use pallet_parachain_staking::{weights::WeightInfo, InflationInfo, Range}; use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -91,7 +91,8 @@ use sp_runtime::{ transaction_validity::{ InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, }, - ApplyExtrinsicResult, FixedPointNumber, Perbill, Permill, Perquintill, + ApplyExtrinsicResult, DispatchErrorWithPostInfo, FixedPointNumber, Perbill, Permill, + Perquintill, }; use sp_std::{ convert::{From, Into}, @@ -714,6 +715,27 @@ impl pallet_parachain_staking::PayoutCollatorReward for PayoutCollatorO } } +pub struct OnInactiveCollator; +impl pallet_parachain_staking::OnInactiveCollator for OnInactiveCollator { + fn on_inactive_collator( + collator_id: AccountId, + round: pallet_parachain_staking::RoundIndex, + ) -> Result> { + let extra_weight = if !MoonbeamOrbiters::is_orbiter(round, collator_id.clone()) { + ParachainStaking::go_offline_inner(collator_id)?; + ::WeightInfo::go_offline( + pallet_parachain_staking::MAX_CANDIDATES, + ) + } else { + Weight::zero() + }; + + Ok(::DbWeight::get() + .reads(1) + .saturating_add(extra_weight)) + } +} + type MonetaryGovernanceOrigin = EitherOfDiverse, governance::custom_origins::GeneralAdmin>; @@ -723,6 +745,8 @@ impl pallet_parachain_staking::Config for Runtime { type MonetaryGovernanceOrigin = MonetaryGovernanceOrigin; /// Minimum round length is 2 minutes (10 * 12 second block times) type MinBlocksPerRound = ConstU32<10>; + /// If a collator doesn't produce any block on this number of rounds, it is notified as inactive + type MaxOfflineRounds = ConstU32<2>; /// Rounds before the collator leaving the candidates request can be executed type LeaveCandidatesDelay = ConstU32<2>; /// Rounds before the candidate bond increase/decrease can be executed @@ -750,6 +774,7 @@ impl pallet_parachain_staking::Config for Runtime { type BlockAuthor = AuthorInherent; type OnCollatorPayout = (); type PayoutCollatorReward = PayoutCollatorOrOrbiterReward; + type OnInactiveCollator = OnInactiveCollator; type OnNewRound = OnNewRound; type WeightInfo = moonbeam_weights::pallet_parachain_staking::WeightInfo; type MaxCandidates = ConstU32<200>; @@ -1689,6 +1714,14 @@ mod tests { ); } + #[test] + fn max_offline_rounds_lower_or_eq_than_reward_payment_delay() { + assert!( + get!(pallet_parachain_staking, MaxOfflineRounds, u32) + <= get!(pallet_parachain_staking, RewardPaymentDelay, u32) + ); + } + #[test] // Required migration is // pallet_parachain_staking::migrations::IncreaseMaxTopDelegationsPerCandidate diff --git a/runtime/moonbeam/src/lib.rs b/runtime/moonbeam/src/lib.rs index a4aa647fc17..1bf36912765 100644 --- a/runtime/moonbeam/src/lib.rs +++ b/runtime/moonbeam/src/lib.rs @@ -40,7 +40,7 @@ pub use fp_evm::GenesisAccount; pub use frame_support::traits::Get; use frame_support::{ construct_runtime, - dispatch::{DispatchClass, GetDispatchInfo}, + dispatch::{DispatchClass, GetDispatchInfo, PostDispatchInfo}, ensure, pallet_prelude::DispatchResult, parameter_types, @@ -71,7 +71,7 @@ use pallet_evm::{ FeeCalculator, GasWeightMapping, IdentityAddressMapping, OnChargeEVMTransaction as OnChargeEVMTransactionT, Runner, }; -pub use pallet_parachain_staking::{InflationInfo, Range}; +pub use pallet_parachain_staking::{weights::WeightInfo, InflationInfo, Range}; use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -89,7 +89,8 @@ use sp_runtime::{ transaction_validity::{ InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, }, - ApplyExtrinsicResult, FixedPointNumber, Perbill, Permill, Perquintill, SaturatedConversion, + ApplyExtrinsicResult, DispatchErrorWithPostInfo, FixedPointNumber, Perbill, Permill, + Perquintill, SaturatedConversion, }; use sp_std::{convert::TryFrom, prelude::*}; @@ -700,6 +701,26 @@ impl pallet_parachain_staking::PayoutCollatorReward for PayoutCollatorO } } +pub struct OnInactiveCollator; +impl pallet_parachain_staking::OnInactiveCollator for OnInactiveCollator { + fn on_inactive_collator( + collator_id: AccountId, + round: pallet_parachain_staking::RoundIndex, + ) -> Result> { + let extra_weight = if !MoonbeamOrbiters::is_orbiter(round, collator_id.clone()) { + ParachainStaking::go_offline_inner(collator_id)?; + ::WeightInfo::go_offline( + pallet_parachain_staking::MAX_CANDIDATES, + ) + } else { + Weight::zero() + }; + + Ok(::DbWeight::get() + .reads(1) + .saturating_add(extra_weight)) + } +} type MonetaryGovernanceOrigin = EitherOfDiverse, governance::custom_origins::GeneralAdmin>; @@ -709,6 +730,8 @@ impl pallet_parachain_staking::Config for Runtime { type MonetaryGovernanceOrigin = MonetaryGovernanceOrigin; /// Minimum round length is 2 minutes (10 * 12 second block times) type MinBlocksPerRound = ConstU32<10>; + /// If a collator doesn't produce any block on this number of rounds, it is notified as inactive + type MaxOfflineRounds = ConstU32<1>; /// Rounds before the collator leaving the candidates request can be executed type LeaveCandidatesDelay = ConstU32<{ 4 * 7 }>; /// Rounds before the candidate bond increase/decrease can be executed @@ -736,6 +759,7 @@ impl pallet_parachain_staking::Config for Runtime { type BlockAuthor = AuthorInherent; type OnCollatorPayout = (); type PayoutCollatorReward = PayoutCollatorOrOrbiterReward; + type OnInactiveCollator = OnInactiveCollator; type OnNewRound = OnNewRound; type WeightInfo = moonbeam_weights::pallet_parachain_staking::WeightInfo; type MaxCandidates = ConstU32<200>; @@ -1726,6 +1750,14 @@ mod tests { ); } + #[test] + fn max_offline_rounds_lower_or_eq_than_reward_payment_delay() { + assert!( + get!(pallet_parachain_staking, MaxOfflineRounds, u32) + <= get!(pallet_parachain_staking, RewardPaymentDelay, u32) + ); + } + #[test] // Required migration is // pallet_parachain_staking::migrations::IncreaseMaxTopDelegationsPerCandidate diff --git a/runtime/moonriver/src/lib.rs b/runtime/moonriver/src/lib.rs index a993ee35593..79ce4bb832c 100644 --- a/runtime/moonriver/src/lib.rs +++ b/runtime/moonriver/src/lib.rs @@ -39,7 +39,7 @@ pub use fp_evm::GenesisAccount; pub use frame_support::traits::Get; use frame_support::{ construct_runtime, - dispatch::{DispatchClass, GetDispatchInfo}, + dispatch::{DispatchClass, GetDispatchInfo, PostDispatchInfo}, ensure, pallet_prelude::DispatchResult, parameter_types, @@ -70,7 +70,7 @@ use pallet_evm::{ FeeCalculator, GasWeightMapping, IdentityAddressMapping, OnChargeEVMTransaction as OnChargeEVMTransactionT, Runner, }; -pub use pallet_parachain_staking::{InflationInfo, Range}; +pub use pallet_parachain_staking::{weights::WeightInfo, InflationInfo, Range}; use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -87,7 +87,8 @@ use sp_runtime::{ transaction_validity::{ InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, }, - ApplyExtrinsicResult, FixedPointNumber, Perbill, Permill, Perquintill, SaturatedConversion, + ApplyExtrinsicResult, DispatchErrorWithPostInfo, FixedPointNumber, Perbill, Permill, + Perquintill, SaturatedConversion, }; use sp_std::{convert::TryFrom, prelude::*}; @@ -702,6 +703,27 @@ impl pallet_parachain_staking::PayoutCollatorReward for PayoutCollatorO } } +pub struct OnInactiveCollator; +impl pallet_parachain_staking::OnInactiveCollator for OnInactiveCollator { + fn on_inactive_collator( + collator_id: AccountId, + round: pallet_parachain_staking::RoundIndex, + ) -> Result> { + let extra_weight = if !MoonbeamOrbiters::is_orbiter(round, collator_id.clone()) { + ParachainStaking::go_offline_inner(collator_id)?; + ::WeightInfo::go_offline( + pallet_parachain_staking::MAX_CANDIDATES, + ) + } else { + Weight::zero() + }; + + Ok(::DbWeight::get() + .reads(1) + .saturating_add(extra_weight)) + } +} + type MonetaryGovernanceOrigin = EitherOfDiverse, governance::custom_origins::GeneralAdmin>; @@ -711,6 +733,8 @@ impl pallet_parachain_staking::Config for Runtime { type MonetaryGovernanceOrigin = MonetaryGovernanceOrigin; /// Minimum round length is 2 minutes (10 * 12 second block times) type MinBlocksPerRound = ConstU32<10>; + /// If a collator doesn't produce any block on this number of rounds, it is notified as inactive + type MaxOfflineRounds = ConstU32<2>; /// Rounds before the collator leaving the candidates request can be executed type LeaveCandidatesDelay = ConstU32<24>; /// Rounds before the candidate bond increase/decrease can be executed @@ -738,6 +762,7 @@ impl pallet_parachain_staking::Config for Runtime { type BlockAuthor = AuthorInherent; type OnCollatorPayout = (); type PayoutCollatorReward = PayoutCollatorOrOrbiterReward; + type OnInactiveCollator = OnInactiveCollator; type OnNewRound = OnNewRound; type WeightInfo = moonbeam_weights::pallet_parachain_staking::WeightInfo; type MaxCandidates = ConstU32<200>; @@ -1734,6 +1759,14 @@ mod tests { ); } + #[test] + fn max_offline_rounds_lower_or_eq_than_reward_payment_delay() { + assert!( + get!(pallet_parachain_staking, MaxOfflineRounds, u32) + <= get!(pallet_parachain_staking, RewardPaymentDelay, u32) + ); + } + #[test] // Required migration is // pallet_parachain_staking::migrations::IncreaseMaxTopDelegationsPerCandidate