1use alloc::{vec, vec::Vec};
38use codec::{self as codec, Decode, Encode};
39use frame_support::traits::{Get, KeyOwnerProofSystem};
40use frame_system::pallet_prelude::{BlockNumberFor, HeaderFor};
41use log::{error, info};
42use sp_consensus_beefy::{
43 check_commitment_signature, AncestryHelper, DoubleVotingProof, ForkVotingProof,
44 FutureBlockVotingProof, ValidatorSetId, KEY_TYPE as BEEFY_KEY_TYPE,
45};
46use sp_runtime::{
47 transaction_validity::{
48 InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
49 TransactionValidityError, ValidTransaction,
50 },
51 DispatchError, KeyTypeId, Perbill, RuntimeAppPublic,
52};
53use sp_session::{GetSessionNumber, GetValidatorCount};
54use sp_staking::{
55 offence::{Kind, Offence, OffenceReportSystem, ReportOffence},
56 SessionIndex,
57};
58
59use super::{Call, Config, Error, Pallet, LOG_TARGET};
60
61#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)]
63pub struct Slot<N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode> {
64 pub set_id: ValidatorSetId,
67 pub round: N,
69 pub equivocation_type: u8,
71}
72
73pub struct EquivocationOffence<Offender, N>
75where
76 N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
77{
78 pub slot: Slot<N>,
80 pub session_index: SessionIndex,
82 pub validator_set_count: u32,
84 pub offender: Offender,
86 maybe_slash_fraction: Option<Perbill>,
88}
89
90impl<Offender: Clone, N> Offence<Offender> for EquivocationOffence<Offender, N>
91where
92 N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
93{
94 const ID: Kind = *b"beefy:equivocati";
95 type Slot = Slot<N>;
96
97 fn offenders(&self) -> Vec<Offender> {
98 vec![self.offender.clone()]
99 }
100
101 fn session_index(&self) -> SessionIndex {
102 self.session_index
103 }
104
105 fn validator_set_count(&self) -> u32 {
106 self.validator_set_count
107 }
108
109 fn slot(&self) -> Self::Slot {
110 self.slot
111 }
112
113 fn slash_fraction(&self, offenders_count: u32) -> Perbill {
114 if let Some(slash_fraction) = self.maybe_slash_fraction {
115 return slash_fraction;
116 }
117
118 Perbill::from_rational(3 * offenders_count, self.validator_set_count).square()
122 }
123}
124
125pub struct EquivocationReportSystem<T, R, P, L>(core::marker::PhantomData<(T, R, P, L)>);
134
135pub enum EquivocationEvidenceFor<T: Config> {
137 DoubleVotingProof(
138 DoubleVotingProof<
139 BlockNumberFor<T>,
140 T::BeefyId,
141 <T::BeefyId as RuntimeAppPublic>::Signature,
142 >,
143 T::KeyOwnerProof,
144 ),
145 ForkVotingProof(
146 ForkVotingProof<
147 HeaderFor<T>,
148 T::BeefyId,
149 <T::AncestryHelper as AncestryHelper<HeaderFor<T>>>::Proof,
150 >,
151 T::KeyOwnerProof,
152 ),
153 FutureBlockVotingProof(FutureBlockVotingProof<BlockNumberFor<T>, T::BeefyId>, T::KeyOwnerProof),
154}
155
156impl<T: Config> EquivocationEvidenceFor<T> {
157 fn equivocation_type(&self) -> u8 {
158 match self {
159 EquivocationEvidenceFor::DoubleVotingProof(..) => 1,
160 EquivocationEvidenceFor::ForkVotingProof(..) => 2,
161 EquivocationEvidenceFor::FutureBlockVotingProof(..) => 3,
162 }
163 }
164
165 fn offender_id(&self) -> &T::BeefyId {
167 match self {
168 EquivocationEvidenceFor::DoubleVotingProof(equivocation_proof, _) => {
169 equivocation_proof.offender_id()
170 },
171 EquivocationEvidenceFor::ForkVotingProof(equivocation_proof, _) => {
172 &equivocation_proof.vote.id
173 },
174 EquivocationEvidenceFor::FutureBlockVotingProof(equivocation_proof, _) => {
175 &equivocation_proof.vote.id
176 },
177 }
178 }
179
180 fn round_number(&self) -> &BlockNumberFor<T> {
182 match self {
183 EquivocationEvidenceFor::DoubleVotingProof(equivocation_proof, _) => {
184 equivocation_proof.round_number()
185 },
186 EquivocationEvidenceFor::ForkVotingProof(equivocation_proof, _) => {
187 &equivocation_proof.vote.commitment.block_number
188 },
189 EquivocationEvidenceFor::FutureBlockVotingProof(equivocation_proof, _) => {
190 &equivocation_proof.vote.commitment.block_number
191 },
192 }
193 }
194
195 fn set_id(&self) -> ValidatorSetId {
197 match self {
198 EquivocationEvidenceFor::DoubleVotingProof(equivocation_proof, _) => {
199 equivocation_proof.set_id()
200 },
201 EquivocationEvidenceFor::ForkVotingProof(equivocation_proof, _) => {
202 equivocation_proof.vote.commitment.validator_set_id
203 },
204 EquivocationEvidenceFor::FutureBlockVotingProof(equivocation_proof, _) => {
205 equivocation_proof.vote.commitment.validator_set_id
206 },
207 }
208 }
209
210 fn key_owner_proof(&self) -> &T::KeyOwnerProof {
212 match self {
213 EquivocationEvidenceFor::DoubleVotingProof(_, key_owner_proof) => key_owner_proof,
214 EquivocationEvidenceFor::ForkVotingProof(_, key_owner_proof) => key_owner_proof,
215 EquivocationEvidenceFor::FutureBlockVotingProof(_, key_owner_proof) => key_owner_proof,
216 }
217 }
218
219 fn checked_offender<P>(&self) -> Option<P::IdentificationTuple>
220 where
221 P: KeyOwnerProofSystem<(KeyTypeId, T::BeefyId), Proof = T::KeyOwnerProof>,
222 {
223 let key = (BEEFY_KEY_TYPE, self.offender_id().clone());
224 P::check_proof(key, self.key_owner_proof().clone())
225 }
226
227 fn check_equivocation_proof(self) -> Result<(), Error<T>> {
228 match self {
229 EquivocationEvidenceFor::DoubleVotingProof(equivocation_proof, _) => {
230 if !sp_consensus_beefy::check_double_voting_proof(&equivocation_proof) {
232 return Err(Error::<T>::InvalidDoubleVotingProof);
233 }
234
235 Ok(())
236 },
237 EquivocationEvidenceFor::ForkVotingProof(equivocation_proof, _) => {
238 let ForkVotingProof { vote, ancestry_proof, header } = equivocation_proof;
239
240 if !<T::AncestryHelper as AncestryHelper<HeaderFor<T>>>::is_proof_optimal(
241 &ancestry_proof,
242 ) {
243 return Err(Error::<T>::InvalidForkVotingProof);
244 }
245
246 let maybe_validation_context = <T::AncestryHelper as AncestryHelper<
247 HeaderFor<T>,
248 >>::extract_validation_context(header);
249 let validation_context = match maybe_validation_context {
250 Some(validation_context) => validation_context,
251 None => {
252 return Err(Error::<T>::InvalidForkVotingProof);
253 },
254 };
255
256 let is_non_canonical =
257 <T::AncestryHelper as AncestryHelper<HeaderFor<T>>>::is_non_canonical(
258 &vote.commitment,
259 ancestry_proof,
260 validation_context,
261 );
262 if !is_non_canonical {
263 return Err(Error::<T>::InvalidForkVotingProof);
264 }
265
266 let is_signature_valid =
267 check_commitment_signature(&vote.commitment, &vote.id, &vote.signature);
268 if !is_signature_valid {
269 return Err(Error::<T>::InvalidForkVotingProof);
270 }
271
272 Ok(())
273 },
274 EquivocationEvidenceFor::FutureBlockVotingProof(equivocation_proof, _) => {
275 let FutureBlockVotingProof { vote } = equivocation_proof;
276 if vote.commitment.block_number < frame_system::Pallet::<T>::block_number() {
278 return Err(Error::<T>::InvalidFutureBlockVotingProof);
279 }
280
281 let is_signature_valid =
282 check_commitment_signature(&vote.commitment, &vote.id, &vote.signature);
283 if !is_signature_valid {
284 return Err(Error::<T>::InvalidForkVotingProof);
285 }
286
287 Ok(())
288 },
289 }
290 }
291
292 fn slash_fraction(&self) -> Option<Perbill> {
293 match self {
294 EquivocationEvidenceFor::DoubleVotingProof(_, _) => None,
295 EquivocationEvidenceFor::ForkVotingProof(_, _) |
296 EquivocationEvidenceFor::FutureBlockVotingProof(_, _) => Some(Perbill::from_percent(50)),
297 }
298 }
299}
300
301impl<T, R, P, L> OffenceReportSystem<Option<T::AccountId>, EquivocationEvidenceFor<T>>
302 for EquivocationReportSystem<T, R, P, L>
303where
304 T: Config + pallet_authorship::Config + frame_system::offchain::CreateBare<Call<T>>,
305 R: ReportOffence<
306 T::AccountId,
307 P::IdentificationTuple,
308 EquivocationOffence<P::IdentificationTuple, BlockNumberFor<T>>,
309 >,
310 P: KeyOwnerProofSystem<(KeyTypeId, T::BeefyId), Proof = T::KeyOwnerProof>,
311 P::IdentificationTuple: Clone,
312 L: Get<u64>,
313{
314 type Longevity = L;
315
316 fn publish_evidence(evidence: EquivocationEvidenceFor<T>) -> Result<(), ()> {
317 use frame_system::offchain::SubmitTransaction;
318
319 let call: Call<T> = evidence.into();
320 let xt = T::create_bare(call.into());
321 let res = SubmitTransaction::<T, Call<T>>::submit_transaction(xt);
322 match res {
323 Ok(_) => info!(target: LOG_TARGET, "Submitted equivocation report."),
324 Err(e) => error!(target: LOG_TARGET, "Error submitting equivocation report: {:?}", e),
325 }
326 res
327 }
328
329 fn check_evidence(
330 evidence: EquivocationEvidenceFor<T>,
331 ) -> Result<(), TransactionValidityError> {
332 let offender = evidence.checked_offender::<P>().ok_or(InvalidTransaction::BadProof)?;
333
334 let slot = Slot {
336 set_id: evidence.set_id(),
337 round: *evidence.round_number(),
338 equivocation_type: evidence.equivocation_type(),
339 };
340 if R::is_known_offence(&[offender], &slot) {
341 Err(InvalidTransaction::Stale.into())
342 } else {
343 Ok(())
344 }
345 }
346
347 fn process_evidence(
348 reporter: Option<T::AccountId>,
349 evidence: EquivocationEvidenceFor<T>,
350 ) -> Result<(), DispatchError> {
351 let maybe_slash_fraction = evidence.slash_fraction();
352 let reporter = reporter.or_else(|| pallet_authorship::Pallet::<T>::author());
353
354 let set_id = evidence.set_id();
356 let set_id_session_index = crate::SetIdSession::<T>::get(set_id)
357 .ok_or(Error::<T>::InvalidEquivocationProofSessionMember)?;
358
359 let key_owner_proof = evidence.key_owner_proof();
362 let validator_count = key_owner_proof.validator_count();
363 let session_index = key_owner_proof.session();
364 if session_index != set_id_session_index {
365 return Err(Error::<T>::InvalidEquivocationProofSession.into());
366 }
367
368 let offender =
370 evidence.checked_offender::<P>().ok_or(Error::<T>::InvalidKeyOwnershipProof)?;
371
372 let round = *evidence.round_number();
373 let equivocation_type = evidence.equivocation_type();
374
375 evidence.check_equivocation_proof()?;
376
377 let offence = EquivocationOffence {
378 slot: Slot { set_id, round, equivocation_type },
379 session_index,
380 validator_set_count: validator_count,
381 offender,
382 maybe_slash_fraction,
383 };
384 R::report_offence(reporter.into_iter().collect(), offence)
385 .map_err(|_| Error::<T>::DuplicateOffenceReport.into())
386 }
387}
388
389impl<T: Config> Pallet<T> {
394 pub fn validate_unsigned(source: TransactionSource, call: &Call<T>) -> TransactionValidity {
395 match source {
397 TransactionSource::Local | TransactionSource::InBlock => { },
398 _ => {
399 log::warn!(
400 target: LOG_TARGET,
401 "rejecting unsigned report equivocation transaction because it is not local/in-block."
402 );
403 return InvalidTransaction::Call.into();
404 },
405 }
406
407 let evidence = call.to_equivocation_evidence_for().ok_or(InvalidTransaction::Call)?;
408 let tag = (evidence.offender_id().clone(), evidence.set_id(), *evidence.round_number());
409 T::EquivocationReportSystem::check_evidence(evidence)?;
410
411 let longevity =
412 <T::EquivocationReportSystem as OffenceReportSystem<_, _>>::Longevity::get();
413 ValidTransaction::with_tag_prefix("BeefyEquivocation")
414 .priority(TransactionPriority::MAX)
416 .and_provides(tag)
418 .longevity(longevity)
419 .propagate(false)
421 .build()
422 }
423
424 pub fn pre_dispatch(call: &Call<T>) -> Result<(), TransactionValidityError> {
425 let evidence = call.to_equivocation_evidence_for().ok_or(InvalidTransaction::Call)?;
426 T::EquivocationReportSystem::check_evidence(evidence)
427 }
428}