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, SlashData, TanssiMessage, 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
    #[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
        /// A stable ID for a validator.
95
        type ValidatorId: Member
96
            + Parameter
97
            + MaybeSerializeDeserialize
98
            + MaxEncodedLen
99
            + TryFrom<Self::AccountId>;
100

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

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

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

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

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

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

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

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

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

            
145
        /// Provider to retrieve the current external index of validators
146
        type ExternalIndexProvider: ExternalIndexProvider;
147

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

            
152
        /// The weight information of this pallet.
153
        type WeightInfo: WeightInfo;
154
    }
155

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

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

            
189
    #[pallet::pallet]
190
    pub struct Pallet<T>(PhantomData<T>);
191

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

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

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

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

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

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

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

            
242
10
            let active_era = T::EraIndexProvider::active_era().index;
243

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

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

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

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

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

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

            
289
635
            let slash_defer_duration = T::SlashDeferDuration::get();
290

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

            
294
634
            let next_slash_id = NextSlashId::<T>::get();
295

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

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

            
314
634
            Slashes::<T>::append(era_to_consider, slash);
315

            
316
634
            NextSlashId::<T>::put(next_slash_id.saturating_add(One::one()));
317
634
            Ok(())
318
        }
319

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

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

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

            
344
                // Validate
345
2
                let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
346

            
347
2
                let outbound_message = TanssiMessage {
348
2
                    id: None,
349
2
                    channel_id,
350
2
                    command,
351
2
                };
352

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

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

            
372
2
            Ok(())
373
        }
374

            
375
        #[pallet::call_index(3)]
376
        #[pallet::weight(T::WeightInfo::set_slashing_mode())]
377
1
        pub fn set_slashing_mode(origin: OriginFor<T>, mode: SlashingModeOption) -> DispatchResult {
378
1
            ensure_root(origin)?;
379

            
380
1
            SlashingMode::<T>::put(mode);
381

            
382
1
            Ok(())
383
        }
384
    }
385

            
386
    #[pallet::hooks]
387
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
388
7591
        fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
389
7591
            let processed = Self::process_slashes_queue(T::QueuedSlashesProcessedPerBlock::get());
390
7591
            T::WeightInfo::process_slashes_queue(processed)
391
7591
        }
392
    }
393
}
394

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

            
423
104
        let slashing_mode = SlashingMode::<T>::get();
424
104
        add_db_reads_writes(1, 0);
425

            
426
104
        if slashing_mode == SlashingModeOption::Disabled {
427
1
            return consumed_weight;
428
103
        }
429

            
430
103
        let active_era = { T::EraIndexProvider::active_era().index };
431
103
        let active_era_start_session_index = T::EraIndexProvider::era_to_session_start(active_era)
432
103
            .unwrap_or_else(|| {
433
                frame_support::print("Error: start_session_index must be set for current_era");
434
                0
435
            });
436

            
437
        // Account reads for active_era and era_to_session_start.
438
103
        add_db_reads_writes(2, 0);
439

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

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

            
462
103
        let slash_defer_duration = T::SlashDeferDuration::get();
463
103
        add_db_reads_writes(1, 0);
464

            
465
103
        let invulnerables = T::InvulnerablesProvider::invulnerables();
466
103
        add_db_reads_writes(1, 0);
467

            
468
103
        let mut next_slash_id = NextSlashId::<T>::get();
469
103
        add_db_reads_writes(1, 0);
470

            
471
103
        for (details, slash_fraction) in offenders.iter().zip(slash_fraction) {
472
103
            let (stash, _) = &details.offender;
473

            
474
            // Skip if the validator is invulnerable.
475
103
            if invulnerables.contains(stash) {
476
7
                continue;
477
96
            }
478

            
479
96
            Self::deposit_event(Event::<T>::SlashReported {
480
96
                validator: stash.clone(),
481
96
                fraction: *slash_fraction,
482
96
                slash_era,
483
96
            });
484

            
485
96
            if slashing_mode == SlashingModeOption::LogOnly {
486
                continue;
487
96
            }
488

            
489
            // Account for one read and one possible write inside compute_slash.
490
96
            add_db_reads_writes(1, 1);
491

            
492
96
            let slash = compute_slash::<T>(
493
96
                *slash_fraction,
494
96
                next_slash_id,
495
96
                slash_era,
496
96
                stash.clone(),
497
96
                slash_defer_duration,
498
96
                external_idx,
499
            );
500

            
501
96
            if let Some(mut slash) = slash {
502
94
                slash.reporters = details.reporters.clone();
503

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

            
514
94
                let apply_slash_at = if slash_defer_duration == 0 {
515
                    // Cover slash defer duration equal to 0
516
                    // Slashes are applied at the end of the current era
517
93
                    active_era.saturating_add(One::one())
518
                } else {
519
                    // Else, slashes are applied after slash_defer_period since the slashed era
520
1
                    slash_era
521
1
                        .saturating_add(slash_defer_duration)
522
1
                        .saturating_add(One::one())
523
                };
524
94
                Slashes::<T>::append(apply_slash_at, slash);
525
94
                add_db_reads_writes(1, 1);
526

            
527
                // Fix unwrap
528
94
                next_slash_id = next_slash_id.saturating_add(One::one());
529
2
            }
530
        }
531
103
        NextSlashId::<T>::put(next_slash_id);
532
103
        add_db_reads_writes(0, 1);
