1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! ExternalValidatorSlashes pallet.
18
//!
19
//! A pallet to store slashes based on offences committed by validators
20
//! Slashes can be cancelled during the DeferPeriod through cancel_deferred_slash
21
//! Slashes can also be forcedly injected via the force_inject_slash extrinsic
22
//! Slashes for a particular era are removed after the bondingPeriod has elapsed
23
//!
24
//! ## OnOffence trait
25
//!
26
//! The pallet also implements the OnOffence trait that reacts to offences being injected by other pallets
27
//! Invulnerables are not slashed and no slashing information is stored for them
28

            
29
#![cfg_attr(not(feature = "std"), no_std)]
30

            
31
use {
32
    frame_support::{pallet_prelude::*, traits::DefensiveSaturating},
33
    frame_system::pallet_prelude::*,
34
    log::log,
35
    pallet_staking::SessionInterface,
36
    parity_scale_codec::FullCodec,
37
    parity_scale_codec::{Decode, Encode},
38
    sp_runtime::traits::{Convert, Debug, One, Saturating, Zero},
39
    sp_runtime::DispatchResult,
40
    sp_runtime::Perbill,
41
    sp_staking::{
42
        offence::{OffenceDetails, OnOffenceHandler},
43
        EraIndex, SessionIndex,
44
    },
45
    sp_std::vec,
46
    sp_std::vec::Vec,
47
    tp_traits::{EraIndexProvider, InvulnerablesProvider, OnEraStart},
48
};
49

            
50
pub use pallet::*;
51

            
52
#[cfg(test)]
53
mod mock;
54

            
55
#[cfg(test)]
56
mod tests;
57

            
58
#[cfg(feature = "runtime-benchmarks")]
59
mod benchmarking;
60
pub mod weights;
61

            
62
14
#[frame_support::pallet]
63
pub mod pallet {
64
    use super::*;
65
    pub use crate::weights::WeightInfo;
66

            
67
    #[pallet::event]
68
10
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
69
    pub enum Event<T: Config> {
70
        /// Removed author data
71
        SlashReported {
72
            validator: T::ValidatorId,
73
            fraction: Perbill,
74
            slash_era: EraIndex,
75
        },
76
    }
77

            
78
    #[pallet::config]
79
    pub trait Config: frame_system::Config {
80
        /// The overarching event type.
81
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
82

            
83
        /// A stable ID for a validator.
84
        type ValidatorId: Member
85
            + Parameter
86
            + MaybeSerializeDeserialize
87
            + MaxEncodedLen
88
            + TryFrom<Self::AccountId>;
89

            
90
        /// A conversion from account ID to validator ID.
91
        type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;
92

            
93
        /// Number of eras that slashes are deferred by, after computation.
94
        ///
95
        /// This should be less than the bonding duration. Set to 0 if slashes
96
        /// should be applied immediately, without opportunity for intervention.
97
        #[pallet::constant]
98
        type SlashDeferDuration: Get<EraIndex>;
99

            
100
        /// Number of eras that staked funds must remain bonded for.
101
        #[pallet::constant]
102
        type BondingDuration: Get<EraIndex>;
103

            
104
        // SlashId type, used as a counter on the number of slashes
105
        type SlashId: Default
106
            + FullCodec
107
            + TypeInfo
108
            + Copy
109
            + Clone
110
            + Debug
111
            + Eq
112
            + Saturating
113
            + One
114
            + Ord
115
            + MaxEncodedLen;
116

            
117
        /// Interface for interacting with a session pallet.
118
        type SessionInterface: SessionInterface<Self::AccountId>;
119

            
120
        /// Era index provider, used to fetch the active era among other things
121
        type EraIndexProvider: EraIndexProvider;
122

            
123
        /// Invulnerable provider, used to get the invulnerables to know when not to slash
124
        type InvulnerablesProvider: InvulnerablesProvider<Self::ValidatorId>;
125

            
126
        /// The weight information of this pallet.
127
        type WeightInfo: WeightInfo;
128
    }
129

            
130
20
    #[pallet::error]
131
    pub enum Error<T> {
132
        /// The era for which the slash wants to be cancelled has no slashes
133
        EmptyTargets,
134
        /// No slash was found to be cancelled at the given index
135
        InvalidSlashIndex,
136
        /// Slash indices to be cancelled are not sorted or unique
137
        NotSortedAndUnique,
138
        /// Provided an era in the future
139
        ProvidedFutureEra,
140
        /// Provided an era that is not slashable
141
        ProvidedNonSlashableEra,
142
        /// The slash to be cancelled has already elapsed the DeferPeriod
143
        DeferPeriodIsOver,
144
        /// There was an error computing the slash
145
        ErrorComputingSlash,
146
    }
147

            
148
2
    #[pallet::pallet]
149
    pub struct Pallet<T>(PhantomData<T>);
150

            
151
    /// All slashing events on validators, mapped by era to the highest slash proportion
152
    /// and slash value of the era.
153
43
    #[pallet::storage]
154
    pub(crate) type ValidatorSlashInEra<T: Config> =
155
        StorageDoubleMap<_, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, Perbill>;
156

            
157
    /// A mapping from still-bonded eras to the first session index of that era.
158
    ///
159
    /// Must contains information for eras for the range:
160
    /// `[active_era - bounding_duration; active_era]`
161
776
    #[pallet::storage]
162
    #[pallet::unbounded]
163
    pub type BondedEras<T: Config> = StorageValue<_, Vec<(EraIndex, SessionIndex)>, ValueQuery>;
164

            
165
    /// A counter on the number of slashes we have performed
166
78
    #[pallet::storage]
167
    #[pallet::getter(fn next_slash_id)]
168
    pub type NextSlashId<T: Config> = StorageValue<_, T::SlashId, ValueQuery>;
169

            
170
    /// All unapplied slashes that are queued for later.
171
251
    #[pallet::storage]
172
    #[pallet::unbounded]
173
    #[pallet::getter(fn slashes)]
174
    pub type Slashes<T: Config> =
175
        StorageMap<_, Twox64Concat, EraIndex, Vec<Slash<T::AccountId, T::SlashId>>, ValueQuery>;
176

            
177
    #[pallet::call]
178
    impl<T: Config> Pallet<T> {
179
        /// Cancel a slash that was deferred for a later era
180
        #[pallet::call_index(0)]
181
        #[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_indices.len() as u32))]
182
        pub fn cancel_deferred_slash(
183
            origin: OriginFor<T>,
184
            era: EraIndex,
185
            slash_indices: Vec<u32>,
186
5
        ) -> DispatchResult {
187
5
            ensure_root(origin)?;
188

            
189
5
            let active_era = T::EraIndexProvider::active_era().index;
190
5

            
191
5
            // We need to be in the defer period
192
5
            ensure!(
193
5
                era <= active_era
194
5
                    .saturating_add(T::SlashDeferDuration::get().saturating_add(One::one()))
195
5
                    && era > active_era,
196
3
                Error::<T>::DeferPeriodIsOver
197
            );
198

            
199
2
            ensure!(!slash_indices.is_empty(), Error::<T>::EmptyTargets);
200
2
            ensure!(
201
2
                is_sorted_and_unique(&slash_indices),
202
                Error::<T>::NotSortedAndUnique
203
            );
204
            // fetch slashes for the era in which we want to defer
205
2
            let mut era_slashes = Slashes::<T>::get(&era);
206
2

            
207
2
            let last_item = slash_indices[slash_indices.len() - 1];
208
2
            ensure!(
209
2
                (last_item as usize) < era_slashes.len(),
210
                Error::<T>::InvalidSlashIndex
211
            );
212

            
213
            // Remove elements starting from the highest index to avoid shifting issues.
214
2
            for index in slash_indices.into_iter().rev() {
215
2
                era_slashes.remove(index as usize);
216
2
            }
217
            // insert back slashes
218
2
            Slashes::<T>::insert(&era, &era_slashes);
219
2
            Ok(())
220
        }
221

            
222
        #[pallet::call_index(1)]
223
        #[pallet::weight(T::WeightInfo::force_inject_slash())]
224
        pub fn force_inject_slash(
225
            origin: OriginFor<T>,
226
            era: EraIndex,
227
            validator: T::AccountId,
228
            percentage: Perbill,
229
8
        ) -> DispatchResult {
230
8
            ensure_root(origin)?;
231
8
            let active_era = T::EraIndexProvider::active_era().index;
232
8

            
233
8
            ensure!(era <= active_era, Error::<T>::ProvidedFutureEra);
234

            
235
7
            let slash_defer_duration = T::SlashDeferDuration::get();
236
7

            
237
7
            let _ = T::EraIndexProvider::era_to_session_start(era)
238
7
                .ok_or(Error::<T>::ProvidedNonSlashableEra)?;
239

            
240
6
            let next_slash_id = NextSlashId::<T>::get();
241

            
242
6
            let slash = compute_slash::<T>(
243
6
                percentage,
244
6
                next_slash_id,
245
6
                era,
246
6
                validator,
247
6
                slash_defer_duration,
248
6
            )
249
6
            .ok_or(Error::<T>::ErrorComputingSlash)?;
250

            
251
            // If we defer duration is 0, we immediately apply and confirm
252
6
            let era_to_consider = if slash_defer_duration == 0 {
253
2
                era
254
            } else {
255
4
                era.saturating_add(slash_defer_duration)
256
4
                    .saturating_add(One::one())
257
            };
258

            
259
6
            Slashes::<T>::mutate(&era_to_consider, |era_slashes| {
260
6
                era_slashes.push(slash);
261
6
            });
262
6

            
263
6
            NextSlashId::<T>::put(next_slash_id.saturating_add(One::one()));
264
6
            Ok(())
265
        }
266
    }
267
}
268

            
269
/// This is intended to be used with `FilterHistoricalOffences`.
270
impl<T: Config>
271
    OnOffenceHandler<T::AccountId, pallet_session::historical::IdentificationTuple<T>, Weight>
