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