referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/
disputes.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Runtime component for handling disputes of parachain candidates.
18
19use crate::{
20	configuration, initializer::SessionChangeNotification, metrics::METRICS, session_info,
21};
22use alloc::{collections::btree_set::BTreeSet, vec::Vec};
23use bitvec::{bitvec, order::Lsb0 as BitOrderLsb0};
24use codec::{Decode, DecodeWithMemTracking, Encode};
25use core::cmp::Ordering;
26use frame_support::{ensure, weights::Weight};
27use frame_system::pallet_prelude::*;
28use polkadot_primitives::{
29	byzantine_threshold, supermajority_threshold, ApprovalVote, ApprovalVoteMultipleCandidates,
30	CandidateHash, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CompactStatement,
31	ConsensusLog, DisputeState, DisputeStatement, DisputeStatementSet, ExplicitDisputeStatement,
32	InvalidDisputeStatementKind, MultiDisputeStatementSet, SessionIndex, SigningContext,
33	ValidDisputeStatementKind, ValidatorId, ValidatorIndex, ValidatorSignature,
34};
35use polkadot_runtime_metrics::get_current_time;
36use scale_info::TypeInfo;
37use sp_runtime::{
38	traits::{AppVerify, One, Saturating, Zero},
39	DispatchError, RuntimeDebug, SaturatedConversion,
40};
41
42#[cfg(test)]
43#[allow(unused_imports)]
44pub(crate) use self::tests::run_to_block;
45
46pub mod slashing;
47#[cfg(test)]
48mod tests;
49
50#[cfg(feature = "runtime-benchmarks")]
51mod benchmarking;
52
53pub mod migration;
54
55const LOG_TARGET: &str = "runtime::disputes";
56
57/// Whether the dispute is local or remote.
58#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
59pub enum DisputeLocation {
60	Local,
61	Remote,
62}
63
64/// The result of a dispute, whether the candidate is deemed valid (for) or invalid (against).
65#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
66pub enum DisputeResult {
67	Valid,
68	Invalid,
69}
70
71/// Reward hooks for disputes.
72pub trait RewardValidators {
73	// Give each validator a reward, likely small, for participating in the dispute.
74	fn reward_dispute_statement(
75		session: SessionIndex,
76		validators: impl IntoIterator<Item = ValidatorIndex>,
77	);
78}
79
80impl RewardValidators for () {
81	fn reward_dispute_statement(_: SessionIndex, _: impl IntoIterator<Item = ValidatorIndex>) {}
82}
83
84/// Punishment hooks for disputes.
85pub trait SlashingHandler<BlockNumber> {
86	/// Punish a series of validators who were for an invalid parablock.
87	/// This is expected to trigger a large punishment for backers
88	/// and a medium punishment for other approvers.
89	fn punish_for_invalid(
90		session: SessionIndex,
91		candidate_hash: CandidateHash,
92		losers: impl IntoIterator<Item = ValidatorIndex>,
93		backers: impl IntoIterator<Item = ValidatorIndex>,
94	);
95
96	/// Punish a series of validators who were against a valid parablock.
97	/// This is expected to be a minor punishment.
98	fn punish_against_valid(
99		session: SessionIndex,
100		candidate_hash: CandidateHash,
101		losers: impl IntoIterator<Item = ValidatorIndex>,
102		backers: impl IntoIterator<Item = ValidatorIndex>,
103	);
104
105	/// Called by the initializer to initialize the slashing pallet.
106	fn initializer_initialize(now: BlockNumber) -> Weight;
107
108	/// Called by the initializer to finalize the slashing pallet.
109	fn initializer_finalize();
110
111	/// Called by the initializer to note that a new session has started.
112	fn initializer_on_new_session(session_index: SessionIndex);
113}
114
115impl<BlockNumber> SlashingHandler<BlockNumber> for () {
116	fn punish_for_invalid(
117		_: SessionIndex,
118		_: CandidateHash,
119		_: impl IntoIterator<Item = ValidatorIndex>,
120		_: impl IntoIterator<Item = ValidatorIndex>,
121	) {
122	}
123
124	fn punish_against_valid(
125		_: SessionIndex,
126		_: CandidateHash,
127		_: impl IntoIterator<Item = ValidatorIndex>,
128		_: impl IntoIterator<Item = ValidatorIndex>,
129	) {
130	}
131
132	fn initializer_initialize(_now: BlockNumber) -> Weight {
133		Weight::zero()
134	}
135
136	fn initializer_finalize() {}
137
138	fn initializer_on_new_session(_: SessionIndex) {}
139}
140
141/// Provide a `Ordering` for the two provided dispute statement sets according to the
142/// following prioritization:
143///  1. Prioritize local disputes over remote disputes
144///  2. Prioritize older disputes over newer disputes
145fn dispute_ordering_compare<T: DisputesHandler<BlockNumber>, BlockNumber: Ord>(
146	a: &DisputeStatementSet,
147	b: &DisputeStatementSet,
148) -> Ordering
149where
150	T: ?Sized,
151{
152	let a_local_block =
153		<T as DisputesHandler<BlockNumber>>::included_state(a.session, a.candidate_hash);
154	let b_local_block =
155		<T as DisputesHandler<BlockNumber>>::included_state(b.session, b.candidate_hash);
156	match (a_local_block, b_local_block) {
157		// Prioritize local disputes over remote disputes.
158		(None, Some(_)) => Ordering::Greater,
159		(Some(_), None) => Ordering::Less,
160		// For local disputes, prioritize those that occur at an earlier height.
161		(Some(a_height), Some(b_height)) =>
162			a_height.cmp(&b_height).then_with(|| a.candidate_hash.cmp(&b.candidate_hash)),
163		// Prioritize earlier remote disputes using session as rough proxy.
164		(None, None) => {
165			let session_ord = a.session.cmp(&b.session);
166			if session_ord == Ordering::Equal {
167				// sort by hash as last resort, to make below dedup work consistently
168				a.candidate_hash.cmp(&b.candidate_hash)
169			} else {
170				session_ord
171			}
172		},
173	}
174}
175
176/// Hook into disputes handling.
177///
178/// Allows decoupling parachains handling from disputes so that it can
179/// potentially be disabled when instantiating a specific runtime.
180pub trait DisputesHandler<BlockNumber: Ord> {
181	/// Whether the chain is frozen, if the chain is frozen it will not accept
182	/// any new parachain blocks for backing or inclusion.
183	fn is_frozen() -> bool;
184
185	/// Remove dispute statement duplicates and sort the non-duplicates based on
186	/// local (lower indices) vs remotes (higher indices) and age (older with lower indices).
187	///
188	/// Returns `Ok(())` if no duplicates were present, `Err(())` otherwise.
189	///
190	/// Unsorted data does not change the return value, while the node side
191	/// is generally expected to pass them in sorted.
192	fn deduplicate_and_sort_dispute_data(
193		statement_sets: &mut MultiDisputeStatementSet,
194	) -> Result<(), ()> {
195		// TODO: Consider trade-of to avoid `O(n * log(n))` average lookups of `included_state`
196		// TODO: instead make a single pass and store the values lazily.
197		// TODO: https://github.com/paritytech/polkadot/issues/4527
198		let n = statement_sets.len();
199
200		statement_sets.sort_by(dispute_ordering_compare::<Self, BlockNumber>);
201		statement_sets
202			.dedup_by(|a, b| a.session == b.session && a.candidate_hash == b.candidate_hash);
203
204		// if there were any duplicates, indicate that to the caller.
205		if n == statement_sets.len() {
206			Ok(())
207		} else {
208			Err(())
209		}
210	}
211
212	/// Filter a single dispute statement set.
213	///
214	/// Used in cases where more granular control is required, i.e. when
215	/// accounting for maximum block weight.
216	fn filter_dispute_data(
217		statement_set: DisputeStatementSet,
218		post_conclusion_acceptance_period: BlockNumber,
219	) -> Option<CheckedDisputeStatementSet>;
220
221	/// Handle sets of dispute statements corresponding to 0 or more candidates.
222	/// Returns a vector of freshly created disputes.
223	fn process_checked_multi_dispute_data(
224		statement_sets: &CheckedMultiDisputeStatementSet,
225	) -> Result<Vec<(SessionIndex, CandidateHash)>, DispatchError>;
226
227	/// Note that the given candidate has been included.
228	fn note_included(
229		session: SessionIndex,
230		candidate_hash: CandidateHash,
231		included_in: BlockNumber,
232	);
233
234	/// Retrieve the included state of a given candidate in a particular session. If it
235	/// returns `Some`, then we have a local dispute for the given `candidate_hash`.
236	fn included_state(session: SessionIndex, candidate_hash: CandidateHash) -> Option<BlockNumber>;
237
238	/// Whether the given candidate concluded invalid in a dispute with supermajority.
239	fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool;
240
241	/// Called by the initializer to initialize the disputes pallet.
242	fn initializer_initialize(now: BlockNumber) -> Weight;
243
244	/// Called by the initializer to finalize the disputes pallet.
245	fn initializer_finalize();
246
247	/// Called by the initializer to note that a new session has started.
248	fn initializer_on_new_session(notification: &SessionChangeNotification<BlockNumber>);
249}
250
251impl<BlockNumber: Ord> DisputesHandler<BlockNumber> for () {
252	fn is_frozen() -> bool {
253		false
254	}
255
256	fn deduplicate_and_sort_dispute_data(
257		statement_sets: &mut MultiDisputeStatementSet,
258	) -> Result<(), ()> {
259		statement_sets.clear();
260		Ok(())
261	}
262
263	fn filter_dispute_data(
264		_set: DisputeStatementSet,
265		_post_conclusion_acceptance_period: BlockNumber,
266	) -> Option<CheckedDisputeStatementSet> {
267		None
268	}
269
270	fn process_checked_multi_dispute_data(
271		_statement_sets: &CheckedMultiDisputeStatementSet,
272	) -> Result<Vec<(SessionIndex, CandidateHash)>, DispatchError> {
273		Ok(Vec::new())
274	}
275
276	fn note_included(
277		_session: SessionIndex,
278		_candidate_hash: CandidateHash,
279		_included_in: BlockNumber,
280	) {
281	}
282
283	fn included_state(
284		_session: SessionIndex,
285		_candidate_hash: CandidateHash,
286	) -> Option<BlockNumber> {
287		None
288	}
289
290	fn concluded_invalid(_session: SessionIndex, _candidate_hash: CandidateHash) -> bool {
291		false
292	}
293
294	fn initializer_initialize(_now: BlockNumber) -> Weight {
295		Weight::zero()
296	}
297
298	fn initializer_finalize() {}
299
300	fn initializer_on_new_session(_notification: &SessionChangeNotification<BlockNumber>) {}
301}
302
303impl<T: Config> DisputesHandler<BlockNumberFor<T>> for pallet::Pallet<T>
304where
305	BlockNumberFor<T>: Ord,
306{
307	fn is_frozen() -> bool {
308		pallet::Pallet::<T>::is_frozen()
309	}
310
311	fn filter_dispute_data(
312		set: DisputeStatementSet,
313		post_conclusion_acceptance_period: BlockNumberFor<T>,
314	) -> Option<CheckedDisputeStatementSet> {
315		pallet::Pallet::<T>::filter_dispute_data(&set, post_conclusion_acceptance_period)
316			.filter_statement_set(set)
317	}
318
319	fn process_checked_multi_dispute_data(
320		statement_sets: &CheckedMultiDisputeStatementSet,
321	) -> Result<Vec<(SessionIndex, CandidateHash)>, DispatchError> {
322		pallet::Pallet::<T>::process_checked_multi_dispute_data(statement_sets)
323	}
324
325	fn note_included(
326		session: SessionIndex,
327		candidate_hash: CandidateHash,
328		included_in: BlockNumberFor<T>,
329	) {
330		pallet::Pallet::<T>::note_included(session, candidate_hash, included_in)
331	}
332
333	fn included_state(
334		session: SessionIndex,
335		candidate_hash: CandidateHash,
336	) -> Option<BlockNumberFor<T>> {
337		pallet::Pallet::<T>::included_state(session, candidate_hash)
338	}
339
340	fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool {
341		pallet::Pallet::<T>::concluded_invalid(session, candidate_hash)
342	}
343
344	fn initializer_initialize(now: BlockNumberFor<T>) -> Weight {
345		pallet::Pallet::<T>::initializer_initialize(now)
346	}
347
348	fn initializer_finalize() {
349		pallet::Pallet::<T>::initializer_finalize()
350	}
351
352	fn initializer_on_new_session(notification: &SessionChangeNotification<BlockNumberFor<T>>) {
353		pallet::Pallet::<T>::initializer_on_new_session(notification)
354	}
355}
356
357pub trait WeightInfo {
358	fn force_unfreeze() -> Weight;
359}
360
361pub struct TestWeightInfo;
362impl WeightInfo for TestWeightInfo {
363	fn force_unfreeze() -> Weight {
364		Weight::zero()
365	}
366}
367
368pub use pallet::*;
369#[frame_support::pallet]
370pub mod pallet {
371	use super::*;
372	use frame_support::pallet_prelude::*;
373
374	#[pallet::config]
375	pub trait Config: frame_system::Config + configuration::Config + session_info::Config {
376		#[allow(deprecated)]
377		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
378		type RewardValidators: RewardValidators;
379		type SlashingHandler: SlashingHandler<BlockNumberFor<Self>>;
380
381		/// Weight information for extrinsics in this pallet.
382		type WeightInfo: WeightInfo;
383	}
384
385	/// The in-code storage version.
386	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
387
388	#[pallet::pallet]
389	#[pallet::without_storage_info]
390	#[pallet::storage_version(STORAGE_VERSION)]
391	pub struct Pallet<T>(_);
392
393	/// The last pruned session, if any. All data stored by this module
394	/// references sessions.
395	#[pallet::storage]
396	pub(super) type LastPrunedSession<T> = StorageValue<_, SessionIndex>;
397
398	/// All ongoing or concluded disputes for the last several sessions.
399	#[pallet::storage]
400	pub(super) type Disputes<T: Config> = StorageDoubleMap<
401		_,
402		Twox64Concat,
403		SessionIndex,
404		Blake2_128Concat,
405		CandidateHash,
406		DisputeState<BlockNumberFor<T>>,
407	>;
408
409	/// Backing votes stored for each dispute.
410	/// This storage is used for slashing.
411	#[pallet::storage]
412	pub(super) type BackersOnDisputes<T: Config> = StorageDoubleMap<
413		_,
414		Twox64Concat,
415		SessionIndex,
416		Blake2_128Concat,
417		CandidateHash,
418		BTreeSet<ValidatorIndex>,
419	>;
420
421	/// All included blocks on the chain, as well as the block number in this chain that
422	/// should be reverted back to if the candidate is disputed and determined to be invalid.
423	#[pallet::storage]
424	pub(super) type Included<T: Config> = StorageDoubleMap<
425		_,
426		Twox64Concat,
427		SessionIndex,
428		Blake2_128Concat,
429		CandidateHash,
430		BlockNumberFor<T>,
431	>;
432
433	/// Whether the chain is frozen. Starts as `None`. When this is `Some`,
434	/// the chain will not accept any new parachain blocks for backing or inclusion,
435	/// and its value indicates the last valid block number in the chain.
436	/// It can only be set back to `None` by governance intervention.
437	#[pallet::storage]
438	pub type Frozen<T: Config> = StorageValue<_, Option<BlockNumberFor<T>>, ValueQuery>;
439
440	#[pallet::event]
441	#[pallet::generate_deposit(pub fn deposit_event)]
442	pub enum Event<T: Config> {
443		/// A dispute has been initiated. \[candidate hash, dispute location\]
444		DisputeInitiated(CandidateHash, DisputeLocation),
445		/// A dispute has concluded for or against a candidate.
446		/// `\[para id, candidate hash, dispute result\]`
447		DisputeConcluded(CandidateHash, DisputeResult),
448		/// A dispute has concluded with supermajority against a candidate.
449		/// Block authors should no longer build on top of this head and should
450		/// instead revert the block at the given height. This should be the
451		/// number of the child of the last known valid block in the chain.
452		Revert(BlockNumberFor<T>),
453	}
454
455	#[pallet::error]
456	pub enum Error<T> {
457		/// Duplicate dispute statement sets provided.
458		DuplicateDisputeStatementSets,
459		/// Ancient dispute statement provided.
460		AncientDisputeStatement,
461		/// Validator index on statement is out of bounds for session.
462		ValidatorIndexOutOfBounds,
463		/// Invalid signature on statement.
464		InvalidSignature,
465		/// Validator vote submitted more than once to dispute.
466		DuplicateStatement,
467		/// A dispute where there are only votes on one side.
468		SingleSidedDispute,
469		/// A dispute vote from a malicious backer.
470		MaliciousBacker,
471		/// No backing votes were provides along dispute statements.
472		MissingBackingVotes,
473		/// Unconfirmed dispute statement sets provided.
474		UnconfirmedDispute,
475	}
476
477	#[pallet::call]
478	impl<T: Config> Pallet<T> {
479		#[pallet::call_index(0)]
480		#[pallet::weight(<T as Config>::WeightInfo::force_unfreeze())]
481		pub fn force_unfreeze(origin: OriginFor<T>) -> DispatchResult {
482			ensure_root(origin)?;
483			Frozen::<T>::set(None);
484			Ok(())
485		}
486	}
487}
488
489bitflags::bitflags! {
490	#[derive(Default)]
491	struct DisputeStateFlags: u8 {
492		/// The byzantine threshold of `f + 1` votes (and hence participating validators) was reached.
493		const CONFIRMED = 0b0001;
494		/// Is the supermajority for validity of the dispute reached.
495		const FOR_SUPERMAJORITY = 0b0010;
496		/// Is the supermajority against the validity of the block reached.
497		const AGAINST_SUPERMAJORITY = 0b0100;
498		/// Is there f+1 against the validity of the block reached
499		const AGAINST_BYZANTINE = 0b1000;
500	}
501}
502
503impl DisputeStateFlags {
504	fn from_state<BlockNumber>(state: &DisputeState<BlockNumber>) -> Self {
505		// Only correct since `DisputeState` is _always_ initialized
506		// with the validator set based on the session.
507		let n = state.validators_for.len();
508
509		let byzantine_threshold = byzantine_threshold(n);
510		let supermajority_threshold = supermajority_threshold(n);
511
512		let mut flags = DisputeStateFlags::default();
513		let all_participants = state.validators_for.clone() | state.validators_against.clone();
514		if all_participants.count_ones() > byzantine_threshold {
515			flags |= DisputeStateFlags::CONFIRMED;
516		}
517
518		if state.validators_for.count_ones() >= supermajority_threshold {
519			flags |= DisputeStateFlags::FOR_SUPERMAJORITY;
520		}
521
522		if state.validators_against.count_ones() > byzantine_threshold {
523			flags |= DisputeStateFlags::AGAINST_BYZANTINE;
524		}
525
526		if state.validators_against.count_ones() >= supermajority_threshold {
527			flags |= DisputeStateFlags::AGAINST_SUPERMAJORITY;
528		}
529
530		flags
531	}
532}
533
534struct ImportSummary<BlockNumber> {
535	/// The new state, with all votes imported.
536	state: DisputeState<BlockNumber>,
537	/// List of validators who backed the candidate being disputed.
538	backers: BTreeSet<ValidatorIndex>,
539	/// Validators to slash for being (wrongly) on the AGAINST side.
540	slash_against: Vec<ValidatorIndex>,
541	/// Validators to slash for being (wrongly) on the FOR side.
542	slash_for: Vec<ValidatorIndex>,
543	// New participants in the dispute.
544	new_participants: bitvec::vec::BitVec<u8, BitOrderLsb0>,
545	// Difference in state flags from previous.
546	new_flags: DisputeStateFlags,
547}
548
549#[derive(RuntimeDebug, PartialEq, Eq)]
550enum VoteImportError {
551	/// Validator index was outside the range of valid validator indices in the given session.
552	ValidatorIndexOutOfBounds,
553	/// Found a duplicate statement in the dispute statement set.
554	DuplicateStatement,
555	/// Found an explicit valid statement after backing statement.
556	/// Backers should not participate in explicit voting so this is
557	/// only possible on malicious backers.
558	MaliciousBacker,
559}
560
561#[derive(RuntimeDebug, Copy, Clone, PartialEq, Eq)]
562enum VoteKind {
563	/// A backing vote that is counted as "for" vote in dispute resolution.
564	Backing,
565	/// Either an approval vote or and explicit dispute "for" vote.
566	ExplicitValid,
567	/// An explicit dispute "against" vote.
568	Invalid,
569}
570
571impl From<&DisputeStatement> for VoteKind {
572	fn from(statement: &DisputeStatement) -> Self {
573		if statement.is_backing() {
574			Self::Backing
575		} else if statement.indicates_validity() {
576			Self::ExplicitValid
577		} else {
578			Self::Invalid
579		}
580	}
581}
582
583impl VoteKind {
584	fn is_valid(&self) -> bool {
585		match self {
586			Self::Backing | Self::ExplicitValid => true,
587			Self::Invalid => false,
588		}
589	}
590
591	fn is_backing(&self) -> bool {
592		match self {
593			Self::Backing => true,
594			Self::Invalid | Self::ExplicitValid => false,
595		}
596	}
597}
598
599impl<T: Config> From<VoteImportError> for Error<T> {
600	fn from(e: VoteImportError) -> Self {
601		match e {
602			VoteImportError::ValidatorIndexOutOfBounds => Error::<T>::ValidatorIndexOutOfBounds,
603			VoteImportError::DuplicateStatement => Error::<T>::DuplicateStatement,
604			VoteImportError::MaliciousBacker => Error::<T>::MaliciousBacker,
605		}
606	}
607}
608
609/// A transport statement bit change for a single validator.
610#[derive(RuntimeDebug, PartialEq, Eq)]
611struct ImportUndo {
612	/// The validator index to which to associate the statement import.
613	validator_index: ValidatorIndex,
614	/// The kind and direction of the vote.
615	vote_kind: VoteKind,
616	/// Has the validator participated before, i.e. in backing or
617	/// with an opposing vote.
618	new_participant: bool,
619}
620
621struct DisputeStateImporter<BlockNumber> {
622	state: DisputeState<BlockNumber>,
623	backers: BTreeSet<ValidatorIndex>,
624	now: BlockNumber,
625	new_participants: bitvec::vec::BitVec<u8, BitOrderLsb0>,
626	pre_flags: DisputeStateFlags,
627	pre_state: DisputeState<BlockNumber>,
628	// The list of backing votes before importing the batch of votes. This field should be
629	// initialized as empty on the first import of the dispute votes and should remain non-empty
630	// afterwards.
631	//
632	// If a dispute has concluded and the candidate was found invalid, we may want to slash as many
633	// backers as possible. This list allows us to slash these backers once their votes have been
634	// imported post dispute conclusion.
635	pre_backers: BTreeSet<ValidatorIndex>,
636}
637
638impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
639	fn new(
640		state: DisputeState<BlockNumber>,
641		backers: BTreeSet<ValidatorIndex>,
642		now: BlockNumber,
643	) -> Self {
644		let pre_flags = DisputeStateFlags::from_state(&state);
645		let new_participants = bitvec::bitvec![u8, BitOrderLsb0; 0; state.validators_for.len()];
646		// consistency checks
647		for i in backers.iter() {
648			debug_assert_eq!(state.validators_for.get(i.0 as usize).map(|b| *b), Some(true));
649		}
650		let pre_state = state.clone();
651		let pre_backers = backers.clone();
652
653		DisputeStateImporter {
654			state,
655			backers,
656			now,
657			new_participants,
658			pre_flags,
659			pre_state,
660			pre_backers,
661		}
662	}
663
664	fn import(
665		&mut self,
666		validator: ValidatorIndex,
667		kind: VoteKind,
668	) -> Result<ImportUndo, VoteImportError> {
669		let (bits, other_bits) = if kind.is_valid() {
670			(&mut self.state.validators_for, &mut self.state.validators_against)
671		} else {
672			(&mut self.state.validators_against, &mut self.state.validators_for)
673		};
674
675		// out of bounds or already participated
676		match bits.get(validator.0 as usize).map(|b| *b) {
677			None => return Err(VoteImportError::ValidatorIndexOutOfBounds),
678			Some(true) => {
679				// We allow backing statements to be imported after an
680				// explicit "for" vote, but not the other way around.
681				match (kind.is_backing(), self.backers.contains(&validator)) {
682					(true, true) | (false, false) =>
683						return Err(VoteImportError::DuplicateStatement),
684					(false, true) => return Err(VoteImportError::MaliciousBacker),
685					(true, false) => {},
686				}
687			},
688			Some(false) => {},
689		}
690
691		// consistency check
692		debug_assert!((validator.0 as usize) < self.new_participants.len());
693
694		let mut undo =
695			ImportUndo { validator_index: validator, vote_kind: kind, new_participant: false };
696
697		bits.set(validator.0 as usize, true);
698		if kind.is_backing() {
699			let is_new = self.backers.insert(validator);
700			// invariant check
701			debug_assert!(is_new);
702		}
703
704		// New participants tracks those validators by index, which didn't appear on either
705		// side of the dispute until now (so they make a first appearance).
706		// To verify this we need to assure they also were not part of the opposing side before.
707		if other_bits.get(validator.0 as usize).map_or(false, |b| !*b) {
708			undo.new_participant = true;
709			self.new_participants.set(validator.0 as usize, true);
710		}
711
712		Ok(undo)
713	}
714
715	/// Revert a done transaction.
716	fn undo(&mut self, undo: ImportUndo) {
717		if undo.vote_kind.is_valid() {
718			self.state.validators_for.set(undo.validator_index.0 as usize, false);
719		} else {
720			self.state.validators_against.set(undo.validator_index.0 as usize, false);
721		}
722
723		if undo.vote_kind.is_backing() {
724			self.backers.remove(&undo.validator_index);
725		}
726
727		if undo.new_participant {
728			self.new_participants.set(undo.validator_index.0 as usize, false);
729		}
730	}
731
732	/// Collect all dispute votes.
733	fn finish(mut self) -> ImportSummary<BlockNumber> {
734		let pre_flags = self.pre_flags;
735		let post_flags = DisputeStateFlags::from_state(&self.state);
736
737		let pre_post_contains = |flags| (pre_flags.contains(flags), post_flags.contains(flags));
738
739		// 1. Check for FOR supermajority.
740		let slash_against = match pre_post_contains(DisputeStateFlags::FOR_SUPERMAJORITY) {
741			(false, true) => {
742				if self.state.concluded_at.is_none() {
743					self.state.concluded_at = Some(self.now.clone());
744				}
745
746				// provide AGAINST voters to slash.
747				self.state
748					.validators_against
749					.iter_ones()
750					.map(|i| ValidatorIndex(i as _))
751					.collect()
752			},
753			(true, true) => {
754				// provide new AGAINST voters to slash.
755				self.state
756					.validators_against
757					.iter_ones()
758					.filter(|i| self.pre_state.validators_against.get(*i).map_or(false, |b| !*b))
759					.map(|i| ValidatorIndex(i as _))
760					.collect()
761			},
762			(true, false) => {
763				log::error!("Dispute statements are never removed. This is a bug");
764				Vec::new()
765			},
766			(false, false) => Vec::new(),
767		};
768
769		// 2. Check for AGAINST supermajority.
770		let slash_for = match pre_post_contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) {
771			(false, true) => {
772				if self.state.concluded_at.is_none() {
773					self.state.concluded_at = Some(self.now.clone());
774				}
775
776				// provide FOR voters to slash.
777				self.state.validators_for.iter_ones().map(|i| ValidatorIndex(i as _)).collect()
778			},
779			(true, true) => {
780				// provide new FOR voters to slash including new backers
781				// who might have voted FOR before
782				let new_backing_vote = |i: &ValidatorIndex| -> bool {
783					!self.pre_backers.contains(i) && self.backers.contains(i)
784				};
785				self.state
786					.validators_for
787					.iter_ones()
788					.filter(|i| {
789						self.pre_state.validators_for.get(*i).map_or(false, |b| !*b) ||
790							new_backing_vote(&ValidatorIndex(*i as _))
791					})
792					.map(|i| ValidatorIndex(i as _))
793					.collect()
794			},
795			(true, false) => {
796				log::error!("Dispute statements are never removed. This is a bug");
797				Vec::new()
798			},
799			(false, false) => Vec::new(),
800		};
801
802		ImportSummary {
803			state: self.state,
804			backers: self.backers,
805			slash_against,
806			slash_for,
807			new_participants: self.new_participants,
808			new_flags: post_flags - pre_flags,
809		}
810	}
811}
812
813// A filter on a dispute statement set.
814#[derive(PartialEq)]
815#[cfg_attr(test, derive(Debug))]
816enum StatementSetFilter {
817	// Remove the entire dispute statement set.
818	RemoveAll,
819	// Remove the votes with given index from the statement set.
820	RemoveIndices(Vec<usize>),
821}
822
823impl StatementSetFilter {
824	fn filter_statement_set(
825		self,
826		mut statement_set: DisputeStatementSet,
827	) -> Option<CheckedDisputeStatementSet> {
828		match self {
829			StatementSetFilter::RemoveAll => None,
830			StatementSetFilter::RemoveIndices(mut indices) => {
831				indices.sort();
832				indices.dedup();
833
834				// reverse order ensures correctness
835				for index in indices.into_iter().rev() {
836					// `swap_remove` guarantees linear complexity.
837					statement_set.statements.swap_remove(index);
838				}
839
840				if statement_set.statements.is_empty() {
841					None
842				} else {
843					// we just checked correctness when filtering.
844					Some(CheckedDisputeStatementSet::unchecked_from_unchecked(statement_set))
845				}
846			},
847		}
848	}
849
850	fn remove_index(&mut self, i: usize) {
851		if let StatementSetFilter::RemoveIndices(ref mut indices) = *self {
852			indices.push(i)
853		}
854	}
855}
856
857impl<T: Config> Pallet<T> {
858	/// Called by the initializer to initialize the disputes module.
859	pub(crate) fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
860		Weight::zero()
861	}
862
863	/// Called by the initializer to finalize the disputes pallet.
864	pub(crate) fn initializer_finalize() {}
865
866	/// Called by the initializer to note a new session in the disputes pallet.
867	pub(crate) fn initializer_on_new_session(
868		notification: &SessionChangeNotification<BlockNumberFor<T>>,
869	) {
870		let config = configuration::ActiveConfig::<T>::get();
871
872		if notification.session_index <= config.dispute_period + 1 {
873			return
874		}
875
876		let pruning_target = notification.session_index - config.dispute_period - 1;
877
878		LastPrunedSession::<T>::mutate(|last_pruned| {
879			let to_prune = if let Some(last_pruned) = last_pruned {
880				*last_pruned + 1..=pruning_target
881			} else {
882				pruning_target..=pruning_target
883			};
884
885			for to_prune in to_prune {
886				// This should be small, as disputes are rare, so `None` is fine.
887				#[allow(deprecated)]
888				Disputes::<T>::remove_prefix(to_prune, None);
889				#[allow(deprecated)]
890				BackersOnDisputes::<T>::remove_prefix(to_prune, None);
891
892				// This is larger, and will be extracted to the `shared` pallet for more proper
893				// pruning. TODO: https://github.com/paritytech/polkadot/issues/3469
894				#[allow(deprecated)]
895				Included::<T>::remove_prefix(to_prune, None);
896			}
897
898			*last_pruned = Some(pruning_target);
899		});
900	}
901
902	/// Handle sets of dispute statements corresponding to 0 or more candidates.
903	/// Returns a vector of freshly created disputes.
904	///
905	/// Assumes `statement_sets` were already de-duplicated.
906	///
907	/// # Warning
908	///
909	/// This functions modifies the state when failing. It is expected to be called in inherent,
910	/// and to fail the extrinsic on error. As invalid inherents are not allowed, the dirty state
911	/// is not committed.
912	pub(crate) fn process_checked_multi_dispute_data(
913		statement_sets: &CheckedMultiDisputeStatementSet,
914	) -> Result<Vec<(SessionIndex, CandidateHash)>, DispatchError> {
915		let config = configuration::ActiveConfig::<T>::get();
916
917		let mut fresh = Vec::with_capacity(statement_sets.len());
918		for statement_set in statement_sets {
919			let dispute_target = {
920				let statement_set = statement_set.as_ref();
921				(statement_set.session, statement_set.candidate_hash)
922			};
923			if Self::process_checked_dispute_data(
924				statement_set,
925				config.dispute_post_conclusion_acceptance_period,
926			)? {
927				fresh.push(dispute_target);
928			}
929		}
930
931		Ok(fresh)
932	}
933
934	// Given a statement set, this produces a filter to be applied to the statement set.
935	// It either removes the entire dispute statement set or some specific votes from it.
936	//
937	// Votes which are duplicate or already known by the chain are filtered out.
938	// The entire set is removed if the dispute is both, ancient and concluded.
939	// Disputes without enough votes to get confirmed are also filtered out.
940	fn filter_dispute_data(
941		set: &DisputeStatementSet,
942		post_conclusion_acceptance_period: BlockNumberFor<T>,
943	) -> StatementSetFilter {
944		let mut filter = StatementSetFilter::RemoveIndices(Vec::new());
945
946		// Dispute statement sets on any dispute which concluded
947		// before this point are to be rejected.
948		let now = frame_system::Pallet::<T>::block_number();
949		let oldest_accepted = now.saturating_sub(post_conclusion_acceptance_period);
950
951		// Load session info to access validators
952		let session_info = match session_info::Sessions::<T>::get(set.session) {
953			Some(s) => s,
954			None => return StatementSetFilter::RemoveAll,
955		};
956
957		let config = configuration::ActiveConfig::<T>::get();
958
959		let n_validators = session_info.validators.len();
960
961		// Check for ancient.
962		let dispute_state = {
963			if let Some(dispute_state) = Disputes::<T>::get(&set.session, &set.candidate_hash) {
964				if dispute_state.concluded_at.as_ref().map_or(false, |c| c < &oldest_accepted) {
965					return StatementSetFilter::RemoveAll
966				}
967
968				dispute_state
969			} else {
970				// No state in storage, this indicates it's the first dispute statement set as well.
971				DisputeState {
972					validators_for: bitvec![u8, BitOrderLsb0; 0; n_validators],
973					validators_against: bitvec![u8, BitOrderLsb0; 0; n_validators],
974					start: now,
975					concluded_at: None,
976				}
977			}
978		};
979
980		let backers =
981			BackersOnDisputes::<T>::get(&set.session, &set.candidate_hash).unwrap_or_default();
982
983		// Check and import all votes.
984		let summary = {
985			let mut importer = DisputeStateImporter::new(dispute_state, backers, now);
986			for (i, (statement, validator_index, signature)) in set.statements.iter().enumerate() {
987				// ensure the validator index is present in the session info
988				// and the signature is valid
989				let validator_public = match session_info.validators.get(*validator_index) {
990					None => {
991						filter.remove_index(i);
992						continue
993					},
994					Some(v) => v,
995				};
996
997				let kind = VoteKind::from(statement);
998
999				let undo = match importer.import(*validator_index, kind) {
1000					Ok(u) => u,
1001					Err(_) => {
1002						filter.remove_index(i);
1003						continue
1004					},
1005				};
1006
1007				// Check signature after attempting import.
1008				//
1009				// Since we expect that this filter will be applied to
1010				// disputes long after they're concluded, 99% of the time,
1011				// the duplicate filter above will catch them before needing
1012				// to do a heavy signature check.
1013				//
1014				// This is only really important until the post-conclusion acceptance threshold
1015				// is reached, and then no part of this loop will be hit.
1016				if let Err(()) = check_signature(
1017					&validator_public,
1018					set.candidate_hash,
1019					set.session,
1020					statement,
1021					signature,
1022					// This is here to prevent malicious nodes of generating
1023					// `ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates` before that
1024					// is enabled, via setting `max_approval_coalesce_count` in the parachain host
1025					// config.
1026					config.approval_voting_params.max_approval_coalesce_count > 1,
1027				) {
1028					log::warn!("Failed to check dispute signature");
1029
1030					importer.undo(undo);
1031					filter.remove_index(i);
1032					continue
1033				};
1034			}
1035
1036			importer.finish()
1037		};
1038
1039		// Reject disputes which don't have at least one vote on each side.
1040		if summary.state.validators_for.count_ones() == 0 ||
1041			summary.state.validators_against.count_ones() == 0
1042		{
1043			return StatementSetFilter::RemoveAll
1044		}
1045
1046		// Reject disputes containing less votes than needed for confirmation.
1047		if (summary.state.validators_for.clone() | &summary.state.validators_against).count_ones() <=
1048			byzantine_threshold(summary.state.validators_for.len())
1049		{
1050			return StatementSetFilter::RemoveAll
1051		}
1052
1053		filter
1054	}
1055
1056	/// Handle a set of dispute statements corresponding to a single candidate.
1057	///
1058	/// Fails if the dispute data is invalid. Returns a Boolean indicating whether the
1059	/// dispute is fresh.
1060	fn process_checked_dispute_data(
1061		set: &CheckedDisputeStatementSet,
1062		dispute_post_conclusion_acceptance_period: BlockNumberFor<T>,
1063	) -> Result<bool, DispatchError> {
1064		// Dispute statement sets on any dispute which concluded
1065		// before this point are to be rejected.
1066		let now = frame_system::Pallet::<T>::block_number();
1067		let oldest_accepted = now.saturating_sub(dispute_post_conclusion_acceptance_period);
1068
1069		let set = set.as_ref();
1070
1071		// Load session info to access validators
1072		let session_info = match session_info::Sessions::<T>::get(set.session) {
1073			Some(s) => s,
1074			None => return Err(Error::<T>::AncientDisputeStatement.into()),
1075		};
1076
1077		let n_validators = session_info.validators.len();
1078
1079		// Check for ancient.
1080		let (fresh, dispute_state) = {
1081			if let Some(dispute_state) = Disputes::<T>::get(&set.session, &set.candidate_hash) {
1082				ensure!(
1083					dispute_state.concluded_at.as_ref().map_or(true, |c| c >= &oldest_accepted),
1084					Error::<T>::AncientDisputeStatement,
1085				);
1086
1087				(false, dispute_state)
1088			} else {
1089				(
1090					true,
1091					DisputeState {
1092						validators_for: bitvec![u8, BitOrderLsb0; 0; n_validators],
1093						validators_against: bitvec![u8, BitOrderLsb0; 0; n_validators],
1094						start: now,
1095						concluded_at: None,
1096					},
1097				)
1098			}
1099		};
1100
1101		let backers =
1102			BackersOnDisputes::<T>::get(&set.session, &set.candidate_hash).unwrap_or_default();
1103
1104		// Import all votes. They were pre-checked.
1105		let summary = {
1106			let mut importer = DisputeStateImporter::new(dispute_state, backers, now);
1107			for (statement, validator_index, _signature) in &set.statements {
1108				let kind = VoteKind::from(statement);
1109
1110				importer.import(*validator_index, kind).map_err(Error::<T>::from)?;
1111			}
1112
1113			importer.finish()
1114		};
1115
1116		// Reject disputes which don't have at least one vote on each side.
1117		ensure!(
1118			summary.state.validators_for.count_ones() > 0 &&
1119				summary.state.validators_against.count_ones() > 0,
1120			Error::<T>::SingleSidedDispute,
1121		);
1122
1123		// Reject disputes containing less votes than needed for confirmation.
1124		ensure!(
1125			(summary.state.validators_for.clone() | &summary.state.validators_against).count_ones() >
1126				byzantine_threshold(summary.state.validators_for.len()),
1127			Error::<T>::UnconfirmedDispute,
1128		);
1129		let backers = summary.backers;
1130		// Reject statements with no accompanying backing votes.
1131		ensure!(!backers.is_empty(), Error::<T>::MissingBackingVotes);
1132		BackersOnDisputes::<T>::insert(&set.session, &set.candidate_hash, backers.clone());
1133		// AUDIT: from now on, no error should be returned.
1134
1135		let DisputeStatementSet { ref session, ref candidate_hash, .. } = set;
1136		let session = *session;
1137		let candidate_hash = *candidate_hash;
1138
1139		if fresh {
1140			let is_local = Included::<T>::contains_key(&session, &candidate_hash);
1141
1142			Self::deposit_event(Event::DisputeInitiated(
1143				candidate_hash,
1144				if is_local { DisputeLocation::Local } else { DisputeLocation::Remote },
1145			));
1146		}
1147
1148		{
1149			if summary.new_flags.contains(DisputeStateFlags::FOR_SUPERMAJORITY) {
1150				Self::deposit_event(Event::DisputeConcluded(candidate_hash, DisputeResult::Valid));
1151			}
1152
1153			// It is possible, although unexpected, for a dispute to conclude twice.
1154			// This would require f+1 validators to vote in both directions.
1155			// A dispute cannot conclude more than once in each direction.
1156
1157			if summary.new_flags.contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) {
1158				Self::deposit_event(Event::DisputeConcluded(
1159					candidate_hash,
1160					DisputeResult::Invalid,
1161				));
1162			}
1163		}
1164
1165		// Reward statements.
1166		T::RewardValidators::reward_dispute_statement(
1167			session,
1168			summary.new_participants.iter_ones().map(|i| ValidatorIndex(i as _)),
1169		);
1170
1171		// Slash participants on a losing side.
1172		{
1173			// a valid candidate, according to 2/3. Punish those on the 'against' side.
1174			T::SlashingHandler::punish_against_valid(
1175				session,
1176				candidate_hash,
1177				summary.slash_against,
1178				backers.clone(),
1179			);
1180
1181			// an invalid candidate, according to 2/3. Punish those on the 'for' side.
1182			T::SlashingHandler::punish_for_invalid(
1183				session,
1184				candidate_hash,
1185				summary.slash_for,
1186				backers,
1187			);
1188		}
1189
1190		Disputes::<T>::insert(&session, &candidate_hash, &summary.state);
1191
1192		// Freeze if the INVALID votes against some local candidate are above the byzantine
1193		// threshold
1194		if summary.new_flags.contains(DisputeStateFlags::AGAINST_BYZANTINE) {
1195			if let Some(revert_to) = Included::<T>::get(&session, &candidate_hash) {
1196				Self::revert_and_freeze(revert_to);
1197			}
1198		}
1199
1200		Ok(fresh)
1201	}
1202
1203	#[allow(unused)]
1204	pub(crate) fn disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState<BlockNumberFor<T>>)>
1205	{
1206		Disputes::<T>::iter().collect()
1207	}
1208
1209	pub(crate) fn note_included(
1210		session: SessionIndex,
1211		candidate_hash: CandidateHash,
1212		included_in: BlockNumberFor<T>,
1213	) {
1214		if included_in.is_zero() {
1215			return
1216		}
1217
1218		let revert_to = included_in - One::one();
1219
1220		Included::<T>::insert(&session, &candidate_hash, revert_to);
1221
1222		if let Some(state) = Disputes::<T>::get(&session, candidate_hash) {
1223			if has_supermajority_against(&state) {
1224				Self::revert_and_freeze(revert_to);
1225			}
1226		}
1227	}
1228
1229	pub(crate) fn included_state(
1230		session: SessionIndex,
1231		candidate_hash: CandidateHash,
1232	) -> Option<BlockNumberFor<T>> {
1233		Included::<T>::get(session, candidate_hash)
1234	}
1235
1236	pub(crate) fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool {
1237		Disputes::<T>::get(&session, &candidate_hash).map_or(false, |dispute| {
1238			// A dispute that has concluded with supermajority-against.
1239			has_supermajority_against(&dispute)
1240		})
1241	}
1242
1243	pub(crate) fn is_frozen() -> bool {
1244		Frozen::<T>::get().is_some()
1245	}
1246
1247	pub(crate) fn revert_and_freeze(revert_to: BlockNumberFor<T>) {
1248		if Frozen::<T>::get().map_or(true, |last| last > revert_to) {
1249			Frozen::<T>::set(Some(revert_to));
1250
1251			// The `Revert` log is about reverting a block, not reverting to a block.
1252			// If we want to revert to block X in the current chain, we need to revert
1253			// block X+1.
1254			let revert = revert_to + One::one();
1255			Self::deposit_event(Event::Revert(revert));
1256			frame_system::Pallet::<T>::deposit_log(
1257				ConsensusLog::Revert(revert.saturated_into()).into(),
1258			);
1259		}
1260	}
1261}
1262
1263fn has_supermajority_against<BlockNumber>(dispute: &DisputeState<BlockNumber>) -> bool {
1264	let supermajority_threshold = supermajority_threshold(dispute.validators_against.len());
1265	dispute.validators_against.count_ones() >= supermajority_threshold
1266}
1267
1268fn check_signature(
1269	validator_public: &ValidatorId,
1270	candidate_hash: CandidateHash,
1271	session: SessionIndex,
1272	statement: &DisputeStatement,
1273	validator_signature: &ValidatorSignature,
1274	approval_multiple_candidates_enabled: bool,
1275) -> Result<(), ()> {
1276	let payload = match statement {
1277		DisputeStatement::Valid(ValidDisputeStatementKind::Explicit) =>
1278			ExplicitDisputeStatement { valid: true, candidate_hash, session }.signing_payload(),
1279		DisputeStatement::Valid(ValidDisputeStatementKind::BackingSeconded(inclusion_parent)) =>
1280			CompactStatement::Seconded(candidate_hash).signing_payload(&SigningContext {
1281				session_index: session,
1282				parent_hash: *inclusion_parent,
1283			}),
1284		DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(inclusion_parent)) =>
1285			CompactStatement::Valid(candidate_hash).signing_payload(&SigningContext {
1286				session_index: session,
1287				parent_hash: *inclusion_parent,
1288			}),
1289		DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking) =>
1290			ApprovalVote(candidate_hash).signing_payload(session),
1291		DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(
1292			candidates,
1293		)) =>
1294			if approval_multiple_candidates_enabled && candidates.contains(&candidate_hash) {
1295				ApprovalVoteMultipleCandidates(candidates).signing_payload(session)
1296			} else {
1297				return Err(())
1298			},
1299		DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit) =>
1300			ExplicitDisputeStatement { valid: false, candidate_hash, session }.signing_payload(),
1301	};
1302
1303	let start = get_current_time();
1304
1305	let res =
1306		if validator_signature.verify(&payload[..], &validator_public) { Ok(()) } else { Err(()) };
1307
1308	let end = get_current_time();
1309
1310	METRICS.on_signature_check_complete(end.saturating_sub(start)); // ns
1311
1312	res
1313}
1314
1315#[cfg(all(not(feature = "runtime-benchmarks"), test))]
1316// Test helper for clearing the on-chain dispute data.
1317pub(crate) fn clear_dispute_storage<T: Config>() {
1318	let _ = Disputes::<T>::clear(u32::MAX, None);
1319	let _ = BackersOnDisputes::<T>::clear(u32::MAX, None);
1320	let _ = Included::<T>::clear(u32::MAX, None);
1321}