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 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 type OffenceHandler: OnOffenceHandler<Self::AccountId, IdentificationTuple<Self>, Weight>;
98
99 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 OffenceCreated { offenders: Vec<(T::AccountId, Perbill)> },
117 }
118
119 #[pallet::error]
120 pub enum Error<T> {
121 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 #[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 #[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 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 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}