referrerpolicy=no-referrer-when-downgrade

pallet_root_offences/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Root Offences Pallet
19//! Pallet that allows the root to create an offence.
20//!
21//! NOTE: This pallet should be used for testing purposes.
22
23#![cfg_attr(not(feature = "std"), no_std)]
24
25#[cfg(test)]
26mod mock;
27#[cfg(test)]
28mod tests;
29
30extern crate alloc;
31use alloc::{vec, vec::Vec};
32pub use pallet::*;
33use pallet_session::historical::IdentificationTuple;
34use sp_runtime::{traits::Convert, Perbill};
35use sp_staking::offence::{Kind, Offence, OnOffenceHandler};
36
37#[frame_support::pallet]
38pub mod pallet {
39	use super::*;
40	use frame_support::pallet_prelude::*;
41	use frame_system::pallet_prelude::*;
42	use sp_staking::{offence::ReportOffence, SessionIndex};
43
44	/// Custom offence type for testing spam scenarios.
45	///
46	/// This allows creating offences with arbitrary kinds and time slots.
47	#[derive(Clone, Debug, Encode, Decode, TypeInfo)]
48	pub struct TestSpamOffence<Offender> {
49		/// The validator being slashed
50		pub offender: Offender,
51		/// The session in which the offence occurred
52		pub session_index: SessionIndex,
53		/// Custom time slot (allows unique offences within same session)
54		pub time_slot: u128,
55		/// Slash fraction to apply
56		pub slash_fraction: Perbill,
57	}
58
59	impl<Offender: Clone> Offence<Offender> for TestSpamOffence<Offender> {
60		const ID: Kind = *b"spamspamspamspam";
61		type TimeSlot = u128;
62
63		fn offenders(&self) -> Vec<Offender> {
64			vec![self.offender.clone()]
65		}
66
67		fn session_index(&self) -> SessionIndex {
68			self.session_index
69		}
70
71		fn time_slot(&self) -> Self::TimeSlot {
72			self.time_slot
73		}
74
75		fn slash_fraction(&self, _offenders_count: u32) -> Perbill {
76			self.slash_fraction
77		}
78
79		fn validator_set_count(&self) -> u32 {
80			unreachable!()
81		}
82	}
83
84	#[pallet::config]
85	pub trait Config:
86		frame_system::Config
87		+ pallet_staking::Config
88		+ pallet_session::Config<ValidatorId = <Self as frame_system::Config>::AccountId>
89		+ pallet_session::historical::Config
90	{
91		#[allow(deprecated)]
92		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
93
94		/// The offence handler provided by the runtime.
95		///
96		/// This is a way to give the offence directly to the handling system (staking, ah-client).
97		type OffenceHandler: OnOffenceHandler<Self::AccountId, IdentificationTuple<Self>, Weight>;
98
99		/// The offence report system provided by the runtime.
100		///
101		/// This is a way to give the offence to the `pallet-offences` next.
102		type ReportOffence: ReportOffence<
103			Self::AccountId,
104			IdentificationTuple<Self>,
105			TestSpamOffence<IdentificationTuple<Self>>,
106		>;
107	}
108
109	#[pallet::pallet]
110	pub struct Pallet<T>(_);
111
112	#[pallet::event]
113	#[pallet::generate_deposit(pub(super) fn deposit_event)]
114	pub enum Event<T: Config> {
115		/// An offence was created by root.
116		OffenceCreated { offenders: Vec<(T::AccountId, Perbill)> },
117	}
118
119	#[pallet::error]
120	pub enum Error<T> {
121		/// Failed to get the active era from the staking pallet.
122		FailedToGetActiveEra,
123	}
124
125	type OffenceDetails<T> = sp_staking::offence::OffenceDetails<
126		<T as frame_system::Config>::AccountId,
127		IdentificationTuple<T>,
128	>;
129
130	#[pallet::call]
131	impl<T: Config> Pallet<T> {
132		/// Allows the `root`, for example sudo to create an offence.
133		///
134		/// If `identifications` is `Some`, then the given identification is used for offence. Else,
135		/// it is fetched live from `session::Historical`.
136		#[pallet::call_index(0)]
137		#[pallet::weight(T::DbWeight::get().reads(2))]
138		pub fn create_offence(
139			origin: OriginFor<T>,
140			offenders: Vec<(T::AccountId, Perbill)>,
141			maybe_identifications: Option<Vec<T::FullIdentification>>,
142			maybe_session_index: Option<SessionIndex>,
143		) -> DispatchResult {
144			ensure_root(origin)?;
145
146			ensure!(
147				maybe_identifications.as_ref().map_or(true, |ids| ids.len() == offenders.len()),
148				"InvalidIdentificationLength"
149			);
150
151			let identifications =
152				maybe_identifications.ok_or("Unreachable-NoIdentification").or_else(|_| {
153					offenders
154						.iter()
155						.map(|(who, _)| {
156							T::FullIdentificationOf::convert(who.clone())
157								.ok_or("failed to call FullIdentificationOf")
158						})
159						.collect::<Result<Vec<_>, _>>()
160				})?;
161
162			let slash_fraction =
163				offenders.clone().into_iter().map(|(_, fraction)| fraction).collect::<Vec<_>>();
164			let offence_details = Self::get_offence_details(offenders.clone(), identifications)?;
165
166			Self::submit_offence(&offence_details, &slash_fraction, maybe_session_index);
167			Self::deposit_event(Event::OffenceCreated { offenders });
168			Ok(())
169		}
170
171		/// Same as [`Pallet::create_offence`], but it reports the offence directly to a
172		/// [`Config::ReportOffence`], aka pallet-offences first.
173		///
174		/// This is useful for more accurate testing of the e2e offence processing pipeline, as it
175		/// won't skip the `pallet-offences` step.
176		///
177		/// It generates an offence of type [`TestSpamOffence`], with cas a fixed `ID`, but can have
178		/// any `time_slot`, `session_index``, and `slash_fraction`. These values are the inputs of
179		/// transaction, int the same order, with an `IdentiticationTuple` coming first.
180		#[pallet::call_index(1)]
181		#[pallet::weight(T::DbWeight::get().reads(2))]
182		pub fn report_offence(
183			origin: OriginFor<T>,
184			offences: Vec<(IdentificationTuple<T>, SessionIndex, u128, u32)>,
185		) -> DispatchResult {
186			ensure_root(origin)?;
187
188			for (offender, session_index, time_slot, slash_ppm) in offences {
189				let slash_fraction = Perbill::from_parts(slash_ppm);
190				Self::deposit_event(Event::OffenceCreated {
191					offenders: vec![(offender.0.clone(), slash_fraction)],
192				});
193				let offence =
194					TestSpamOffence { offender, session_index, time_slot, slash_fraction };
195
196				T::ReportOffence::report_offence(Default::default(), offence).unwrap();
197			}
198
199			Ok(())
200		}
201	}
202
203	impl<T: Config> Pallet<T> {
204		/// Returns a vector of offenders that are going to be slashed.
205		fn get_offence_details(
206			offenders: Vec<(T::AccountId, Perbill)>,
207			identifications: Vec<T::FullIdentification>,
208		) -> Result<Vec<OffenceDetails<T>>, DispatchError> {
209			Ok(offenders
210				.clone()
211				.into_iter()
212				.zip(identifications.into_iter())
213				.map(|((o, _), i)| OffenceDetails::<T> {
214					offender: (o.clone(), i),
215					reporters: Default::default(),
216				})
217				.collect())
218		}
219
220		/// Submits the offence by calling the `on_offence` function.
221		fn submit_offence(
222			offenders: &[OffenceDetails<T>],
223			slash_fraction: &[Perbill],
224			maybe_session_index: Option<SessionIndex>,
225		) {
226			let session_index = maybe_session_index.unwrap_or_else(|| {
227				<pallet_session::Pallet<T> as frame_support::traits::ValidatorSet<
228						T::AccountId,
229					>>::session_index()
230			});
231			T::OffenceHandler::on_offence(&offenders, &slash_fraction, session_index);
232		}
233	}
234}