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
extern crate alloc;
31

            
32
use {
33
    alloc::{collections::vec_deque::VecDeque, vec, vec::Vec},
34
    frame_support::{pallet_prelude::*, traits::DefensiveSaturating},
35
    frame_system::pallet_prelude::*,
36
    log::log,
37
    pallet_staking::SessionInterface,
38
    parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, FullCodec},
39
    sp_core::H256,
40
    sp_runtime::{
41
        traits::{Convert, Debug, One, Saturating, Zero},
42
        DispatchResult, Perbill,
43
    },
44
    sp_staking::{
45
        offence::{OffenceDetails, OnOffenceHandler},
46
        EraIndex, SessionIndex,
47
    },
48
    tp_traits::{
49
        apply, derive_storage_traits, EraIndexProvider, ExternalIndexProvider,
50
        InvulnerablesProvider, OnEraStart,
51
    },
52
};
53

            
54
use {
55
    snowbridge_core::ChannelId,
56
    tp_bridge::{Command, DeliverMessage, Message, SlashData, TicketInfo, ValidateMessage},
57
};
58

            
59
pub use pallet::*;
60

            
61
#[cfg(test)]
62
mod mock;
63

            
64
#[cfg(test)]
65
mod tests;
66

            
67
#[cfg(feature = "runtime-benchmarks")]
68
mod benchmarking;
69
pub mod weights;
70

            
71
#[frame_support::pallet]
72
pub mod pallet {
73
    use super::*;
74
    pub use crate::weights::WeightInfo;
75

            
76
    #[pallet::event]
77
171
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
78
    pub enum Event<T: Config> {
79
        /// Removed author data
80
        SlashReported {
81
            validator: T::ValidatorId,
82
            fraction: Perbill,
83
            slash_era: EraIndex,
84
        },
85
        /// The slashes message was sent correctly.
86
        SlashesMessageSent {
87
            message_id: H256,
88
            slashes_command: Command,
89
        },
90
    }
91

            
92
    #[pallet::config]
93
    pub trait Config: frame_system::Config {
94
        /// The overarching event type.
95
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
96

            
97
        /// A stable ID for a validator.
98
        type ValidatorId: Member
99
            + Parameter
100
            + MaybeSerializeDeserialize
101
            + MaxEncodedLen
102
            + TryFrom<Self::AccountId>;
103

            
104
        /// A conversion from account ID to validator ID.
105
        type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;
106

            
107
        /// Number of eras that slashes are deferred by, after computation.
108
        ///
109
        /// This should be less than the bonding duration. Set to 0 if slashes
110
        /// should be applied immediately, without opportunity for intervention.
111
        #[pallet::constant]
112
        type SlashDeferDuration: Get<EraIndex>;
113

            
114
        /// Number of eras that staked funds must remain bonded for.
115
        #[pallet::constant]
116
        type BondingDuration: Get<EraIndex>;
117

            
118
        // SlashId type, used as a counter on the number of slashes
119
        type SlashId: Default
120
            + FullCodec
121
            + TypeInfo
122
            + Copy
123
            + Clone
124
            + Debug
125
            + Eq
126
            + Saturating
127
            + One
128
            + Ord
129
            + MaxEncodedLen;
130

            
131
        /// Interface for interacting with a session pallet.
132
        type SessionInterface: SessionInterface<Self::AccountId>;
133

            
134
        /// Era index provider, used to fetch the active era among other things
135
        type EraIndexProvider: EraIndexProvider;
136

            
137
        /// Invulnerable provider, used to get the invulnerables to know when not to slash
138
        type InvulnerablesProvider: InvulnerablesProvider<Self::ValidatorId>;
139

            
140
        /// Validate a message that will be sent to Ethereum.
141
        type ValidateMessage: ValidateMessage;
142

            
143
        /// Send a message to Ethereum. Needs to be validated first.
144
        type OutboundQueue: DeliverMessage<
145
            Ticket = <<Self as pallet::Config>::ValidateMessage as ValidateMessage>::Ticket,
146
        >;
147

            
148
        /// Provider to retrieve the current external index of validators
149
        type ExternalIndexProvider: ExternalIndexProvider;
150

            
151
        /// How many queued slashes are being processed per block.
152
        #[pallet::constant]
153
        type QueuedSlashesProcessedPerBlock: Get<u32>;
154

            
155
        /// The weight information of this pallet.
156
        type WeightInfo: WeightInfo;
157
    }
158

            
159
    #[pallet::error]
160
    pub enum Error<T> {
161
        /// The era for which the slash wants to be cancelled has no slashes
162
        EmptyTargets,
163
        /// No slash was found to be cancelled at the given index
164
        InvalidSlashIndex,
165
        /// Slash indices to be cancelled are not sorted or unique
166
        NotSortedAndUnique,
167
        /// Provided an era in the future
168
        ProvidedFutureEra,
169
        /// Provided an era that is not slashable
170
        ProvidedNonSlashableEra,
171
        /// The slash to be cancelled has already elapsed the DeferPeriod
172
        DeferPeriodIsOver,
173
        /// There was an error computing the slash
174
        ErrorComputingSlash,
175
        /// Failed to validate the message that was going to be sent to Ethereum
176
        EthereumValidateFail,
177
        /// Failed to deliver the message to Ethereum
178
        EthereumDeliverFail,
179
        /// Invalid params for root_test_send_msg_to_eth
180
        RootTestInvalidParams,
181
    }
182

            
183
    #[apply(derive_storage_traits)]
184
    #[derive(MaxEncodedLen, DecodeWithMemTracking, Default)]
185
    pub enum SlashingModeOption {
186
        #[default]
187
        Enabled,
188
        LogOnly,
189
1
        Disabled,
190
    }
191

            
192
326
    #[pallet::pallet]
193
    pub struct Pallet<T>(PhantomData<T>);
194

            
195
    /// All slashing events on validators, mapped by era to the highest slash proportion
196
    /// and slash value of the era.
197
1475
    #[pallet::storage]
198
    pub(crate) type ValidatorSlashInEra<T: Config> =
199
        StorageDoubleMap<_, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, Perbill>;
200

            
201
    /// A mapping from still-bonded eras to the first session index of that era.
202
    ///
203
    /// Must contains information for eras for the range:
204
    /// `[active_era - bounding_duration; active_era]`
205
2998
    #[pallet::storage]
206
    #[pallet::unbounded]
207
    pub type BondedEras<T: Config> =
208
        StorageValue<_, Vec<(EraIndex, SessionIndex, u64)>, ValueQuery>;
209

            
210
    /// A counter on the number of slashes we have performed
211
2950
    #[pallet::storage]
212
    #[pallet::getter(fn next_slash_id)]
213
    pub type NextSlashId<T: Config> = StorageValue<_, T::SlashId, ValueQuery>;
214

            
215
    /// All unapplied slashes that are queued for later.
216
1538
    #[pallet::storage]
217
    #[pallet::unbounded]
218
    #[pallet::getter(fn slashes)]
219
    pub type Slashes<T: Config> =
220
        StorageMap<_, Twox64Concat, EraIndex, Vec<Slash<T::AccountId, T::SlashId>>, ValueQuery>;
221

            
222
    /// All unreported slashes that will be processed in the future.
223
32818
    #[pallet::storage]
224
    #[pallet::unbounded]
225
    #[pallet::getter(fn unreported_slashes)]
226
    pub type UnreportedSlashesQueue<T: Config> =
227
        StorageValue<_, VecDeque<Slash<T::AccountId, T::SlashId>>, ValueQuery>;
228

            
229
    // Turns slashing on or off
230
210
    #[pallet::storage]
231
    pub type SlashingMode<T: Config> = StorageValue<_, SlashingModeOption, ValueQuery>;
232

            
233
    #[pallet::call]
234
    impl<T: Config> Pallet<T> {
235
        /// Cancel a slash that was deferred for a later era
236
        #[pallet::call_index(0)]
237
        #[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_indices.len() as u32))]
238
        pub fn cancel_deferred_slash(
239
            origin: OriginFor<T>,
240
            era: EraIndex,
241
            slash_indices: Vec<u32>,
242
10
        ) -> DispatchResult {
243
10
            ensure_root(origin)?;
244

            
245
10
            let active_era = T::EraIndexProvider::active_era().index;
246
10

            
247
10
            // We need to be in the defer period
248
10
            ensure!(
249
10
                era <= active_era
250
10
                    .saturating_add(T::SlashDeferDuration::get().saturating_add(One::one()))
251
10
                    && era > active_era,
252
4
                Error::<T>::DeferPeriodIsOver
253
            );
254

            
255
6
            ensure!(!slash_indices.is_empty(), Error::<T>::EmptyTargets);
256
6
            ensure!(
257
6
                is_sorted_and_unique(&slash_indices),
258
2
                Error::<T>::NotSortedAndUnique
259
            );
260
            // fetch slashes for the era in which we want to defer
261
4
            let mut era_slashes = Slashes::<T>::get(era);
262
4

            
263
4
            let last_item = slash_indices[slash_indices.len().saturating_sub(1)];
264
4
            ensure!(
265
4
                (last_item as usize) < era_slashes.len(),
266
1
                Error::<T>::InvalidSlashIndex
267
            );
268

            
269
            // Remove elements starting from the highest index to avoid shifting issues.
270
3
            for index in slash_indices.into_iter().rev() {
271
3
                era_slashes.remove(index as usize);
272
3
            }
273
            // insert back slashes
274
3
            Slashes::<T>::insert(era, &era_slashes);
275
3
            Ok(())
276
        }
277

            
278
        #[pallet::call_index(1)]
279
        #[pallet::weight(T::WeightInfo::force_inject_slash())]
280
        pub fn force_inject_slash(
281
            origin: OriginFor<T>,
282
            era: EraIndex,
283
            validator: T::AccountId,
284
            percentage: Perbill,
285
            external_idx: u64,
286
636
        ) -> DispatchResult {
287
636
            ensure_root(origin)?;
288
636
            let active_era = T::EraIndexProvider::active_era().index;
289
636

            
290
636
            ensure!(era <= active_era, Error::<T>::ProvidedFutureEra);
291

            
292
635
            let slash_defer_duration = T::SlashDeferDuration::get();
293
635

            
294
635
            let _ = T::EraIndexProvider::era_to_session_start(era)
295
635
                .ok_or(Error::<T>::ProvidedNonSlashableEra)?;
296

            
297
634
            let next_slash_id = NextSlashId::<T>::get();
298

            
299
634
            let slash = compute_slash::<T>(
300
634
                percentage,
301
634
                next_slash_id,
302
634
                era,
303
634
                validator,
304
634
                slash_defer_duration,
305
634
                external_idx,
306
634
            )
307
634
            .ok_or(Error::<T>::ErrorComputingSlash)?;
308

            
309
            // If we defer duration is 0, we immediately apply and confirm
310
634
            let era_to_consider = if slash_defer_duration == 0 {
311
626
                era.saturating_add(One::one())
312
            } else {
313
8
                era.saturating_add(slash_defer_duration)
314
8
                    .saturating_add(One::one())
315
            };
316

            
317
634
            Slashes::<T>::mutate(era_to_consider, |era_slashes| {
318
634
                era_slashes.push(slash);
319
634
            });
320
634

            
321
634
            NextSlashId::<T>::put(next_slash_id.saturating_add(One::one()));
322
634
            Ok(())
323
        }
324

            
325
        #[pallet::call_index(2)]
326
        #[pallet::weight(T::WeightInfo::root_test_send_msg_to_eth())]
327
        pub fn root_test_send_msg_to_eth(
328
            origin: OriginFor<T>,
329
            nonce: H256,
330
            num_msgs: u32,
331
            msg_size: u32,
332
2
        ) -> DispatchResult {
333
2
            ensure_root(origin)?;
334

            
335
            // Ensure we don't accidentally pass huge params that would stall the chain
336
2
            ensure!(
337
2
                num_msgs <= 100 && msg_size <= 2048,
338
                Error::<T>::RootTestInvalidParams
339
            );
340

            
341
2
            for i in 0..num_msgs {
342
                // Make sure each message has a different payload
343
2
                let mut payload = sp_core::blake2_256((nonce, i).encode().as_ref()).to_vec();
344
2
                // Extend with zeros until msg_size is reached
345
2
                payload.resize(msg_size as usize, 0);
346
2
                // Example command, this should be something like "ReportSlashes"
347
2
                let command = Command::Test(payload);
348
2

            
349
2
                // Validate
350
2
                let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
351
2

            
352
2
                let outbound_message = Message {
353
2
                    id: None,
354
2
                    channel_id,
355
2
                    command,
356
2
                };
357

            
358
                // validate the message
359
                // Ignore fee because for now only root can send messages
360
2
                let (ticket, _fee) =
361
2
                    T::ValidateMessage::validate(&outbound_message).map_err(|err| {
362
                        log::error!(
363
                            "root_test_send_msg_to_eth: validation of message {i} failed. {err:?}"
364
                        );
365
                        crate::pallet::Error::<T>::EthereumValidateFail
366
2
                    })?;
367

            
368
                // Deliver
369
2
                T::OutboundQueue::deliver(ticket).map_err(|err| {
370
                    log::error!(
371
                        "root_test_send_msg_to_eth: delivery of message {i} failed. {err:?}"
372
                    );
373
                    crate::pallet::Error::<T>::EthereumDeliverFail
374
2
                })?;
375
            }
376

            
377
2
            Ok(())
378
        }
379

            
380
        #[pallet::call_index(3)]
381
        #[pallet::weight(T::WeightInfo::set_slashing_mode())]
382
1
        pub fn set_slashing_mode(origin: OriginFor<T>, mode: SlashingModeOption) -> DispatchResult {
383
1
            ensure_root(origin)?;
384

            
385
1
            SlashingMode::<T>::put(mode);
386
1

            
387
1
            Ok(())
388
        }
389
    }
390

            
391
3
    #[pallet::hooks]
392
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
393
7475
        fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
394
7475
            let processed = Self::process_slashes_queue(T::QueuedSlashesProcessedPerBlock::get());
395
7475
            T::WeightInfo::process_slashes_queue(processed)
396
7475
        }
