diff --git a/node/src/parachain/dev_chain_spec.rs b/node/src/parachain/dev_chain_spec.rs index 0f0f920e..cba4e58b 100644 --- a/node/src/parachain/dev_chain_spec.rs +++ b/node/src/parachain/dev_chain_spec.rs @@ -133,6 +133,7 @@ fn configure_genesis( parachain_staking: ParachainStakingConfig { stakers, max_candidate_stake: staking::MAX_COLLATOR_STAKE, + slashing_enabled: true, }, inflation_manager: Default::default(), block_reward: BlockRewardConfig { diff --git a/node/src/parachain/krest_chain_spec.rs b/node/src/parachain/krest_chain_spec.rs index 347386ae..726b12ef 100644 --- a/node/src/parachain/krest_chain_spec.rs +++ b/node/src/parachain/krest_chain_spec.rs @@ -118,6 +118,7 @@ fn configure_genesis( parachain_staking: ParachainStakingConfig { stakers, max_candidate_stake: staking::MAX_COLLATOR_STAKE, + slashing_enabled: true, }, inflation_manager: Default::default(), block_reward: BlockRewardConfig { diff --git a/node/src/parachain/peaq_chain_spec.rs b/node/src/parachain/peaq_chain_spec.rs index c74e3f23..44354057 100644 --- a/node/src/parachain/peaq_chain_spec.rs +++ b/node/src/parachain/peaq_chain_spec.rs @@ -122,6 +122,7 @@ fn configure_genesis( parachain_staking: ParachainStakingConfig { stakers, max_candidate_stake: staking::MAX_COLLATOR_STAKE, + slashing_enabled: true, }, inflation_manager: Default::default(), block_reward: BlockRewardConfig { diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index d36dba90..083212f3 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -519,6 +519,15 @@ pub mod pallet { /// The commission for a collator has been changed. /// \[collator's account, new commission\] CollatorCommissionChanged(T::AccountId, Permill), + /// A collator have been slashed + /// \[collator's account, amount slashed\] + CollatorSlashed(T::AccountId, BalanceOf), + /// Slashing has been enabled/disabled + /// \[new slashing status\] + SlashingEnabledChanged(bool), + /// A collator was kicked out of the candidate pool because of malicious behavior + /// \[collator's account] + CollatorKicked(T::AccountId), } #[pallet::hooks] @@ -534,7 +543,13 @@ pub mod pallet { crate::migrations::on_runtime_upgrade::() } - fn on_finalize(_n: BlockNumberFor) { + fn on_finalize(n: BlockNumberFor) { + // Check if it's the first block of the round + let current_round = Self::round(); + if SlashingEnabled::::get() && current_round.first == n { + // Slash any collators that didn't author blocks in previous round + Self::get_collators_without_blocks(current_round.current - 1); + } Self::payout_collator(); } } @@ -677,15 +692,25 @@ pub mod pallet { pub(crate) type DelayedPayoutInfo = StorageValue<_, DelayedPayoutInfoT>, OptionQuery>; + // Slashing enabled/disabled option + #[pallet::storage] + #[pallet::getter(fn slashing_enabled)] + pub(crate) type SlashingEnabled = StorageValue<_, bool, ValueQuery>; + #[pallet::genesis_config] pub struct GenesisConfig { pub stakers: GenesisStaker, pub max_candidate_stake: BalanceOf, + pub slashing_enabled: bool, } impl Default for GenesisConfig { fn default() -> Self { - Self { stakers: Default::default(), max_candidate_stake: Default::default() } + Self { + stakers: Default::default(), + max_candidate_stake: Default::default(), + slashing_enabled: true, + } } } @@ -693,6 +718,7 @@ pub mod pallet { impl BuildGenesisConfig for GenesisConfig { fn build(&self) { MaxCollatorCandidateStake::::put(self.max_candidate_stake); + SlashingEnabled::::put(self.slashing_enabled); // Setup delegate & collators for &(ref actor, ref opt_val, balance) in &self.stakers { @@ -1986,6 +2012,15 @@ pub mod pallet { )); Ok(()) } + + #[pallet::call_index(21)] + #[pallet::weight(::WeightInfo::set_slashing_enabled())] + pub fn set_slashing_enabled(origin: OriginFor, enabled: bool) -> DispatchResult { + ensure_root(origin)?; + SlashingEnabled::::put(enabled); + Self::deposit_event(Event::SlashingEnabledChanged(enabled)); + Ok(()) + } } impl Pallet { @@ -2797,6 +2832,40 @@ pub mod pallet { T::PotId::get().into_account_truncating() } + // Get collators that didn't author blocks in previous round + fn get_collators_without_blocks(round: SessionIndex) { + let selected_candidates = Self::selected_candidates(); + selected_candidates.into_iter().for_each(|collator| { + if !CollatorBlocks::::contains_key(round, &collator) { + Self::kickout_faulty_collator(collator); + } + }); + } + + fn kickout_faulty_collator(collator: T::AccountId) { + let state = CandidatePool::::get(&collator).expect("Collator must exist"); + let mut candidates = TopCandidates::::get(); + if (candidates.len() as u32) <= T::MinRequiredCollators::get() { + return; + } + + if Self::remove_candidate(&collator, &state).is_err() { + log::error!("Failed to remove collator {:?}", collator); + } + + if candidates + .remove(&Stake { owner: collator.clone(), amount: state.total }) + .is_some() + { + // update top candidates + TopCandidates::::put(candidates); + // update total amount at stake from scratch + Self::update_total_stake(); + }; + + Self::deposit_event(Event::CollatorKicked(collator)); + } + /// Handles staking reward payout for previous session for one collator and their delegators /// At Worst: 5 DB Reads and 'MaxSelectedCandidate + 1' DB Writes /// Complexity: O(n) diff --git a/pallets/parachain-staking/src/migrations.rs b/pallets/parachain-staking/src/migrations.rs index 6c7a9b55..60e3b59a 100644 --- a/pallets/parachain-staking/src/migrations.rs +++ b/pallets/parachain-staking/src/migrations.rs @@ -2,13 +2,15 @@ use crate::{ pallet::{Config, Pallet, OLD_STAKING_ID, STAKING_ID}, - types::{Candidate, OldCandidate}, + types::{AccountIdOf, Candidate, OldCandidate}, CandidatePool, ForceNewRound, Round, }; use frame_support::{ - pallet_prelude::{GetStorageVersion, StorageVersion}, + pallet_prelude::{GetStorageVersion, StorageVersion, ValueQuery}, + storage_alias, traits::{Get, LockableCurrency, WithdrawReasons}, weights::Weight, + Twox64Concat, }; use pallet_balances::Locks; use sp_runtime::Permill; @@ -20,8 +22,9 @@ pub enum Versions { _V8 = 8, V9 = 9, V10 = 10, - #[default] V11 = 11, + #[default] + V12 = 12, } pub(crate) fn on_runtime_upgrade() -> Weight { @@ -31,7 +34,11 @@ pub(crate) fn on_runtime_upgrade() -> Weight { mod upgrade { use super::*; + use crate::pallet::SlashingEnabled; + #[storage_alias] + type CollatorBlock = + StorageMap, Twox64Concat, AccountIdOf, u32, ValueQuery>; /// Migration implementation that deletes the old reward rate config and changes the staking ID. pub struct Migrate(sp_std::marker::PhantomData); @@ -102,6 +109,21 @@ mod upgrade { log::info!("V11 Migrating Done."); } + + if onchain_storage_version < StorageVersion::new(Versions::V12 as u16) { + log::info!( + "Running storage migration from version {:?} to {:?}", + onchain_storage_version, + Versions::default() as u16 + ); + + // enable slashing + SlashingEnabled::::put(true); + weight_writes += 1; + + log::info!("V12 Migrating Done."); + } + // update onchain storage version StorageVersion::new(Versions::default() as u16).put::>(); weight_writes += 1; diff --git a/pallets/parachain-staking/src/mock.rs b/pallets/parachain-staking/src/mock.rs index b1816d15..fdaaa9ea 100644 --- a/pallets/parachain-staking/src/mock.rs +++ b/pallets/parachain-staking/src/mock.rs @@ -278,9 +278,13 @@ impl ExtBuilder { for delegator in self.delegators.clone() { stakers.push((delegator.0, Some(delegator.1), delegator.2)); } - stake::GenesisConfig:: { stakers, max_candidate_stake: 160_000_000 * DECIMALS } - .assimilate_storage(&mut t) - .expect("Parachain Staking's storage can be assimilated"); + stake::GenesisConfig:: { + stakers, + max_candidate_stake: 160_000_000 * DECIMALS, + slashing_enabled: true, + } + .assimilate_storage(&mut t) + .expect("Parachain Staking's storage can be assimilated"); // stashes are the AccountId let session_keys: Vec<_> = self diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index 0b3dff9f..d8e015e1 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -350,6 +350,7 @@ fn collator_exit_executes_after_delay() { .with_delegators(vec![(3, 1, 100), (4, 1, 100), (5, 2, 100), (6, 2, 100)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); assert_eq!(CandidatePool::::count(), 3); assert_eq!( StakePallet::total_collator_stake(), @@ -389,6 +390,7 @@ fn collator_exit_executes_after_delay() { // (within the last T::StakeDuration blocks) roll_to(25, vec![]); let expected = vec![ + Event::SlashingEnabledChanged(false), Event::MaxSelectedCandidatesSet(2, 5), Event::NewRound(5, 1), Event::NewRound(10, 2), @@ -420,6 +422,7 @@ fn collator_selection_chooses_top_candidates() { .with_collators(vec![(1, 100), (2, 90), (3, 80), (4, 70), (5, 60), (6, 50)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2]); assert_eq!( StakePallet::total_collator_stake(), @@ -433,7 +436,11 @@ fn collator_selection_chooses_top_candidates() { roll_to(8, vec![]); // should choose top MaxSelectedCandidates (5), in order assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2, 3, 4, 5]); - let expected = vec![Event::MaxSelectedCandidatesSet(2, 5), Event::NewRound(5, 1)]; + let expected = vec![ + Event::SlashingEnabledChanged(false), + Event::MaxSelectedCandidatesSet(2, 5), + Event::NewRound(5, 1), + ]; assert_eq!(events(), expected); assert_ok!(StakePallet::init_leave_candidates(RuntimeOrigin::signed(6))); assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2, 3, 4, 5],); @@ -454,6 +461,7 @@ fn collator_selection_chooses_top_candidates() { roll_to(27, vec![]); // should choose top MaxSelectedCandidates (5), in order let expected = vec![ + Event::SlashingEnabledChanged(false), Event::MaxSelectedCandidatesSet(2, 5), Event::NewRound(5, 1), Event::LeftTopCandidates(6), @@ -490,6 +498,7 @@ fn exit_queue_with_events() { .with_collators(vec![(1, 100), (2, 90), (3, 80), (4, 70), (5, 60), (6, 50)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); assert_eq!(CandidatePool::::count(), 6); assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2]); assert_ok!(StakePallet::set_max_selected_candidates(RuntimeOrigin::root(), 5)); @@ -498,7 +507,11 @@ fn exit_queue_with_events() { roll_to(8, vec![]); // should choose top MaxSelectedCandidates (5), in order assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2, 3, 4, 5]); - let mut expected = vec![Event::MaxSelectedCandidatesSet(2, 5), Event::NewRound(5, 1)]; + let mut expected = vec![ + Event::SlashingEnabledChanged(false), + Event::MaxSelectedCandidatesSet(2, 5), + Event::NewRound(5, 1), + ]; assert_eq!(events(), expected); assert_ok!(StakePallet::init_leave_candidates(RuntimeOrigin::signed(6))); assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2, 3, 4, 5]); @@ -586,6 +599,7 @@ fn execute_leave_candidates_with_delay() { .with_delegators(vec![(11, 1, 110), (12, 1, 120), (13, 2, 130), (14, 2, 140)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); assert_eq!(CandidatePool::::count(), 10); assert_eq!( StakePallet::total_collator_stake(), @@ -880,11 +894,16 @@ fn multiple_delegations() { .set_blocks_per_round(5) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); assert_ok!(StakePallet::set_max_selected_candidates(RuntimeOrigin::root(), 5)); roll_to(8, vec![]); // chooses top MaxSelectedCandidates (5), in order assert_eq!(StakePallet::selected_candidates().into_inner(), vec![1, 2, 3, 4, 5]); - let mut expected = vec![Event::MaxSelectedCandidatesSet(2, 5), Event::NewRound(5, 1)]; + let mut expected = vec![ + Event::SlashingEnabledChanged(false), + Event::MaxSelectedCandidatesSet(2, 5), + Event::NewRound(5, 1), + ]; assert_eq!(events(), expected); assert_noop!( StakePallet::delegate_another_candidate(RuntimeOrigin::signed(6), 1, 10), @@ -1048,6 +1067,7 @@ fn should_update_total_stake() { .set_blocks_per_round(5) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); let mut old_stake = StakePallet::total_collator_stake(); assert_eq!(old_stake, TotalStake { collators: 40, delegators: 30 }); assert_ok!(StakePallet::candidate_stake_more(RuntimeOrigin::signed(1), 50)); @@ -1164,6 +1184,8 @@ fn collators_bond() { .set_blocks_per_round(5) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false) + .expect("Failed to disable slashing"); roll_to(4, vec![]); assert_noop!( StakePallet::candidate_stake_more(RuntimeOrigin::signed(6), 50), @@ -1255,6 +1277,7 @@ fn delegators_bond() { .set_blocks_per_round(5) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); roll_to(4, vec![]); assert_noop!( StakePallet::join_delegators(RuntimeOrigin::signed(6), 2, 50), @@ -1414,11 +1437,12 @@ fn round_transitions() { .with_delegators(vec![(2, 1, 10), (3, 1, 10)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); // Default round every 5 blocks, but MinBlocksPerRound is 3 and we set it to min // 3 blocks roll_to(6, vec![]); // chooses top MaxSelectedCandidates (5), in order - let init = vec![Event::NewRound(5, 1)]; + let init = vec![Event::SlashingEnabledChanged(false), Event::NewRound(5, 1)]; assert_eq!(events(), init); assert_ok!(StakePallet::set_blocks_per_round(RuntimeOrigin::root(), 3)); assert_eq!(last_event(), MetaEvent::StakePallet(Event::BlocksPerRoundSet(1, 5, 5, 3))); @@ -1443,11 +1467,12 @@ fn round_transitions() { .with_delegators(vec![(2, 1, 10), (3, 1, 10)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); // Default round every 5 blocks, but MinBlocksPerRound is 3 and we set it to min // 3 blocks roll_to(7, vec![]); // chooses top MaxSelectedCandidates (5), in order - let init = vec![Event::NewRound(5, 1)]; + let init = vec![Event::SlashingEnabledChanged(false), Event::NewRound(5, 1)]; assert_eq!(events(), init); assert_ok!(StakePallet::set_blocks_per_round(RuntimeOrigin::root(), 3)); @@ -1697,6 +1722,7 @@ fn should_reward_delegators_below_min_stake() { .with_delegators(vec![(3, 2, stake_num)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); // impossible but lets assume it happened let mut state = StakePallet::candidate_pool(1).expect("CollatorState cannot be missing"); @@ -2440,6 +2466,7 @@ fn candidate_leaves() { .with_delegators(vec![(12, 1, 100), (13, 1, 10)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); assert_eq!( StakePallet::top_candidates().into_iter().map(|s| s.owner).collect::>(), vec![1, 2] @@ -3124,6 +3151,7 @@ fn force_new_round() { .with_collators(vec![(1, 100), (2, 100), (3, 100), (4, 100)]) .build() .execute_with(|| { + StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false).unwrap(); let mut round = RoundInfo { current: 0, first: 0, length: 5 }; assert_eq!(StakePallet::round(), round); assert_eq!(Session::validators(), vec![1, 2]); @@ -3990,3 +4018,106 @@ fn check_snapshot_is_cleared() { assert_eq!(at_stake.len(), 0); }); } + +#[test] +fn check_collator_kickout_without_slash() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)]) + .with_collators(vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 10), (6, 10)]) + .build() + .execute_with(|| { + let authors: Vec> = (0u64..=22).map(|i| Some(i % 2 + 1)).collect(); + + assert_ok!(StakePallet::set_max_selected_candidates(RuntimeOrigin::root(), 3)); + + // roll to new round + roll_to(10, authors.clone()); + + // sanity check + let round = RoundInfo { current: 2, first: 10, length: 5 }; + assert_eq!(StakePallet::round(), round); + assert_eq!(Session::validators(), vec![1, 2, 3]); + assert_eq!(Session::current_index(), 2); + let collator_blocks = + >::iter_prefix(1).collect::>(); + assert_eq!(CandidatePool::::count(), 5); + assert_eq!(collator_blocks.len(), 2); + assert!(events().contains(&Event::CollatorKicked(3))); + roll_to(20, authors.clone()); + + assert_eq!(StakePallet::selected_candidates().contains(&1), true); + assert_eq!(StakePallet::selected_candidates().contains(&2), true); + assert_eq!(StakePallet::selected_candidates().contains(&3), false); + assert_eq!(StakePallet::selected_candidates().contains(&4), false); + assert_eq!(StakePallet::selected_candidates().contains(&5), false); + assert_eq!(StakePallet::selected_candidates().contains(&6), true); + }); +} + +#[test] +fn check_no_slashing() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)]) + .with_collators(vec![(1, 10), (2, 10), (3, 10)]) + .build() + .execute_with(|| { + let authors: Vec> = (0u64..=22).map(|i| Some(i % 3 + 1)).collect(); + + assert_ok!(StakePallet::set_max_selected_candidates(RuntimeOrigin::root(), 3)); + + // roll to new round + roll_to(10, authors.clone()); + + // sanity check + let round = RoundInfo { current: 2, first: 10, length: 5 }; + assert_eq!(StakePallet::round(), round); + assert_eq!(Session::validators(), vec![1, 2, 3]); + assert_eq!(Session::current_index(), 2); + let collator_blocks = + >::iter_prefix(1).collect::>(); + assert_eq!(CandidatePool::::count(), 3); + assert_eq!(collator_blocks.len(), 3); + assert!(!events().iter().any(|event| { matches!(event, Event::CollatorKicked(_)) })); + roll_to(20, authors.clone()); + + assert_eq!(StakePallet::selected_candidates().contains(&1), true); + assert_eq!(StakePallet::selected_candidates().contains(&2), true); + assert_eq!(StakePallet::selected_candidates().contains(&3), true); + }); +} + +#[test] +fn check_disable_slashing() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)]) + .with_collators(vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 10), (6, 10)]) + .build() + .execute_with(|| { + let authors: Vec> = (0u64..=22).map(|i| Some(i % 2 + 1)).collect(); + + assert_ok!(StakePallet::set_max_selected_candidates(RuntimeOrigin::root(), 3)); + assert_ok!(StakePallet::set_slashing_enabled(RuntimeOrigin::root(), false)); + + // roll to new round + roll_to(10, authors.clone()); + + // sanity check + let round = RoundInfo { current: 2, first: 10, length: 5 }; + assert_eq!(StakePallet::round(), round); + assert_eq!(Session::validators(), vec![1, 2, 3]); + assert_eq!(Session::current_index(), 2); + let collator_blocks = + >::iter_prefix(1).collect::>(); + assert_eq!(CandidatePool::::count(), 6); + assert_eq!(collator_blocks.len(), 2); + assert!(!events().iter().any(|event| { matches!(event, Event::CollatorKicked(_)) })); + roll_to(20, authors.clone()); + + assert_eq!(StakePallet::selected_candidates().contains(&1), true); + assert_eq!(StakePallet::selected_candidates().contains(&2), true); + assert_eq!(StakePallet::selected_candidates().contains(&3), true); + assert_eq!(StakePallet::selected_candidates().contains(&4), false); + assert_eq!(StakePallet::selected_candidates().contains(&5), false); + assert_eq!(StakePallet::selected_candidates().contains(&6), false); + }); +} diff --git a/pallets/parachain-staking/src/weightinfo.rs b/pallets/parachain-staking/src/weightinfo.rs index 4c8420d1..ccd5b5e9 100644 --- a/pallets/parachain-staking/src/weightinfo.rs +++ b/pallets/parachain-staking/src/weightinfo.rs @@ -24,4 +24,5 @@ pub trait WeightInfo { fn unlock_unstaked(u: u32) -> Weight; fn set_max_candidate_stake() -> Weight; fn set_commission(n: u32, m: u32) -> Weight; + fn set_slashing_enabled() -> Weight; } diff --git a/pallets/parachain-staking/src/weights.rs b/pallets/parachain-staking/src/weights.rs index 89557e0b..5cdba254 100644 --- a/pallets/parachain-staking/src/weights.rs +++ b/pallets/parachain-staking/src/weights.rs @@ -537,4 +537,15 @@ impl crate::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `ParachainStaking::SlashingEnabled` (r:0 w:1) + /// Proof: `ParachainStaking::SlashingEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + fn set_slashing_enabled() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(4_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/precompiles/parachain-staking/src/mock.rs b/precompiles/parachain-staking/src/mock.rs index 83647784..ed7ea6e4 100644 --- a/precompiles/parachain-staking/src/mock.rs +++ b/precompiles/parachain-staking/src/mock.rs @@ -333,6 +333,7 @@ impl ExtBuilder { parachain_staking::GenesisConfig:: { stakers, max_candidate_stake: 160_000_000 * DECIMALS, + slashing_enabled: true, } .assimilate_storage(&mut t) .expect("Parachain Staking's storage can be assimilated");