272
    for Pallet<T>
273
where
274
    T: Config<ValidatorId = <T as frame_system::Config>::AccountId>,
275
    T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
276
    T: pallet_session::historical::Config,
277
    T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>,
278
    T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>,
279
    <T as pallet::Config>::ValidatorIdOf: Convert<
280
        <T as frame_system::Config>::AccountId,
281
        Option<<T as frame_system::Config>::AccountId>,
282
    >,
283
{
284
13
    fn on_offence(
285
13
        offenders: &[OffenceDetails<
286
13
            T::AccountId,
287
13
            pallet_session::historical::IdentificationTuple<T>,
288
13
        >],
289
13
        slash_fraction: &[Perbill],
290
13
        slash_session: SessionIndex,
291
13
    ) -> Weight {
292
13
        let active_era = {
293
13
            let active_era = T::EraIndexProvider::active_era().index;
294
13
            active_era
295
13
        };
296
13
        let active_era_start_session_index = T::EraIndexProvider::era_to_session_start(active_era)
297
13
            .unwrap_or_else(|| {
298
                frame_support::print("Error: start_session_index must be set for current_era");
299
                0
300
13
            });
301

            
302
        // Fast path for active-era report - most likely.
303
        // `slash_session` cannot be in a future active era. It must be in `active_era` or before.
304
13
        let slash_era = if slash_session >= active_era_start_session_index {
305
10
            active_era
306
        } else {
307
3
            let eras = BondedEras::<T>::get();
308
3

            
309
3
            // Reverse because it's more likely to find reports from recent eras.
310
6
            match eras.iter().rev().find(|&(_, sesh)| sesh <= &slash_session) {
311
3
                Some((slash_era, _)) => *slash_era,
312
                // Before bonding period. defensive - should be filtered out.
313
                None => return Weight::default(),
314
            }
315
        };
316

            
317
13
        let slash_defer_duration = T::SlashDeferDuration::get();
318
13

            
319
13
        let invulnerables = T::InvulnerablesProvider::invulnerables();
320
13

            
321
13
        let mut next_slash_id = NextSlashId::<T>::get();
322

            
323
13
        for (details, slash_fraction) in offenders.iter().zip(slash_fraction) {
324
13
            let (stash, _) = &details.offender;
325
13

            
326
13
            // Skip if the validator is invulnerable.
327
13
            if invulnerables.contains(stash) {
328
3
                continue;
329
10
            }
330
10

            
331
10
            let slash = compute_slash::<T>(
332
10
                *slash_fraction,
333
10
                next_slash_id,
334
10
                slash_era,
335
10
                stash.clone(),
336
10
                slash_defer_duration,
337
10
            );
338
10

            
339
10
            Self::deposit_event(Event::<T>::SlashReported {
340
10
                validator: stash.clone(),
341
10
                fraction: *slash_fraction,
342
10
                slash_era,
343
10
            });
344

            
345
10
            if let Some(mut slash) = slash {
346
9
                slash.reporters = details.reporters.clone();
347
9

            
348
9
                // Defer to end of some `slash_defer_duration` from now.
349
9
                log!(
350
9
                    log::Level::Debug,
351
                    "deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
352
                    slash_fraction,
353
                    slash_era,
354
                    active_era,
355
                    slash_era + slash_defer_duration + 1,
356
                );
357

            
358
                // Cover slash defer duration equal to 0
359
9
                if slash_defer_duration == 0 {
360
1
                    Slashes::<T>::mutate(slash_era, move |for_now| for_now.push(slash));
361
8
                } else {
362
8
                    Slashes::<T>::mutate(
363
8
                        slash_era
364
8
                            .saturating_add(slash_defer_duration)
365
8
                            .saturating_add(One::one()),
366
8
                        move |for_later| for_later.push(slash),
367
8
                    );
368
8
                }
369

            
370
                // Fix unwrap
371
9
                next_slash_id = next_slash_id.saturating_add(One::one());
372
1
            }
373
        }
374
13
        NextSlashId::<T>::put(next_slash_id);
375
13
        Weight::default()
376
13
    }
377
}
378

            
379
impl<T: Config> OnEraStart for Pallet<T> {
380
192
    fn on_era_start(era_index: EraIndex, session_start: SessionIndex) {
381
        // This should be small, as slashes are limited by the num of validators
382
        // let's put 1000 as a conservative measure
383
        const REMOVE_LIMIT: u32 = 1000;
384

            
385
192
        let bonding_duration = T::BondingDuration::get();
386
192

            
387
192
        BondedEras::<T>::mutate(|bonded| {
388
192
            bonded.push((era_index, session_start));
389
192

            
390
192
            if era_index > bonding_duration {
391
12
                let first_kept = era_index.defensive_saturating_sub(bonding_duration);
392
12

            
393
12
                // Prune out everything that's from before the first-kept index.
394
12
                let n_to_prune = bonded
395
12
                    .iter()
396
24
                    .take_while(|&&(era_idx, _)| era_idx < first_kept)
397
12
                    .count();
398

            
399
                // Kill slashing metadata.
400
12
                for (pruned_era, _) in bonded.drain(..n_to_prune) {
401
12
                    let removal_result =
402
12
                        ValidatorSlashInEra::<T>::clear_prefix(&pruned_era, REMOVE_LIMIT, None);
403
12
                    if removal_result.maybe_cursor.is_some() {
404
                        log::error!(
405
                            "Not all validator slashes were remove for era {:?}",
406
                            pruned_era
407
                        );
408
12
                    }
409
12
                    Slashes::<T>::remove(&pruned_era);
410
                }
411

            
412
12
                if let Some(&(_, first_session)) = bonded.first() {
413
12
                    T::SessionInterface::prune_historical_up_to(first_session);
414
12
                }
415
180
            }
416
192
        });
417
192

            
418
192
        Self::confirm_unconfirmed_slashes(era_index);
419
192
    }
420
}
421

            
422
impl<T: Config> Pallet<T> {
423
    /// Apply previously-unapplied slashes on the beginning of a new era, after a delay.
424
192
    fn confirm_unconfirmed_slashes(active_era: EraIndex) {
425
192
        Slashes::<T>::mutate(&active_era, |era_slashes| {
426
192
            log!(
427
192
                log::Level::Debug,
428
                "found {} slashes scheduled to be confirmed in era {:?}",
429
                era_slashes.len(),
430
                active_era,
431
            );
432
195
            for slash in era_slashes {
433
3
                slash.confirmed = true;
434
3
            }
435
192
        });
436
192
    }
437
}
438

            
439
/// A pending slash record. The value of the slash has been computed but not applied yet,
440
/// rather deferred for several eras.
441
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, Clone, PartialEq)]
442
pub struct Slash<AccountId, SlashId> {
443
    /// The stash ID of the offending validator.
444
    pub validator: AccountId,
445
    /// Reporters of the offence; bounty payout recipients.
446
    pub reporters: Vec<AccountId>,
447
    /// The amount of payout.
448
    pub slash_id: SlashId,
449
    pub percentage: Perbill,
450
    // Whether the slash is confirmed or still needs to go through deferred period
451
    pub confirmed: bool,
452
}
453

            
454
impl<AccountId, SlashId: One> Slash<AccountId, SlashId> {
455
    /// Initializes the default object using the given `validator`.
456
    pub fn default_from(validator: AccountId) -> Self {
457
        Self {
458
            validator,
459
            reporters: vec![],
460
            slash_id: One::one(),
461
            percentage: Perbill::from_percent(50),
462
            confirmed: false,
463
        }
464
    }
465
}
466

            
467
/// Computes a slash of a validator and nominators. It returns an unapplied
468
/// record to be applied at some later point. Slashing metadata is updated in storage,
469
/// since unapplied records are only rarely intended to be dropped.
470
///
471
/// The pending slash record returned does not have initialized reporters. Those have
472
/// to be set at a higher level, if any.
473
16
pub(crate) fn compute_slash<T: Config>(
474
16
    slash_fraction: Perbill,
475
16
    slash_id: T::SlashId,
476
16
    slash_era: EraIndex,
477
16
    stash: T::AccountId,
478
16
    slash_defer_duration: EraIndex,
479
16
) -> Option<Slash<T::AccountId, T::SlashId>> {
480
16
    let prior_slash_p = ValidatorSlashInEra::<T>::get(&slash_era, &stash).unwrap_or(Zero::zero());
481
16

            
482
16
    // compare slash proportions rather than slash values to avoid issues due to rounding
483
16
    // error.
484
16
    if slash_fraction.deconstruct() > prior_slash_p.deconstruct() {
485
15
        ValidatorSlashInEra::<T>::insert(&slash_era, &stash, &slash_fraction);
486
15
    } else {
487
        // we slash based on the max in era - this new event is not the max,
488
        // so neither the validator or any nominators will need an update.
489
        //
490
        // this does lead to a divergence of our system from the paper, which
491
        // pays out some reward even if the latest report is not max-in-era.
492
        // we opt to avoid the nominator lookups and edits and leave more rewards
493
        // for more drastic misbehavior.
494
1
        return None;
495
    }
496

            
497
15
    let confirmed = slash_defer_duration.is_zero();
498
15
    Some(Slash {
499
15
        validator: stash.clone(),
500
15
        percentage: slash_fraction,
501
15
        slash_id,
502
15
        reporters: Vec::new(),
503
15
        confirmed,
504
15
    })
505
16
}
506

            
507
/// Check that list is sorted and has no duplicates.
508
8
fn is_sorted_and_unique(list: &[u32]) -> bool {
509
8
    list.windows(2).all(|w| w[0] < w[1])
510
8
}