pallet_root_offences/
lib.rs1#![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 #[derive(Clone, Debug, Encode, Decode, TypeInfo)]
48 pub struct TestSpamOffence<Offender> {
49 pub offender: Offender,
51 pub session_index: SessionIndex,
53 pub time_slot: u128,
55 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 type OffenceHandler: OnOffenceHandler<Self::AccountId, IdentificationTuple<Self>, Weight>;
97
98 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 OffenceCreated { offenders: Vec<(T::AccountId, Perbill)> },
116 }
117
118 #[pallet::error]
119 pub enum Error<T> {
120 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 #[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 #[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 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 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}