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