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 Slot = 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 slot(&self) -> Self::Slot {
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_session::Config<ValidatorId = <Self as frame_system::Config>::AccountId>
88		+ pallet_session::historical::Config
89	{
90		#[allow(deprecated)]
91		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
92
93		/// The offence handler provided by the runtime.
94		///
95		/// This is a way to give the offence directly to the handling system (staking, ah-client).
96		type OffenceHandler: OnOffenceHandler<Self::AccountId, IdentificationTuple<Self>, Weight>;
97
98		/// The offence report system provided by the runtime.
99		///
100		/// This is a way to give the offence to the `pallet-offences` next.
101		type ReportOffence: ReportOffence<
102			Self::AccountId,
103			IdentificationTuple<Self>,
104			TestSpamOffence<IdentificationTuple<Self>>,
105		>;
106	}
107
108	#[pallet::pallet]
109	pub struct Pallet<T>(_);
110
111	#[pallet::event]
112	#[pallet::generate_deposit(pub(super) fn deposit_event)]
113	pub enum Event<T: Config> {
114		/// An offence was created by root.
115		OffenceCreated { offenders: Vec<(T::AccountId, Perbill)> },
116	}
117
118	#[pallet::error]
119	pub enum Error<T> {
120		/// Failed to get the active era from the staking pallet.
121		FailedToGetActiveEra,
122	}
123
124	type OffenceDetails<T> = sp_staking::offence::OffenceDetails<
125		<T as frame_system::Config>::AccountId,
126		IdentificationTuple<T>,
127	>;
128
129	#[pallet::call]
130	impl<T: Config> Pallet<T> {
131		/// Allows the `root`, for example sudo to create an offence.
132		///
133		/// If `identifications` is `Some`, then the given identification is used for offence. Else,
134		/// it is fetched live from `session::Historical`.
135		#[pallet::call_index(0)]
136		#[pallet::weight(T::DbWeight::get().reads(2))]
137		pub fn create_offence(
138			origin: OriginFor<T>,
139			offenders: Vec<(T::AccountId, Perbill)>,
140			maybe_identifications: Option<Vec<T::FullIdentification>>,
141			maybe_session_index: Option<SessionIndex>,
142		) -> DispatchResult {
143			ensure_root(origin)?;
144
145			ensure!(
146				maybe_identifications.as_ref().map_or(true, |ids| ids.len() == offenders.len()),
147				"InvalidIdentificationLength"
148			);
149
150			let identifications =
151				maybe_identifications.ok_or("Unreachable-NoIdentification").or_else(|_| {
152					offenders
153						.iter()
154						.map(|(who, _)| {
155							T::FullIdentificationOf::convert(who.clone())
156								.ok_or("failed to call FullIdentificationOf")
157						})
158						.collect::<Result<Vec<_>, _>>()
159				})?;
160
161			let slash_fraction =
162				offenders.clone().into_iter().map(|(_, fraction)| fraction).collect::<Vec<_>>();
163			let offence_details = Self::get_offence_details(offenders.clone(), identifications)?;
164
165			Self::submit_offence(&offence_details, &slash_fraction, maybe_session_index);
166			Self::deposit_event(Event::OffenceCreated { offenders });
167			Ok(())
168		}
169
170		/// Same as [`Pallet::create_offence`], but it reports the offence directly to a
171		/// [`Config::ReportOffence`], aka pallet-offences first.
172		///
173		/// This is useful for more accurate testing of the e2e offence processing pipeline, as it
174		/// won't skip the `pallet-offences` step.
175		///
176		/// It generates an offence of type [`TestSpamOffence`], with cas a fixed `ID`, but can have
177		/// any `time_slot`, `session_index``, and `slash_fraction`. These values are the inputs of
178		/// transaction, int the same order, with an `IdentiticationTuple` coming first.
179		#[pallet::call_index(1)]
180		#[pallet::weight(T::DbWeight::get().reads(2))]
181		pub fn report_offence(
182			origin: OriginFor<T>,
183			offences: Vec<(IdentificationTuple<T>, SessionIndex, u128, u32)>,
184		) -> DispatchResult {
185			ensure_root(origin)?;
186
187			for (offender, session_index, time_slot, slash_ppm) in offences {
188				let slash_fraction = Perbill::from_parts(slash_ppm);
189				Self::deposit_event(Event::OffenceCreated {
190					offenders: vec![(offender.0.clone(), slash_fraction)],
191				});
192				let offence =
193					TestSpamOffence { offender, session_index, time_slot, slash_fraction };
194
195				T::ReportOffence::report_offence(Default::default(), offence).unwrap();
196			}
197
198			Ok(())
199		}
200	}
201
202	impl<T: Config> Pallet<T> {
203		/// Returns a vector of offenders that are going to be slashed.
204		fn get_offence_details(
205			offenders: Vec<(T::AccountId, Perbill)>,
206			identifications: Vec<T::FullIdentification>,
207		) -> Result<Vec<OffenceDetails<T>>, DispatchError> {
208			Ok(offenders
209				.clone()
210				.into_iter()
211				.zip(identifications.into_iter())
212				.map(|((o, _), i)| OffenceDetails::<T> {
213					offender: (o.clone(), i),
214					reporters: Default::default(),
215				})
216				.collect())
217		}
218
219		/// Submits the offence by calling the `on_offence` function.
220		fn submit_offence(
221			offenders: &[OffenceDetails<T>],
222			slash_fraction: &[Perbill],
223			maybe_session_index: Option<SessionIndex>,
224		) {
225			let session_index = maybe_session_index.unwrap_or_else(|| {
226				<pallet_session::Pallet<T> as frame_support::traits::ValidatorSet<
227						T::AccountId,
228					>>::session_index()
229			});
230			T::OffenceHandler::on_offence(&offenders, &slash_fraction, session_index);
231		}
232	}
233}