From 065e64d1e414ca4478c9d3665889994c04705c10 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov <tsvetomir@parity.io> Date: Sun, 16 Mar 2025 10:36:02 +0200 Subject: [PATCH] Revert "[Staking] Bounded Slashing: Paginated Offence Processing & Slash Application (#7424)" This reverts commit dda2cb5969985ccbf67581e18eb7c579849e27bb. --- polkadot/runtime/test-runtime/src/lib.rs | 4 +- polkadot/runtime/westend/src/lib.rs | 1 - .../westend/src/weights/pallet_staking.rs | 28 - prdoc/pr_7424.prdoc | 37 - substrate/bin/node/runtime/src/lib.rs | 54 +- substrate/frame/babe/src/mock.rs | 4 +- substrate/frame/beefy/src/mock.rs | 4 +- .../test-staking-e2e/src/mock.rs | 9 +- substrate/frame/grandpa/src/mock.rs | 4 +- .../frame/offences/benchmarking/src/inner.rs | 15 +- .../frame/offences/benchmarking/src/mock.rs | 5 +- substrate/frame/root-offences/src/lib.rs | 17 +- substrate/frame/root-offences/src/mock.rs | 11 +- substrate/frame/root-offences/src/tests.rs | 12 +- .../frame/session/benchmarking/src/mock.rs | 6 +- substrate/frame/staking/src/benchmarking.rs | 70 +- substrate/frame/staking/src/lib.rs | 42 +- substrate/frame/staking/src/migrations.rs | 340 ++++++--- substrate/frame/staking/src/mock.rs | 42 +- substrate/frame/staking/src/pallet/impls.rs | 259 +++---- substrate/frame/staking/src/pallet/mod.rs | 195 +----- substrate/frame/staking/src/slashing.rs | 400 ++++------- substrate/frame/staking/src/tests.rs | 651 ++++++++++++------ substrate/frame/staking/src/weights.rs | 56 +- 24 files changed, 1102 insertions(+), 1164 deletions(-) delete mode 100644 prdoc/pr_7424.prdoc diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 226e22c0783..694077dd21c 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -323,8 +323,8 @@ impl pallet_session::Config for Runtime { } impl pallet_session::historical::Config for Runtime { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<AccountId, Balance>; + type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>; } pallet_staking_reward_curve::build! { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index b5dc9b8f55c..86358afb23e 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1874,7 +1874,6 @@ pub mod migrations { parachains_shared::migration::MigrateToV1<Runtime>, parachains_scheduler::migration::MigrateV2ToV3<Runtime>, pallet_staking::migrations::v16::MigrateV15ToV16<Runtime>, - pallet_staking::migrations::v17::MigrateV16ToV17<Runtime>, pallet_session::migrations::v1::MigrateV0ToV1< Runtime, pallet_staking::migrations::v17::MigrateDisabledToSession<Runtime>, diff --git a/polkadot/runtime/westend/src/weights/pallet_staking.rs b/polkadot/runtime/westend/src/weights/pallet_staking.rs index 496bc01e5e3..b92d54f3b94 100644 --- a/polkadot/runtime/westend/src/weights/pallet_staking.rs +++ b/polkadot/runtime/westend/src/weights/pallet_staking.rs @@ -847,34 +847,6 @@ impl<T: frame_system::Config> pallet_staking::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } - /// Storage: `Staking::ActiveEra` (r:1 w:0) - /// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`) - /// Storage: `Staking::UnappliedSlashes` (r:1 w:1) - /// Proof: `Staking::UnappliedSlashes` (`max_values`: None, `max_size`: Some(3231), added: 5706, mode: `MaxEncodedLen`) - /// Storage: `Staking::Bonded` (r:65 w:0) - /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) - /// Storage: `Staking::Ledger` (r:65 w:65) - /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) - /// Storage: `NominationPools::ReversePoolIdLookup` (r:65 w:0) - /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) - /// Storage: `DelegatedStaking::Agents` (r:65 w:65) - /// Proof: `DelegatedStaking::Agents` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:65 w:65) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Staking::VirtualStakers` (r:65 w:0) - /// Proof: `Staking::VirtualStakers` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) - /// Storage: `Balances::Holds` (r:65 w:65) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) - fn apply_slash() -> Weight { - // Proof Size summary in bytes: - // Measured: `29228` - // Estimated: `232780` - // Minimum execution time: 3_571_461_000 picoseconds. - Weight::from_parts(3_638_696_000, 0) - .saturating_add(Weight::from_parts(0, 232780)) - .saturating_add(T::DbWeight::get().reads(457)) - .saturating_add(T::DbWeight::get().writes(261)) - } /// Storage: `Staking::CurrentEra` (r:1 w:0) /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0) diff --git a/prdoc/pr_7424.prdoc b/prdoc/pr_7424.prdoc deleted file mode 100644 index e177f41371b..00000000000 --- a/prdoc/pr_7424.prdoc +++ /dev/null @@ -1,37 +0,0 @@ -# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 -# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json - -title: 'Bounded Slashing: Paginated Offence Processing & Slash Application' - -doc: - - audience: Runtime Dev - description: | - This PR refactors the slashing mechanism in `pallet-staking` to be bounded by introducing paged offence processing and paged slash application. - - ### Key Changes - - Offences are queued instead of being processed immediately. - - Slashes are computed in pages, stored as a `StorageDoubleMap` with `(Validator, SlashFraction, PageIndex)` to uniquely identify them. - - Slashes are applied incrementally across multiple blocks instead of a single unbounded operation. - - New storage items: `OffenceQueue`, `ProcessingOffence`, `OffenceQueueEras`. - - Updated API for cancelling and applying slashes. - - Preliminary benchmarks added; further optimizations planned. - - This enables staking slashing to scale efficiently and removes a major blocker for staking migration to a parachain (AH). - -crates: -- name: pallet-babe - bump: patch -- name: pallet-staking - bump: major -- name: pallet-grandpa - bump: patch -- name: westend-runtime - bump: minor -- name: pallet-beefy - bump: patch -- name: pallet-offences-benchmarking - bump: patch -- name: pallet-session-benchmarking - bump: patch -- name: pallet-root-offences - bump: patch \ No newline at end of file diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 94729a26b6d..c618831c0a7 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -680,6 +680,8 @@ impl_opaque_keys! { #[cfg(feature = "staking-playground")] pub mod staking_playground { + use pallet_staking::Exposure; + use super::*; /// An adapter to make the chain work with --dev only, even though it is running a large staking @@ -714,43 +716,61 @@ pub mod staking_playground { } } - impl pallet_session::historical::SessionManager<AccountId, ()> for AliceAsOnlyValidator { + impl pallet_session::historical::SessionManager<AccountId, Exposure<AccountId, Balance>> + for AliceAsOnlyValidator + { fn end_session(end_index: sp_staking::SessionIndex) { - <Staking as pallet_session::historical::SessionManager<AccountId, ()>>::end_session( - end_index, - ) + <Staking as pallet_session::historical::SessionManager< + AccountId, + Exposure<AccountId, Balance>, + >>::end_session(end_index) } - fn new_session(new_index: sp_staking::SessionIndex) -> Option<Vec<(AccountId, ())>> { - <Staking as pallet_session::historical::SessionManager<AccountId, ()>>::new_session( - new_index, - ) + fn new_session( + new_index: sp_staking::SessionIndex, + ) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> { + <Staking as pallet_session::historical::SessionManager< + AccountId, + Exposure<AccountId, Balance>, + >>::new_session(new_index) .map(|_ignored| { // construct a fake exposure for alice. - vec![(sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(), ())] + vec![( + sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(), + pallet_staking::Exposure { + total: 1_000_000_000, + own: 1_000_000_000, + others: vec![], + }, + )] }) } fn new_session_genesis( new_index: sp_staking::SessionIndex, - ) -> Option<Vec<(AccountId, ())>> { + ) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> { <Staking as pallet_session::historical::SessionManager< AccountId, - (), + Exposure<AccountId, Balance>, >>::new_session_genesis(new_index) .map(|_ignored| { // construct a fake exposure for alice. vec![( sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(), - (), + pallet_staking::Exposure { + total: 1_000_000_000, + own: 1_000_000_000, + others: vec![], + }, )] }) } fn start_session(start_index: sp_staking::SessionIndex) { - <Staking as pallet_session::historical::SessionManager<AccountId, ()>>::start_session( - start_index, - ) + <Staking as pallet_session::historical::SessionManager< + AccountId, + Exposure<AccountId, Balance>, + >>::start_session(start_index) } } } @@ -776,8 +796,8 @@ impl pallet_session::Config for Runtime { } impl pallet_session::historical::Config for Runtime { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<AccountId, Balance>; + type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>; } pallet_staking_reward_curve::build! { diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index ea977a547fe..eeaebe02d3e 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -105,8 +105,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<u64, u128>; + type FullIdentificationOf = pallet_staking::ExposureOf<Self>; } impl pallet_authorship::Config for Test { diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index 275bf18fe87..46491996623 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -189,8 +189,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<u64, u128>; + type FullIdentificationOf = pallet_staking::ExposureOf<Self>; } impl pallet_authorship::Config for Test { diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index e4b77975707..135a52fece6 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -147,8 +147,8 @@ impl pallet_session::Config for Runtime { type WeightInfo = (); } impl pallet_session::historical::Config for Runtime { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<AccountId, Balance>; + type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>; } frame_election_provider_support::generate_solution_type!( @@ -909,7 +909,10 @@ pub(crate) fn on_offence_now( // Add offence to validator, slash it. pub(crate) fn add_slash(who: &AccountId) { on_offence_now( - &[OffenceDetails { offender: (*who, ()), reporters: vec![] }], + &[OffenceDetails { + offender: (*who, Staking::eras_stakers(active_era(), who)), + reporters: vec![], + }], &[Perbill::from_percent(10)], ); } diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs index 482e767d32f..2fd0cbb5ffd 100644 --- a/substrate/frame/grandpa/src/mock.rs +++ b/substrate/frame/grandpa/src/mock.rs @@ -109,8 +109,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<u64, u128>; + type FullIdentificationOf = pallet_staking::ExposureOf<Self>; } impl pallet_authorship::Config for Test { diff --git a/substrate/frame/offences/benchmarking/src/inner.rs b/substrate/frame/offences/benchmarking/src/inner.rs index fa4349d1d94..3d3cd470bc2 100644 --- a/substrate/frame/offences/benchmarking/src/inner.rs +++ b/substrate/frame/offences/benchmarking/src/inner.rs @@ -170,13 +170,6 @@ fn make_offenders<T: Config>( Ok(id_tuples) } -#[cfg(test)] -fn run_staking_next_block<T: Config>() { - use frame_support::traits::Hooks; - System::<T>::set_block_number(System::<T>::block_number().saturating_add(1u32.into())); - Staking::<T>::on_initialize(System::<T>::block_number()); -} - #[cfg(test)] fn assert_all_slashes_applied<T>(offender_count: usize) where @@ -189,10 +182,10 @@ where // make sure that all slashes have been applied // deposit to reporter + reporter account endowed. assert_eq!(System::<T>::read_events_for_pallet::<pallet_balances::Event<T>>().len(), 2); - // (n nominators + one validator) * slashed + Slash Reported + Slash Computed + // (n nominators + one validator) * slashed + Slash Reported assert_eq!( System::<T>::read_events_for_pallet::<pallet_staking::Event<T>>().len(), - 1 * (offender_count + 1) as usize + 2 + 1 * (offender_count + 1) as usize + 1 ); // offence assert_eq!(System::<T>::read_events_for_pallet::<pallet_offences::Event>().len(), 1); @@ -239,8 +232,6 @@ mod benchmarks { #[cfg(test)] { - // slashes applied at the next block. - run_staking_next_block::<T>(); assert_all_slashes_applied::<T>(n as usize); } @@ -275,8 +266,6 @@ mod benchmarks { } #[cfg(test)] { - // slashes applied at the next block. - run_staking_next_block::<T>(); assert_all_slashes_applied::<T>(n as usize); } diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs index 63e440d9e00..f37dbf55f52 100644 --- a/substrate/frame/offences/benchmarking/src/mock.rs +++ b/substrate/frame/offences/benchmarking/src/mock.rs @@ -33,6 +33,7 @@ use sp_runtime::{ }; type AccountId = u64; +type Balance = u64; #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { @@ -53,8 +54,8 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<AccountId, Balance>; + type FullIdentificationOf = pallet_staking::ExposureOf<Test>; } sp_runtime::impl_opaque_keys! { diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs index 8e91c4ecfd1..fd6ffc55e40 100644 --- a/substrate/frame/root-offences/src/lib.rs +++ b/substrate/frame/root-offences/src/lib.rs @@ -31,7 +31,7 @@ extern crate alloc; use alloc::vec::Vec; use pallet_session::historical::IdentificationTuple; -use pallet_staking::Pallet as Staking; +use pallet_staking::{BalanceOf, Exposure, ExposureOf, Pallet as Staking}; use sp_runtime::Perbill; use sp_staking::offence::OnOffenceHandler; @@ -49,8 +49,11 @@ pub mod pallet { + pallet_staking::Config + pallet_session::Config<ValidatorId = <Self as frame_system::Config>::AccountId> + pallet_session::historical::Config< - FullIdentification = (), - FullIdentificationOf = pallet_staking::NullIdentity, + FullIdentification = Exposure< + <Self as frame_system::Config>::AccountId, + BalanceOf<Self>, + >, + FullIdentificationOf = ExposureOf<Self>, > { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; @@ -103,11 +106,15 @@ pub mod pallet { fn get_offence_details( offenders: Vec<(T::AccountId, Perbill)>, ) -> Result<Vec<OffenceDetails<T>>, DispatchError> { + let now = pallet_staking::ActiveEra::<T>::get() + .map(|e| e.index) + .ok_or(Error::<T>::FailedToGetActiveEra)?; + Ok(offenders .clone() .into_iter() .map(|(o, _)| OffenceDetails::<T> { - offender: (o.clone(), ()), + offender: (o.clone(), Staking::<T>::eras_stakers(now, &o)), reporters: Default::default(), }) .collect()) @@ -117,7 +124,7 @@ pub mod pallet { fn submit_offence(offenders: &[OffenceDetails<T>], slash_fraction: &[Perbill]) { let session_index = <pallet_session::Pallet<T> as frame_support::traits::ValidatorSet<T::AccountId>>::session_index(); - <Staking<T> as OnOffenceHandler< + <pallet_staking::Pallet<T> as OnOffenceHandler< T::AccountId, IdentificationTuple<T>, Weight, diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs index ce55bdcbdd3..09223802f67 100644 --- a/substrate/frame/root-offences/src/mock.rs +++ b/substrate/frame/root-offences/src/mock.rs @@ -28,7 +28,7 @@ use frame_support::{ traits::{ConstU32, ConstU64, OneSessionHandler}, BoundedVec, }; -use pallet_staking::{BalanceOf, StakerStatus}; +use pallet_staking::StakerStatus; use sp_core::ConstBool; use sp_runtime::{curve::PiecewiseLinear, testing::UintAuthorityId, traits::Zero, BuildStorage}; use sp_staking::{EraIndex, SessionIndex}; @@ -148,8 +148,8 @@ impl pallet_staking::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Exposure<AccountId, Balance>; + type FullIdentificationOf = pallet_staking::ExposureOf<Test>; } sp_runtime::impl_opaque_keys! { @@ -298,11 +298,6 @@ pub(crate) fn run_to_block(n: BlockNumber) { ); } -/// Progress by n block. -pub(crate) fn advance_blocks(n: u64) { - run_to_block(System::block_number() + n); -} - pub(crate) fn active_era() -> EraIndex { pallet_staking::ActiveEra::<Test>::get().unwrap().index } diff --git a/substrate/frame/root-offences/src/tests.rs b/substrate/frame/root-offences/src/tests.rs index da6c49895be..289bb708efb 100644 --- a/substrate/frame/root-offences/src/tests.rs +++ b/substrate/frame/root-offences/src/tests.rs @@ -17,10 +17,7 @@ use super::*; use frame_support::{assert_err, assert_ok}; -use mock::{ - active_era, advance_blocks, start_session, ExtBuilder, RootOffences, RuntimeOrigin, System, - Test as T, -}; +use mock::{active_era, start_session, ExtBuilder, RootOffences, RuntimeOrigin, System, Test as T}; use pallet_staking::asset; #[test] @@ -45,10 +42,6 @@ fn create_offence_works_given_root_origin() { assert_ok!(RootOffences::create_offence(RuntimeOrigin::root(), offenders.clone())); System::assert_last_event(Event::OffenceCreated { offenders }.into()); - - // offence is processed in the following block. - advance_blocks(1); - // the slash should be applied right away. assert_eq!(asset::staked::<T>(&11), 500); @@ -73,9 +66,6 @@ fn create_offence_wont_slash_non_active_validators() { System::assert_last_event(Event::OffenceCreated { offenders }.into()); - // advance to the next block so offence gets processed. - advance_blocks(1); - // so 31 didn't get slashed. assert_eq!(asset::staked::<T>(&31), 500); diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs index 746c3b12e97..235209f14ca 100644 --- a/substrate/frame/session/benchmarking/src/mock.rs +++ b/substrate/frame/session/benchmarking/src/mock.rs @@ -27,11 +27,11 @@ use frame_support::{ derive_impl, parameter_types, traits::{ConstU32, ConstU64}, }; -use pallet_staking::NullIdentity; use sp_runtime::{traits::IdentityLookup, BuildStorage, KeyTypeId}; type AccountId = u64; type Nonce = u32; +type Balance = u64; type Block = frame_system::mocking::MockBlock<Test>; @@ -68,8 +68,8 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = NullIdentity; + type FullIdentification = pallet_staking::Exposure<AccountId, Balance>; + type FullIdentificationOf = pallet_staking::ExposureOf<Test>; } sp_runtime::impl_opaque_keys! { diff --git a/substrate/frame/staking/src/benchmarking.rs b/substrate/frame/staking/src/benchmarking.rs index c4299449196..ce4f0178a24 100644 --- a/substrate/frame/staking/src/benchmarking.rs +++ b/substrate/frame/staking/src/benchmarking.rs @@ -802,33 +802,21 @@ mod benchmarks { #[benchmark] fn cancel_deferred_slash(s: Linear<1, MAX_SLASHES>) { + let mut unapplied_slashes = Vec::new(); let era = EraIndex::one(); - let dummy_account = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); - - // Insert `s` unapplied slashes with the new key structure - for i in 0..s { - let slash_key = (dummy_account(), Perbill::from_percent(i as u32 % 100), i); - let unapplied_slash = UnappliedSlash::<T> { - validator: slash_key.0.clone(), - own: Zero::zero(), - others: WeakBoundedVec::default(), - reporter: Default::default(), - payout: Zero::zero(), - }; - UnappliedSlashes::<T>::insert(era, slash_key.clone(), unapplied_slash); + let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); + for _ in 0..MAX_SLASHES { + unapplied_slashes + .push(UnappliedSlash::<T::AccountId, BalanceOf<T>>::default_from(dummy())); } + UnappliedSlashes::<T>::insert(era, &unapplied_slashes); - let slash_keys: Vec<_> = (0..s) - .map(|i| (dummy_account(), Perbill::from_percent(i as u32 % 100), i)) - .collect(); + let slash_indices: Vec<u32> = (0..s).collect(); #[extrinsic_call] - _(RawOrigin::Root, era, slash_keys.clone()); + _(RawOrigin::Root, era, slash_indices); - // Ensure all `s` slashes are removed - for key in &slash_keys { - assert!(UnappliedSlashes::<T>::get(era, key).is_none()); - } + assert_eq!(UnappliedSlashes::<T>::get(&era).len(), (MAX_SLASHES - s) as usize); } #[benchmark] @@ -1149,46 +1137,6 @@ mod benchmarks { Ok(()) } - #[benchmark] - fn apply_slash() -> Result<(), BenchmarkError> { - let era = EraIndex::one(); - ActiveEra::<T>::put(ActiveEraInfo { index: era, start: None }); - let (validator, nominators) = create_validator_with_nominators::<T>( - T::MaxExposurePageSize::get() as u32, - T::MaxExposurePageSize::get() as u32, - false, - true, - RewardDestination::Staked, - era, - )?; - let slash_fraction = Perbill::from_percent(10); - let page_index = 0; - let slashed_balance = BalanceOf::<T>::from(10u32); - - let slash_key = (validator.clone(), slash_fraction, page_index); - let slashed_nominators = - nominators.iter().map(|(n, _)| (n.clone(), slashed_balance)).collect::<Vec<_>>(); - - let unapplied_slash = UnappliedSlash::<T> { - validator: validator.clone(), - own: slashed_balance, - others: WeakBoundedVec::force_from(slashed_nominators, None), - reporter: Default::default(), - payout: Zero::zero(), - }; - - // Insert an unapplied slash to be processed. - UnappliedSlashes::<T>::insert(era, slash_key.clone(), unapplied_slash); - - #[extrinsic_call] - _(RawOrigin::Signed(validator.clone()), era, slash_key.clone()); - - // Ensure the slash has been applied and removed. - assert!(UnappliedSlashes::<T>::get(era, &slash_key).is_none()); - - Ok(()) - } - #[benchmark] fn manual_slash() -> Result<(), BenchmarkError> { let era = EraIndex::zero(); diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 922df9f8c32..1247470edf4 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -353,7 +353,7 @@ use frame_support::{ ConstU32, Contains, Defensive, DefensiveMax, DefensiveSaturating, Get, LockIdentifier, }, weights::Weight, - BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, WeakBoundedVec, + BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use scale_info::TypeInfo; use sp_runtime::{ @@ -923,19 +923,31 @@ impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEn /// A pending slash record. The value of the slash has been computed but not applied yet, /// rather deferred for several eras. -#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEqNoBound)] -#[scale_info(skip_type_params(T))] -pub struct UnappliedSlash<T: Config> { +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct UnappliedSlash<AccountId, Balance: HasCompact> { /// The stash ID of the offending validator. - validator: T::AccountId, + validator: AccountId, /// The validator's own slash. - own: BalanceOf<T>, + own: Balance, /// All other slashed stakers and amounts. - others: WeakBoundedVec<(T::AccountId, BalanceOf<T>), T::MaxExposurePageSize>, + others: Vec<(AccountId, Balance)>, /// Reporters of the offence; bounty payout recipients. - reporter: Option<T::AccountId>, + reporters: Vec<AccountId>, /// The amount of payout. - payout: BalanceOf<T>, + payout: Balance, +} + +impl<AccountId, Balance: HasCompact + Zero> UnappliedSlash<AccountId, Balance> { + /// Initializes the default object using the given `validator`. + pub fn default_from(validator: AccountId) -> Self { + Self { + validator, + own: Zero::zero(), + others: vec![], + reporters: vec![], + payout: Zero::zero(), + } + } } /// Something that defines the maximum number of nominations per nominator based on a curve. @@ -983,7 +995,10 @@ pub trait SessionInterface<AccountId> { impl<T: Config> SessionInterface<<T as frame_system::Config>::AccountId> for T where T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>, - T: pallet_session::historical::Config, + T: pallet_session::historical::Config< + FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>, + FullIdentificationOf = ExposureOf<T>, + >, T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>, T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>, T::ValidatorIdOf: Convert< @@ -1127,13 +1142,6 @@ impl<T: Config> Convert<T::AccountId, Option<Exposure<T::AccountId, BalanceOf<T> } } -pub struct NullIdentity; -impl<T> Convert<T, Option<()>> for NullIdentity { - fn convert(_: T) -> Option<()> { - Some(()) - } -} - /// Filter historical offences out and only allow those from the bonding period. pub struct FilterHistoricalOffences<T, R> { _inner: core::marker::PhantomData<(T, R)>, diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index 5b0118da67e..274ed212b3a 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -18,12 +18,12 @@ //! [CHANGELOG.md](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/staking/CHANGELOG.md). use super::*; +use frame_election_provider_support::SortedListProvider; use frame_support::{ migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias, traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade}, - Twox64Concat, }; #[cfg(feature = "try-runtime")] @@ -36,6 +36,10 @@ use sp_runtime::TryRuntimeError; /// Obsolete from v13. Keeping around to make encoding/decoding of old migration code easier. #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] enum ObsoleteReleases { + V1_0_0Ancient, + V2_0_0, + V3_0_0, + V4_0_0, V5_0_0, // blockable validators. V6_0_0, // removal of all storage associated with offchain phragmen. V7_0_0, // keep track of number of nominators / validators in map @@ -62,86 +66,6 @@ type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, Value pub mod v17 { use super::*; - #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] - struct OldUnappliedSlash<T: Config> { - validator: T::AccountId, - /// The validator's own slash. - own: BalanceOf<T>, - /// All other slashed stakers and amounts. - others: Vec<(T::AccountId, BalanceOf<T>)>, - /// Reporters of the offence; bounty payout recipients. - reporters: Vec<T::AccountId>, - /// The amount of payout. - payout: BalanceOf<T>, - } - - #[frame_support::storage_alias] - pub type OldUnappliedSlashes<T: Config> = - StorageMap<Pallet<T>, Twox64Concat, EraIndex, Vec<OldUnappliedSlash<T>>, ValueQuery>; - - #[frame_support::storage_alias] - pub type DisabledValidators<T: Config> = - StorageValue<Pallet<T>, BoundedVec<(u32, OffenceSeverity), ConstU32<100>>, ValueQuery>; - - pub struct VersionUncheckedMigrateV16ToV17<T>(core::marker::PhantomData<T>); - impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV16ToV17<T> { - fn on_runtime_upgrade() -> Weight { - let mut weight: Weight = Weight::zero(); - - OldUnappliedSlashes::<T>::drain().for_each(|(era, slashes)| { - weight.saturating_accrue(T::DbWeight::get().reads(1)); - - for slash in slashes { - let validator = slash.validator.clone(); - let new_slash = UnappliedSlash { - validator: validator.clone(), - own: slash.own, - others: WeakBoundedVec::force_from(slash.others, None), - payout: slash.payout, - reporter: slash.reporters.first().cloned(), - }; - - // creating a slash key which is improbable to conflict with a new offence. - let slash_key = (validator, Perbill::from_percent(99), 9999); - UnappliedSlashes::<T>::insert(era, slash_key, new_slash); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - } - }); - - weight - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> { - let mut expected_slashes: u32 = 0; - OldUnappliedSlashes::<T>::iter().for_each(|(_, slashes)| { - expected_slashes += slashes.len() as u32; - }); - - Ok(expected_slashes.encode()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> { - let expected_slash_count = - u32::decode(&mut state.as_slice()).expect("Failed to decode state"); - - let actual_slash_count = UnappliedSlashes::<T>::iter().count() as u32; - - ensure!(expected_slash_count == actual_slash_count, "Slash count mismatch"); - - Ok(()) - } - } - - pub type MigrateV16ToV17<T> = VersionedMigration< - 16, - 17, - VersionUncheckedMigrateV16ToV17<T>, - Pallet<T>, - <T as frame_system::Config>::DbWeight, - >; - pub struct MigrateDisabledToSession<T>(core::marker::PhantomData<T>); impl<T: Config> pallet_session::migrations::v1::MigrateDisabledValidators for MigrateDisabledToSession<T> @@ -543,3 +467,257 @@ pub mod v11 { } } } + +pub mod v10 { + use super::*; + use frame_support::storage_alias; + + #[storage_alias] + type EarliestUnappliedSlash<T: Config> = StorageValue<Pallet<T>, EraIndex>; + + /// Apply any pending slashes that where queued. + /// + /// That means we might slash someone a bit too early, but we will definitely + /// won't forget to slash them. The cap of 512 is somewhat randomly taken to + /// prevent us from iterating over an arbitrary large number of keys `on_runtime_upgrade`. + pub struct MigrateToV10<T>(core::marker::PhantomData<T>); + impl<T: Config> OnRuntimeUpgrade for MigrateToV10<T> { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + if StorageVersion::<T>::get() == ObsoleteReleases::V9_0_0 { + let pending_slashes = UnappliedSlashes::<T>::iter().take(512); + for (era, slashes) in pending_slashes { + for slash in slashes { + // in the old slashing scheme, the slash era was the key at which we read + // from `UnappliedSlashes`. + log!(warn, "prematurely applying a slash ({:?}) for era {:?}", slash, era); + slashing::apply_slash::<T>(slash, era); + } + } + + EarliestUnappliedSlash::<T>::kill(); + StorageVersion::<T>::put(ObsoleteReleases::V10_0_0); + + log!(info, "MigrateToV10 executed successfully"); + T::DbWeight::get().reads_writes(1, 2) + } else { + log!(warn, "MigrateToV10 should be removed."); + T::DbWeight::get().reads(1) + } + } + } +} + +pub mod v9 { + use super::*; + #[cfg(feature = "try-runtime")] + use alloc::vec::Vec; + #[cfg(feature = "try-runtime")] + use codec::{Decode, Encode}; + + /// Migration implementation that injects all validators into sorted list. + /// + /// This is only useful for chains that started their `VoterList` just based on nominators. + pub struct InjectValidatorsIntoVoterList<T>(core::marker::PhantomData<T>); + impl<T: Config> OnRuntimeUpgrade for InjectValidatorsIntoVoterList<T> { + fn on_runtime_upgrade() -> Weight { + if StorageVersion::<T>::get() == ObsoleteReleases::V8_0_0 { + let prev_count = T::VoterList::count(); + let weight_of_cached = Pallet::<T>::weight_of_fn(); + for (v, _) in Validators::<T>::iter() { + let weight = weight_of_cached(&v); + let _ = T::VoterList::on_insert(v.clone(), weight).map_err(|err| { + log!(warn, "failed to insert {:?} into VoterList: {:?}", v, err) + }); + } + + log!( + info, + "injected a total of {} new voters, prev count: {} next count: {}, updating to version 9", + Validators::<T>::count(), + prev_count, + T::VoterList::count(), + ); + + StorageVersion::<T>::put(ObsoleteReleases::V9_0_0); + T::BlockWeights::get().max_block + } else { + log!( + warn, + "InjectValidatorsIntoVoterList being executed on the wrong storage \ + version, expected ObsoleteReleases::V8_0_0" + ); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> { + frame_support::ensure!( + StorageVersion::<T>::get() == ObsoleteReleases::V8_0_0, + "must upgrade linearly" + ); + + let prev_count = T::VoterList::count(); + Ok(prev_count.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(prev_count: Vec<u8>) -> Result<(), TryRuntimeError> { + let prev_count: u32 = Decode::decode(&mut prev_count.as_slice()).expect( + "the state parameter should be something that was generated by pre_upgrade", + ); + let post_count = T::VoterList::count(); + let validators = Validators::<T>::count(); + ensure!( + post_count == prev_count + validators, + "`VoterList` count after the migration must equal to the sum of \ + previous count and the current number of validators" + ); + + frame_support::ensure!( + StorageVersion::<T>::get() == ObsoleteReleases::V9_0_0, + "must upgrade" + ); + Ok(()) + } + } +} + +pub mod v8 { + use super::*; + use crate::{Config, Nominators, Pallet, Weight}; + use frame_election_provider_support::SortedListProvider; + use frame_support::traits::Get; + + #[cfg(feature = "try-runtime")] + pub fn pre_migrate<T: Config>() -> Result<(), &'static str> { + frame_support::ensure!( + StorageVersion::<T>::get() == ObsoleteReleases::V7_0_0, + "must upgrade linearly" + ); + + crate::log!(info, "👜 staking bags-list migration passes PRE migrate checks ✅",); + Ok(()) + } + + /// Migration to sorted `VoterList`. + pub fn migrate<T: Config>() -> Weight { + if StorageVersion::<T>::get() == ObsoleteReleases::V7_0_0 { + crate::log!(info, "migrating staking to ObsoleteReleases::V8_0_0"); + + let migrated = T::VoterList::unsafe_regenerate( + Nominators::<T>::iter().map(|(id, _)| id), + Pallet::<T>::weight_of_fn(), + ); + + StorageVersion::<T>::put(ObsoleteReleases::V8_0_0); + crate::log!( + info, + "👜 completed staking migration to ObsoleteReleases::V8_0_0 with {} voters migrated", + migrated, + ); + + T::BlockWeights::get().max_block + } else { + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + pub fn post_migrate<T: Config>() -> Result<(), &'static str> { + T::VoterList::try_state().map_err(|_| "VoterList is not in a sane state.")?; + crate::log!(info, "👜 staking bags-list migration passes POST migrate checks ✅",); + Ok(()) + } +} + +pub mod v7 { + use super::*; + use frame_support::storage_alias; + + #[storage_alias] + type CounterForValidators<T: Config> = StorageValue<Pallet<T>, u32>; + #[storage_alias] + type CounterForNominators<T: Config> = StorageValue<Pallet<T>, u32>; + + pub fn pre_migrate<T: Config>() -> Result<(), &'static str> { + assert!( + CounterForValidators::<T>::get().unwrap().is_zero(), + "CounterForValidators already set." + ); + assert!( + CounterForNominators::<T>::get().unwrap().is_zero(), + "CounterForNominators already set." + ); + assert!(Validators::<T>::count().is_zero(), "Validators already set."); + assert!(Nominators::<T>::count().is_zero(), "Nominators already set."); + assert!(StorageVersion::<T>::get() == ObsoleteReleases::V6_0_0); + Ok(()) + } + + pub fn migrate<T: Config>() -> Weight { + log!(info, "Migrating staking to ObsoleteReleases::V7_0_0"); + let validator_count = Validators::<T>::iter().count() as u32; + let nominator_count = Nominators::<T>::iter().count() as u32; + + CounterForValidators::<T>::put(validator_count); + CounterForNominators::<T>::put(nominator_count); + + StorageVersion::<T>::put(ObsoleteReleases::V7_0_0); + log!(info, "Completed staking migration to ObsoleteReleases::V7_0_0"); + + T::DbWeight::get().reads_writes(validator_count.saturating_add(nominator_count).into(), 2) + } +} + +pub mod v6 { + use super::*; + use frame_support::{storage_alias, traits::Get, weights::Weight}; + + // NOTE: value type doesn't matter, we just set it to () here. + #[storage_alias] + type SnapshotValidators<T: Config> = StorageValue<Pallet<T>, ()>; + #[storage_alias] + type SnapshotNominators<T: Config> = StorageValue<Pallet<T>, ()>; + #[storage_alias] + type QueuedElected<T: Config> = StorageValue<Pallet<T>, ()>; + #[storage_alias] + type QueuedScore<T: Config> = StorageValue<Pallet<T>, ()>; + #[storage_alias] + type EraElectionStatus<T: Config> = StorageValue<Pallet<T>, ()>; + #[storage_alias] + type IsCurrentSessionFinal<T: Config> = StorageValue<Pallet<T>, ()>; + + /// check to execute prior to migration. + pub fn pre_migrate<T: Config>() -> Result<(), &'static str> { + // these may or may not exist. + log!(info, "SnapshotValidators.exits()? {:?}", SnapshotValidators::<T>::exists()); + log!(info, "SnapshotNominators.exits()? {:?}", SnapshotNominators::<T>::exists()); + log!(info, "QueuedElected.exits()? {:?}", QueuedElected::<T>::exists()); + log!(info, "QueuedScore.exits()? {:?}", QueuedScore::<T>::exists()); + // these must exist. + assert!( + IsCurrentSessionFinal::<T>::exists(), + "IsCurrentSessionFinal storage item not found!" + ); + assert!(EraElectionStatus::<T>::exists(), "EraElectionStatus storage item not found!"); + Ok(()) + } + + /// Migrate storage to v6. + pub fn migrate<T: Config>() -> Weight { + log!(info, "Migrating staking to ObsoleteReleases::V6_0_0"); + + SnapshotValidators::<T>::kill(); + SnapshotNominators::<T>::kill(); + QueuedElected::<T>::kill(); + QueuedScore::<T>::kill(); + EraElectionStatus::<T>::kill(); + IsCurrentSessionFinal::<T>::kill(); + + StorageVersion::<T>::put(ObsoleteReleases::V6_0_0); + + log!(info, "Done."); + T::DbWeight::get().writes(6 + 1) + } +} diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index cf1b2c7912a..b74cc24a13b 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -154,8 +154,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = NullIdentity; + type FullIdentification = crate::Exposure<AccountId, Balance>; + type FullIdentificationOf = crate::ExposureOf<Test>; } impl pallet_authorship::Config for Test { type FindAuthor = Author11; @@ -728,11 +728,6 @@ pub(crate) fn run_to_block(n: BlockNumber) { ); } -/// Progress by n block. -pub(crate) fn advance_blocks(n: u64) { - run_to_block(System::block_number() + n); -} - /// Progresses from the current block number (whatever that may be) to the `P * session_index + 1`. pub(crate) fn start_session(end_session_idx: SessionIndex) { let period = Period::get(); @@ -835,14 +830,7 @@ pub(crate) fn on_offence_in_era( >], slash_fraction: &[Perbill], era: EraIndex, - advance_processing_blocks: bool, ) { - // counter to keep track of how many blocks we need to advance to process all the offences. - let mut process_blocks = 0u32; - for detail in offenders { - process_blocks += EraInfo::<Test>::get_page_count(era, &detail.offender.0); - } - let bonded_eras = crate::BondedEras::<Test>::get(); for &(bonded_era, start_session) in bonded_eras.iter() { if bonded_era == era { @@ -851,9 +839,6 @@ pub(crate) fn on_offence_in_era( slash_fraction, start_session, ); - if advance_processing_blocks { - advance_blocks(process_blocks as u64); - } return } else if bonded_era > era { break @@ -866,9 +851,6 @@ pub(crate) fn on_offence_in_era( slash_fraction, pallet_staking::ErasStartSessionIndex::<Test>::get(era).unwrap(), ); - if advance_processing_blocks { - advance_blocks(process_blocks as u64); - } } else { panic!("cannot slash in era {}", era); } @@ -880,23 +862,19 @@ pub(crate) fn on_offence_now( pallet_session::historical::IdentificationTuple<Test>, >], slash_fraction: &[Perbill], - advance_processing_blocks: bool, ) { let now = pallet_staking::ActiveEra::<Test>::get().unwrap().index; - on_offence_in_era(offenders, slash_fraction, now, advance_processing_blocks); -} -pub(crate) fn offence_from( - offender: AccountId, - reporter: Option<AccountId>, -) -> OffenceDetails<AccountId, pallet_session::historical::IdentificationTuple<Test>> { - OffenceDetails { - offender: (offender, ()), - reporters: reporter.map(|r| vec![(r)]).unwrap_or_default(), - } + on_offence_in_era(offenders, slash_fraction, now) } pub(crate) fn add_slash(who: &AccountId) { - on_offence_now(&[offence_from(*who, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { + offender: (*who, Staking::eras_stakers(active_era(), who)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); } /// Make all validator and nominator request their payment diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 10e8c679fd6..128914091cb 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -34,7 +34,9 @@ use frame_support::{ use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; use pallet_session::historical; use sp_runtime::{ - traits::{Bounded, CheckedAdd, Convert, SaturatedConversion, Saturating, StaticLookup, Zero}, + traits::{ + Bounded, CheckedAdd, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero, + }, ArithmeticError, DispatchResult, Perbill, Percent, }; use sp_staking::{ @@ -47,16 +49,15 @@ use sp_staking::{ use crate::{ asset, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, - BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, Forcing, IndividualExposure, - LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, MaxWinnersPerPageOf, Nominations, - NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, SnapshotStatus, - StakingLedger, ValidatorPrefs, STAKING_ID, + BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, ExposureOf, Forcing, + IndividualExposure, LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, MaxWinnersPerPageOf, + Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, + SnapshotStatus, StakingLedger, ValidatorPrefs, STAKING_ID, }; use alloc::{boxed::Box, vec, vec::Vec}; use super::pallet::*; -use crate::slashing::OffenceRecord; #[cfg(feature = "try-runtime")] use frame_support::ensure; #[cfg(any(test, feature = "try-runtime"))] @@ -575,6 +576,8 @@ impl<T: Config> Pallet<T> { } } }); + + Self::apply_unapplied_slashes(active_era); } /// Compute payout for era. @@ -976,19 +979,17 @@ impl<T: Config> Pallet<T> { } /// Apply previously-unapplied slashes on the beginning of a new era, after a delay. - pub(crate) fn apply_unapplied_slashes(active_era: EraIndex) { - let mut slashes = UnappliedSlashes::<T>::iter_prefix(&active_era).take(1); - if let Some((key, slash)) = slashes.next() { - log!( - debug, - "🦹 found slash {:?} scheduled to be executed in era {:?}", - slash, - active_era, - ); - let offence_era = active_era.saturating_sub(T::SlashDeferDuration::get()); - slashing::apply_slash::<T>(slash, offence_era); - // remove the slash - UnappliedSlashes::<T>::remove(&active_era, &key); + fn apply_unapplied_slashes(active_era: EraIndex) { + let era_slashes = UnappliedSlashes::<T>::take(&active_era); + log!( + debug, + "found {} slashes scheduled to be executed in era {:?}", + era_slashes.len(), + active_era, + ); + for slash in era_slashes { + let slash_era = active_era.saturating_sub(T::SlashDeferDuration::get()); + slashing::apply_slash::<T>(slash, slash_era); } } @@ -1769,23 +1770,6 @@ impl<T: Config> historical::SessionManager<T::AccountId, Exposure<T::AccountId, } } -impl<T: Config> historical::SessionManager<T::AccountId, ()> for Pallet<T> { - fn new_session(new_index: SessionIndex) -> Option<Vec<(T::AccountId, ())>> { - <Self as pallet_session::SessionManager<_>>::new_session(new_index) - .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) - } - fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<(T::AccountId, ())>> { - <Self as pallet_session::SessionManager<_>>::new_session_genesis(new_index) - .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) - } - fn start_session(start_index: SessionIndex) { - <Self as pallet_session::SessionManager<_>>::start_session(start_index) - } - fn end_session(end_index: SessionIndex) { - <Self as pallet_session::SessionManager<_>>::end_session(end_index) - } -} - /// Add reward points to block authors: /// * 20 points to the block producer for producing a (non-uncle) block, impl<T> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> @@ -1803,7 +1787,10 @@ impl<T: Config> for Pallet<T> where T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>, - T: pallet_session::historical::Config, + T: pallet_session::historical::Config< + FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>, + FullIdentificationOf = ExposureOf<T>, + >, T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>, T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>, T::ValidatorIdOf: Convert< @@ -1811,12 +1798,12 @@ where Option<<T as frame_system::Config>::AccountId>, >, { - /// When an offence is reported, it is split into pages and put in the offence queue. - /// As offence queue is processed, computed slashes are queued to be applied after the - /// `SlashDeferDuration`. fn on_offence( - offenders: &[OffenceDetails<T::AccountId, historical::IdentificationTuple<T>>], - slash_fractions: &[Perbill], + offenders: &[OffenceDetails< + T::AccountId, + pallet_session::historical::IdentificationTuple<T>, + >], + slash_fraction: &[Perbill], slash_session: SessionIndex, ) -> Weight { log!( @@ -1845,48 +1832,52 @@ impl<T: Config> Pallet<T> { slash_fractions: &[Perbill], slash_session: SessionIndex, ) -> Weight { - // todo(ank4n): Needs to be properly benched. - let mut consumed_weight = Weight::zero(); + let reward_proportion = SlashRewardFraction::<T>::get(); + let mut consumed_weight = Weight::from_parts(0, 0); let mut add_db_reads_writes = |reads, writes| { consumed_weight += T::DbWeight::get().reads_writes(reads, writes); }; - // Find the era to which offence belongs. - add_db_reads_writes(1, 0); - let Some(active_era) = ActiveEra::<T>::get() else { - log!(warn, "🦹 on_offence: no active era; ignoring offence"); - return consumed_weight + let active_era = { + let active_era = ActiveEra::<T>::get(); + add_db_reads_writes(1, 0); + if active_era.is_none() { + // This offence need not be re-submitted. + return consumed_weight + } + active_era.expect("value checked not to be `None`; qed").index }; - + let active_era_start_session_index = ErasStartSessionIndex::<T>::get(active_era) + .unwrap_or_else(|| { + frame_support::print("Error: start_session_index must be set for current_era"); + 0 + }); add_db_reads_writes(1, 0); - let active_era_start_session = - ErasStartSessionIndex::<T>::get(active_era.index).unwrap_or(0); + + let window_start = active_era.saturating_sub(T::BondingDuration::get()); // Fast path for active-era report - most likely. // `slash_session` cannot be in a future active era. It must be in `active_era` or before. - let offence_era = if slash_session >= active_era_start_session { - active_era.index + let slash_era = if slash_session >= active_era_start_session_index { + active_era } else { + let eras = BondedEras::<T>::get(); add_db_reads_writes(1, 0); - match BondedEras::<T>::get() - .iter() - // Reverse because it's more likely to find reports from recent eras. - .rev() - .find(|&(_, sesh)| sesh <= &slash_session) - .map(|(era, _)| *era) - { - Some(era) => era, - None => { - // defensive: this implies offence is for a discarded era, and should already be - // filtered out. - log!(warn, "🦹 on_offence: no era found for slash_session; ignoring offence"); - return Weight::default() - }, + + // Reverse because it's more likely to find reports from recent eras. + match eras.iter().rev().find(|&(_, sesh)| sesh <= &slash_session) { + Some((slash_era, _)) => *slash_era, + // Before bonding period. defensive - should be filtered out. + None => return consumed_weight, } }; - add_db_reads_writes(1, 0); + add_db_reads_writes(1, 1); + + let slash_defer_duration = T::SlashDeferDuration::get(); + let invulnerables = Invulnerables::<T>::get(); + add_db_reads_writes(1, 0); for (details, slash_fraction) in offenders.zip(slash_fractions) { let validator = &details.offender; @@ -1910,10 +1901,10 @@ impl<T: Config> Pallet<T> { continue; }; - Self::deposit_event(Event::<T>::OffenceReported { - validator: validator.clone(), + Self::deposit_event(Event::<T>::SlashReported { + validator: stash.clone(), fraction: *slash_fraction, - offence_era, + slash_era, }); if offence_era == active_era.index { @@ -1925,99 +1916,56 @@ impl<T: Config> Pallet<T> { OffenceSeverity(*slash_fraction), ); } - add_db_reads_writes(1, 0); - let prior_slash_fraction = ValidatorSlashInEra::<T>::get(offence_era, validator) - .map_or(Zero::zero(), |(f, _)| f); - add_db_reads_writes(1, 0); - if let Some(existing) = OffenceQueue::<T>::get(offence_era, validator) { - if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() { - add_db_reads_writes(0, 2); - OffenceQueue::<T>::insert( - offence_era, - validator, - OffenceRecord { - reporter: details.reporters.first().cloned(), - reported_era: active_era.index, - slash_fraction: *slash_fraction, - ..existing - }, - ); + let unapplied = slashing::compute_slash::<T>(slashing::SlashParams { + stash, + slash: *slash_fraction, + exposure, + slash_era, + window_start, + now: active_era, + reward_proportion, + }); - // update the slash fraction in the `ValidatorSlashInEra` storage. - ValidatorSlashInEra::<T>::insert( - offence_era, - validator, - (slash_fraction, exposure_overview.own), - ); + if let Some(mut unapplied) = unapplied { + let nominators_len = unapplied.others.len() as u64; + let reporters_len = details.reporters.len() as u64; - log!( - debug, - "🦹 updated slash for {:?}: {:?} (prior: {:?})", - validator, - slash_fraction, - prior_slash_fraction, - ); + { + let upper_bound = 1 /* Validator/NominatorSlashInEra */ + 2 /* fetch_spans */; + let rw = upper_bound + nominators_len * upper_bound; + add_db_reads_writes(rw, rw); + } + unapplied.reporters = details.reporters.clone(); + if slash_defer_duration == 0 { + // Apply right away. + slashing::apply_slash::<T>(unapplied, slash_era); + { + let slash_cost = (6, 5); + let reward_cost = (2, 2); + add_db_reads_writes( + (1 + nominators_len) * slash_cost.0 + reward_cost.0 * reporters_len, + (1 + nominators_len) * slash_cost.1 + reward_cost.1 * reporters_len, + ); + } } else { + // Defer to end of some `slash_defer_duration` from now. log!( debug, - "🦹 ignored slash for {:?}: {:?} (existing prior is larger: {:?})", - validator, + "deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}", slash_fraction, - prior_slash_fraction, + slash_era, + active_era, + slash_era + slash_defer_duration + 1, + ); + UnappliedSlashes::<T>::mutate( + slash_era.saturating_add(slash_defer_duration).saturating_add(One::one()), + move |for_later| for_later.push(unapplied), ); + add_db_reads_writes(1, 1); } - } else if slash_fraction.deconstruct() > prior_slash_fraction.deconstruct() { - add_db_reads_writes(0, 3); - ValidatorSlashInEra::<T>::insert( - offence_era, - validator, - (slash_fraction, exposure_overview.own), - ); - - OffenceQueue::<T>::insert( - offence_era, - validator, - OffenceRecord { - reporter: details.reporters.first().cloned(), - reported_era: active_era.index, - // there are cases of validator with no exposure, hence 0 page, so we - // saturate to avoid underflow. - exposure_page: exposure_overview.page_count.saturating_sub(1), - slash_fraction: *slash_fraction, - prior_slash_fraction, - }, - ); - - OffenceQueueEras::<T>::mutate(|q| { - if let Some(eras) = q { - log!(debug, "🦹 inserting offence era {} into existing queue", offence_era); - eras.binary_search(&offence_era) - .err() - .map(|idx| eras.try_insert(idx, offence_era).defensive()); - } else { - let mut eras = BoundedVec::default(); - log!(debug, "🦹 inserting offence era {} into empty queue", offence_era); - let _ = eras.try_push(offence_era).defensive(); - *q = Some(eras); - } - }); - - log!( - debug, - "🦹 queued slash for {:?}: {:?} (prior: {:?})", - validator, - slash_fraction, - prior_slash_fraction, - ); } else { - log!( - debug, - "🦹 ignored slash for {:?}: {:?} (already slashed in era with prior: {:?})", - validator, - slash_fraction, - prior_slash_fraction, - ); + add_db_reads_writes(4 /* fetch_spans */, 5 /* kick_out_if_recent */) } } @@ -2454,7 +2402,6 @@ impl<T: Config> Pallet<T> { /// /// -- SHOULD ONLY BE CALLED AT THE END OF A GIVEN BLOCK. pub fn ensure_snapshot_metadata_state(now: BlockNumberFor<T>) -> Result<(), TryRuntimeError> { - use sp_runtime::traits::One; let next_election = Self::next_election_prediction(now); let pages = Self::election_pages().saturated_into::<BlockNumberFor<T>>(); let election_prep_start = next_election - pages; diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index bd035bbc0f0..076a2eb6e6a 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -77,7 +77,7 @@ pub mod pallet { use frame_election_provider_support::{ElectionDataProvider, PageIndex}; /// The in-code storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(17); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(16); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -649,67 +649,15 @@ pub mod pallet { #[pallet::storage] pub type CanceledSlashPayout<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>; - /// Stores reported offences in a queue until they are processed in subsequent blocks. - /// - /// Each offence is recorded under the corresponding era index and the offending validator's - /// account. If an offence spans multiple pages, only one page is processed at a time. Offences - /// are handled sequentially, with their associated slashes computed and stored in - /// `UnappliedSlashes`. These slashes are then applied in a future era as determined by - /// `SlashDeferDuration`. - /// - /// Any offences tied to an era older than `BondingDuration` are automatically dropped. - /// Processing always prioritizes the oldest era first. - #[pallet::storage] - pub type OffenceQueue<T: Config> = StorageDoubleMap< - _, - Twox64Concat, - EraIndex, - Twox64Concat, - T::AccountId, - slashing::OffenceRecord<T::AccountId>, - >; - - /// Tracks the eras that contain offences in `OffenceQueue`, sorted from **earliest to latest**. - /// - /// - This ensures efficient retrieval of the oldest offence without iterating through - /// `OffenceQueue`. - /// - When a new offence is added to `OffenceQueue`, its era is **inserted in sorted order** - /// if not already present. - /// - When all offences for an era are processed, it is **removed** from this list. - /// - The maximum length of this vector is bounded by `BondingDuration`. - /// - /// This eliminates the need for expensive iteration and sorting when fetching the next offence - /// to process. - #[pallet::storage] - pub type OffenceQueueEras<T: Config> = StorageValue<_, BoundedVec<u32, T::BondingDuration>>; - - /// Tracks the currently processed offence record from the `OffenceQueue`. - /// - /// - When processing offences, an offence record is **popped** from the oldest era in - /// `OffenceQueue` and stored here. - /// - The function `process_offence` reads from this storage, processing one page of exposure at - /// a time. - /// - After processing a page, the `exposure_page` count is **decremented** until it reaches - /// zero. - /// - Once fully processed, the offence record is removed from this storage. - /// - /// This ensures that offences are processed incrementally, preventing excessive computation - /// in a single block while maintaining correct slashing behavior. - #[pallet::storage] - pub type ProcessingOffence<T: Config> = - StorageValue<_, (EraIndex, T::AccountId, slashing::OffenceRecord<T::AccountId>)>; - /// All unapplied slashes that are queued for later. #[pallet::storage] - pub type UnappliedSlashes<T: Config> = StorageDoubleMap< + #[pallet::unbounded] + pub type UnappliedSlashes<T: Config> = StorageMap< _, Twox64Concat, EraIndex, - Twox64Concat, - // Unique key for unapplied slashes: (validator, slash fraction, page index). - (T::AccountId, Perbill, u32), - UnappliedSlash<T>, - OptionQuery, + Vec<UnappliedSlash<T::AccountId, BalanceOf<T>>>, + ValueQuery, >; /// A mapping from still-bonded eras to the first session index of that era. @@ -978,6 +926,13 @@ pub mod pallet { staker: T::AccountId, amount: BalanceOf<T>, }, + /// A slash for the given validator, for the given percentage of their stake, at the given + /// era as been reported. + SlashReported { + validator: T::AccountId, + fraction: Perbill, + slash_era: EraIndex, + }, /// An old slashing report from a prior era was discarded because it could /// not be processed. OldSlashingReportDiscarded { @@ -1061,26 +1016,6 @@ pub mod pallet { page: PageIndex, result: Result<u32, u32>, }, - /// An offence for the given validator, for the given percentage of their stake, at the - /// given era as been reported. - OffenceReported { - offence_era: EraIndex, - validator: T::AccountId, - fraction: Perbill, - }, - /// An offence has been processed and the corresponding slash has been computed. - SlashComputed { - offence_era: EraIndex, - slash_era: EraIndex, - offender: T::AccountId, - page: u32, - }, - /// An unapplied slash has been cancelled. - SlashCancelled { - slash_era: EraIndex, - slash_key: (T::AccountId, Perbill, u32), - payout: BalanceOf<T>, - }, } #[pallet::error] @@ -1098,8 +1033,8 @@ pub mod pallet { EmptyTargets, /// Duplicate index. DuplicateIndex, - /// Slash record not found. - InvalidSlashRecord, + /// Slash record index out of bounds. + InvalidSlashIndex, /// Cannot have a validator or nominator role, with value less than the minimum defined by /// governance (see `MinValidatorBond` and `MinNominatorBond`). If unbonding is the /// intention, `chill` first to remove one's role as validator/nominator. @@ -1114,6 +1049,8 @@ pub mod pallet { InvalidEraToReward, /// Invalid number of nominations. InvalidNumberOfNominations, + /// Items are not sorted and unique. + NotSortedAndUnique, /// Rewards for this era have already been claimed for this validator. AlreadyClaimed, /// No nominators exist on this page. @@ -1154,8 +1091,6 @@ pub mod pallet { CannotReapStash, /// The stake of this account is already migrated to `Fungible` holds. AlreadyMigrated, - /// Era not yet started. - EraNotStarted, /// Account is restricted from participation in staking. This may happen if the account is /// staking in another way already, such as via pool. Restricted, @@ -1167,21 +1102,6 @@ pub mod pallet { /// that the `ElectableStashes` has been populated with all validators from all pages at /// the time of the election. fn on_initialize(now: BlockNumberFor<T>) -> Weight { - // todo(ank4n): Hacky bench. Do it properly. - let mut consumed_weight = slashing::process_offence::<T>(); - - consumed_weight.saturating_accrue(T::DbWeight::get().reads(1)); - if let Some(active_era) = ActiveEra::<T>::get() { - let max_slash_page_size = T::MaxExposurePageSize::get(); - consumed_weight.saturating_accrue( - T::DbWeight::get().reads_writes( - 3 * max_slash_page_size as u64, - 3 * max_slash_page_size as u64, - ), - ); - Self::apply_unapplied_slashes(active_era.index); - } - let pages = Self::election_pages(); // election ongoing, fetch the next page. @@ -1209,9 +1129,7 @@ pub mod pallet { } }; - consumed_weight.saturating_accrue(inner_weight); - - consumed_weight + T::WeightInfo::on_initialize_noop().saturating_add(inner_weight) } fn on_finalize(_n: BlockNumberFor<T>) { @@ -1977,35 +1895,33 @@ pub mod pallet { Ok(()) } - /// Cancels scheduled slashes for a given era before they are applied. + /// Cancel enactment of a deferred slash. /// - /// This function allows `T::AdminOrigin` to selectively remove pending slashes from - /// the `UnappliedSlashes` storage, preventing their enactment. + /// Can be called by the `T::AdminOrigin`. /// - /// ## Parameters - /// - `era`: The staking era for which slashes were deferred. - /// - `slash_keys`: A list of slash keys identifying the slashes to remove. This is a tuple - /// of `(stash, slash_fraction, page_index)`. + /// Parameters: era and indices of the slashes for that era to kill. #[pallet::call_index(17)] - #[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_keys.len() as u32))] + #[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_indices.len() as u32))] pub fn cancel_deferred_slash( origin: OriginFor<T>, era: EraIndex, - slash_keys: Vec<(T::AccountId, Perbill, u32)>, + slash_indices: Vec<u32>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - ensure!(!slash_keys.is_empty(), Error::<T>::EmptyTargets); - - // Remove the unapplied slashes. - slash_keys.into_iter().for_each(|i| { - UnappliedSlashes::<T>::take(&era, &i).map(|unapplied_slash| { - Self::deposit_event(Event::<T>::SlashCancelled { - slash_era: era, - slash_key: i, - payout: unapplied_slash.payout, - }); - }); - }); + + ensure!(!slash_indices.is_empty(), Error::<T>::EmptyTargets); + ensure!(is_sorted_and_unique(&slash_indices), Error::<T>::NotSortedAndUnique); + + let mut unapplied = UnappliedSlashes::<T>::get(&era); + let last_item = slash_indices[slash_indices.len() - 1]; + ensure!((last_item as usize) < unapplied.len(), Error::<T>::InvalidSlashIndex); + + for (removed, index) in slash_indices.into_iter().enumerate() { + let index = (index as usize) - removed; + unapplied.remove(index); + } + + UnappliedSlashes::<T>::insert(&era, &unapplied); Ok(()) } @@ -2569,45 +2485,6 @@ pub mod pallet { Ok(Pays::No.into()) } - /// Manually applies a deferred slash for a given era. - /// - /// Normally, slashes are automatically applied shortly after the start of the `slash_era`. - /// This function exists as a **fallback mechanism** in case slashes were not applied due to - /// unexpected reasons. It allows anyone to manually apply an unapplied slash. - /// - /// ## Parameters - /// - `slash_era`: The staking era in which the slash was originally scheduled. - /// - `slash_key`: A unique identifier for the slash, represented as a tuple: - /// - `stash`: The stash account of the validator being slashed. - /// - `slash_fraction`: The fraction of the stake that was slashed. - /// - `page_index`: The index of the exposure page being processed. - /// - /// ## Behavior - /// - The function is **permissionless**—anyone can call it. - /// - The `slash_era` **must be the current era or a past era**. If it is in the future, the - /// call fails with `EraNotStarted`. - /// - The fee is waived if the slash is successfully applied. - /// - /// ## TODO: Future Improvement - /// - Implement an **off-chain worker (OCW) task** to automatically apply slashes when there - /// is unused block space, improving efficiency. - #[pallet::call_index(31)] - #[pallet::weight(T::WeightInfo::apply_slash())] - pub fn apply_slash( - origin: OriginFor<T>, - slash_era: EraIndex, - slash_key: (T::AccountId, Perbill, u32), - ) -> DispatchResultWithPostInfo { - let _ = ensure_signed(origin)?; - let active_era = ActiveEra::<T>::get().map(|a| a.index).unwrap_or_default(); - ensure!(slash_era <= active_era, Error::<T>::EraNotStarted); - let unapplied_slash = UnappliedSlashes::<T>::take(&slash_era, &slash_key) - .ok_or(Error::<T>::InvalidSlashRecord)?; - slashing::apply_slash::<T>(unapplied_slash, slash_era); - - Ok(Pays::No.into()) - } - /// This function allows governance to manually slash a validator and is a /// **fallback mechanism**. /// diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 30d4197a888..f31669756e2 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -58,12 +58,12 @@ use alloc::vec::Vec; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ ensure, - traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced}, + traits::{Defensive, DefensiveSaturating, Imbalance, OnUnbalanced}, }; use scale_info::TypeInfo; use sp_runtime::{ traits::{Saturating, Zero}, - DispatchResult, RuntimeDebug, WeakBoundedVec, Weight, + DispatchResult, RuntimeDebug, }; use sp_staking::{EraIndex, StakingInterface}; @@ -209,12 +209,8 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> { pub(crate) stash: &'a T::AccountId, /// The proportion of the slash. pub(crate) slash: Perbill, - /// The prior slash proportion of the validator if the validator has been reported multiple - /// times in the same era, and a new greater slash replaces the old one. - /// Invariant: slash > prior_slash - pub(crate) prior_slash: Perbill, /// The exposure of the stash and all nominators. - pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>, + pub(crate) exposure: &'a Exposure<T::AccountId, BalanceOf<T>>, /// The era where the offence occurred. pub(crate) slash_era: EraIndex, /// The first era in the current bonding period. @@ -226,248 +222,78 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> { pub(crate) reward_proportion: Perbill, } -/// Represents an offence record within the staking system, capturing details about a slashing -/// event. -#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebug)] -pub struct OffenceRecord<AccountId> { - /// The account ID of the entity that reported the offence. - pub reporter: Option<AccountId>, - - /// Era at which the offence was reported. - pub reported_era: EraIndex, - - /// The specific page of the validator's exposure currently being processed. - /// - /// Since a validator's total exposure can span multiple pages, this field serves as a pointer - /// to the current page being evaluated. The processing order starts from the last page - /// and moves backward, decrementing this value with each processed page. - /// - /// This ensures that all pages are systematically handled, and it helps track when - /// the entire exposure has been processed. - pub exposure_page: u32, - - /// The fraction of the validator's stake to be slashed for this offence. - pub slash_fraction: Perbill, - - /// The previous slash fraction of the validator's stake before being updated. - /// If a new, higher slash fraction is reported, this field stores the prior fraction - /// that was overwritten. This helps in tracking changes in slashes across multiple reports for - /// the same era. - pub prior_slash_fraction: Perbill, -} - -/// Loads next offence in the processing offence and returns the offense record to be processed. +/// Computes a slash of a validator and nominators. It returns an unapplied +/// record to be applied at some later point. Slashing metadata is updated in storage, +/// since unapplied records are only rarely intended to be dropped. /// -/// Note: this can mutate the following storage -/// - `ProcessingOffence` -/// - `OffenceQueue` -/// - `OffenceQueueEras` -fn next_offence<T: Config>() -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> { - let processing_offence = ProcessingOffence::<T>::get(); - - if let Some((offence_era, offender, offence_record)) = processing_offence { - // If the exposure page is 0, then the offence has been processed. - if offence_record.exposure_page == 0 { - ProcessingOffence::<T>::kill(); - return Some((offence_era, offender, offence_record)) - } - - // Update the next page. - ProcessingOffence::<T>::put(( - offence_era, - &offender, - OffenceRecord { - // decrement the page index. - exposure_page: offence_record.exposure_page.defensive_saturating_sub(1), - ..offence_record.clone() - }, - )); - - return Some((offence_era, offender, offence_record)) - } - - // Nothing in processing offence. Try to enqueue the next offence. - let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None }; - let Some(&oldest_era) = eras.first() else { return None }; - - let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era); - let next_offence = offence_iter.next(); - - if let Some((ref validator, ref offence_record)) = next_offence { - // Update the processing offence if the offence is multi-page. - if offence_record.exposure_page > 0 { - // update processing offence with the next page. - ProcessingOffence::<T>::put(( - oldest_era, - validator.clone(), - OffenceRecord { - exposure_page: offence_record.exposure_page.defensive_saturating_sub(1), - ..offence_record.clone() - }, - )); - } - - // Remove from `OffenceQueue` - OffenceQueue::<T>::remove(oldest_era, &validator); - } +/// The pending slash record returned does not have initialized reporters. Those have +/// to be set at a higher level, if any. +pub(crate) fn compute_slash<T: Config>( + params: SlashParams<T>, +) -> Option<UnappliedSlash<T::AccountId, BalanceOf<T>>> { + let mut reward_payout = Zero::zero(); + let mut val_slashed = Zero::zero(); - // If there are no offences left for the era, remove the era from `OffenceQueueEras`. - if offence_iter.next().is_none() { - if eras.len() == 1 { - // If there is only one era left, remove the entire queue. - OffenceQueueEras::<T>::kill(); - } else { - // Remove the oldest era - eras.remove(0); - OffenceQueueEras::<T>::put(eras); - } + // is the slash amount here a maximum for the era? + let own_slash = params.slash * params.exposure.own; + if params.slash * params.exposure.total == Zero::zero() { + // kick out the validator even if they won't be slashed, + // as long as the misbehavior is from their most recent slashing span. + kick_out_if_recent::<T>(params); + return None } - next_offence.map(|(v, o)| (oldest_era, v, o)) -} + let prior_slash_p = ValidatorSlashInEra::<T>::get(¶ms.slash_era, params.stash) + .map_or(Zero::zero(), |(prior_slash_proportion, _)| prior_slash_proportion); -/// Infallible function to process an offence. -pub(crate) fn process_offence<T: Config>() -> Weight { - // todo(ank4n): this needs to be properly benched. - let mut consumed_weight = Weight::from_parts(0, 0); - let mut add_db_reads_writes = |reads, writes| { - consumed_weight += T::DbWeight::get().reads_writes(reads, writes); - }; - - add_db_reads_writes(1, 1); - let Some((offence_era, offender, offence_record)) = next_offence::<T>() else { - return consumed_weight - }; - - log!( - debug, - "🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}", - offender, - offence_era, - offence_record.slash_fraction, - ); - - add_db_reads_writes(1, 0); - let reward_proportion = SlashRewardFraction::<T>::get(); - - add_db_reads_writes(2, 0); - let Some(exposure) = - EraInfo::<T>::get_paged_exposure(offence_era, &offender, offence_record.exposure_page) - else { - // this can only happen if the offence was valid at the time of reporting but became too old - // at the time of computing and should be discarded. - return consumed_weight - }; - - let slash_page = offence_record.exposure_page; - let slash_defer_duration = T::SlashDeferDuration::get(); - let slash_era = offence_era.saturating_add(slash_defer_duration); - let window_start = offence_record.reported_era.saturating_sub(T::BondingDuration::get()); - - add_db_reads_writes(3, 3); - let Some(mut unapplied) = compute_slash::<T>(SlashParams { - stash: &offender, - slash: offence_record.slash_fraction, - prior_slash: offence_record.prior_slash_fraction, - exposure: &exposure, - slash_era: offence_era, - window_start, - now: offence_record.reported_era, - reward_proportion, - }) else { - log!( - debug, - "🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash", - offence_record.slash_fraction, - offence_era, - offence_record.reported_era, + // compare slash proportions rather than slash values to avoid issues due to rounding + // error. + if params.slash.deconstruct() > prior_slash_p.deconstruct() { + ValidatorSlashInEra::<T>::insert( + ¶ms.slash_era, + params.stash, + &(params.slash, own_slash), ); - // No slash to apply. Discard. - return consumed_weight - }; - - <Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed { - offence_era, - slash_era, - offender: offender.clone(), - page: slash_page, - }); - - log!( - debug, - "🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed", - offence_record.slash_fraction, - offence_era, - offence_record.reported_era, - ); + } else { + // we slash based on the max in era - this new event is not the max, + // so neither the validator or any nominators will need an update. + // + // this does lead to a divergence of our system from the paper, which + // pays out some reward even if the latest report is not max-in-era. + // we opt to avoid the nominator lookups and edits and leave more rewards + // for more drastic misbehavior. + return None + } - // add the reporter to the unapplied slash. - unapplied.reporter = offence_record.reporter; - - if slash_defer_duration == 0 { - // Apply right away. - log!( - debug, - "🦹 applying slash instantly of {:?}% happened in {:?} (reported in {:?}) to {:?}", - offence_record.slash_fraction, - offence_era, - offence_record.reported_era, - offender, + // apply slash to validator. + { + let mut spans = fetch_spans::<T>( + params.stash, + params.window_start, + &mut reward_payout, + &mut val_slashed, + params.reward_proportion, ); - let accounts_slashed = unapplied.others.len() as u64 + 1; - add_db_reads_writes(3 * accounts_slashed, 3 * accounts_slashed); - apply_slash::<T>(unapplied, offence_era); - } else { - // Historical Note: Previously, with BondingDuration = 28 and SlashDeferDuration = 27, - // slashes were applied at the start of the 28th era from `offence_era`. - // However, with paged slashing, applying slashes now takes multiple blocks. - // To account for this delay, slashes are now applied at the start of the 27th era from - // `offence_era`. - log!( - debug, - "🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}", - offence_record.slash_fraction, - offence_era, - offence_record.reported_era, - slash_era, - ); + let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash); - add_db_reads_writes(0, 1); - UnappliedSlashes::<T>::insert( - slash_era, - (offender, offence_record.slash_fraction, slash_page), - unapplied, - ); + if target_span == Some(spans.span_index()) { + // misbehavior occurred within the current slashing span - end current span. + // Check <https://github.com/paritytech/polkadot-sdk/issues/2650> for details. + spans.end_span(params.now); + } } - consumed_weight -} - -/// Computes a slash of a validator and nominators. It returns an unapplied -/// record to be applied at some later point. Slashing metadata is updated in storage, -/// since unapplied records are only rarely intended to be dropped. -/// -/// The pending slash record returned does not have initialized reporters. Those have -/// to be set at a higher level, if any. -/// -/// If `nomintors_only` is set to `true`, only the nominator slashes will be computed. -pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> { - let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone()); + add_offending_validator::<T>(¶ms); let mut nominators_slashed = Vec::new(); - let (nom_slashed, nom_reward_payout) = - slash_nominators::<T>(params.clone(), &mut nominators_slashed); - reward_payout += nom_reward_payout; + reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed); - (nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash { + Some(UnappliedSlash { validator: params.stash.clone(), own: val_slashed, - others: WeakBoundedVec::force_from( - nominators_slashed, - Some("slashed nominators not expected to be larger than the bounds"), - ), - reporter: None, + others: nominators_slashed, + reporters: Vec::new(), payout: reward_payout, }) } @@ -490,6 +316,64 @@ fn kick_out_if_recent<T: Config>(params: SlashParams<T>) { // Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details spans.end_span(params.now); } + + add_offending_validator::<T>(¶ms); +} + +/// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of +/// validators provided by [`decision`]. +pub(crate) fn add_offending_validator<T: Config>( + stash: &T::AccountId, + slash: Perbill, + offence_era: EraIndex, +) { + DisabledValidators::<T>::mutate(|disabled| { + let new_severity = OffenceSeverity(slash); + let decision = T::DisablingStrategy::decision(stash, new_severity, offence_era, &disabled); + + if let Some(offender_idx) = decision.disable { + // Check if the offender is already disabled + match disabled.binary_search_by_key(&offender_idx, |(index, _)| *index) { + // Offender is already disabled, update severity if the new one is higher + Ok(index) => { + let (_, old_severity) = &mut disabled[index]; + if new_severity > *old_severity { + *old_severity = new_severity; + } + }, + Err(index) => { + // Offender is not disabled, add to `DisabledValidators` and disable it + if disabled.try_insert(index, (offender_idx, new_severity)).defensive().is_ok() + { + // Propagate disablement to session level + T::SessionInterface::disable_validator(offender_idx); + // Emit event that a validator got disabled + <Pallet<T>>::deposit_event(super::Event::<T>::ValidatorDisabled { + stash: stash.clone(), + }); + } + }, + } + } + + if let Some(reenable_idx) = decision.reenable { + // Remove the validator from `DisabledValidators` and re-enable it. + if let Ok(index) = disabled.binary_search_by_key(&reenable_idx, |(index, _)| *index) { + disabled.remove(index); + // Propagate re-enablement to session level + T::SessionInterface::enable_validator(reenable_idx); + // Emit event that a validator got re-enabled + let reenabled_stash = + T::SessionInterface::validators()[reenable_idx as usize].clone(); + <Pallet<T>>::deposit_event(super::Event::<T>::ValidatorReenabled { + stash: reenabled_stash, + }); + } + } + }); + + // `DisabledValidators` should be kept sorted + debug_assert!(DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] < pair[1])); } /// Compute the slash for a validator. Returns the amount slashed and the reward payout. @@ -539,23 +423,23 @@ fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceO /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator. /// -/// Returns the total amount slashed and amount of reward to pay out. +/// Returns the amount of reward to pay out. fn slash_nominators<T: Config>( params: SlashParams<T>, + prior_slash_p: Perbill, nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>, -) -> (BalanceOf<T>, BalanceOf<T>) { - let mut reward_payout = BalanceOf::<T>::zero(); - let mut total_slashed = BalanceOf::<T>::zero(); +) -> BalanceOf<T> { + let mut reward_payout = Zero::zero(); - nominators_slashed.reserve(params.exposure.exposure_page.others.len()); - for nominator in ¶ms.exposure.exposure_page.others { + nominators_slashed.reserve(params.exposure.others.len()); + for nominator in ¶ms.exposure.others { let stash = &nominator.who; let mut nom_slashed = Zero::zero(); - // the era slash of a nominator always grows, if the validator had a new max slash for the - // era. + // the era slash of a nominator always grows, if the validator + // had a new max slash for the era. let era_slash = { - let own_slash_prior = params.prior_slash * nominator.value; + let own_slash_prior = prior_slash_p * nominator.value; let own_slash_by_validator = params.slash * nominator.value; let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior); @@ -585,10 +469,9 @@ fn slash_nominators<T: Config>( } } nominators_slashed.push((stash.clone(), nom_slashed)); - total_slashed.saturating_accrue(nom_slashed); } - (total_slashed, reward_payout) + reward_payout } // helper struct for managing a set of spans we are currently inspecting. @@ -802,25 +685,22 @@ pub fn do_slash<T: Config>( } /// Apply a previously-unapplied slash. -pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) { +pub(crate) fn apply_slash<T: Config>( + unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>, + slash_era: EraIndex, +) { let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero(); let mut reward_payout = unapplied_slash.payout; - if unapplied_slash.own > Zero::zero() { - do_slash::<T>( - &unapplied_slash.validator, - unapplied_slash.own, - &mut reward_payout, - &mut slashed_imbalance, - slash_era, - ); - } + do_slash::<T>( + &unapplied_slash.validator, + unapplied_slash.own, + &mut reward_payout, + &mut slashed_imbalance, + slash_era, + ); for &(ref nominator, nominator_slash) in &unapplied_slash.others { - if nominator_slash.is_zero() { - continue - } - do_slash::<T>( nominator, nominator_slash, @@ -830,11 +710,7 @@ pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_e ); } - pay_reporters::<T>( - reward_payout, - slashed_imbalance, - &unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(), - ); + pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters); } /// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance. diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 554c705bfbb..d6b4b9435b5 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -49,7 +49,7 @@ use sp_runtime::{ }; use sp_staking::{ offence::{OffenceDetails, OnOffenceHandler}, - SessionIndex, StakingInterface, + SessionIndex, }; use substrate_test_utils::assert_eq_uvec; @@ -753,7 +753,10 @@ fn nominators_also_get_slashed_pro_rata() { let exposed_nominator = initial_exposure.others.first().unwrap().value; // 11 goes offline - on_offence_now(&[offence_from(11, None)], &[slash_percent], true); + on_offence_now( + &[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }], + &[slash_percent], + ); // both stakes must have been decreased. assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake); @@ -2450,7 +2453,13 @@ fn reward_validator_slashing_validator_does_not_overflow() { ); // Check slashing - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(100)], + ); assert_eq!(asset::stakeable_balance::<Test>(&11), stake - 1); assert_eq!(asset::stakeable_balance::<Test>(&2), 1); @@ -2543,7 +2552,13 @@ fn era_is_always_same_length() { #[test] fn offence_doesnt_force_new_era() { ExtBuilder::default().build_and_execute(|| { - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(5)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(5)], + ); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); }); @@ -2555,7 +2570,13 @@ fn offence_ensures_new_era_without_clobbering() { assert_ok!(Staking::force_new_era_always(RuntimeOrigin::root())); assert_eq!(ForceEra::<Test>::get(), Forcing::ForceAlways); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(5)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(5)], + ); assert_eq!(ForceEra::<Test>::get(), Forcing::ForceAlways); }); @@ -2573,7 +2594,13 @@ fn offence_deselects_validator_even_when_slash_is_zero() { assert!(Session::validators().contains(&11)); assert!(<Validators<Test>>::contains_key(11)); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + ); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); assert!(is_disabled(11)); @@ -2593,10 +2620,16 @@ fn slashing_performed_according_exposure() { assert_eq!(Staking::eras_stakers(active_era(), &11).own, 1000); // Handle an offence with a historical exposure. - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Exposure { total: 500, own: 500, others: vec![] }), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + ); // The stash account should be slashed for 250 (50% of 500). - assert_eq!(asset::stakeable_balance::<Test>(&11), 1000 / 2); + assert_eq!(asset::stakeable_balance::<Test>(&11), 1000 - 250); }); } @@ -2611,7 +2644,13 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() { assert!(<Validators<Test>>::contains_key(11)); assert!(Session::validators().contains(&11)); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + ); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); assert!(is_disabled(11)); @@ -2627,7 +2666,14 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() { mock::start_active_era(3); // an offence committed in era 1 is reported in era 3 - on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(0)], 1, true); + on_offence_in_era( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + 1, + ); // the validator doesn't get disabled for an old offence assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11)); @@ -2637,11 +2683,13 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() { assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); on_offence_in_era( - &[offence_from(11, None)], + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], // NOTE: A 100% slash here would clean up the account, causing de-registration. &[Perbill::from_percent(95)], 1, - true, ); // the validator doesn't get disabled again @@ -2653,9 +2701,9 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() { } #[test] -fn only_first_reporter_receive_the_slice() { - // This test verifies that the first reporter of the offence receive their slice from the - // slashed amount. +fn reporters_receive_their_slice() { + // This test verifies that the reporters of the offence receive their slice from the slashed + // amount. ExtBuilder::default().build_and_execute(|| { // The reporters' reward is calculated from the total exposure. let initial_balance = 1125; @@ -2663,16 +2711,19 @@ fn only_first_reporter_receive_the_slice() { assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance); on_offence_now( - &[OffenceDetails { offender: (11, ()), reporters: vec![1, 2] }], + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![1, 2], + }], &[Perbill::from_percent(50)], - true, ); // F1 * (reward_proportion * slash - 0) // 50% * (10% * initial_balance / 2) let reward = (initial_balance / 20) / 2; - assert_eq!(asset::total_balance::<Test>(&1), 10 + reward); - assert_eq!(asset::total_balance::<Test>(&2), 20 + 0); + let reward_each = reward / 2; // split into two pieces. + assert_eq!(asset::total_balance::<Test>(&1), 10 + reward_each); + assert_eq!(asset::total_balance::<Test>(&2), 20 + reward_each); }); } @@ -2686,14 +2737,26 @@ fn subsequent_reports_in_same_span_pay_out_less() { assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance); - on_offence_now(&[offence_from(11, Some(1))], &[Perbill::from_percent(20)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![1], + }], + &[Perbill::from_percent(20)], + ); // F1 * (reward_proportion * slash - 0) // 50% * (10% * initial_balance * 20%) let reward = (initial_balance / 5) / 20; assert_eq!(asset::total_balance::<Test>(&1), 10 + reward); - on_offence_now(&[offence_from(11, Some(1))], &[Perbill::from_percent(50)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![1], + }], + &[Perbill::from_percent(50)], + ); let prior_payout = reward; @@ -2721,9 +2784,17 @@ fn invulnerables_are_not_slashed() { .collect(); on_offence_now( - &[offence_from(11, None), offence_from(21, None)], + &[ + OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }, + OffenceDetails { + offender: (21, Staking::eras_stakers(active_era(), &21)), + reporters: vec![], + }, + ], &[Perbill::from_percent(50), Perbill::from_percent(20)], - true, ); // The validator 11 hasn't been slashed, but 21 has been. @@ -2747,7 +2818,13 @@ fn dont_slash_if_fraction_is_zero() { ExtBuilder::default().build_and_execute(|| { assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + ); // The validator hasn't been slashed. The new era is not forced. assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); @@ -2762,18 +2839,36 @@ fn only_slash_for_max_in_era() { ExtBuilder::default().build_and_execute(|| { assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + ); // The validator has been slashed and has been force-chilled. assert_eq!(asset::stakeable_balance::<Test>(&11), 500); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(25)], + ); // The validator has not been slashed additionally. assert_eq!(asset::stakeable_balance::<Test>(&11), 500); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(60)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(60)], + ); // The validator got slashed 10% more. assert_eq!(asset::stakeable_balance::<Test>(&11), 400); @@ -2789,13 +2884,25 @@ fn garbage_collection_after_slashing() { .build_and_execute(|| { assert_eq!(asset::stakeable_balance::<Test>(&11), 2000); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); assert_eq!(asset::stakeable_balance::<Test>(&11), 2000 - 200); assert!(SlashingSpans::<Test>::get(&11).is_some()); assert_eq!(SpanSlash::<Test>::get(&(11, 0)).amount(), &200); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(100)], + ); // validator and nominator slash in era are garbage-collected by era change, // so we don't test those here. @@ -2833,7 +2940,13 @@ fn garbage_collection_on_window_pruning() { assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value; - add_slash(&11); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(now, &11)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); assert_eq!(asset::stakeable_balance::<Test>(&11), 900); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000 - (nominated_value / 10)); @@ -2871,7 +2984,14 @@ fn slashing_nominators_by_span_max() { let nominated_value_11 = exposure_11.others.iter().find(|o| o.who == 101).unwrap().value; let nominated_value_21 = exposure_21.others.iter().find(|o| o.who == 101).unwrap().value; - on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(10)], 2, true); + on_offence_in_era( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + 2, + ); assert_eq!(asset::stakeable_balance::<Test>(&11), 900); @@ -2890,7 +3010,14 @@ fn slashing_nominators_by_span_max() { assert_eq!(get_span(101).iter().collect::<Vec<_>>(), expected_spans); // second slash: higher era, higher value, same span. - on_offence_in_era(&[offence_from(21, None)], &[Perbill::from_percent(30)], 3, true); + on_offence_in_era( + &[OffenceDetails { + offender: (21, Staking::eras_stakers(active_era(), &21)), + reporters: vec![], + }], + &[Perbill::from_percent(30)], + 3, + ); // 11 was not further slashed, but 21 and 101 were. assert_eq!(asset::stakeable_balance::<Test>(&11), 900); @@ -2904,7 +3031,14 @@ fn slashing_nominators_by_span_max() { // third slash: in same era and on same validator as first, higher // in-era value, but lower slash value than slash 2. - on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(20)], 2, true); + on_offence_in_era( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(20)], + 2, + ); // 11 was further slashed, but 21 and 101 were not. assert_eq!(asset::stakeable_balance::<Test>(&11), 800); @@ -2931,7 +3065,13 @@ fn slashes_are_summed_across_spans() { let get_span = |account| SlashingSpans::<Test>::get(&account).unwrap(); - on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { + offender: (21, Staking::eras_stakers(active_era(), &21)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); let expected_spans = vec![ slashing::SlashingSpan { index: 1, start: 4, length: None }, @@ -2948,7 +3088,13 @@ fn slashes_are_summed_across_spans() { assert_eq!(Staking::slashable_balance_of(&21), 900); - on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { + offender: (21, Staking::eras_stakers(active_era(), &21)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); let expected_spans = vec![ slashing::SlashingSpan { index: 2, start: 5, length: None }, @@ -2974,10 +3120,13 @@ fn deferred_slashes_are_deferred() { System::reset_events(); - // only 1 page of exposure, so slashes will be applied in one block. - assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 1); - - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); // nominations are not removed regardless of the deferring. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); @@ -2990,37 +3139,27 @@ fn deferred_slashes_are_deferred() { assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); - assert!(matches!( - staking_events_since_last_call().as_slice(), - &[ - Event::OffenceReported { validator: 11, offence_era: 1, .. }, - Event::SlashComputed { offence_era: 1, slash_era: 3, page: 0, .. }, - Event::PagedElectionProceeded { page: 0, result: Ok(2) }, - Event::StakersElected, - .., - ] - )); - - // the slashes for era 1 will start applying in era 3, to end before era 4. mock::start_active_era(3); - // Slashes not applied yet. Will apply in the next block after era starts. + assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); - // trigger slashing by advancing block. - advance_blocks(1); + + // at the start of era 4, slashes from era 1 are processed, + // after being deferred for at least 2 full eras. + mock::start_active_era(4); + assert_eq!(asset::stakeable_balance::<Test>(&11), 900); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000 - (nominated_value / 10)); assert!(matches!( staking_events_since_last_call().as_slice(), &[ - // era 3 elections + Event::SlashReported { validator: 11, slash_era: 1, .. }, Event::PagedElectionProceeded { page: 0, result: Ok(2) }, Event::StakersElected, - Event::EraPaid { .. }, - // slashes applied from era 1 between era 3 and 4. + .., Event::Slashed { staker: 11, amount: 100 }, - Event::Slashed { staker: 101, amount: 12 }, + Event::Slashed { staker: 101, amount: 12 } ] )); }) @@ -3032,26 +3171,25 @@ fn retroactive_deferred_slashes_two_eras_before() { assert_eq!(BondingDuration::get(), 3); mock::start_active_era(1); + let exposure_11_at_era1 = Staking::eras_stakers(active_era(), &11); + + mock::start_active_era(3); assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); System::reset_events(); on_offence_in_era( - &[offence_from(11, None)], + &[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }], &[Perbill::from_percent(10)], - 1, // should be deferred for two eras, and applied at the beginning of era 3. - true, + 1, // should be deferred for two full eras, and applied at the beginning of era 4. ); - mock::start_active_era(3); - // Slashes not applied yet. Will apply in the next block after era starts. - advance_blocks(1); + mock::start_active_era(4); assert!(matches!( staking_events_since_last_call().as_slice(), &[ - Event::OffenceReported { validator: 11, offence_era: 1, .. }, - Event::SlashComputed { offence_era: 1, slash_era: 3, offender: 11, page: 0 }, + Event::SlashReported { validator: 11, slash_era: 1, .. }, .., Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 101, amount: 12 } @@ -3065,6 +3203,9 @@ fn retroactive_deferred_slashes_one_before() { ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| { assert_eq!(BondingDuration::get(), 3); + mock::start_active_era(1); + let exposure_11_at_era1 = Staking::eras_stakers(active_era(), &11); + // unbond at slash era. mock::start_active_era(2); assert_ok!(Staking::chill(RuntimeOrigin::signed(11))); @@ -3073,23 +3214,21 @@ fn retroactive_deferred_slashes_one_before() { mock::start_active_era(3); System::reset_events(); on_offence_in_era( - &[offence_from(11, None)], + &[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }], &[Perbill::from_percent(10)], - 2, // should be deferred for two eras, and applied before the beginning of era 4. - true, + 2, // should be deferred for two full eras, and applied at the beginning of era 5. ); mock::start_active_era(4); assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000); - // slash happens at next blocks. - advance_blocks(1); + // slash happens after the next line. + mock::start_active_era(5); assert!(matches!( staking_events_since_last_call().as_slice(), &[ - Event::OffenceReported { validator: 11, offence_era: 2, .. }, - Event::SlashComputed { offence_era: 2, slash_era: 4, offender: 11, page: 0 }, + Event::SlashReported { validator: 11, slash_era: 2, .. }, .., Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 101, amount: 12 } @@ -3115,7 +3254,13 @@ fn staker_cannot_bail_deferred_slash() { let exposure = Staking::eras_stakers(active_era(), &11); let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value; - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); // now we chill assert_ok!(Staking::chill(RuntimeOrigin::signed(101))); @@ -3184,44 +3329,23 @@ fn remove_deferred() { assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value; - // deferred to start of era 3. - let slash_fraction_one = Perbill::from_percent(10); - on_offence_now(&[offence_from(11, None)], &[slash_fraction_one], true); + // deferred to start of era 4. + on_offence_now( + &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(10)], + ); assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); mock::start_active_era(2); - // reported later, but deferred to start of era 3 as well. + // reported later, but deferred to start of era 4 as well. System::reset_events(); - let slash_fraction_two = Perbill::from_percent(15); - on_offence_in_era(&[offence_from(11, None)], &[slash_fraction_two], 1, true); - - assert_eq!( - UnappliedSlashes::<Test>::iter_prefix(&3).collect::<Vec<_>>(), - vec![ - ( - (11, slash_fraction_one, 0), - UnappliedSlash { - validator: 11, - own: 100, - others: bounded_vec![(101, 12)], - reporter: None, - payout: 5 - } - ), - ( - (11, slash_fraction_two, 0), - UnappliedSlash { - validator: 11, - own: 50, - others: bounded_vec![(101, 7)], - reporter: None, - payout: 6 - } - ), - ] + on_offence_in_era( + &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(15)], + 1, ); // fails if empty @@ -3230,13 +3354,8 @@ fn remove_deferred() { Error::<Test>::EmptyTargets ); - // cancel the slash with 10%. - assert_ok!(Staking::cancel_deferred_slash( - RuntimeOrigin::root(), - 3, - vec![(11, slash_fraction_one, 0)] - )); - assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&3).count(), 1); + // cancel one of them. + assert_ok!(Staking::cancel_deferred_slash(RuntimeOrigin::root(), 4, vec![0])); assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); @@ -3246,29 +3365,23 @@ fn remove_deferred() { assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); - // at the next blocks, slashes from era 1 are processed, 1 page a block, - // after being deferred for 2 eras. - advance_blocks(1); + // at the start of era 4, slashes from era 1 are processed, + // after being deferred for at least 2 full eras. + mock::start_active_era(4); // the first slash for 10% was cancelled, but the 15% one not. assert!(matches!( staking_events_since_last_call().as_slice(), &[ - Event::OffenceReported { validator: 11, offence_era: 1, .. }, - Event::SlashComputed { offence_era: 1, slash_era: 3, offender: 11, page: 0 }, - Event::SlashCancelled { - slash_era: 3, - slash_key: (11, fraction, 0), - payout: 5 - }, + Event::SlashReported { validator: 11, slash_era: 1, .. }, .., Event::Slashed { staker: 11, amount: 50 }, Event::Slashed { staker: 101, amount: 7 } - ] if fraction == slash_fraction_one + ] )); let slash_10 = Perbill::from_percent(10); - let slash_15 = slash_fraction_two; + let slash_15 = Perbill::from_percent(15); let initial_slash = slash_10 * nominated_value; let total_slash = slash_15 * nominated_value; @@ -3282,48 +3395,67 @@ fn remove_deferred() { #[test] fn remove_multi_deferred() { - ExtBuilder::default() - .slash_defer_duration(2) - .validator_count(4) - .set_status(41, StakerStatus::Validator) - .set_status(51, StakerStatus::Validator) - .build_and_execute(|| { - mock::start_active_era(1); + ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| { + mock::start_active_era(1); - assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); - assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); + assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true); + let exposure = Staking::eras_stakers(active_era(), &11); + assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); - on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(10)], + ); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true); + on_offence_now( + &[OffenceDetails { + offender: (21, Staking::eras_stakers(active_era(), &21)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); - on_offence_now(&[offence_from(41, None)], &[Perbill::from_percent(25)], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); - on_offence_now(&[offence_from(51, None)], &[Perbill::from_percent(25)], true); + on_offence_now( + &[OffenceDetails { offender: (42, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); - // there are 5 slashes to be applied in era 3. - assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&3).count(), 5); + on_offence_now( + &[OffenceDetails { offender: (69, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); - // lets cancel 3 of them. - assert_ok!(Staking::cancel_deferred_slash( - RuntimeOrigin::root(), - 3, - vec![ - (11, Perbill::from_percent(10), 0), - (11, Perbill::from_percent(25), 0), - (51, Perbill::from_percent(25), 0), - ] - )); + assert_eq!(UnappliedSlashes::<Test>::get(&4).len(), 5); - let slashes = UnappliedSlashes::<Test>::iter_prefix(&3).collect::<Vec<_>>(); - assert_eq!(slashes.len(), 2); - // the first item in the remaining slashes belongs to validator 41. - assert_eq!(slashes[0].0, (41, Perbill::from_percent(25), 0)); - // the second and last item in the remaining slashes belongs to validator 21. - assert_eq!(slashes[1].0, (21, Perbill::from_percent(10), 0)); - }) + // fails if list is not sorted + assert_noop!( + Staking::cancel_deferred_slash(RuntimeOrigin::root(), 1, vec![2, 0, 4]), + Error::<Test>::NotSortedAndUnique + ); + // fails if list is not unique + assert_noop!( + Staking::cancel_deferred_slash(RuntimeOrigin::root(), 1, vec![0, 2, 2]), + Error::<Test>::NotSortedAndUnique + ); + // fails if bad index + assert_noop!( + Staking::cancel_deferred_slash(RuntimeOrigin::root(), 1, vec![1, 2, 3, 4, 5]), + Error::<Test>::InvalidSlashIndex + ); + + assert_ok!(Staking::cancel_deferred_slash(RuntimeOrigin::root(), 4, vec![0, 2, 4])); + + let slashes = UnappliedSlashes::<Test>::get(&4); + assert_eq!(slashes.len(), 2); + assert_eq!(slashes[0].validator, 21); + assert_eq!(slashes[1].validator, 42); + }) } #[test] @@ -3352,7 +3484,10 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid assert_eq!(exposure_11.total, 1000 + 125); assert_eq!(exposure_21.total, 1000 + 375); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(10)], + ); assert_eq!( staking_events_since_last_call(), @@ -3360,12 +3495,11 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid Event::PagedElectionProceeded { page: 0, result: Ok(7) }, Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::OffenceReported { + Event::SlashReported { validator: 11, fraction: Perbill::from_percent(10), - offence_era: 1 + slash_era: 1 }, - Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 }, Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 101, amount: 12 }, ] @@ -3412,14 +3546,23 @@ fn non_slashable_offence_disables_validator() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); + let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21); + // offence with no slash associated - on_offence_now(&[offence_from(11, None)], &[Perbill::zero()], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::zero()], + ); // it does NOT affect the nominator. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); // offence that slashes 25% of the bond - on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(25)], true); + on_offence_now( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); // it DOES NOT affect the nominator. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); @@ -3430,16 +3573,18 @@ fn non_slashable_offence_disables_validator() { Event::PagedElectionProceeded { page: 0, result: Ok(7) }, Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::OffenceReported { + Event::SlashReported { validator: 11, fraction: Perbill::from_percent(0), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 11 }, Event::OffenceReported { validator: 21, fraction: Perbill::from_percent(25), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 21 }, Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 }, Event::Slashed { staker: 21, amount: 250 }, Event::Slashed { staker: 101, amount: 94 } @@ -3472,11 +3617,18 @@ fn slashing_independent_of_disabling_validator() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51]); + let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21); + let now = ActiveEra::<Test>::get().unwrap().index; // --- Disable without a slash --- // offence with no slash associated - on_offence_in_era(&[offence_from(11, None)], &[Perbill::zero()], now, true); + on_offence_in_era( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::zero()], + now, + ); // nomination remains untouched. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); @@ -3486,10 +3638,18 @@ fn slashing_independent_of_disabling_validator() { // --- Slash without disabling --- // offence that slashes 50% of the bond (setup for next slash) - on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(50)], now, true); + on_offence_in_era( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(50)], + now, + ); // offence that slashes 25% of the bond but does not disable - on_offence_in_era(&[offence_from(21, None)], &[Perbill::from_percent(25)], now, true); + on_offence_in_era( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + now, + ); // nomination remains untouched. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); @@ -3504,25 +3664,24 @@ fn slashing_independent_of_disabling_validator() { Event::PagedElectionProceeded { page: 0, result: Ok(5) }, Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::OffenceReported { + Event::SlashReported { validator: 11, fraction: Perbill::from_percent(0), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 11 }, Event::OffenceReported { validator: 11, fraction: Perbill::from_percent(50), - offence_era: 1 + slash_era: 1 }, - Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 }, Event::Slashed { staker: 11, amount: 500 }, Event::Slashed { staker: 101, amount: 62 }, - Event::OffenceReported { + Event::SlashReported { validator: 21, fraction: Perbill::from_percent(25), - offence_era: 1 + slash_era: 1 }, - Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 }, Event::Slashed { staker: 21, amount: 250 }, Event::Slashed { staker: 101, amount: 94 } ] @@ -3558,14 +3717,25 @@ fn offence_threshold_doesnt_plan_new_era() { // we have 4 validators and an offending validator threshold of 1/3, // even if the third validator commits an offence a new era should not be forced - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true); + + let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21); + let exposure_31 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &31); + + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(50)], + ); // 11 should be disabled because the byzantine threshold is 1 assert!(is_disabled(11)); assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); - on_offence_now(&[offence_from(21, None)], &[Perbill::zero()], true); + on_offence_now( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::zero()], + ); // 21 should not be disabled because the number of disabled validators will be above the // byzantine threshold @@ -3573,7 +3743,10 @@ fn offence_threshold_doesnt_plan_new_era() { assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing); - on_offence_now(&[offence_from(31, None)], &[Perbill::zero()], true); + on_offence_now( + &[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }], + &[Perbill::zero()], + ); // same for 31 assert!(!is_disabled(31)); @@ -3595,7 +3768,13 @@ fn disabled_validators_are_kept_disabled_for_whole_era() { assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); assert_eq!(<Test as Config>::SessionsPerEra::get(), 3); - on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(25)], true); + let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21); + + on_offence_now( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); // nominations are not updated. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); @@ -3609,7 +3788,10 @@ fn disabled_validators_are_kept_disabled_for_whole_era() { assert!(is_disabled(21)); // validator 11 commits an offence - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); // nominations are not updated. assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]); @@ -3725,9 +3907,14 @@ fn zero_slash_keeps_nominators() { mock::start_active_era(1); assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); + + let exposure = Staking::eras_stakers(active_era(), &11); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(0)], + ); assert_eq!(asset::stakeable_balance::<Test>(&11), 1000); assert_eq!(asset::stakeable_balance::<Test>(&101), 2000); @@ -4720,7 +4907,6 @@ fn bond_during_era_does_not_populate_legacy_claimed_rewards() { } #[test] -#[ignore] fn offences_weight_calculated_correctly() { ExtBuilder::default().nominate(true).build_and_execute(|| { // On offence with zero offenders: 4 Reads, 1 Write @@ -4743,7 +4929,7 @@ fn offences_weight_calculated_correctly() { >, > = (1..10) .map(|i| OffenceDetails { - offender: (i, ()), + offender: (i, Staking::eras_stakers(active_era(), &i)), reporters: vec![], }) .collect(); @@ -4757,7 +4943,10 @@ fn offences_weight_calculated_correctly() { ); // On Offence with one offenders, Applied - let one_offender = [offence_from(11, Some(1))]; + let one_offender = [OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![1], + }]; let n = 1; // Number of offenders let rw = 3 + 3 * n; // rw reads and writes @@ -6850,7 +7039,13 @@ mod staking_interface { #[test] fn do_withdraw_unbonded_with_wrong_slash_spans_works_as_expected() { ExtBuilder::default().build_and_execute(|| { - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(100)], + ); assert_eq!(Staking::bonded(&11), Some(11)); @@ -7134,7 +7329,13 @@ mod staking_unchecked { let exposed_nominator = initial_exposure.others.first().unwrap().value; // 11 goes offline - on_offence_now(&[offence_from(11, None)], &[slash_percent], true); + on_offence_now( + &[OffenceDetails { + offender: (11, initial_exposure.clone()), + reporters: vec![], + }], + &[slash_percent], + ); let slash_amount = slash_percent * exposed_stake; let validator_share = @@ -7200,7 +7401,13 @@ mod staking_unchecked { let nominator_stake = Staking::ledger(101.into()).unwrap().total; // 11 goes offline - on_offence_now(&[offence_from(11, None)], &[slash_percent], true); + on_offence_now( + &[OffenceDetails { + offender: (11, initial_exposure.clone()), + reporters: vec![], + }], + &[slash_percent], + ); // both stakes must have been decreased to 0. assert_eq!(Staking::ledger(101.into()).unwrap().active, 0); @@ -8340,9 +8547,19 @@ fn reenable_lower_offenders_mock() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); + let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); + let exposure_31 = Staking::eras_stakers(Staking::active_era().unwrap().index, &31); + // offence with a low slash - on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true); - on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(20)], true); + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(10)], + ); + on_offence_now( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(20)], + ); // it does NOT affect the nominator. assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); @@ -8352,7 +8569,10 @@ fn reenable_lower_offenders_mock() { assert!(is_disabled(21)); // offence with a higher slash - on_offence_now(&[offence_from(31, None)], &[Perbill::from_percent(50)], true); + on_offence_now( + &[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }], + &[Perbill::from_percent(50)], + ); // First offender is no longer disabled assert!(!is_disabled(11)); @@ -8367,27 +8587,31 @@ fn reenable_lower_offenders_mock() { Event::PagedElectionProceeded { page: 0, result: Ok(7) }, Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::OffenceReported { + Event::SlashReported { validator: 11, fraction: Perbill::from_percent(10), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 11 }, Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 }, Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 101, amount: 12 }, - Event::OffenceReported { + Event::SlashReported { validator: 21, fraction: Perbill::from_percent(20), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 21 }, Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 }, Event::Slashed { staker: 21, amount: 200 }, Event::Slashed { staker: 101, amount: 75 }, - Event::OffenceReported { + Event::SlashReported { validator: 31, fraction: Perbill::from_percent(50), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 31 }, + Event::ValidatorReenabled { stash: 11 }, Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 31, page: 0 }, Event::Slashed { staker: 31, amount: 250 }, ] @@ -8418,17 +8642,33 @@ fn do_not_reenable_higher_offenders_mock() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); + let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); + let exposure_31 = Staking::eras_stakers(Staking::active_era().unwrap().index, &31); + // offence with a major slash on_offence_now( - &[offence_from(11, None), offence_from(21, None), offence_from(31, None)], - &[Perbill::from_percent(50), Perbill::from_percent(50), Perbill::from_percent(10)], - true, + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(50)], + ); + on_offence_now( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(50)], ); // both validators should be disabled assert!(is_disabled(11)); assert!(is_disabled(21)); + // offence with a minor slash + on_offence_now( + &[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }], + &[Perbill::from_percent(10)], + ); + + // First and second offenders are still disabled + assert!(is_disabled(11)); + assert!(is_disabled(21)); // New offender is not disabled as limit is reached and his prio is lower assert!(!is_disabled(31)); @@ -8438,22 +8678,23 @@ fn do_not_reenable_higher_offenders_mock() { Event::PagedElectionProceeded { page: 0, result: Ok(7) }, Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::OffenceReported { + Event::SlashReported { validator: 11, fraction: Perbill::from_percent(50), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 11 }, Event::OffenceReported { validator: 21, fraction: Perbill::from_percent(50), - offence_era: 1 + slash_era: 1 }, + Event::ValidatorDisabled { stash: 21 }, Event::OffenceReported { validator: 31, fraction: Perbill::from_percent(10), - offence_era: 1 + slash_era: 1 }, - Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 31, page: 0 }, Event::Slashed { staker: 31, amount: 50 }, Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 }, Event::Slashed { staker: 21, amount: 500 }, diff --git a/substrate/frame/staking/src/weights.rs b/substrate/frame/staking/src/weights.rs index 1ccb534e4c5..d252387e741 100644 --- a/substrate/frame/staking/src/weights.rs +++ b/substrate/frame/staking/src/weights.rs @@ -106,7 +106,6 @@ pub trait WeightInfo { fn set_min_commission() -> Weight; fn restore_ledger() -> Weight; fn migrate_currency() -> Weight; - fn apply_slash() -> Weight; fn manual_slash() -> Weight; } @@ -871,33 +870,7 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } - /// Storage: `Staking::ActiveEra` (r:1 w:0) - /// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`) - /// Storage: `Staking::UnappliedSlashes` (r:1 w:1) - /// Proof: `Staking::UnappliedSlashes` (`max_values`: None, `max_size`: Some(1694), added: 4169, mode: `MaxEncodedLen`) - /// Storage: `Staking::Bonded` (r:33 w:0) - /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) - /// Storage: `Staking::Ledger` (r:33 w:33) - /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) - /// Storage: `NominationPools::ReversePoolIdLookup` (r:33 w:0) - /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) - /// Storage: `DelegatedStaking::Agents` (r:33 w:33) - /// Proof: `DelegatedStaking::Agents` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:33 w:33) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Staking::VirtualStakers` (r:33 w:0) - /// Proof: `Staking::VirtualStakers` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) - /// Storage: `Balances::Holds` (r:33 w:33) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(427), added: 2902, mode: `MaxEncodedLen`) - fn apply_slash() -> Weight { - // Proof Size summary in bytes: - // Measured: `14542` - // Estimated: `118668` - // Minimum execution time: 1_628_472_000 picoseconds. - Weight::from_parts(1_647_487_000, 118668) - .saturating_add(T::DbWeight::get().reads(233_u64)) - .saturating_add(T::DbWeight::get().writes(133_u64)) - } + /// Storage: `Staking::CurrentEra` (r:1 w:0) /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0) @@ -1689,33 +1662,6 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } - /// Storage: `Staking::ActiveEra` (r:1 w:0) - /// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`) - /// Storage: `Staking::UnappliedSlashes` (r:1 w:1) - /// Proof: `Staking::UnappliedSlashes` (`max_values`: None, `max_size`: Some(1694), added: 4169, mode: `MaxEncodedLen`) - /// Storage: `Staking::Bonded` (r:33 w:0) - /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) - /// Storage: `Staking::Ledger` (r:33 w:33) - /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) - /// Storage: `NominationPools::ReversePoolIdLookup` (r:33 w:0) - /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) - /// Storage: `DelegatedStaking::Agents` (r:33 w:33) - /// Proof: `DelegatedStaking::Agents` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:33 w:33) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Staking::VirtualStakers` (r:33 w:0) - /// Proof: `Staking::VirtualStakers` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) - /// Storage: `Balances::Holds` (r:33 w:33) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(427), added: 2902, mode: `MaxEncodedLen`) - fn apply_slash() -> Weight { - // Proof Size summary in bytes: - // Measured: `14542` - // Estimated: `118668` - // Minimum execution time: 1_628_472_000 picoseconds. - Weight::from_parts(1_647_487_000, 118668) - .saturating_add(RocksDbWeight::get().reads(233_u64)) - .saturating_add(RocksDbWeight::get().writes(133_u64)) - } /// Storage: `Staking::CurrentEra` (r:1 w:0) /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0) -- GitLab