// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Offences Pallet //! //! Tracks reported offences // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] pub mod migration; mod mock; mod tests; use core::marker::PhantomData; use codec::Encode; use frame_support::weights::Weight; use sp_runtime::{traits::Hash, Perbill}; use sp_staking::{ offence::{Kind, Offence, OffenceDetails, OffenceError, OnOffenceHandler, ReportOffence}, SessionIndex, }; use sp_std::prelude::*; pub use pallet::*; /// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`. type OpaqueTimeSlot = Vec; /// A type alias for a report identifier. type ReportIdOf = ::Hash; const LOG_TARGET: &str = "runtime::offences"; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] #[pallet::without_storage_info] pub struct Pallet(_); /// The pallet's config trait. #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. type RuntimeEvent: From + IsType<::RuntimeEvent>; /// Full identification of the validator. type IdentificationTuple: Parameter; /// A handler called for every offence report. type OnOffenceHandler: OnOffenceHandler; } /// The primary structure that holds all offence records keyed by report identifiers. #[pallet::storage] #[pallet::getter(fn reports)] pub type Reports = StorageMap< _, Twox64Concat, ReportIdOf, OffenceDetails, >; /// A vector of reports of the same kind that happened at the same time slot. #[pallet::storage] pub type ConcurrentReportsIndex = StorageDoubleMap< _, Twox64Concat, Kind, Twox64Concat, OpaqueTimeSlot, Vec>, ValueQuery, >; /// Events type. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// There is an offence reported of the given `kind` happened at the `session_index` and /// (kind-specific) time slot. This event is not deposited for duplicate slashes. /// \[kind, timeslot\]. Offence { kind: Kind, timeslot: OpaqueTimeSlot }, } } impl ReportOffence for Pallet where T: Config, O: Offence, { fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { let offenders = offence.offenders(); let time_slot = offence.time_slot(); // Go through all offenders in the offence report and find all offenders that were spotted // in unique reports. let TriageOutcome { concurrent_offenders } = match Self::triage_offence_report::(reporters, &time_slot, offenders) { Some(triage) => triage, // The report contained only duplicates, so there is no need to slash again. None => return Err(OffenceError::DuplicateReport), }; let offenders_count = concurrent_offenders.len() as u32; // The amount new offenders are slashed let new_fraction = offence.slash_fraction(offenders_count); let slash_perbill: Vec<_> = (0..concurrent_offenders.len()).map(|_| new_fraction).collect(); T::OnOffenceHandler::on_offence( &concurrent_offenders, &slash_perbill, offence.session_index(), ); // Deposit the event. Self::deposit_event(Event::Offence { kind: O::ID, timeslot: time_slot.encode() }); Ok(()) } fn is_known_offence(offenders: &[T::IdentificationTuple], time_slot: &O::TimeSlot) -> bool { let any_unknown = offenders.iter().any(|offender| { let report_id = Self::report_id::(time_slot, offender); !>::contains_key(&report_id) }); !any_unknown } } impl Pallet { /// Compute the ID for the given report properties. /// /// The report id depends on the offence kind, time slot and the id of offender. fn report_id>( time_slot: &O::TimeSlot, offender: &T::IdentificationTuple, ) -> ReportIdOf { (O::ID, time_slot.encode(), offender).using_encoded(T::Hashing::hash) } /// Triages the offence report and returns the set of offenders that was involved in unique /// reports along with the list of the concurrent offences. fn triage_offence_report>( reporters: Vec, time_slot: &O::TimeSlot, offenders: Vec, ) -> Option> { let mut storage = ReportIndexStorage::::load(time_slot); let mut any_new = false; for offender in offenders { let report_id = Self::report_id::(time_slot, &offender); if !>::contains_key(&report_id) { any_new = true; >::insert( &report_id, OffenceDetails { offender, reporters: reporters.clone() }, ); storage.insert(report_id); } } if any_new { // Load report details for the all reports happened at the same time. let concurrent_offenders = storage .concurrent_reports .iter() .filter_map(>::get) .collect::>(); storage.save(); Some(TriageOutcome { concurrent_offenders }) } else { None } } } struct TriageOutcome { /// Other reports for the same report kinds. concurrent_offenders: Vec>, } /// An auxiliary struct for working with storage of indexes localized for a specific offence /// kind (specified by the `O` type parameter). /// /// This struct is responsible for aggregating storage writes and the underlying storage should not /// accessed directly meanwhile. #[must_use = "The changes are not saved without called `save`"] struct ReportIndexStorage> { opaque_time_slot: OpaqueTimeSlot, concurrent_reports: Vec>, _phantom: PhantomData, } impl> ReportIndexStorage { /// Preload indexes from the storage for the specific `time_slot` and the kind of the offence. fn load(time_slot: &O::TimeSlot) -> Self { let opaque_time_slot = time_slot.encode(); let concurrent_reports = >::get(&O::ID, &opaque_time_slot); Self { opaque_time_slot, concurrent_reports, _phantom: Default::default() } } /// Insert a new report to the index. fn insert(&mut self, report_id: ReportIdOf) { // Update the list of concurrent reports. self.concurrent_reports.push(report_id); } /// Dump the indexes to the storage. fn save(self) { >::insert( &O::ID, &self.opaque_time_slot, &self.concurrent_reports, ); } }