// Copyright (C) Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! Runtime component for handling disputes of parachain candidates. use crate::{ configuration, initializer::SessionChangeNotification, metrics::METRICS, session_info, }; use bitvec::{bitvec, order::Lsb0 as BitOrderLsb0}; use frame_support::{ensure, weights::Weight}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; use polkadot_runtime_metrics::get_current_time; use primitives::{ byzantine_threshold, supermajority_threshold, ApprovalVote, CandidateHash, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CompactStatement, ConsensusLog, DisputeState, DisputeStatement, DisputeStatementSet, ExplicitDisputeStatement, InvalidDisputeStatementKind, MultiDisputeStatementSet, SessionIndex, SigningContext, ValidDisputeStatementKind, ValidatorId, ValidatorIndex, ValidatorSignature, }; use scale_info::TypeInfo; use sp_runtime::{ traits::{AppVerify, One, Saturating, Zero}, DispatchError, RuntimeDebug, SaturatedConversion, }; use sp_std::{cmp::Ordering, collections::btree_set::BTreeSet, prelude::*}; #[cfg(test)] #[allow(unused_imports)] pub(crate) use self::tests::run_to_block; pub mod slashing; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod migration; const LOG_TARGET: &str = "runtime::disputes"; /// Whether the dispute is local or remote. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub enum DisputeLocation { Local, Remote, } /// The result of a dispute, whether the candidate is deemed valid (for) or invalid (against). #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub enum DisputeResult { Valid, Invalid, } /// Reward hooks for disputes. pub trait RewardValidators { // Give each validator a reward, likely small, for participating in the dispute. fn reward_dispute_statement( session: SessionIndex, validators: impl IntoIterator, ); } impl RewardValidators for () { fn reward_dispute_statement(_: SessionIndex, _: impl IntoIterator) {} } /// Punishment hooks for disputes. pub trait SlashingHandler { /// Punish a series of validators who were for an invalid parablock. This is /// expected to be a major punishment. fn punish_for_invalid( session: SessionIndex, candidate_hash: CandidateHash, losers: impl IntoIterator, backers: impl IntoIterator, ); /// Punish a series of validators who were against a valid parablock. This /// is expected to be a minor punishment. fn punish_against_valid( session: SessionIndex, candidate_hash: CandidateHash, losers: impl IntoIterator, backers: impl IntoIterator, ); /// Called by the initializer to initialize the slashing pallet. fn initializer_initialize(now: BlockNumber) -> Weight; /// Called by the initializer to finalize the slashing pallet. fn initializer_finalize(); /// Called by the initializer to note that a new session has started. fn initializer_on_new_session(session_index: SessionIndex); } impl SlashingHandler for () { fn punish_for_invalid( _: SessionIndex, _: CandidateHash, _: impl IntoIterator, _: impl IntoIterator, ) { } fn punish_against_valid( _: SessionIndex, _: CandidateHash, _: impl IntoIterator, _: impl IntoIterator, ) { } fn initializer_initialize(_now: BlockNumber) -> Weight { Weight::zero() } fn initializer_finalize() {} fn initializer_on_new_session(_: SessionIndex) {} } /// Binary discriminator to determine if the expensive signature /// checks are necessary. #[derive(Clone, Copy)] pub enum VerifyDisputeSignatures { /// Yes, verify the signatures. Yes, /// No, skip the signature verification. /// /// Only done if there exists an invariant that /// can guaranteed the signature was checked before. Skip, } /// Provide a `Ordering` for the two provided dispute statement sets according to the /// following prioritization: /// 1. Prioritize local disputes over remote disputes /// 2. Prioritize older disputes over newer disputes fn dispute_ordering_compare, BlockNumber: Ord>( a: &DisputeStatementSet, b: &DisputeStatementSet, ) -> Ordering where T: ?Sized, { let a_local_block = >::included_state(a.session, a.candidate_hash); let b_local_block = >::included_state(b.session, b.candidate_hash); match (a_local_block, b_local_block) { // Prioritize local disputes over remote disputes. (None, Some(_)) => Ordering::Greater, (Some(_), None) => Ordering::Less, // For local disputes, prioritize those that occur at an earlier height. (Some(a_height), Some(b_height)) => a_height.cmp(&b_height).then_with(|| a.candidate_hash.cmp(&b.candidate_hash)), // Prioritize earlier remote disputes using session as rough proxy. (None, None) => { let session_ord = a.session.cmp(&b.session); if session_ord == Ordering::Equal { // sort by hash as last resort, to make below dedup work consistently a.candidate_hash.cmp(&b.candidate_hash) } else { session_ord } }, } } use super::paras_inherent::IsSortedBy; /// Returns `true` if duplicate items were found, otherwise `false`. /// /// `check_equal(a: &T, b: &T)` _must_ return `true`, iff `a` and `b` are equal, otherwise `false. /// The definition of _equal_ is to be defined by the user. /// /// Attention: Requires the input `iter` to be sorted, such that _equals_ /// would be adjacent in respect whatever `check_equal` defines as equality! fn contains_duplicates_in_sorted_iter< 'a, T: 'a, I: 'a + IntoIterator, C: 'static + FnMut(&T, &T) -> bool, >( iter: I, mut check_equal: C, ) -> bool { let mut iter = iter.into_iter(); if let Some(mut previous) = iter.next() { while let Some(current) = iter.next() { if check_equal(previous, current) { return true } previous = current; } } return false } /// Hook into disputes handling. /// /// Allows decoupling parachains handling from disputes so that it can /// potentially be disabled when instantiating a specific runtime. pub trait DisputesHandler { /// Whether the chain is frozen, if the chain is frozen it will not accept /// any new parachain blocks for backing or inclusion. fn is_frozen() -> bool; /// Assure sanity fn assure_deduplicated_and_sorted(statement_sets: &MultiDisputeStatementSet) -> Result<(), ()> { if !IsSortedBy::is_sorted_by( statement_sets.as_slice(), dispute_ordering_compare::, ) { return Err(()) } // Sorted, so according to session and candidate hash, this will detect duplicates. if contains_duplicates_in_sorted_iter(statement_sets, |previous, current| { current.session == previous.session && current.candidate_hash == previous.candidate_hash }) { return Err(()) } Ok(()) } /// Remove dispute statement duplicates and sort the non-duplicates based on /// local (lower indicies) vs remotes (higher indices) and age (older with lower indices). /// /// Returns `Ok(())` if no duplicates were present, `Err(())` otherwise. /// /// Unsorted data does not change the return value, while the node side /// is generally expected to pass them in sorted. fn deduplicate_and_sort_dispute_data( statement_sets: &mut MultiDisputeStatementSet, ) -> Result<(), ()> { // TODO: Consider trade-of to avoid `O(n * log(n))` average lookups of `included_state` // TODO: instead make a single pass and store the values lazily. // TODO: https://github.com/paritytech/polkadot/issues/4527 let n = statement_sets.len(); statement_sets.sort_by(dispute_ordering_compare::); statement_sets .dedup_by(|a, b| a.session == b.session && a.candidate_hash == b.candidate_hash); // if there were any duplicates, indicate that to the caller. if n == statement_sets.len() { Ok(()) } else { Err(()) } } /// Filter a single dispute statement set. /// /// Used in cases where more granular control is required, i.e. when /// accounting for maximum block weight. fn filter_dispute_data( statement_set: DisputeStatementSet, post_conclusion_acceptance_period: BlockNumber, verify_sigs: VerifyDisputeSignatures, ) -> Option; /// Handle sets of dispute statements corresponding to 0 or more candidates. /// Returns a vector of freshly created disputes. fn process_checked_multi_dispute_data( statement_sets: &CheckedMultiDisputeStatementSet, ) -> Result, DispatchError>; /// Note that the given candidate has been included. fn note_included( session: SessionIndex, candidate_hash: CandidateHash, included_in: BlockNumber, ); /// Retrieve the included state of a given candidate in a particular session. If it /// returns `Some`, then we have a local dispute for the given `candidate_hash`. fn included_state(session: SessionIndex, candidate_hash: CandidateHash) -> Option; /// Whether the given candidate concluded invalid in a dispute with supermajority. fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool; /// Called by the initializer to initialize the disputes pallet. fn initializer_initialize(now: BlockNumber) -> Weight; /// Called by the initializer to finalize the disputes pallet. fn initializer_finalize(); /// Called by the initializer to note that a new session has started. fn initializer_on_new_session(notification: &SessionChangeNotification); } impl DisputesHandler for () { fn is_frozen() -> bool { false } fn deduplicate_and_sort_dispute_data( statement_sets: &mut MultiDisputeStatementSet, ) -> Result<(), ()> { statement_sets.clear(); Ok(()) } fn filter_dispute_data( _set: DisputeStatementSet, _post_conclusion_acceptance_period: BlockNumber, _verify_sigs: VerifyDisputeSignatures, ) -> Option { None } fn process_checked_multi_dispute_data( _statement_sets: &CheckedMultiDisputeStatementSet, ) -> Result, DispatchError> { Ok(Vec::new()) } fn note_included( _session: SessionIndex, _candidate_hash: CandidateHash, _included_in: BlockNumber, ) { } fn included_state( _session: SessionIndex, _candidate_hash: CandidateHash, ) -> Option { None } fn concluded_invalid(_session: SessionIndex, _candidate_hash: CandidateHash) -> bool { false } fn initializer_initialize(_now: BlockNumber) -> Weight { Weight::zero() } fn initializer_finalize() {} fn initializer_on_new_session(_notification: &SessionChangeNotification) {} } impl DisputesHandler for pallet::Pallet where T::BlockNumber: Ord, { fn is_frozen() -> bool { pallet::Pallet::::is_frozen() } fn filter_dispute_data( set: DisputeStatementSet, post_conclusion_acceptance_period: T::BlockNumber, verify_sigs: VerifyDisputeSignatures, ) -> Option { pallet::Pallet::::filter_dispute_data( &set, post_conclusion_acceptance_period, verify_sigs, ) .filter_statement_set(set) } fn process_checked_multi_dispute_data( statement_sets: &CheckedMultiDisputeStatementSet, ) -> Result, DispatchError> { pallet::Pallet::::process_checked_multi_dispute_data(statement_sets) } fn note_included( session: SessionIndex, candidate_hash: CandidateHash, included_in: T::BlockNumber, ) { pallet::Pallet::::note_included(session, candidate_hash, included_in) } fn included_state( session: SessionIndex, candidate_hash: CandidateHash, ) -> Option { pallet::Pallet::::included_state(session, candidate_hash) } fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool { pallet::Pallet::::concluded_invalid(session, candidate_hash) } fn initializer_initialize(now: T::BlockNumber) -> Weight { pallet::Pallet::::initializer_initialize(now) } fn initializer_finalize() { pallet::Pallet::::initializer_finalize() } fn initializer_on_new_session(notification: &SessionChangeNotification) { pallet::Pallet::::initializer_on_new_session(notification) } } pub trait WeightInfo { fn force_unfreeze() -> Weight; } pub struct TestWeightInfo; impl WeightInfo for TestWeightInfo { fn force_unfreeze() -> Weight { Weight::zero() } } pub use pallet::*; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config + configuration::Config + session_info::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type RewardValidators: RewardValidators; type SlashingHandler: SlashingHandler; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); /// The last pruned session, if any. All data stored by this module /// references sessions. #[pallet::storage] pub(super) type LastPrunedSession = StorageValue<_, SessionIndex>; /// All ongoing or concluded disputes for the last several sessions. #[pallet::storage] pub(super) type Disputes = StorageDoubleMap< _, Twox64Concat, SessionIndex, Blake2_128Concat, CandidateHash, DisputeState, >; /// Backing votes stored for each dispute. /// This storage is used for slashing. #[pallet::storage] pub(super) type BackersOnDisputes = StorageDoubleMap< _, Twox64Concat, SessionIndex, Blake2_128Concat, CandidateHash, BTreeSet, >; /// All included blocks on the chain, as well as the block number in this chain that /// should be reverted back to if the candidate is disputed and determined to be invalid. #[pallet::storage] pub(super) type Included = StorageDoubleMap< _, Twox64Concat, SessionIndex, Blake2_128Concat, CandidateHash, T::BlockNumber, >; /// Whether the chain is frozen. Starts as `None`. When this is `Some`, /// the chain will not accept any new parachain blocks for backing or inclusion, /// and its value indicates the last valid block number in the chain. /// It can only be set back to `None` by governance intervention. #[pallet::storage] #[pallet::getter(fn last_valid_block)] pub(super) type Frozen = StorageValue<_, Option, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub fn deposit_event)] pub enum Event { /// A dispute has been initiated. \[candidate hash, dispute location\] DisputeInitiated(CandidateHash, DisputeLocation), /// A dispute has concluded for or against a candidate. /// `\[para id, candidate hash, dispute result\]` DisputeConcluded(CandidateHash, DisputeResult), /// A dispute has concluded with supermajority against a candidate. /// Block authors should no longer build on top of this head and should /// instead revert the block at the given height. This should be the /// number of the child of the last known valid block in the chain. Revert(T::BlockNumber), } #[pallet::error] pub enum Error { /// Duplicate dispute statement sets provided. DuplicateDisputeStatementSets, /// Ancient dispute statement provided. AncientDisputeStatement, /// Validator index on statement is out of bounds for session. ValidatorIndexOutOfBounds, /// Invalid signature on statement. InvalidSignature, /// Validator vote submitted more than once to dispute. DuplicateStatement, /// A dispute where there are only votes on one side. SingleSidedDispute, /// A dispute vote from a malicious backer. MaliciousBacker, /// No backing votes were provides along dispute statements. MissingBackingVotes, /// Unconfirmed dispute statement sets provided. UnconfirmedDispute, } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::force_unfreeze())] pub fn force_unfreeze(origin: OriginFor) -> DispatchResult { ensure_root(origin)?; Frozen::::set(None); Ok(()) } } } bitflags::bitflags! { #[derive(Default)] struct DisputeStateFlags: u8 { /// The byzantine threshold of `f + 1` votes (and hence participating validators) was reached. const CONFIRMED = 0b0001; /// Is the supermajority for validity of the dispute reached. const FOR_SUPERMAJORITY = 0b0010; /// Is the supermajority against the validity of the block reached. const AGAINST_SUPERMAJORITY = 0b0100; } } impl DisputeStateFlags { fn from_state(state: &DisputeState) -> Self { // Only correct since `DisputeState` is _always_ initialized // with the validator set based on the session. let n = state.validators_for.len(); let byzantine_threshold = byzantine_threshold(n); let supermajority_threshold = supermajority_threshold(n); let mut flags = DisputeStateFlags::default(); let all_participants = state.validators_for.clone() | state.validators_against.clone(); if all_participants.count_ones() > byzantine_threshold { flags |= DisputeStateFlags::CONFIRMED; } if state.validators_for.count_ones() >= supermajority_threshold { flags |= DisputeStateFlags::FOR_SUPERMAJORITY; } if state.validators_against.count_ones() >= supermajority_threshold { flags |= DisputeStateFlags::AGAINST_SUPERMAJORITY; } flags } } struct ImportSummary { /// The new state, with all votes imported. state: DisputeState, /// List of validators who backed the candidate being disputed. backers: BTreeSet, /// Validators to slash for being (wrongly) on the AGAINST side. slash_against: Vec, /// Validators to slash for being (wrongly) on the FOR side. slash_for: Vec, // New participants in the dispute. new_participants: bitvec::vec::BitVec, // Difference in state flags from previous. new_flags: DisputeStateFlags, } #[derive(RuntimeDebug, PartialEq, Eq)] enum VoteImportError { /// Validator index was outside the range of valid validator indices in the given session. ValidatorIndexOutOfBounds, /// Found a duplicate statement in the dispute statement set. DuplicateStatement, /// Found an explicit valid statement after backing statement. /// Backers should not participate in explicit voting so this is /// only possible on malicious backers. MaliciousBacker, } #[derive(RuntimeDebug, Copy, Clone, PartialEq, Eq)] enum VoteKind { /// A backing vote that is counted as "for" vote in dispute resolution. Backing, /// Either an approval vote or and explicit dispute "for" vote. ExplicitValid, /// An explicit dispute "against" vote. Invalid, } impl From<&DisputeStatement> for VoteKind { fn from(statement: &DisputeStatement) -> Self { if statement.is_backing() { Self::Backing } else if statement.indicates_validity() { Self::ExplicitValid } else { Self::Invalid } } } impl VoteKind { fn is_valid(&self) -> bool { match self { Self::Backing | Self::ExplicitValid => true, Self::Invalid => false, } } fn is_backing(&self) -> bool { match self { Self::Backing => true, Self::Invalid | Self::ExplicitValid => false, } } } impl From for Error { fn from(e: VoteImportError) -> Self { match e { VoteImportError::ValidatorIndexOutOfBounds => Error::::ValidatorIndexOutOfBounds, VoteImportError::DuplicateStatement => Error::::DuplicateStatement, VoteImportError::MaliciousBacker => Error::::MaliciousBacker, } } } /// A transport statement bit change for a single validator. #[derive(RuntimeDebug, PartialEq, Eq)] struct ImportUndo { /// The validator index to which to associate the statement import. validator_index: ValidatorIndex, /// The kind and direction of the vote. vote_kind: VoteKind, /// Has the validator participated before, i.e. in backing or /// with an opposing vote. new_participant: bool, } struct DisputeStateImporter { state: DisputeState, backers: BTreeSet, now: BlockNumber, new_participants: bitvec::vec::BitVec, pre_flags: DisputeStateFlags, pre_state: DisputeState, // The list of backing votes before importing the batch of votes. This field should be // initialized as empty on the first import of the dispute votes and should remain non-empty // afterwards. // // If a dispute has concluded and the candidate was found invalid, we may want to slash as many // backers as possible. This list allows us to slash these backers once their votes have been // imported post dispute conclusion. pre_backers: BTreeSet, } impl DisputeStateImporter { fn new( state: DisputeState, backers: BTreeSet, now: BlockNumber, ) -> Self { let pre_flags = DisputeStateFlags::from_state(&state); let new_participants = bitvec::bitvec![u8, BitOrderLsb0; 0; state.validators_for.len()]; // consistency checks for i in backers.iter() { debug_assert_eq!(state.validators_for.get(i.0 as usize).map(|b| *b), Some(true)); } let pre_state = state.clone(); let pre_backers = backers.clone(); DisputeStateImporter { state, backers, now, new_participants, pre_flags, pre_state, pre_backers, } } fn import( &mut self, validator: ValidatorIndex, kind: VoteKind, ) -> Result { let (bits, other_bits) = if kind.is_valid() { (&mut self.state.validators_for, &mut self.state.validators_against) } else { (&mut self.state.validators_against, &mut self.state.validators_for) }; // out of bounds or already participated match bits.get(validator.0 as usize).map(|b| *b) { None => return Err(VoteImportError::ValidatorIndexOutOfBounds), Some(true) => { // We allow backing statements to be imported after an // explicit "for" vote, but not the other way around. match (kind.is_backing(), self.backers.contains(&validator)) { (true, true) | (false, false) => return Err(VoteImportError::DuplicateStatement), (false, true) => return Err(VoteImportError::MaliciousBacker), (true, false) => {}, } }, Some(false) => {}, } // consistency check debug_assert!((validator.0 as usize) < self.new_participants.len()); let mut undo = ImportUndo { validator_index: validator, vote_kind: kind, new_participant: false }; bits.set(validator.0 as usize, true); if kind.is_backing() { let is_new = self.backers.insert(validator); // invariant check debug_assert!(is_new); } // New participants tracks those validators by index, which didn't appear on either // side of the dispute until now (so they make a first appearance). // To verify this we need to assure they also were not part of the opposing side before. if other_bits.get(validator.0 as usize).map_or(false, |b| !*b) { undo.new_participant = true; self.new_participants.set(validator.0 as usize, true); } Ok(undo) } /// Revert a done transaction. fn undo(&mut self, undo: ImportUndo) { if undo.vote_kind.is_valid() { self.state.validators_for.set(undo.validator_index.0 as usize, false); } else { self.state.validators_against.set(undo.validator_index.0 as usize, false); } if undo.vote_kind.is_backing() { self.backers.remove(&undo.validator_index); } if undo.new_participant { self.new_participants.set(undo.validator_index.0 as usize, false); } } /// Collect all dispute votes. fn finish(mut self) -> ImportSummary { let pre_flags = self.pre_flags; let post_flags = DisputeStateFlags::from_state(&self.state); let pre_post_contains = |flags| (pre_flags.contains(flags), post_flags.contains(flags)); // 1. Check for FOR supermajority. let slash_against = match pre_post_contains(DisputeStateFlags::FOR_SUPERMAJORITY) { (false, true) => { if self.state.concluded_at.is_none() { self.state.concluded_at = Some(self.now.clone()); } // provide AGAINST voters to slash. self.state .validators_against .iter_ones() .map(|i| ValidatorIndex(i as _)) .collect() }, (true, true) => { // provide new AGAINST voters to slash. self.state .validators_against .iter_ones() .filter(|i| self.pre_state.validators_against.get(*i).map_or(false, |b| !*b)) .map(|i| ValidatorIndex(i as _)) .collect() }, (true, false) => { log::error!("Dispute statements are never removed. This is a bug"); Vec::new() }, (false, false) => Vec::new(), }; // 2. Check for AGAINST supermajority. let slash_for = match pre_post_contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) { (false, true) => { if self.state.concluded_at.is_none() { self.state.concluded_at = Some(self.now.clone()); } // provide FOR voters to slash. self.state.validators_for.iter_ones().map(|i| ValidatorIndex(i as _)).collect() }, (true, true) => { // provide new FOR voters to slash including new backers // who might have voted FOR before let new_backing_vote = |i: &ValidatorIndex| -> bool { !self.pre_backers.contains(i) && self.backers.contains(i) }; self.state .validators_for .iter_ones() .filter(|i| { self.pre_state.validators_for.get(*i).map_or(false, |b| !*b) || new_backing_vote(&ValidatorIndex(*i as _)) }) .map(|i| ValidatorIndex(i as _)) .collect() }, (true, false) => { log::error!("Dispute statements are never removed. This is a bug"); Vec::new() }, (false, false) => Vec::new(), }; ImportSummary { state: self.state, backers: self.backers, slash_against, slash_for, new_participants: self.new_participants, new_flags: post_flags - pre_flags, } } } // A filter on a dispute statement set. #[derive(PartialEq)] #[cfg_attr(test, derive(Debug))] enum StatementSetFilter { // Remove the entire dispute statement set. RemoveAll, // Remove the votes with given index from the statement set. RemoveIndices(Vec), } impl StatementSetFilter { fn filter_statement_set( self, mut statement_set: DisputeStatementSet, ) -> Option { match self { StatementSetFilter::RemoveAll => None, StatementSetFilter::RemoveIndices(mut indices) => { indices.sort(); indices.dedup(); // reverse order ensures correctness for index in indices.into_iter().rev() { // `swap_remove` guarantees linear complexity. statement_set.statements.swap_remove(index); } if statement_set.statements.is_empty() { None } else { // we just checked correctness when filtering. Some(CheckedDisputeStatementSet::unchecked_from_unchecked(statement_set)) } }, } } fn remove_index(&mut self, i: usize) { if let StatementSetFilter::RemoveIndices(ref mut indices) = *self { indices.push(i) } } } impl Pallet { /// Called by the initializer to initialize the disputes module. pub(crate) fn initializer_initialize(_now: T::BlockNumber) -> Weight { Weight::zero() } /// Called by the initializer to finalize the disputes pallet. pub(crate) fn initializer_finalize() {} /// Called by the initializer to note a new session in the disputes pallet. pub(crate) fn initializer_on_new_session( notification: &SessionChangeNotification, ) { let config = >::config(); if notification.session_index <= config.dispute_period + 1 { return } let pruning_target = notification.session_index - config.dispute_period - 1; LastPrunedSession::::mutate(|last_pruned| { let to_prune = if let Some(last_pruned) = last_pruned { *last_pruned + 1..=pruning_target } else { pruning_target..=pruning_target }; for to_prune in to_prune { // This should be small, as disputes are rare, so `None` is fine. #[allow(deprecated)] >::remove_prefix(to_prune, None); #[allow(deprecated)] >::remove_prefix(to_prune, None); // This is larger, and will be extracted to the `shared` pallet for more proper pruning. // TODO: https://github.com/paritytech/polkadot/issues/3469 #[allow(deprecated)] >::remove_prefix(to_prune, None); } *last_pruned = Some(pruning_target); }); } /// Handle sets of dispute statements corresponding to 0 or more candidates. /// Returns a vector of freshly created disputes. /// /// Assumes `statement_sets` were already de-duplicated. /// /// # Warning /// /// This functions modifies the state when failing. It is expected to be called in inherent, /// and to fail the extrinsic on error. As invalid inherents are not allowed, the dirty state /// is not committed. pub(crate) fn process_checked_multi_dispute_data( statement_sets: &CheckedMultiDisputeStatementSet, ) -> Result, DispatchError> { let config = >::config(); let mut fresh = Vec::with_capacity(statement_sets.len()); for statement_set in statement_sets { let dispute_target = { let statement_set = statement_set.as_ref(); (statement_set.session, statement_set.candidate_hash) }; if Self::process_checked_dispute_data( statement_set, config.dispute_post_conclusion_acceptance_period, )? { fresh.push(dispute_target); } } Ok(fresh) } // Given a statement set, this produces a filter to be applied to the statement set. // It either removes the entire dispute statement set or some specific votes from it. // // Votes which are duplicate or already known by the chain are filtered out. // The entire set is removed if the dispute is both, ancient and concluded. // Disputes without enough votes to get confirmed are also filtered out. fn filter_dispute_data( set: &DisputeStatementSet, post_conclusion_acceptance_period: ::BlockNumber, verify_sigs: VerifyDisputeSignatures, ) -> StatementSetFilter { let mut filter = StatementSetFilter::RemoveIndices(Vec::new()); // Dispute statement sets on any dispute which concluded // before this point are to be rejected. let now = >::block_number(); let oldest_accepted = now.saturating_sub(post_conclusion_acceptance_period); // Load session info to access validators let session_info = match >::session_info(set.session) { Some(s) => s, None => return StatementSetFilter::RemoveAll, }; let n_validators = session_info.validators.len(); // Check for ancient. let dispute_state = { if let Some(dispute_state) = >::get(&set.session, &set.candidate_hash) { if dispute_state.concluded_at.as_ref().map_or(false, |c| c < &oldest_accepted) { return StatementSetFilter::RemoveAll } dispute_state } else { // No state in storage, this indicates it's the first dispute statement set as well. DisputeState { validators_for: bitvec![u8, BitOrderLsb0; 0; n_validators], validators_against: bitvec![u8, BitOrderLsb0; 0; n_validators], start: now, concluded_at: None, } } }; let backers = >::get(&set.session, &set.candidate_hash).unwrap_or_default(); // Check and import all votes. let summary = { let mut importer = DisputeStateImporter::new(dispute_state, backers, now); for (i, (statement, validator_index, signature)) in set.statements.iter().enumerate() { // ensure the validator index is present in the session info // and the signature is valid let validator_public = match session_info.validators.get(*validator_index) { None => { filter.remove_index(i); continue }, Some(v) => v, }; let kind = VoteKind::from(statement); let undo = match importer.import(*validator_index, kind) { Ok(u) => u, Err(_) => { filter.remove_index(i); continue }, }; // Avoid checking signatures repeatedly. if let VerifyDisputeSignatures::Yes = verify_sigs { // Check signature after attempting import. // // Since we expect that this filter will be applied to // disputes long after they're concluded, 99% of the time, // the duplicate filter above will catch them before needing // to do a heavy signature check. // // This is only really important until the post-conclusion acceptance threshold // is reached, and then no part of this loop will be hit. if let Err(()) = check_signature( &validator_public, set.candidate_hash, set.session, statement, signature, ) { importer.undo(undo); filter.remove_index(i); continue } } } importer.finish() }; // Reject disputes which don't have at least one vote on each side. if summary.state.validators_for.count_ones() == 0 || summary.state.validators_against.count_ones() == 0 { return StatementSetFilter::RemoveAll } // Reject disputes containing less votes than needed for confirmation. if (summary.state.validators_for.clone() | &summary.state.validators_against).count_ones() <= byzantine_threshold(summary.state.validators_for.len()) { return StatementSetFilter::RemoveAll } filter } /// Handle a set of dispute statements corresponding to a single candidate. /// /// Fails if the dispute data is invalid. Returns a Boolean indicating whether the /// dispute is fresh. fn process_checked_dispute_data( set: &CheckedDisputeStatementSet, dispute_post_conclusion_acceptance_period: T::BlockNumber, ) -> Result { // Dispute statement sets on any dispute which concluded // before this point are to be rejected. let now = >::block_number(); let oldest_accepted = now.saturating_sub(dispute_post_conclusion_acceptance_period); let set = set.as_ref(); // Load session info to access validators let session_info = match >::session_info(set.session) { Some(s) => s, None => return Err(Error::::AncientDisputeStatement.into()), }; let n_validators = session_info.validators.len(); // Check for ancient. let (fresh, dispute_state) = { if let Some(dispute_state) = >::get(&set.session, &set.candidate_hash) { ensure!( dispute_state.concluded_at.as_ref().map_or(true, |c| c >= &oldest_accepted), Error::::AncientDisputeStatement, ); (false, dispute_state) } else { ( true, DisputeState { validators_for: bitvec![u8, BitOrderLsb0; 0; n_validators], validators_against: bitvec![u8, BitOrderLsb0; 0; n_validators], start: now, concluded_at: None, }, ) } }; let backers = >::get(&set.session, &set.candidate_hash).unwrap_or_default(); // Import all votes. They were pre-checked. let summary = { let mut importer = DisputeStateImporter::new(dispute_state, backers, now); for (statement, validator_index, _signature) in &set.statements { let kind = VoteKind::from(statement); importer.import(*validator_index, kind).map_err(Error::::from)?; } importer.finish() }; // Reject disputes which don't have at least one vote on each side. ensure!( summary.state.validators_for.count_ones() > 0 && summary.state.validators_against.count_ones() > 0, Error::::SingleSidedDispute, ); // Reject disputes containing less votes than needed for confirmation. ensure!( (summary.state.validators_for.clone() | &summary.state.validators_against).count_ones() > byzantine_threshold(summary.state.validators_for.len()), Error::::UnconfirmedDispute, ); let backers = summary.backers; // Reject statements with no accompanying backing votes. ensure!(!backers.is_empty(), Error::::MissingBackingVotes); >::insert(&set.session, &set.candidate_hash, backers.clone()); // AUDIT: from now on, no error should be returned. let DisputeStatementSet { ref session, ref candidate_hash, .. } = set; let session = *session; let candidate_hash = *candidate_hash; if fresh { let is_local = >::contains_key(&session, &candidate_hash); Self::deposit_event(Event::DisputeInitiated( candidate_hash, if is_local { DisputeLocation::Local } else { DisputeLocation::Remote }, )); } { if summary.new_flags.contains(DisputeStateFlags::FOR_SUPERMAJORITY) { Self::deposit_event(Event::DisputeConcluded(candidate_hash, DisputeResult::Valid)); } // It is possible, although unexpected, for a dispute to conclude twice. // This would require f+1 validators to vote in both directions. // A dispute cannot conclude more than once in each direction. if summary.new_flags.contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) { Self::deposit_event(Event::DisputeConcluded( candidate_hash, DisputeResult::Invalid, )); } } // Reward statements. T::RewardValidators::reward_dispute_statement( session, summary.new_participants.iter_ones().map(|i| ValidatorIndex(i as _)), ); // Slash participants on a losing side. { // a valid candidate, according to 2/3. Punish those on the 'against' side. T::SlashingHandler::punish_against_valid( session, candidate_hash, summary.slash_against, backers.clone(), ); // an invalid candidate, according to 2/3. Punish those on the 'for' side. T::SlashingHandler::punish_for_invalid( session, candidate_hash, summary.slash_for, backers, ); } >::insert(&session, &candidate_hash, &summary.state); // Freeze if just concluded against some local candidate if summary.new_flags.contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) { if let Some(revert_to) = >::get(&session, &candidate_hash) { Self::revert_and_freeze(revert_to); } } Ok(fresh) } #[allow(unused)] pub(crate) fn disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { >::iter().collect() } pub(crate) fn note_included( session: SessionIndex, candidate_hash: CandidateHash, included_in: T::BlockNumber, ) { if included_in.is_zero() { return } let revert_to = included_in - One::one(); >::insert(&session, &candidate_hash, revert_to); if let Some(state) = >::get(&session, candidate_hash) { if has_supermajority_against(&state) { Self::revert_and_freeze(revert_to); } } } pub(crate) fn included_state( session: SessionIndex, candidate_hash: CandidateHash, ) -> Option { >::get(session, candidate_hash) } pub(crate) fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool { >::get(&session, &candidate_hash).map_or(false, |dispute| { // A dispute that has concluded with supermajority-against. has_supermajority_against(&dispute) }) } pub(crate) fn is_frozen() -> bool { Self::last_valid_block().is_some() } pub(crate) fn revert_and_freeze(revert_to: T::BlockNumber) { if Self::last_valid_block().map_or(true, |last| last > revert_to) { Frozen::::set(Some(revert_to)); // The `Revert` log is about reverting a block, not reverting to a block. // If we want to revert to block X in the current chain, we need to revert // block X+1. let revert = revert_to + One::one(); Self::deposit_event(Event::Revert(revert)); frame_system::Pallet::::deposit_log( ConsensusLog::Revert(revert.saturated_into()).into(), ); } } } fn has_supermajority_against(dispute: &DisputeState) -> bool { let supermajority_threshold = supermajority_threshold(dispute.validators_against.len()); dispute.validators_against.count_ones() >= supermajority_threshold } fn check_signature( validator_public: &ValidatorId, candidate_hash: CandidateHash, session: SessionIndex, statement: &DisputeStatement, validator_signature: &ValidatorSignature, ) -> Result<(), ()> { let payload = match *statement { DisputeStatement::Valid(ValidDisputeStatementKind::Explicit) => ExplicitDisputeStatement { valid: true, candidate_hash, session }.signing_payload(), DisputeStatement::Valid(ValidDisputeStatementKind::BackingSeconded(inclusion_parent)) => CompactStatement::Seconded(candidate_hash).signing_payload(&SigningContext { session_index: session, parent_hash: inclusion_parent, }), DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(inclusion_parent)) => CompactStatement::Valid(candidate_hash).signing_payload(&SigningContext { session_index: session, parent_hash: inclusion_parent, }), DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking) => ApprovalVote(candidate_hash).signing_payload(session), DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit) => ExplicitDisputeStatement { valid: false, candidate_hash, session }.signing_payload(), }; let start = get_current_time(); let res = if validator_signature.verify(&payload[..], &validator_public) { Ok(()) } else { Err(()) }; let end = get_current_time(); METRICS.on_signature_check_complete(end.saturating_sub(start)); // ns res }