397
    }
398
}
399

            
400
/// This is intended to be used with `FilterHistoricalOffences`.
401
impl<T: Config>
402
    OnOffenceHandler<T::AccountId, pallet_session::historical::IdentificationTuple<T>, Weight>
403
    for Pallet<T>
404
where
405
    T: Config<ValidatorId = <T as frame_system::Config>::AccountId>,
406
    T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
407
    T: pallet_session::historical::Config,
408
    T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>,
409
    T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>,
410
    <T as pallet::Config>::ValidatorIdOf: Convert<
411
        <T as frame_system::Config>::AccountId,
412
        Option<<T as frame_system::Config>::AccountId>,
413
    >,
414
{
415
104
    fn on_offence(
416
104
        offenders: &[OffenceDetails<
417
104
            T::AccountId,
418
104
            pallet_session::historical::IdentificationTuple<T>,
419
104
        >],
420
104
        slash_fraction: &[Perbill],
421
104
        slash_session: SessionIndex,
422
104
    ) -> Weight {
423
104
        let mut consumed_weight = Weight::default();
424
912
        let mut add_db_reads_writes = |reads, writes| {
425
912
            consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
426
912
        };
427

            
428
104
        let slashing_mode = SlashingMode::<T>::get();
429
104
        add_db_reads_writes(1, 0);
430
104

            
431
104
        if slashing_mode == SlashingModeOption::Disabled {
432
1
            return consumed_weight;
433
103
        }
434
103

            
435
103
        let active_era = { T::EraIndexProvider::active_era().index };
436
103
        let active_era_start_session_index = T::EraIndexProvider::era_to_session_start(active_era)
437
103
            .unwrap_or_else(|| {
438
                frame_support::print("Error: start_session_index must be set for current_era");
439
                0
440
103
            });
441
103

            
442
103
        // Account reads for active_era and era_to_session_start.
443
103
        add_db_reads_writes(2, 0);
444

            
445
        // Fast path for active-era report - most likely.
446
        // `slash_session` cannot be in a future active era. It must be in `active_era` or before.
447
103
        let (slash_era, external_idx) = if slash_session >= active_era_start_session_index {
448
            // Account for get_external_index read.
449
49
            add_db_reads_writes(1, 0);
450
49
            (active_era, T::ExternalIndexProvider::get_external_index())
451
        } else {
452
54
            let eras = BondedEras::<T>::get();
453
54
            add_db_reads_writes(1, 0);
454
54

            
455
54
            // Reverse because it's more likely to find reports from recent eras.
456
54
            match eras
457
54
                .iter()
458
54
                .rev()
459
108
                .find(|&(_, sesh, _)| sesh <= &slash_session)
460
            {
461
54
                Some((slash_era, _, external_idx)) => (*slash_era, *external_idx),
462
                // Before bonding period. defensive - should be filtered out.
463
                None => return consumed_weight,
464
            }
465
        };
466

            
467
103
        let slash_defer_duration = T::SlashDeferDuration::get();
468
103
        add_db_reads_writes(1, 0);
469
103

            
470
103
        let invulnerables = T::InvulnerablesProvider::invulnerables();
471
103
        add_db_reads_writes(1, 0);
472
103

            
473
103
        let mut next_slash_id = NextSlashId::<T>::get();
474
103
        add_db_reads_writes(1, 0);
475

            
476
103
        for (details, slash_fraction) in offenders.iter().zip(slash_fraction) {
477
103
            let (stash, _) = &details.offender;
478
103

            
479
103
            // Skip if the validator is invulnerable.
480
103
            if invulnerables.contains(stash) {
481
7
                continue;
482
96
            }
483
96

            
484
96
            Self::deposit_event(Event::<T>::SlashReported {
485
96
                validator: stash.clone(),
486
96
                fraction: *slash_fraction,
487
96
                slash_era,
488
96
            });
489
96

            
490
96
            if slashing_mode == SlashingModeOption::LogOnly {
491
                continue;
492
96
            }
493
96

            
494
96
            // Account for one read and one possible write inside compute_slash.
495
96
            add_db_reads_writes(1, 1);
496
96

            
497
96
            let slash = compute_slash::<T>(
498
96
                *slash_fraction,
499
96
                next_slash_id,
500
96
                slash_era,
501
96
                stash.clone(),
502
96
                slash_defer_duration,
503
96
                external_idx,
504
96
            );
505

            
506
96
            if let Some(mut slash) = slash {
507
94
                slash.reporters = details.reporters.clone();
508
94

            
509
94
                // Defer to end of some `slash_defer_duration` from now.
510
94
                log!(
511
94
                    log::Level::Debug,
512
                    "deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
513
                    slash_fraction,
514
                    slash_era,
515
                    active_era,
516
                    slash_era + slash_defer_duration + 1,
517
                );
518

            
519
                // Cover slash defer duration equal to 0
520
                // Slashes are applied at the end of the current era
521
94
                if slash_defer_duration == 0 {
522
93
                    Slashes::<T>::mutate(active_era.saturating_add(One::one()), move |for_now| {
523
93
                        for_now.push(slash)
524
93
                    });
525
93
                    add_db_reads_writes(1, 1);
526
93
                } else {
527
1
                    // Else, slashes are applied after slash_defer_period since the slashed era
528
1
                    Slashes::<T>::mutate(
529
1
                        slash_era
530
1
                            .saturating_add(slash_defer_duration)
531
1
                            .saturating_add(One::one()),
532
1
                        move |for_later| for_later.push(slash),
533
1
                    );
534
1
                    add_db_reads_writes(1, 1);
535
1
                }
536

            
537
                // Fix unwrap
538
94
                next_slash_id = next_slash_id.saturating_add(One::one());
539
2
            }
540
        }
541
103
        NextSlashId::<T>::put(next_slash_id);
542
103
        add_db_reads_writes(0, 1);
543
103
        consumed_weight
544
104
    }
545
}
546

            
547
impl<T: Config> OnEraStart for Pallet<T> {
548
718
    fn on_era_start(era_index: EraIndex, session_start: SessionIndex, external_idx: u64) {
549
        // This should be small, as slashes are limited by the num of validators
550
        // let's put 1000 as a conservative measure
551
        const REMOVE_LIMIT: u32 = 1000;
552

            
553
718
        let bonding_duration = T::BondingDuration::get();
554
718

            
555
718
        BondedEras::<T>::mutate(|bonded| {
556
718
            bonded.push((era_index, session_start, external_idx));
557
718

            
558
718
            if era_index > bonding_duration {
559
17
                let first_kept = era_index.defensive_saturating_sub(bonding_duration);
560
17

            
561
17
                // Prune out everything that's from before the first-kept index.
562
17
                let n_to_prune = bonded
563
17
                    .iter()
564
34
                    .take_while(|&&(era_idx, _, _)| era_idx < first_kept)
565
17
                    .count();
566

            
567
                // Kill slashing metadata.
568
17
                for (pruned_era, _, _) in bonded.drain(..n_to_prune) {
569
17
                    let removal_result =
570
17
                        ValidatorSlashInEra::<T>::clear_prefix(pruned_era, REMOVE_LIMIT, None);
571
17
                    if removal_result.maybe_cursor.is_some() {
572
                        log::error!(
573
                            "Not all validator slashes were remove for era {:?}",
574
                            pruned_era
575
                        );
576
17
                    }
577
17
                    Slashes::<T>::remove(pruned_era);
578
                }
579

            
580
17
                if let Some(&(_, first_session, _)) = bonded.first() {
581
17
                    T::SessionInterface::prune_historical_up_to(first_session);
582
17
                }
583
701
            }
584
718
        });
585
718

            
586
718
        Self::add_era_slashes_to_queue(era_index);
587
718
    }
588
}
589

            
590
impl<T: Config> Pallet<T> {
591
718
    fn add_era_slashes_to_queue(active_era: EraIndex) {
592
718
        let mut slashes: VecDeque<_> = Slashes::<T>::get(active_era).into();
593
718

            
594
718
        UnreportedSlashesQueue::<T>::mutate(|queue| queue.append(&mut slashes));
595
718
    }
596

            
597
    /// Returns number of slashes that were sent to ethereum.
598
7475
    fn process_slashes_queue(amount: u32) -> u32 {
599
7475
        let mut slashes_to_send: Vec<_> = vec![];
600
7475
        let era_index = T::EraIndexProvider::active_era().index;
601
7475

            
602
7475
        UnreportedSlashesQueue::<T>::mutate(|queue| {
603
7475
            for _ in 0..amount {
604
8113
                let Some(slash) = queue.pop_front() else {
605
                    // no more slashes to process in the queue
606
7410
                    break;
607
                };
608

            
609
703
                slashes_to_send.push(SlashData {
610
703
                    encoded_validator_id: slash.validator.clone().encode(),
611
703
                    slash_fraction: slash.percentage.deconstruct(),
612
703
                    external_idx: slash.external_idx,
613
703
                });
614
            }
615
7475
        });
616
7475

            
617
7475
        if slashes_to_send.is_empty() {
618
7400
            return 0;
619
75
        }
620
75

            
621
75
        let slashes_count = slashes_to_send.len() as u32;
622
75

            
623
75
        // Build command with slashes.
624
75
        let command = Command::ReportSlashes {
625
75
            era_index,
626
75
            slashes: slashes_to_send,
627
75
        };
628
75

            
629
75
        let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
630
75

            
631
75
        let outbound_message = Message {
632
75
            id: None,
633
75
            channel_id,
634
75
            command: command.clone(),
635
75
        };
636
75

            
637
75
        // Validate and deliver the message
638
75
        match T::ValidateMessage::validate(&outbound_message) {
639
75
            Ok((ticket, _fee)) => {
640
75
                let message_id = ticket.message_id();
641
75
                if let Err(err) = T::OutboundQueue::deliver(ticket) {
642
                    log::error!(target: "ext_validators_slashes", "OutboundQueue delivery of message failed. {err:?}");
643
75
                } else {
644
75
                    Self::deposit_event(Event::SlashesMessageSent {
645
75
                        message_id,
646
75
                        slashes_command: command,
647
75
                    });
648
75
                }
649
            }
650
            Err(err) => {
651
                log::error!(target: "ext_validators_slashes", "OutboundQueue validation of message failed. {err:?}");
652
            }
653
        };
654

            
655
75
        slashes_count
656
7475
    }
657
}
658

            
659
/// A pending slash record. The value of the slash has been computed but not applied yet,
660
/// rather deferred for several eras.
661
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, Clone, PartialEq)]
662
pub struct Slash<AccountId, SlashId> {
663
    /// external index identifying a given set of validators
664
    pub external_idx: u64,
665
    /// The stash ID of the offending validator.
666
    pub validator: AccountId,
667
    /// Reporters of the offence; bounty payout recipients.
668
    pub reporters: Vec<AccountId>,
669
    /// The amount of payout.
670
    pub slash_id: SlashId,
671
    pub percentage: Perbill,
672
    // Whether the slash is confirmed or still needs to go through deferred period
673
    pub confirmed: bool,
674
}
675

            
676
impl<AccountId, SlashId: One> Slash<AccountId, SlashId> {
677
    /// Initializes the default object using the given `validator`.
678
    pub fn default_from(validator: AccountId) -> Self {
679
        Self {
680
            external_idx: 0,
681
            validator,
682
            reporters: vec![],
683
            slash_id: One::one(),
684
            percentage: Perbill::from_percent(50),
685
            confirmed: false,
686
        }
687
    }
688
}
689

            
690
/// Computes a slash of a validator and nominators. It returns an unapplied
691
/// record to be applied at some later point. Slashing metadata is updated in storage,
692
/// since unapplied records are only rarely intended to be dropped.
693
///
694
/// The pending slash record returned does not have initialized reporters. Those have
695
/// to be set at a higher level, if any.
696
730
pub(crate) fn compute_slash<T: Config>(
697
730
    slash_fraction: Perbill,
698
730
    slash_id: T::SlashId,
699
730
    slash_era: EraIndex,
700
730
    stash: T::AccountId,
701
730
    slash_defer_duration: EraIndex,
702
730
    external_idx: u64,
703
730
) -> Option<Slash<T::AccountId, T::SlashId>> {
704
730
    let prior_slash_p = ValidatorSlashInEra::<T>::get(slash_era, &stash).unwrap_or(Zero::zero());
705
730

            
706
730
    // compare slash proportions rather than slash values to avoid issues due to rounding
707
730
    // error.
708
730
    if slash_fraction.deconstruct() > prior_slash_p.deconstruct() {
709
728
        ValidatorSlashInEra::<T>::insert(slash_era, &stash, slash_fraction);
710
728
    } else {
711
        // we slash based on the max in era - this new event is not the max,
712
        // so neither the validator or any nominators will need an update.
713
        //
714
        // this does lead to a divergence of our system from the paper, which
715
        // pays out some reward even if the latest report is not max-in-era.
716
        // we opt to avoid the nominator lookups and edits and leave more rewards
717
        // for more drastic misbehavior.
718
2
        return None;
719
    }
720

            
721
728
    let confirmed = slash_defer_duration.is_zero();
722
728
    Some(Slash {
723
728
        external_idx,
724
728
        validator: stash.clone(),
725
728
        percentage: slash_fraction,
726
728
        slash_id,
727
728
        reporters: Vec::new(),
728
728
        confirmed,
729
728
    })
730
730
}
731

            
732
/// Check that list is sorted and has no duplicates.
733
38
fn is_sorted_and_unique(list: &[u32]) -> bool {
734
38
    list.windows(2).all(|w| w[0] < w[1])
735
38
}