Disputes runtime (#2947)

* disputes module skeleton and storage

* implement dispute module initialization logic

* implement disputes session change logic

* provide dispute skeletons

* deduplication & ancient check

* fix a couple of warnings

* begin provide_dispute_data impl

* flesh out statement set import somewhat

* move ApprovalVote to shared primitives

* add a signing-payload API to explicit dispute statements

* implement statement signature checking

* some bitflags glue for observing changes in disputes

* implement dispute vote import logic

* flesh out everything except slashing

* guide: tweaks

* declare and use punishment trait

* punish validators for inconclusive disputes

* guide: tiny fix

* guide: update docs

* add disputes getter fn

* guide: small change to spam slots handling

* improve spam slots handling and fix some bugs

* finish API of disputes runtime

* define and deposit `RevertTo` log

* begin integrating disputes into para_inherent

* use precomputed slash_for/against

* return candidate hash from process_bitfields

* implement inclusion::collect_disputed

* finish integration into rest of runtime

* add Disputes to initializer

* address suggestions

* use pallet macro

* fix typo

* Update runtime/parachains/src/disputes.rs

* add test: fix pruning

* document specific behavior

* deposit events on dispute changes

* add an allow(unused) on fn disputes

* add a dummy PunishValidators implementation

* add disputes module to Rococo

* add disputes module to westend runtime

* add disputes module to test runtime

* add disputes module to kusama runtime

* guide: prepare for runtime API for checking frozenness

* remove revert digests in favor of state variable

* merge reversions

* Update runtime/parachains/src/disputes.rs

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

* Update runtime/parachains/src/disputes.rs

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

* Update runtime/parachains/src/disputes.rs

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

* add byzantine_threshold and supermajority_threshold utilities to primitives

* use primitive helpers

* deposit revert event when freezing chain

* deposit revert log when freezing chain

* test revert event and log are generated when freezing

* add trait to decouple disputes handling from paras inherent handling

* runtime: fix compilation and setup dispute handler

* disputes: add hook for filtering out dispute statements

* disputes: add initializer hooks to DisputesHandler

* runtime: remove disputes pallet from all runtimes

* tag TODOs

* don't import any dispute statements just yet...

* address grumbles

* fix spellcheck, hopefully

* maybe now?

* last spellcheck round

* fix runtime tests

* fix test-runtime

Co-authored-by: thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>
Co-authored-by: André Silva <andrerfosilva@gmail.com>
This commit is contained in:
Robert Habermeier
2021-07-19 11:49:19 -05:00
committed by GitHub
parent 4636daa7ce
commit ec98120f0e
15 changed files with 2291 additions and 42 deletions
File diff suppressed because it is too large Load Diff
+28 -7
View File
@@ -35,7 +35,7 @@ use parity_scale_codec::{Encode, Decode};
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use sp_runtime::{DispatchError, traits::{One, Saturating}};
use crate::{configuration, paras, dmp, ump, hrmp, shared, scheduler::CoreAssignment};
use crate::{configuration, disputes, paras, dmp, ump, hrmp, shared, scheduler::CoreAssignment};
/// A bitfield signed by a validator indicating that it is keeping its piece of the erasure-coding
/// for any backed candidates referred to by a `1` bit available.
@@ -118,6 +118,7 @@ pub trait Config:
+ configuration::Config
{
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
type DisputesHandler: disputes::DisputesHandler<Self::BlockNumber>;
type RewardValidators: RewardValidators;
}
@@ -238,7 +239,7 @@ impl<T: Config> Module<T> {
expected_bits: usize,
unchecked_bitfields: UncheckedSignedAvailabilityBitfields,
core_lookup: impl Fn(CoreIndex) -> Option<ParaId>,
) -> Result<Vec<CoreIndex>, DispatchError> {
) -> Result<Vec<(CoreIndex, CandidateHash)>, DispatchError> {
let validators = shared::Module::<T>::active_validator_keys();
let session_index = shared::Module::<T>::session_index();
@@ -247,7 +248,6 @@ impl<T: Config> Module<T> {
.map(|core_para| core_para.map(|p| (p, PendingAvailability::<T>::get(&p))))
.collect();
// do sanity checks on the bitfields:
// 1. no more than one bitfield per validator
// 2. bitfields are ascending by validator index.
@@ -368,15 +368,12 @@ impl<T: Config> Module<T> {
pending_availability.backing_group,
);
freed_cores.push(pending_availability.core);
freed_cores.push((pending_availability.core, pending_availability.hash));
} else {
<PendingAvailability<T>>::insert(&para_id, &pending_availability);
}
}
// TODO: pass available candidates onwards to validity module once implemented.
// https://github.com/paritytech/polkadot/issues/1251
Ok(freed_cores)
}
@@ -754,6 +751,28 @@ impl<T: Config> Module<T> {
cleaned_up_cores
}
/// Cleans up all paras pending availability that are in the given list of disputed candidates.
///
/// Returns a vector of cleaned-up core IDs.
pub(crate) fn collect_disputed(disputed: Vec<CandidateHash>) -> Vec<CoreIndex> {
let mut cleaned_up_ids = Vec::new();
let mut cleaned_up_cores = Vec::new();
for (para_id, pending_record) in <PendingAvailability<T>>::iter() {
if disputed.contains(&pending_record.hash) {
cleaned_up_ids.push(para_id);
cleaned_up_cores.push(pending_record.core);
}
}
for para_id in cleaned_up_ids {
let _ = <PendingAvailability<T>>::take(&para_id);
let _ = <PendingAvailabilityCommitments>::take(&para_id);
}
cleaned_up_cores
}
/// Forcibly enact the candidate with the given ID as though it had been deemed available
/// by bitfields.
///
@@ -2553,4 +2572,6 @@ mod tests {
assert!(<PendingAvailabilityCommitments>::iter().collect::<Vec<_>>().is_empty());
});
}
// TODO [now]: test `collect_disputed`
}
+19 -1
View File
@@ -25,6 +25,7 @@ use frame_support::traits::{Randomness, OneSessionHandler};
use parity_scale_codec::{Encode, Decode};
use crate::{
configuration::{self, HostConfiguration},
disputes::DisputesHandler,
shared, paras, scheduler, inclusion, session_info, dmp, ump, hrmp,
};
@@ -127,7 +128,7 @@ pub mod pallet {
// - Scheduler
// - Inclusion
// - SessionInfo
// - Validity
// - Disputes
// - DMP
// - UMP
// - HRMP
@@ -137,6 +138,7 @@ pub mod pallet {
scheduler::Module::<T>::initializer_initialize(now) +
inclusion::Module::<T>::initializer_initialize(now) +
session_info::Module::<T>::initializer_initialize(now) +
T::DisputesHandler::initializer_initialize(now) +
dmp::Module::<T>::initializer_initialize(now) +
ump::Module::<T>::initializer_initialize(now) +
hrmp::Module::<T>::initializer_initialize(now);
@@ -151,6 +153,7 @@ pub mod pallet {
hrmp::Module::<T>::initializer_finalize();
ump::Module::<T>::initializer_finalize();
dmp::Module::<T>::initializer_finalize();
T::DisputesHandler::initializer_finalize();
session_info::Module::<T>::initializer_finalize();
inclusion::Module::<T>::initializer_finalize();
scheduler::Module::<T>::initializer_finalize();
@@ -234,6 +237,7 @@ impl<T: Config> Pallet<T> {
scheduler::Module::<T>::initializer_on_new_session(&notification);
inclusion::Module::<T>::initializer_on_new_session(&notification);
session_info::Module::<T>::initializer_on_new_session(&notification);
T::DisputesHandler::initializer_on_new_session(&notification);
dmp::Module::<T>::initializer_on_new_session(&notification, &outgoing_paras);
ump::Module::<T>::initializer_on_new_session(&notification, &outgoing_paras);
hrmp::Module::<T>::initializer_on_new_session(&notification, &outgoing_paras);
@@ -268,6 +272,20 @@ impl<T: Config> Pallet<T> {
}
}
// Allow to trigger on_new_session in tests, this is needed as long as pallet_session is not
// implemented in mock.
#[cfg(test)]
pub(crate) fn test_trigger_on_new_session<'a, I: 'a>(
changed: bool,
session_index: SessionIndex,
validators: I,
queued: Option<I>,
)
where I: Iterator<Item=(&'a T::AccountId, ValidatorId)>
{
Self::on_new_session(changed, session_index, validators, queued)
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
+1
View File
@@ -23,6 +23,7 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod configuration;
pub mod disputes;
pub mod shared;
pub mod inclusion;
pub mod initializer;
+56 -2
View File
@@ -21,7 +21,9 @@ use sp_core::H256;
use sp_runtime::traits::{
BlakeTwo256, IdentityLookup,
};
use primitives::v1::{AuthorityDiscoveryId, Balance, BlockNumber, Header, ValidatorIndex};
use primitives::v1::{
AuthorityDiscoveryId, Balance, BlockNumber, Header, ValidatorIndex, SessionIndex,
};
use frame_support::parameter_types;
use frame_support::traits::GenesisBuild;
use frame_support_test::TestRandomness;
@@ -29,7 +31,7 @@ use std::cell::RefCell;
use std::collections::HashMap;
use crate::{
inclusion, scheduler, dmp, ump, hrmp, session_info, paras, configuration,
initializer, shared,
initializer, shared, disputes,
};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
@@ -53,6 +55,7 @@ frame_support::construct_runtime!(
Ump: ump::{Pallet, Call, Storage, Event},
Hrmp: hrmp::{Pallet, Call, Storage, Event},
SessionInfo: session_info::{Pallet, Call, Storage},
Disputes: disputes::{Pallet, Storage, Event<T>},
}
);
@@ -62,6 +65,8 @@ parameter_types! {
frame_system::limits::BlockWeights::simple_max(4 * 1024 * 1024);
}
pub type AccountId = u64;
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::AllowAll;
type BlockWeights = BlockWeights;
@@ -136,10 +141,59 @@ impl crate::hrmp::Config for Test {
type Currency = pallet_balances::Pallet<Test>;
}
impl crate::disputes::Config for Test {
type Event = Event;
type RewardValidators = Self;
type PunishValidators = Self;
}
thread_local! {
pub static REWARD_VALIDATORS: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
pub static PUNISH_VALIDATORS_FOR: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
pub static PUNISH_VALIDATORS_AGAINST: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
pub static PUNISH_VALIDATORS_INCONCLUSIVE: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
}
impl crate::disputes::RewardValidators for Test {
fn reward_dispute_statement(
session: SessionIndex,
validators: impl IntoIterator<Item=ValidatorIndex>
) {
REWARD_VALIDATORS.with(|r| r.borrow_mut().push((session, validators.into_iter().collect())))
}
}
impl crate::disputes::PunishValidators for Test {
fn punish_for_invalid(
session: SessionIndex,
validators: impl IntoIterator<Item=ValidatorIndex>,
) {
PUNISH_VALIDATORS_FOR
.with(|r| r.borrow_mut().push((session, validators.into_iter().collect())))
}
fn punish_against_valid(
session: SessionIndex,
validators: impl IntoIterator<Item=ValidatorIndex>,
) {
PUNISH_VALIDATORS_AGAINST
.with(|r| r.borrow_mut().push((session, validators.into_iter().collect())))
}
fn punish_inconclusive(
session: SessionIndex,
validators: impl IntoIterator<Item=ValidatorIndex>,
) {
PUNISH_VALIDATORS_INCONCLUSIVE
.with(|r| r.borrow_mut().push((session, validators.into_iter().collect())))
}
}
impl crate::scheduler::Config for Test { }
impl crate::inclusion::Config for Test {
type Event = Event;
type DisputesHandler = Disputes;
type RewardValidators = TestRewardValidators;
}
@@ -35,8 +35,10 @@ use frame_support::{
};
use frame_system::ensure_none;
use crate::{
disputes::DisputesHandler,
inclusion,
scheduler::{self, FreedReason},
shared,
ump,
};
@@ -68,6 +70,8 @@ decl_error! {
/// The hash of the submitted parent header doesn't correspond to the saved block hash of
/// the parent.
InvalidParentHeader,
/// Potentially invalid candidate.
CandidateCouldBeInvalid,
}
}
@@ -99,7 +103,7 @@ decl_module! {
bitfields: signed_bitfields,
backed_candidates,
parent_header,
disputes: _,
disputes,
} = data;
ensure_none(origin)?;
@@ -112,6 +116,36 @@ decl_module! {
Error::<T>::InvalidParentHeader,
);
// Handle disputes logic.
let current_session = <shared::Module<T>>::session_index();
let freed_disputed: Vec<(_, FreedReason)> = {
let fresh_disputes = T::DisputesHandler::provide_multi_dispute_data(disputes)?;
if T::DisputesHandler::is_frozen() {
// The relay chain we are currently on is invalid. Proceed no further on parachains.
Included::set(Some(()));
return Ok(Some(
MINIMAL_INCLUSION_INHERENT_WEIGHT
).into());
}
let any_current_session_disputes = fresh_disputes.iter()
.any(|(s, _)| s == &current_session);
if any_current_session_disputes {
let current_session_disputes: Vec<_> = fresh_disputes.iter()
.filter(|(s, _)| s == &current_session)
.map(|(_, c)| *c)
.collect();
<inclusion::Module<T>>::collect_disputed(current_session_disputes)
.into_iter()
.map(|core| (core, FreedReason::Concluded))
.collect()
} else {
Vec::new()
}
};
// Process new availability bitfields, yielding any availability cores whose
// work has now concluded.
let expected_bits = <scheduler::Module<T>>::availability_cores().len();
@@ -121,6 +155,12 @@ decl_module! {
<scheduler::Module<T>>::core_para,
)?;
// Inform the disputes module of all included candidates.
let now = <frame_system::Pallet<T>>::block_number();
for (_, candidate_hash) in &freed_concluded {
T::DisputesHandler::note_included(current_session, *candidate_hash, now);
}
// Handle timeouts for any availability core work.
let availability_pred = <scheduler::Module<T>>::availability_timeout_predicate();
let freed_timeout = if let Some(pred) = availability_pred {
@@ -130,8 +170,12 @@ decl_module! {
};
// Schedule paras again, given freed cores, and reasons for freeing.
let freed = freed_concluded.into_iter().map(|c| (c, FreedReason::Concluded))
.chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut)));
let mut freed = freed_disputed.into_iter()
.chain(freed_concluded.into_iter().map(|(c, _hash)| (c, FreedReason::Concluded)))
.chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut)))
.collect::<Vec<_>>();
freed.sort_unstable_by_key(|pair| pair.0); // sort by core index
<scheduler::Module<T>>::clear();
<scheduler::Module<T>>::schedule(
@@ -142,6 +186,17 @@ decl_module! {
let backed_candidates = limit_backed_candidates::<T>(backed_candidates);
let backed_candidates_len = backed_candidates.len() as Weight;
// Refuse to back any candidates that are disputed or invalid.
for candidate in &backed_candidates {
ensure!(
!T::DisputesHandler::could_be_invalid(
current_session,
candidate.candidate.hash(),
),
Error::<T>::CandidateCouldBeInvalid,
);
}
// Process backed candidates according to scheduled cores.
let parent_storage_root = parent_header.state_root().clone();
let occupied = <inclusion::Module<T>>::process_candidates(
@@ -216,7 +271,7 @@ impl<T: Config> ProvideInherent for Module<T> {
const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER;
fn create_inherent(data: &InherentData) -> Option<Self::Call> {
let inherent_data: ParachainsInherentData<T::Header>
let mut inherent_data: ParachainsInherentData<T::Header>
= match data.get_data(&Self::INHERENT_IDENTIFIER)
{
Ok(Some(d)) => d,
@@ -231,6 +286,9 @@ impl<T: Config> ProvideInherent for Module<T> {
}
};
// filter out any unneeded dispute statements
T::DisputesHandler::filter_multi_dispute_data(&mut inherent_data.disputes);
// Sanity check: session changes can invalidate an inherent, and we _really_ don't want that to happen.
// See github.com/paritytech/polkadot/issues/1327
let inherent_data = match Self::enter(