533
103
        consumed_weight
534
104
    }
535
}
536

            
537
impl<T: Config> OnEraStart for Pallet<T> {
538
936
    fn on_era_start(era_index: EraIndex, session_start: SessionIndex, external_idx: u64) {
539
        // This should be small, as slashes are limited by the num of validators
540
        // let's put 1000 as a conservative measure
541
        const REMOVE_LIMIT: u32 = 1000;
542

            
543
936
        let bonding_duration = T::BondingDuration::get();
544

            
545
936
        BondedEras::<T>::mutate(|bonded| {
546
936
            bonded.push((era_index, session_start, external_idx));
547

            
548
936
            if era_index > bonding_duration {
549
17
                let first_kept = era_index.defensive_saturating_sub(bonding_duration);
550

            
551
                // Prune out everything that's from before the first-kept index.
552
17
                let n_to_prune = bonded
553
17
                    .iter()
554
34
                    .take_while(|&&(era_idx, _, _)| era_idx < first_kept)
555
17
                    .count();
556

            
557
                // Kill slashing metadata.
558
17
                for (pruned_era, _, _) in bonded.drain(..n_to_prune) {
559
17
                    let removal_result =
560
17
                        ValidatorSlashInEra::<T>::clear_prefix(pruned_era, REMOVE_LIMIT, None);
561
17
                    if removal_result.maybe_cursor.is_some() {
562
                        log::error!(
563
                            "Not all validator slashes were remove for era {:?}",
564
                            pruned_era
565
                        );
566
17
                    }
567
17
                    Slashes::<T>::remove(pruned_era);
568
                }
569

            
570
17
                if let Some(&(_, first_session, _)) = bonded.first() {
571
17
                    T::SessionInterface::prune_historical_up_to(first_session);
572
17
                }
573
919
            }
574
936
        });
575

            
576
936
        Self::add_era_slashes_to_queue(era_index);
577
936
    }
578
}
579

            
580
impl<T: Config> Pallet<T> {
581
936
    fn add_era_slashes_to_queue(active_era: EraIndex) {
582
936
        let mut slashes: VecDeque<_> = Slashes::<T>::get(active_era).into();
583

            
584
936
        UnreportedSlashesQueue::<T>::mutate(|queue| queue.append(&mut slashes));
585
936
    }
586

            
587
    /// Returns number of slashes that were sent to ethereum.
588
7591
    fn process_slashes_queue(amount: u32) -> u32 {
589
7591
        let mut slashes_to_send: Vec<_> = vec![];
590
7591
        let era_index = T::EraIndexProvider::active_era().index;
591

            
592
7591
        UnreportedSlashesQueue::<T>::mutate(|queue| {
593
7591
            for _ in 0..amount {
594
8229
                let Some(slash) = queue.pop_front() else {
595
                    // no more slashes to process in the queue
596
7526
                    break;
597
                };
598

            
599
703
                slashes_to_send.push(SlashData {
600
703
                    encoded_validator_id: slash.validator.clone().encode(),
601
703
                    slash_fraction: slash.percentage.deconstruct(),
602
703
                    external_idx: slash.external_idx,
603
703
                });
604
            }
605
7591
        });
606

            
607
7591
        if slashes_to_send.is_empty() {
608
7516
            return 0;
609
75
        }
610

            
611
75
        let slashes_count = slashes_to_send.len() as u32;
612

            
613
        // Build command with slashes.
614
75
        let command = Command::ReportSlashes {
615
75
            era_index,
616
75
            slashes: slashes_to_send,
617
75
        };
618

            
619
75
        let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
620

            
621
75
        let outbound_message = TanssiMessage {
622
75
            id: None,
623
75
            channel_id,
624
75
            command: command.clone(),
625
75
        };
626

            
627
        // Validate and deliver the message
628
75
        match T::ValidateMessage::validate(&outbound_message) {
629
75
            Ok((ticket, _fee)) => {
630
75
                let message_id = ticket.message_id();
631
75
                if let Err(err) = T::OutboundQueue::deliver(ticket) {
632
                    log::error!(target: "ext_validators_slashes", "OutboundQueue delivery of message failed. {err:?}");
633
75
                } else {
634
75
                    Self::deposit_event(Event::SlashesMessageSent {
635
75
                        message_id,
636
75
                        slashes_command: command,
637
75
                    });
638
75
                }
639
            }
640
            Err(err) => {
641
                log::error!(target: "ext_validators_slashes", "OutboundQueue validation of message failed. {err:?}");
642
            }
643
        };
644

            
645
75
        slashes_count
646
7591
    }
647
}
648

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

            
666
impl<AccountId, SlashId: One> Slash<AccountId, SlashId> {
667
    /// Initializes the default object using the given `validator`.
668
    pub fn default_from(validator: AccountId) -> Self {
669
        Self {
670
            external_idx: 0,
671
            validator,
672
            reporters: vec![],
673
            slash_id: One::one(),
674
            percentage: Perbill::from_percent(50),
675
            confirmed: false,
676
        }
677
    }
678
}
679

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

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

            
711
728
    let confirmed = slash_defer_duration.is_zero();
712
728
    Some(Slash {
713
728
        external_idx,
714
728
        validator: stash.clone(),
715
728
        percentage: slash_fraction,
716
728
        slash_id,
717
728
        reporters: Vec::new(),
718
728
        confirmed,
719
728
    })
720
730
}
721

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