Skip to content

Commit dda2cb5

Browse files
Ank4ngpestanakianenigmagui1117re-gius
authored
[Staking] Bounded Slashing: Paginated Offence Processing & Slash Application (#7424)
closes #3610. helps #6344, but need to migrate storage `Offences::Reports` before we can remove exposure dependency in RC pallets. replaces #6788. ## Context Slashing in staking is unbounded currently, which is a major blocker until staking can move to a parachain (AH). ### Current Slashing Process (Unbounded) 1. **Offence Reported** - Offences include multiple validators, each with potentially large exposure pages. - Slashes are **computed immediately** and scheduled for application after **28 eras**. 2. **Slash Applied** - All unapplied slashes are executed in **one block** at the start of the **28th era**. This is an **unbounded operation**. ### Proposed Slashing Process (Bounded) 1. **Offence Queueing** - Offences are **queued** after basic sanity checks. 2. **Paged Offence Processing (Computing Slash)** - Slashes are **computed one validator exposure page at a time**. - **Unapplied slashes** are stored in a **double map**: - **Key 1 (k1):** `EraIndex` - **Key 2 (k2):** `(Validator, SlashFraction, PageIndex)` — a unique identifier for each slash page 3. **Paged Slash Application** - Slashes are **applied one page at a time** across multiple blocks. - Slash application starts at the **27th era** (one era earlier than before) to ensure all slashes are applied **before stakers can unbond** (which starts from era 28 onwards). --- ## Worst-Case Block Calculation for Slash Application ### Polkadot: - **1 era = 24 hours**, **1 block = 6s** → **14,400 blocks/era** - On parachains (**12s blocks**) → **7,200 blocks/era** ### Kusama: - **1 era = 6 hours**, **1 block = 6s** → **3,600 blocks/era** - On parachains (**12s blocks**) → **1,800 blocks/era** ### Worst-Case Assumptions: - **Total stakers:** 40,000 nominators, 1000 validators. (Polkadot currently has ~23k nominators and 500 validators) - **Max slashed:** 50% so 20k nominators, 250 validators. - **Page size:** Validators with multiple page: (512 + 1)/2 = 256 , Validators with single page: 1 ### Calculation: There might be a more accurate way to calculate this worst-case number, and this estimate could be significantly higher than necessary, but it shouldn’t exceed this value. Blocks needed: 250 + 20k/256 = ~330 blocks. ## *Potential Improvement:* - Consider adding an **Offchain Worker (OCW)** task to further optimize slash application in future updates. - Dynamically batch unapplied slashes based on number of nominators in the page, or process until reserved weight limit is exhausted. ---- ## Summary of Changes ### Storage - **New:** - `OffenceQueue` *(StorageDoubleMap)* - **K1:** Era - **K2:** Offending validator account - **V:** `OffenceRecord` - `OffenceQueueEras` *(StorageValue)* - **V:** `BoundedVec<EraIndex, BoundingDuration>` - `ProcessingOffence` *(StorageValue)* - **V:** `(Era, offending validator account, OffenceRecord)` - **Changed:** - `UnappliedSlashes`: - **Old:** `StorageMap<K -> Era, V -> Vec<UnappliedSlash>>` - **New:** `StorageDoubleMap<K1 -> Era, K2 -> (validator_acc, perbill, page_index), V -> UnappliedSlash>` ### Events - **New:** - `SlashComputed { offence_era, slash_era, offender, page }` - `SlashCancelled { slash_era, slash_key, payout }` ### Error - **Changed:** - `InvalidSlashIndex` → Renamed to `InvalidSlashRecord` - **Removed:** - `NotSortedAndUnique` - **Added:** - `EraNotStarted` ### Call - **Changed:** - `cancel_deferred_slash(era, slash_indices: Vec<u32>)` → Now takes `Vec<(validator_acc, slash_fraction, page_index)>` - **New:** - `apply_slash(slash_era, slash_key: (validator_acc, slash_fraction, page_index))` ### Runtime Config - `FullIdentification` is now set to a unit type (`()`) / null identity, replacing the previous exposure type for all runtimes using `pallet_session::historical`. ## TODO - [x] Fixed broken `CancelDeferredSlashes`. - [x] Ensure on_offence called only with validator account for identification everywhere. - [ ] Ensure we never need to read full exposure. - [x] Tests for multi block processing and application of slash. - [x] Migrate UnappliedSlashes - [x] Bench (crude, needs proper bench as followup) - [x] on_offence() - [x] process_offence() - [x] apply_slash() ## Followups (tracker [link](#7596)) - [ ] OCW task to process offence + apply slashes. - [ ] Minimum time for governance to cancel deferred slash. - [ ] Allow root or staking admin to add a custom slash. - [ ] Test HistoricalSession proof works fine with eras before removing exposure as full identity. - [ ] Properly bench offence processing and slashing. - [ ] Handle Offences::Reports migration when removing validator exposure as identity. --------- Co-authored-by: Gonçalo Pestana <[email protected]> Co-authored-by: command-bot <> Co-authored-by: Kian Paimani <[email protected]> Co-authored-by: Guillaume Thiolliere <[email protected]> Co-authored-by: kianenigma <[email protected]> Co-authored-by: Giuseppe Re <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 6b6dae8 commit dda2cb5

File tree

24 files changed

+1605
-1061
lines changed

24 files changed

+1605
-1061
lines changed

polkadot/runtime/test-runtime/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,8 @@ impl pallet_session::Config for Runtime {
322322
}
323323

324324
impl pallet_session::historical::Config for Runtime {
325-
type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
326-
type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
325+
type FullIdentification = ();
326+
type FullIdentificationOf = pallet_staking::NullIdentity;
327327
}
328328

329329
pallet_staking_reward_curve::build! {

polkadot/runtime/westend/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,7 @@ pub mod migrations {
18711871
parachains_shared::migration::MigrateToV1<Runtime>,
18721872
parachains_scheduler::migration::MigrateV2ToV3<Runtime>,
18731873
pallet_staking::migrations::v16::MigrateV15ToV16<Runtime>,
1874+
pallet_staking::migrations::v17::MigrateV16ToV17<Runtime>,
18741875
// permanent
18751876
pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>,
18761877
);

polkadot/runtime/westend/src/weights/pallet_staking.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,4 +805,8 @@ impl<T: frame_system::Config> pallet_staking::WeightInfo for WeightInfo<T> {
805805
.saturating_add(T::DbWeight::get().reads(6))
806806
.saturating_add(T::DbWeight::get().writes(2))
807807
}
808+
fn apply_slash() -> Weight {
809+
// TODO CI-FAIL: run CI bench bot
810+
Weight::zero()
811+
}
808812
}

prdoc/pr_7424.prdoc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
2+
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
3+
4+
title: 'Bounded Slashing: Paginated Offence Processing & Slash Application'
5+
6+
doc:
7+
- audience: Runtime Dev
8+
description: |
9+
This PR refactors the slashing mechanism in `pallet-staking` to be bounded by introducing paged offence processing and paged slash application.
10+
11+
### Key Changes
12+
- Offences are queued instead of being processed immediately.
13+
- Slashes are computed in pages, stored as a `StorageDoubleMap` with `(Validator, SlashFraction, PageIndex)` to uniquely identify them.
14+
- Slashes are applied incrementally across multiple blocks instead of a single unbounded operation.
15+
- New storage items: `OffenceQueue`, `ProcessingOffence`, `OffenceQueueEras`.
16+
- Updated API for cancelling and applying slashes.
17+
- Preliminary benchmarks added; further optimizations planned.
18+
19+
This enables staking slashing to scale efficiently and removes a major blocker for staking migration to a parachain (AH).
20+
21+
crates:
22+
- name: pallet-babe
23+
bump: patch
24+
- name: pallet-staking
25+
bump: major
26+
- name: pallet-grandpa
27+
bump: patch
28+
- name: westend-runtime
29+
bump: minor
30+
- name: pallet-beefy
31+
bump: patch
32+
- name: pallet-offences-benchmarking
33+
bump: patch
34+
- name: pallet-session-benchmarking
35+
bump: patch
36+
- name: pallet-root-offences
37+
bump: patch

substrate/bin/node/runtime/src/lib.rs

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -676,8 +676,6 @@ impl_opaque_keys! {
676676

677677
#[cfg(feature = "staking-playground")]
678678
pub mod staking_playground {
679-
use pallet_staking::Exposure;
680-
681679
use super::*;
682680

683681
/// An adapter to make the chain work with --dev only, even though it is running a large staking
@@ -712,61 +710,43 @@ pub mod staking_playground {
712710
}
713711
}
714712

715-
impl pallet_session::historical::SessionManager<AccountId, Exposure<AccountId, Balance>>
716-
for AliceAsOnlyValidator
717-
{
713+
impl pallet_session::historical::SessionManager<AccountId, ()> for AliceAsOnlyValidator {
718714
fn end_session(end_index: sp_staking::SessionIndex) {
719-
<Staking as pallet_session::historical::SessionManager<
720-
AccountId,
721-
Exposure<AccountId, Balance>,
722-
>>::end_session(end_index)
715+
<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::end_session(
716+
end_index,
717+
)
723718
}
724719

725-
fn new_session(
726-
new_index: sp_staking::SessionIndex,
727-
) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> {
728-
<Staking as pallet_session::historical::SessionManager<
729-
AccountId,
730-
Exposure<AccountId, Balance>,
731-
>>::new_session(new_index)
720+
fn new_session(new_index: sp_staking::SessionIndex) -> Option<Vec<(AccountId, ())>> {
721+
<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::new_session(
722+
new_index,
723+
)
732724
.map(|_ignored| {
733725
// construct a fake exposure for alice.
734-
vec![(
735-
sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(),
736-
pallet_staking::Exposure {
737-
total: 1_000_000_000,
738-
own: 1_000_000_000,
739-
others: vec![],
740-
},
741-
)]
726+
vec![(sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(), ())]
742727
})
743728
}
744729

745730
fn new_session_genesis(
746731
new_index: sp_staking::SessionIndex,
747-
) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> {
732+
) -> Option<Vec<(AccountId, ())>> {
748733
<Staking as pallet_session::historical::SessionManager<
749734
AccountId,
750-
Exposure<AccountId, Balance>,
735+
(),
751736
>>::new_session_genesis(new_index)
752737
.map(|_ignored| {
753738
// construct a fake exposure for alice.
754739
vec![(
755740
sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(),
756-
pallet_staking::Exposure {
757-
total: 1_000_000_000,
758-
own: 1_000_000_000,
759-
others: vec![],
760-
},
741+
(),
761742
)]
762743
})
763744
}
764745

765746
fn start_session(start_index: sp_staking::SessionIndex) {
766-
<Staking as pallet_session::historical::SessionManager<
767-
AccountId,
768-
Exposure<AccountId, Balance>,
769-
>>::start_session(start_index)
747+
<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::start_session(
748+
start_index,
749+
)
770750
}
771751
}
772752
}
@@ -790,8 +770,8 @@ impl pallet_session::Config for Runtime {
790770
}
791771

792772
impl pallet_session::historical::Config for Runtime {
793-
type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
794-
type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
773+
type FullIdentification = ();
774+
type FullIdentificationOf = pallet_staking::NullIdentity;
795775
}
796776

797777
pallet_staking_reward_curve::build! {

substrate/frame/babe/src/mock.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ impl pallet_session::Config for Test {
104104
}
105105

106106
impl pallet_session::historical::Config for Test {
107-
type FullIdentification = pallet_staking::Exposure<u64, u128>;
108-
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
107+
type FullIdentification = ();
108+
type FullIdentificationOf = pallet_staking::NullIdentity;
109109
}
110110

111111
impl pallet_authorship::Config for Test {

substrate/frame/beefy/src/mock.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ impl pallet_session::Config for Test {
188188
}
189189

190190
impl pallet_session::historical::Config for Test {
191-
type FullIdentification = pallet_staking::Exposure<u64, u128>;
192-
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
191+
type FullIdentification = ();
192+
type FullIdentificationOf = pallet_staking::NullIdentity;
193193
}
194194

195195
impl pallet_authorship::Config for Test {

substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ impl pallet_session::Config for Runtime {
145145
type WeightInfo = ();
146146
}
147147
impl pallet_session::historical::Config for Runtime {
148-
type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
149-
type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
148+
type FullIdentification = ();
149+
type FullIdentificationOf = pallet_staking::NullIdentity;
150150
}
151151

152152
frame_election_provider_support::generate_solution_type!(
@@ -908,10 +908,7 @@ pub(crate) fn on_offence_now(
908908
// Add offence to validator, slash it.
909909
pub(crate) fn add_slash(who: &AccountId) {
910910
on_offence_now(
911-
&[OffenceDetails {
912-
offender: (*who, Staking::eras_stakers(active_era(), who)),
913-
reporters: vec![],
914-
}],
911+
&[OffenceDetails { offender: (*who, ()), reporters: vec![] }],
915912
&[Perbill::from_percent(10)],
916913
);
917914
}

substrate/frame/grandpa/src/mock.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ impl pallet_session::Config for Test {
108108
}
109109

110110
impl pallet_session::historical::Config for Test {
111-
type FullIdentification = pallet_staking::Exposure<u64, u128>;
112-
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
111+
type FullIdentification = ();
112+
type FullIdentificationOf = pallet_staking::NullIdentity;
113113
}
114114

115115
impl pallet_authorship::Config for Test {

substrate/frame/offences/benchmarking/src/inner.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ fn make_offenders<T: Config>(
170170
Ok(id_tuples)
171171
}
172172

173+
#[cfg(test)]
174+
fn run_staking_next_block<T: Config>() {
175+
use frame_support::traits::Hooks;
176+
System::<T>::set_block_number(System::<T>::block_number().saturating_add(1u32.into()));
177+
Staking::<T>::on_initialize(System::<T>::block_number());
178+
}
179+
173180
#[cfg(test)]
174181
fn assert_all_slashes_applied<T>(offender_count: usize)
175182
where
@@ -182,10 +189,10 @@ where
182189
// make sure that all slashes have been applied
183190
// deposit to reporter + reporter account endowed.
184191
assert_eq!(System::<T>::read_events_for_pallet::<pallet_balances::Event<T>>().len(), 2);
185-
// (n nominators + one validator) * slashed + Slash Reported
192+
// (n nominators + one validator) * slashed + Slash Reported + Slash Computed
186193
assert_eq!(
187194
System::<T>::read_events_for_pallet::<pallet_staking::Event<T>>().len(),
188-
1 * (offender_count + 1) as usize + 1
195+
1 * (offender_count + 1) as usize + 2
189196
);
190197
// offence
191198
assert_eq!(System::<T>::read_events_for_pallet::<pallet_offences::Event>().len(), 1);
@@ -232,6 +239,8 @@ mod benchmarks {
232239

233240
#[cfg(test)]
234241
{
242+
// slashes applied at the next block.
243+
run_staking_next_block::<T>();
235244
assert_all_slashes_applied::<T>(n as usize);
236245
}
237246

@@ -266,6 +275,8 @@ mod benchmarks {
266275
}
267276
#[cfg(test)]
268277
{
278+
// slashes applied at the next block.
279+
run_staking_next_block::<T>();
269280
assert_all_slashes_applied::<T>(n as usize);
270281
}
271282

0 commit comments

Comments
 (0)