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::{Decode, Encode, FullCodec},
37
    sp_core::H256,
38
    sp_runtime::{
39
        traits::{Convert, Debug, One, Saturating, Zero},
40
        DispatchResult, Perbill,
41
    },
42
    sp_staking::{
43
        offence::{OffenceDetails, OnOffenceHandler},
44
        EraIndex, SessionIndex,
45
    },
46
    sp_std::{collections::vec_deque::VecDeque, vec, vec::Vec},
47
    tp_traits::{
48
        apply, derive_storage_traits, EraIndexProvider, ExternalIndexProvider,
49
        InvulnerablesProvider, OnEraStart,
50
    },
51
};
52

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

            
58
pub use pallet::*;
59

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
376
2
            Ok(())
377
        }
378

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

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

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

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

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

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

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

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

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

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

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

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

            
472
103
        let invulnerables = T::InvulnerablesProvider::invulnerables();
473
103
        add_db_reads_writes(1, 0);
474
103

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

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

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

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

            
492
96
            if slashing_mode == SlashingModeOption::LogOnly {
493
                continue;
494
96
            }
495
96

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

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

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

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

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

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

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

            
555
716
        let bonding_duration = T::BondingDuration::get();
556
716

            
557
716
        BondedEras::<T>::mutate(|bonded| {
558
716
            bonded.push((era_index, session_start, external_idx));
559
716

            
560
716
            if era_index > bonding_duration {
561
17
                let first_kept = era_index.defensive_saturating_sub(bonding_duration);
562
17

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

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

            
582
17
                if let Some(&(_, first_session, _)) = bonded.first() {
583
17
                    T::SessionInterface::prune_historical_up_to(first_session);
584
17
                }
585
699
            }
586
716
        });
587
716

            
588
716
        Self::add_era_slashes_to_queue(era_index);
589
716
    }
590
}
591

            
592
impl<T: Config> Pallet<T> {
593
716
    fn add_era_slashes_to_queue(active_era: EraIndex) {
594
716
        let mut slashes: VecDeque<_> = Slashes::<T>::get(&active_era).into();
595
716

            
596
716
        UnreportedSlashesQueue::<T>::mutate(|queue| queue.append(&mut slashes));
597
716
    }
598

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

            
604
7299
        UnreportedSlashesQueue::<T>::mutate(|queue| {
605
7299
            for _ in 0..amount {
606
7937
                let Some(slash) = queue.pop_front() else {
607
                    // no more slashes to process in the queue
608
7234
                    break;
609
                };
610

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

            
619
7299
        if slashes_to_send.is_empty() {
620
7224
            return 0;
621
75
        }
622
75

            
623
75
        let slashes_count = slashes_to_send.len() as u32;
624
75

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

            
631
75
        let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
632
75

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

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

            
657
75
        slashes_count
658
7299
    }
659
}
660

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

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

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

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

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

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