mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 21:01:02 +00:00
Some late short-term fixes for dispute slashing (#6249)
* disputes/slashing: slash only backers for ForInvalid * add an assertion in mock impl * fix tests * do not slash backers on onconcluded disputes * slash an intersection of backers and losers * zombienet/disputes: check for offence only for invalid disputes * add backing votes to disputes bench builder * Update runtime/parachains/src/builder.rs * Brad implementers guide revisions 2 (#6239) * Add disputes subsystems fix * Updated dispute approval vote import reasoning * Improved wording of my changes * Resolving issues brought up in comments * Update disputes prioritisation in `dispute-coordinator` (#6130) * Scraper processes CandidateBacked events * Change definition of best-effort * Fix `dispute-coordinator` tests * Unit test for dispute filtering * Clarification comment * Add tests * Fix logic If a dispute is not backed, not included and not confirmed we don't participate but we do import votes. * Add metrics for refrained participations * Revert "Add tests" This reverts commit 7b8391a087922ced942cde9cd2b50ff3f633efc0. * Revert "Unit test for dispute filtering" This reverts commit 92ba5fe678214ab360306313a33c781338e600a0. * fix dispute-coordinator tests * Fix scraping * new tests * Small fixes in guide * Apply suggestions from code review Co-authored-by: Andrei Sandu <54316454+sandreim@users.noreply.github.com> * Fix some comments and remove a pointless test * Code review feedback * Clarification comment in tests * Some tests * Reference counted `CandidateHash` in scraper * Proper handling for Backed and Included candidates in scraper Backed candidates which are not included should be kept for a predetermined window of finalized blocks. E.g. if a candidate is backed but not included in block 2, and the window size is 2, the same candidate should be cleaned after block 4 is finalized. Add reference counting for candidates in scraper. A candidate can be added on multiple block heights so we have to make sure we don't clean it prematurely from the scraper. Add tests. * Update comments in tests * Guide update * Fix cleanup logic for `backed_candidates_by_block_number` * Simplify cleanup * Make spellcheck happy * Update tests * Extract candidate backing logic in separate struct * Code review feedback * Treat backed and included candidates in the same fashion * Update some comments * Small improvements in test * spell check * Fix some more comments * clean -> prune * Code review feedback * Reword comment * spelling Co-authored-by: Andrei Sandu <54316454+sandreim@users.noreply.github.com> * approval-voting: remove redundant validation check (#6266) * approval-voting: remove a redundant check * candidate-validation: remove unreachable check * remove fill_block (#6200) Co-authored-by: parity-processbot <> * fix a compilation warning (#6279) Fixes #6277. * Only report concluded if there is an actual dispute. (#6270) * Only report concluded if there is an actual dispute. Hence no "non"-disputes will be added to disputes anymore. * Fix redundant check. * Test for no onesided disputes. Co-authored-by: eskimor <eskimor@no-such-url.com> * [ci] fix buildah image (#6281) * Revert special casing of Kusama for grandpa rounds. (#6217) Co-authored-by: eskimor <eskimor@no-such-url.com> * Fixes "for loop over an `Option`" warnings (#6291) Was seeing these warnings when running `cargo check --all`: ``` warning: for loop over an `Option`. This is more readably written as an `if let` statement --> node/core/approval-voting/src/lib.rs:1147:21 | 1147 | for activated in update.activated { | ^^^^^^^^^^^^^^^^ | = note: `#[warn(for_loops_over_fallibles)]` on by default help: to check pattern in a loop use `while let` | 1147 | while let Some(activated) = update.activated { | ~~~~~~~~~~~~~~~ ~~~ help: consider using `if let` to clear intent | 1147 | if let Some(activated) = update.activated { | ~~~~~~~~~~~~ ~~~ ``` My guess is that `activated` used to be a SmallVec or similar, as is `deactivated`. It was changed to an `Option`, the `for` still compiled (it's technically correct, just weird), and the compiler didn't catch it until now. * companion for #12599 (#6290) * companion for #12599 * update Cargo.lock * use cargo path instead of diener * update lockfile for {"substrate"} Co-authored-by: parity-processbot <> * remove the runtime check and test * append keys on past-session slashing * runtime/disputes: allow importing backing votes after explicit for * explicit MaliciousBacker error and a test * update an outdated comment * Revert "update an outdated comment" This reverts commit 7c4c3f5a848f16e2b61435e981d814f00333ed41. * Revert "remove the runtime check and test" This reverts commit a5bff0c75e77effb5b7d3a1691de1b14bcdbd648. * incremental punishment post conclusion + test * punish backers post FOR vote * remove unnecessary lifetime annotation * add a comment to zombinet test * typo * fmt * post merge test fixes * fix test after changes in master * address review nits --------- Co-authored-by: Bradley Olson <34992650+BradleyOlson64@users.noreply.github.com> Co-authored-by: Tsvetomir Dimitrov <tsvetomir@parity.io> Co-authored-by: Andrei Sandu <54316454+sandreim@users.noreply.github.com> Co-authored-by: Sergej Sakac <73715684+Szegoo@users.noreply.github.com> Co-authored-by: eskimor <eskimor@users.noreply.github.com> Co-authored-by: eskimor <eskimor@no-such-url.com> Co-authored-by: Alexander Samusev <41779041+alvicsam@users.noreply.github.com> Co-authored-by: Marcin S <marcin@bytedude.com> Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>
This commit is contained in:
@@ -595,6 +595,7 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
|
|||||||
|
|
||||||
let (para_id, core_idx, group_idx) = self.create_indexes(seed);
|
let (para_id, core_idx, group_idx) = self.create_indexes(seed);
|
||||||
let candidate_hash = CandidateHash(H256::from(byte32_slice_from(seed)));
|
let candidate_hash = CandidateHash(H256::from(byte32_slice_from(seed)));
|
||||||
|
let relay_parent = H256::from(byte32_slice_from(seed));
|
||||||
|
|
||||||
Self::add_availability(
|
Self::add_availability(
|
||||||
para_id,
|
para_id,
|
||||||
@@ -614,9 +615,12 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
|
|||||||
// so we make sure that we have a super majority with valid statements.
|
// so we make sure that we have a super majority with valid statements.
|
||||||
let dispute_statement = if validator_index % 4 == 0 {
|
let dispute_statement = if validator_index % 4 == 0 {
|
||||||
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit)
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit)
|
||||||
|
} else if validator_index < 3 {
|
||||||
|
// Set two votes as backing for the dispute set to be accepted
|
||||||
|
DisputeStatement::Valid(
|
||||||
|
ValidDisputeStatementKind::BackingValid(relay_parent)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Note that in the future we could use some availability votes as an
|
|
||||||
// implicit valid kind.
|
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit)
|
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit)
|
||||||
};
|
};
|
||||||
let data = dispute_statement.payload_data(candidate_hash.clone(), session);
|
let data = dispute_statement.payload_data(candidate_hash.clone(), session);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use sp_runtime::{
|
|||||||
traits::{AppVerify, One, Saturating, Zero},
|
traits::{AppVerify, One, Saturating, Zero},
|
||||||
DispatchError, RuntimeDebug, SaturatedConversion,
|
DispatchError, RuntimeDebug, SaturatedConversion,
|
||||||
};
|
};
|
||||||
use sp_std::{cmp::Ordering, prelude::*};
|
use sp_std::{cmp::Ordering, collections::btree_set::BTreeSet, prelude::*};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
@@ -85,6 +85,7 @@ pub trait SlashingHandler<BlockNumber> {
|
|||||||
session: SessionIndex,
|
session: SessionIndex,
|
||||||
candidate_hash: CandidateHash,
|
candidate_hash: CandidateHash,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Punish a series of validators who were against a valid parablock. This
|
/// Punish a series of validators who were against a valid parablock. This
|
||||||
@@ -93,6 +94,7 @@ pub trait SlashingHandler<BlockNumber> {
|
|||||||
session: SessionIndex,
|
session: SessionIndex,
|
||||||
candidate_hash: CandidateHash,
|
candidate_hash: CandidateHash,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Called by the initializer to initialize the slashing pallet.
|
/// Called by the initializer to initialize the slashing pallet.
|
||||||
@@ -110,6 +112,7 @@ impl<BlockNumber> SlashingHandler<BlockNumber> for () {
|
|||||||
_: SessionIndex,
|
_: SessionIndex,
|
||||||
_: CandidateHash,
|
_: CandidateHash,
|
||||||
_: impl IntoIterator<Item = ValidatorIndex>,
|
_: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
_: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +120,7 @@ impl<BlockNumber> SlashingHandler<BlockNumber> for () {
|
|||||||
_: SessionIndex,
|
_: SessionIndex,
|
||||||
_: CandidateHash,
|
_: CandidateHash,
|
||||||
_: impl IntoIterator<Item = ValidatorIndex>,
|
_: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
_: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +463,18 @@ pub mod pallet {
|
|||||||
DisputeState<T::BlockNumber>,
|
DisputeState<T::BlockNumber>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/// Backing votes stored for each dispute.
|
||||||
|
/// This storage is used for slashing.
|
||||||
|
#[pallet::storage]
|
||||||
|
pub(super) type BackersOnDisputes<T: Config> = StorageDoubleMap<
|
||||||
|
_,
|
||||||
|
Twox64Concat,
|
||||||
|
SessionIndex,
|
||||||
|
Blake2_128Concat,
|
||||||
|
CandidateHash,
|
||||||
|
BTreeSet<ValidatorIndex>,
|
||||||
|
>;
|
||||||
|
|
||||||
/// All included blocks on the chain, as well as the block number in this chain that
|
/// 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.
|
/// should be reverted back to if the candidate is disputed and determined to be invalid.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
@@ -511,7 +527,11 @@ pub mod pallet {
|
|||||||
DuplicateStatement,
|
DuplicateStatement,
|
||||||
/// A dispute where there are only votes on one side.
|
/// A dispute where there are only votes on one side.
|
||||||
SingleSidedDispute,
|
SingleSidedDispute,
|
||||||
/// Unconfirmed dispute statement sets provided
|
/// A dispute vote from a malicious backer.
|
||||||
|
MaliciousBacker,
|
||||||
|
/// No backing votes were provides along dispute statements.
|
||||||
|
MissingBackingVotes,
|
||||||
|
/// Unconfirmed dispute statement sets provided.
|
||||||
UnconfirmedDispute,
|
UnconfirmedDispute,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,6 +589,8 @@ impl DisputeStateFlags {
|
|||||||
struct ImportSummary<BlockNumber> {
|
struct ImportSummary<BlockNumber> {
|
||||||
/// The new state, with all votes imported.
|
/// The new state, with all votes imported.
|
||||||
state: DisputeState<BlockNumber>,
|
state: DisputeState<BlockNumber>,
|
||||||
|
/// List of validators who backed the candidate being disputed.
|
||||||
|
backers: BTreeSet<ValidatorIndex>,
|
||||||
/// Validators to slash for being (wrongly) on the AGAINST side.
|
/// Validators to slash for being (wrongly) on the AGAINST side.
|
||||||
slash_against: Vec<ValidatorIndex>,
|
slash_against: Vec<ValidatorIndex>,
|
||||||
/// Validators to slash for being (wrongly) on the FOR side.
|
/// Validators to slash for being (wrongly) on the FOR side.
|
||||||
@@ -585,6 +607,48 @@ enum VoteImportError {
|
|||||||
ValidatorIndexOutOfBounds,
|
ValidatorIndexOutOfBounds,
|
||||||
/// Found a duplicate statement in the dispute statement set.
|
/// Found a duplicate statement in the dispute statement set.
|
||||||
DuplicateStatement,
|
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<T: Config> From<VoteImportError> for Error<T> {
|
impl<T: Config> From<VoteImportError> for Error<T> {
|
||||||
@@ -592,6 +656,7 @@ impl<T: Config> From<VoteImportError> for Error<T> {
|
|||||||
match e {
|
match e {
|
||||||
VoteImportError::ValidatorIndexOutOfBounds => Error::<T>::ValidatorIndexOutOfBounds,
|
VoteImportError::ValidatorIndexOutOfBounds => Error::<T>::ValidatorIndexOutOfBounds,
|
||||||
VoteImportError::DuplicateStatement => Error::<T>::DuplicateStatement,
|
VoteImportError::DuplicateStatement => Error::<T>::DuplicateStatement,
|
||||||
|
VoteImportError::MaliciousBacker => Error::<T>::MaliciousBacker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,8 +666,8 @@ impl<T: Config> From<VoteImportError> for Error<T> {
|
|||||||
struct ImportUndo {
|
struct ImportUndo {
|
||||||
/// The validator index to which to associate the statement import.
|
/// The validator index to which to associate the statement import.
|
||||||
validator_index: ValidatorIndex,
|
validator_index: ValidatorIndex,
|
||||||
/// The direction of the vote, for block validity (`true`) or invalidity (`false`).
|
/// The kind and direction of the vote.
|
||||||
valid: bool,
|
vote_kind: VoteKind,
|
||||||
/// Has the validator participated before, i.e. in backing or
|
/// Has the validator participated before, i.e. in backing or
|
||||||
/// with an opposing vote.
|
/// with an opposing vote.
|
||||||
new_participant: bool,
|
new_participant: bool,
|
||||||
@@ -610,25 +675,53 @@ struct ImportUndo {
|
|||||||
|
|
||||||
struct DisputeStateImporter<BlockNumber> {
|
struct DisputeStateImporter<BlockNumber> {
|
||||||
state: DisputeState<BlockNumber>,
|
state: DisputeState<BlockNumber>,
|
||||||
|
backers: BTreeSet<ValidatorIndex>,
|
||||||
now: BlockNumber,
|
now: BlockNumber,
|
||||||
new_participants: bitvec::vec::BitVec<u8, BitOrderLsb0>,
|
new_participants: bitvec::vec::BitVec<u8, BitOrderLsb0>,
|
||||||
pre_flags: DisputeStateFlags,
|
pre_flags: DisputeStateFlags,
|
||||||
|
pre_state: DisputeState<BlockNumber>,
|
||||||
|
// 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<ValidatorIndex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
|
impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
|
||||||
fn new(state: DisputeState<BlockNumber>, now: BlockNumber) -> Self {
|
fn new(
|
||||||
|
state: DisputeState<BlockNumber>,
|
||||||
|
backers: BTreeSet<ValidatorIndex>,
|
||||||
|
now: BlockNumber,
|
||||||
|
) -> Self {
|
||||||
let pre_flags = DisputeStateFlags::from_state(&state);
|
let pre_flags = DisputeStateFlags::from_state(&state);
|
||||||
let new_participants = bitvec::bitvec![u8, BitOrderLsb0; 0; state.validators_for.len()];
|
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, now, new_participants, pre_flags }
|
DisputeStateImporter {
|
||||||
|
state,
|
||||||
|
backers,
|
||||||
|
now,
|
||||||
|
new_participants,
|
||||||
|
pre_flags,
|
||||||
|
pre_state,
|
||||||
|
pre_backers,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import(
|
fn import(
|
||||||
&mut self,
|
&mut self,
|
||||||
validator: ValidatorIndex,
|
validator: ValidatorIndex,
|
||||||
valid: bool,
|
kind: VoteKind,
|
||||||
) -> Result<ImportUndo, VoteImportError> {
|
) -> Result<ImportUndo, VoteImportError> {
|
||||||
let (bits, other_bits) = if valid {
|
let (bits, other_bits) = if kind.is_valid() {
|
||||||
(&mut self.state.validators_for, &mut self.state.validators_against)
|
(&mut self.state.validators_for, &mut self.state.validators_against)
|
||||||
} else {
|
} else {
|
||||||
(&mut self.state.validators_against, &mut self.state.validators_for)
|
(&mut self.state.validators_against, &mut self.state.validators_for)
|
||||||
@@ -637,18 +730,31 @@ impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
|
|||||||
// out of bounds or already participated
|
// out of bounds or already participated
|
||||||
match bits.get(validator.0 as usize).map(|b| *b) {
|
match bits.get(validator.0 as usize).map(|b| *b) {
|
||||||
None => return Err(VoteImportError::ValidatorIndexOutOfBounds),
|
None => return Err(VoteImportError::ValidatorIndexOutOfBounds),
|
||||||
Some(true) => return Err(VoteImportError::DuplicateStatement),
|
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) => {},
|
Some(false) => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// inefficient, and just for extra sanity.
|
// consistency check
|
||||||
if validator.0 as usize >= self.new_participants.len() {
|
debug_assert!((validator.0 as usize) < self.new_participants.len());
|
||||||
return Err(VoteImportError::ValidatorIndexOutOfBounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut undo = ImportUndo { validator_index: validator, valid, new_participant: false };
|
let mut undo =
|
||||||
|
ImportUndo { validator_index: validator, vote_kind: kind, new_participant: false };
|
||||||
|
|
||||||
bits.set(validator.0 as usize, true);
|
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
|
// 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).
|
// side of the dispute until now (so they make a first appearance).
|
||||||
@@ -663,12 +769,16 @@ impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
|
|||||||
|
|
||||||
/// Revert a done transaction.
|
/// Revert a done transaction.
|
||||||
fn undo(&mut self, undo: ImportUndo) {
|
fn undo(&mut self, undo: ImportUndo) {
|
||||||
if undo.valid {
|
if undo.vote_kind.is_valid() {
|
||||||
self.state.validators_for.set(undo.validator_index.0 as usize, false);
|
self.state.validators_for.set(undo.validator_index.0 as usize, false);
|
||||||
} else {
|
} else {
|
||||||
self.state.validators_against.set(undo.validator_index.0 as usize, false);
|
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 {
|
if undo.new_participant {
|
||||||
self.new_participants.set(undo.validator_index.0 as usize, false);
|
self.new_participants.set(undo.validator_index.0 as usize, false);
|
||||||
}
|
}
|
||||||
@@ -681,9 +791,9 @@ impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
|
|||||||
|
|
||||||
let pre_post_contains = |flags| (pre_flags.contains(flags), post_flags.contains(flags));
|
let pre_post_contains = |flags| (pre_flags.contains(flags), post_flags.contains(flags));
|
||||||
|
|
||||||
// 1. Check for fresh FOR supermajority. Only if not already concluded.
|
// 1. Check for FOR supermajority.
|
||||||
let slash_against =
|
let slash_against = match pre_post_contains(DisputeStateFlags::FOR_SUPERMAJORITY) {
|
||||||
if let (false, true) = pre_post_contains(DisputeStateFlags::FOR_SUPERMAJORITY) {
|
(false, true) => {
|
||||||
if self.state.concluded_at.is_none() {
|
if self.state.concluded_at.is_none() {
|
||||||
self.state.concluded_at = Some(self.now.clone());
|
self.state.concluded_at = Some(self.now.clone());
|
||||||
}
|
}
|
||||||
@@ -694,25 +804,59 @@ impl<BlockNumber: Clone> DisputeStateImporter<BlockNumber> {
|
|||||||
.iter_ones()
|
.iter_ones()
|
||||||
.map(|i| ValidatorIndex(i as _))
|
.map(|i| ValidatorIndex(i as _))
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
},
|
||||||
|
(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()
|
Vec::new()
|
||||||
|
},
|
||||||
|
(false, false) => Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Check for fresh AGAINST supermajority.
|
// 2. Check for AGAINST supermajority.
|
||||||
let slash_for =
|
let slash_for = match pre_post_contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) {
|
||||||
if let (false, true) = pre_post_contains(DisputeStateFlags::AGAINST_SUPERMAJORITY) {
|
(false, true) => {
|
||||||
if self.state.concluded_at.is_none() {
|
if self.state.concluded_at.is_none() {
|
||||||
self.state.concluded_at = Some(self.now.clone());
|
self.state.concluded_at = Some(self.now.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// provide FOR voters to slash.
|
// provide FOR voters to slash.
|
||||||
self.state.validators_for.iter_ones().map(|i| ValidatorIndex(i as _)).collect()
|
self.state.validators_for.iter_ones().map(|i| ValidatorIndex(i as _)).collect()
|
||||||
} else {
|
},
|
||||||
|
(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()
|
Vec::new()
|
||||||
|
},
|
||||||
|
(false, false) => Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ImportSummary {
|
ImportSummary {
|
||||||
state: self.state,
|
state: self.state,
|
||||||
|
backers: self.backers,
|
||||||
slash_against,
|
slash_against,
|
||||||
slash_for,
|
slash_for,
|
||||||
new_participants: self.new_participants,
|
new_participants: self.new_participants,
|
||||||
@@ -815,6 +959,8 @@ impl<T: Config> Pallet<T> {
|
|||||||
// This should be small, as disputes are rare, so `None` is fine.
|
// This should be small, as disputes are rare, so `None` is fine.
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
<Disputes<T>>::remove_prefix(to_prune, None);
|
<Disputes<T>>::remove_prefix(to_prune, None);
|
||||||
|
#[allow(deprecated)]
|
||||||
|
<BackersOnDisputes<T>>::remove_prefix(to_prune, None);
|
||||||
|
|
||||||
// This is larger, and will be extracted to the `shared` pallet for more proper pruning.
|
// This is larger, and will be extracted to the `shared` pallet for more proper pruning.
|
||||||
// TODO: https://github.com/paritytech/polkadot/issues/3469
|
// TODO: https://github.com/paritytech/polkadot/issues/3469
|
||||||
@@ -903,11 +1049,15 @@ impl<T: Config> Pallet<T> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let backers =
|
||||||
|
<BackersOnDisputes<T>>::get(&set.session, &set.candidate_hash).unwrap_or_default();
|
||||||
|
|
||||||
// Check and import all votes.
|
// Check and import all votes.
|
||||||
let summary = {
|
let summary = {
|
||||||
let mut importer = DisputeStateImporter::new(dispute_state, now);
|
let mut importer = DisputeStateImporter::new(dispute_state, backers, now);
|
||||||
for (i, (statement, validator_index, signature)) in set.statements.iter().enumerate() {
|
for (i, (statement, validator_index, signature)) in set.statements.iter().enumerate() {
|
||||||
// assure the validator index and is present in the session info
|
// 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) {
|
let validator_public = match session_info.validators.get(*validator_index) {
|
||||||
None => {
|
None => {
|
||||||
filter.remove_index(i);
|
filter.remove_index(i);
|
||||||
@@ -916,9 +1066,9 @@ impl<T: Config> Pallet<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
};
|
};
|
||||||
|
|
||||||
let valid = statement.indicates_validity();
|
let kind = VoteKind::from(statement);
|
||||||
|
|
||||||
let undo = match importer.import(*validator_index, valid) {
|
let undo = match importer.import(*validator_index, kind) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
filter.remove_index(i);
|
filter.remove_index(i);
|
||||||
@@ -1016,13 +1166,16 @@ impl<T: Config> Pallet<T> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let backers =
|
||||||
|
<BackersOnDisputes<T>>::get(&set.session, &set.candidate_hash).unwrap_or_default();
|
||||||
|
|
||||||
// Import all votes. They were pre-checked.
|
// Import all votes. They were pre-checked.
|
||||||
let summary = {
|
let summary = {
|
||||||
let mut importer = DisputeStateImporter::new(dispute_state, now);
|
let mut importer = DisputeStateImporter::new(dispute_state, backers, now);
|
||||||
for (statement, validator_index, _signature) in &set.statements {
|
for (statement, validator_index, _signature) in &set.statements {
|
||||||
let valid = statement.indicates_validity();
|
let kind = VoteKind::from(statement);
|
||||||
|
|
||||||
importer.import(*validator_index, valid).map_err(Error::<T>::from)?;
|
importer.import(*validator_index, kind).map_err(Error::<T>::from)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
importer.finish()
|
importer.finish()
|
||||||
@@ -1041,6 +1194,11 @@ impl<T: Config> Pallet<T> {
|
|||||||
byzantine_threshold(summary.state.validators_for.len()),
|
byzantine_threshold(summary.state.validators_for.len()),
|
||||||
Error::<T>::UnconfirmedDispute,
|
Error::<T>::UnconfirmedDispute,
|
||||||
);
|
);
|
||||||
|
let backers = summary.backers;
|
||||||
|
// Reject statements with no accompanying backing votes.
|
||||||
|
ensure!(!backers.is_empty(), Error::<T>::MissingBackingVotes);
|
||||||
|
<BackersOnDisputes<T>>::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 DisputeStatementSet { ref session, ref candidate_hash, .. } = set;
|
||||||
let session = *session;
|
let session = *session;
|
||||||
@@ -1085,10 +1243,16 @@ impl<T: Config> Pallet<T> {
|
|||||||
session,
|
session,
|
||||||
candidate_hash,
|
candidate_hash,
|
||||||
summary.slash_against,
|
summary.slash_against,
|
||||||
|
backers.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// an invalid candidate, according to 2/3. Punish those on the 'for' side.
|
// an invalid candidate, according to 2/3. Punish those on the 'for' side.
|
||||||
T::SlashingHandler::punish_for_invalid(session, candidate_hash, summary.slash_for);
|
T::SlashingHandler::punish_for_invalid(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
summary.slash_for,
|
||||||
|
backers,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
<Disputes<T>>::insert(&session, &candidate_hash, &summary.state);
|
<Disputes<T>>::insert(&session, &candidate_hash, &summary.state);
|
||||||
|
|||||||
@@ -16,32 +16,31 @@
|
|||||||
|
|
||||||
//! Dispute slashing pallet.
|
//! Dispute slashing pallet.
|
||||||
//!
|
//!
|
||||||
//! Once a dispute is concluded, we want to slash validators
|
//! Once a dispute is concluded, we want to slash validators who were on the
|
||||||
//! who were on the wrong side of the dispute. The slashing amount
|
//! wrong side of the dispute. The slashing amount depends on whether the
|
||||||
//! depends on whether the candidate was valid (small) or invalid (big).
|
//! candidate was valid (none at the moment) or invalid (big). In addition to
|
||||||
//! In addition to that, we might want to kick out the validators from the
|
//! that, we might want to kick out the validators from the active set.
|
||||||
//! active set.
|
//! Currently, we limit slashing to the backing group for invalid disputes.
|
||||||
//!
|
//!
|
||||||
//! The `offences` pallet from Substrate provides us with a way to do both.
|
//! The `offences` pallet from Substrate provides us with a way to do both.
|
||||||
//! Currently, the interface expects us to provide staking information
|
//! Currently, the interface expects us to provide staking information including
|
||||||
//! including nominator exposure in order to submit an offence.
|
//! nominator exposure in order to submit an offence.
|
||||||
//!
|
//!
|
||||||
//! Normally, we'd able to fetch this information from the runtime as soon as
|
//! Normally, we'd able to fetch this information from the runtime as soon as
|
||||||
//! the dispute is concluded. This is also what `im-online` pallet does.
|
//! the dispute is concluded. This is also what `im-online` pallet does.
|
||||||
//! However, since a dispute can conclude several sessions after the candidate
|
//! However, since a dispute can conclude several sessions after the candidate
|
||||||
//! was backed (see `dispute_period` in `HostConfiguration`), we can't rely on
|
//! was backed (see `dispute_period` in `HostConfiguration`), we can't rely on
|
||||||
//! this information be available in the context of the current block. The
|
//! this information being available in the context of the current block. The
|
||||||
//! `babe` and `grandpa` equivocation handlers also have to deal
|
//! `babe` and `grandpa` equivocation handlers also have to deal with this
|
||||||
//! with this problem.
|
//! problem.
|
||||||
//!
|
//!
|
||||||
//! Our implementation looks like a hybrid of `im-online` and `grandpa`
|
//! Our implementation looks like a hybrid of `im-online` and `grandpa`
|
||||||
//! equivocation handlers. Meaning, we submit an `offence` for the concluded
|
//! equivocation handlers. Meaning, we submit an `offence` for the concluded
|
||||||
//! disputes about the current session candidate directly from the runtime.
|
//! disputes about the current session candidate directly from the runtime. If,
|
||||||
//! If, however, the dispute is about a past session, we record unapplied
|
//! however, the dispute is about a past session, we record unapplied slashes on
|
||||||
//! slashes on chain, without `FullIdentification` of the offenders.
|
//! chain, without `FullIdentification` of the offenders. Later on, a block
|
||||||
//! Later on, a block producer can submit an unsigned transaction with
|
//! producer can submit an unsigned transaction with `KeyOwnershipProof` of an
|
||||||
//! `KeyOwnershipProof` of an offender and submit it to the runtime
|
//! offender and submit it to the runtime to produce an offence.
|
||||||
//! to produce an offence.
|
|
||||||
|
|
||||||
use crate::{disputes, initializer::ValidatorSetCount, session_info::IdentificationTuple};
|
use crate::{disputes, initializer::ValidatorSetCount, session_info::IdentificationTuple};
|
||||||
use frame_support::{
|
use frame_support::{
|
||||||
@@ -64,7 +63,10 @@ use sp_runtime::{
|
|||||||
use sp_session::{GetSessionNumber, GetValidatorCount};
|
use sp_session::{GetSessionNumber, GetValidatorCount};
|
||||||
use sp_staking::offence::{DisableStrategy, Kind, Offence, OffenceError, ReportOffence};
|
use sp_staking::offence::{DisableStrategy, Kind, Offence, OffenceError, ReportOffence};
|
||||||
use sp_std::{
|
use sp_std::{
|
||||||
collections::btree_map::{BTreeMap, Entry},
|
collections::{
|
||||||
|
btree_map::{BTreeMap, Entry},
|
||||||
|
btree_set::BTreeSet,
|
||||||
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ const LOG_TARGET: &str = "runtime::parachains::slashing";
|
|||||||
// These are constants, but we want to make them configurable
|
// These are constants, but we want to make them configurable
|
||||||
// via `HostConfiguration` in the future.
|
// via `HostConfiguration` in the future.
|
||||||
const SLASH_FOR_INVALID: Perbill = Perbill::from_percent(100);
|
const SLASH_FOR_INVALID: Perbill = Perbill::from_percent(100);
|
||||||
const SLASH_AGAINST_VALID: Perbill = Perbill::from_perthousand(1);
|
const SLASH_AGAINST_VALID: Perbill = Perbill::zero();
|
||||||
const DEFENSIVE_PROOF: &'static str = "disputes module should bail on old session";
|
const DEFENSIVE_PROOF: &'static str = "disputes module should bail on old session";
|
||||||
|
|
||||||
#[cfg(feature = "runtime-benchmarks")]
|
#[cfg(feature = "runtime-benchmarks")]
|
||||||
@@ -228,18 +230,30 @@ where
|
|||||||
candidate_hash: CandidateHash,
|
candidate_hash: CandidateHash,
|
||||||
kind: SlashingOffenceKind,
|
kind: SlashingOffenceKind,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
let losers: Vec<ValidatorIndex> = losers.into_iter().collect();
|
// sanity check for the current implementation
|
||||||
if losers.is_empty() {
|
if kind == SlashingOffenceKind::AgainstValid {
|
||||||
// Nothing to do
|
debug_assert!(false, "should only slash ForInvalid disputes");
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let losers: BTreeSet<_> = losers.into_iter().collect();
|
||||||
|
if losers.is_empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let backers: BTreeSet<_> = backers.into_iter().collect();
|
||||||
|
let to_punish: Vec<ValidatorIndex> = losers.intersection(&backers).cloned().collect();
|
||||||
|
if to_punish.is_empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let session_info = crate::session_info::Pallet::<T>::session_info(session_index);
|
let session_info = crate::session_info::Pallet::<T>::session_info(session_index);
|
||||||
let session_info = match session_info.defensive_proof(DEFENSIVE_PROOF) {
|
let session_info = match session_info.defensive_proof(DEFENSIVE_PROOF) {
|
||||||
Some(info) => info,
|
Some(info) => info,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let maybe = Self::maybe_identify_validators(session_index, losers.iter().cloned());
|
|
||||||
|
let maybe = Self::maybe_identify_validators(session_index, to_punish.iter().cloned());
|
||||||
if let Some(offenders) = maybe {
|
if let Some(offenders) = maybe {
|
||||||
let validator_set_count = session_info.discovery_keys.len() as ValidatorSetCount;
|
let validator_set_count = session_info.discovery_keys.len() as ValidatorSetCount;
|
||||||
let offence = SlashingOffence::new(
|
let offence = SlashingOffence::new(
|
||||||
@@ -255,12 +269,20 @@ where
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys = losers
|
let keys = to_punish
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|i| session_info.validators.get(i).cloned().map(|id| (i, id)))
|
.filter_map(|i| session_info.validators.get(i).cloned().map(|id| (i, id)))
|
||||||
.collect();
|
.collect();
|
||||||
let unapplied = PendingSlashes { keys, kind };
|
let unapplied = PendingSlashes { keys, kind };
|
||||||
<UnappliedSlashes<T>>::insert(session_index, candidate_hash, unapplied);
|
|
||||||
|
let append = |old: &mut Option<PendingSlashes>| {
|
||||||
|
let old = old
|
||||||
|
.get_or_insert(PendingSlashes { keys: Default::default(), kind: unapplied.kind });
|
||||||
|
debug_assert_eq!(old.kind, unapplied.kind);
|
||||||
|
|
||||||
|
old.keys.extend(unapplied.keys)
|
||||||
|
};
|
||||||
|
<UnappliedSlashes<T>>::mutate(session_index, candidate_hash, append);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,18 +294,20 @@ where
|
|||||||
session_index: SessionIndex,
|
session_index: SessionIndex,
|
||||||
candidate_hash: CandidateHash,
|
candidate_hash: CandidateHash,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
let kind = SlashingOffenceKind::ForInvalid;
|
let kind = SlashingOffenceKind::ForInvalid;
|
||||||
Self::do_punish(session_index, candidate_hash, kind, losers);
|
Self::do_punish(session_index, candidate_hash, kind, losers, backers);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punish_against_valid(
|
fn punish_against_valid(
|
||||||
session_index: SessionIndex,
|
_session_index: SessionIndex,
|
||||||
candidate_hash: CandidateHash,
|
_candidate_hash: CandidateHash,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
_losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
_backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
let kind = SlashingOffenceKind::AgainstValid;
|
// do nothing for now
|
||||||
Self::do_punish(session_index, candidate_hash, kind, losers);
|
// NOTE: changing that requires modifying `do_punish` implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initializer_initialize(now: T::BlockNumber) -> Weight {
|
fn initializer_initialize(now: T::BlockNumber) -> Weight {
|
||||||
|
|||||||
@@ -109,8 +109,9 @@ where
|
|||||||
|
|
||||||
let validator_index = ValidatorIndex(0);
|
let validator_index = ValidatorIndex(0);
|
||||||
let losers = [validator_index].into_iter();
|
let losers = [validator_index].into_iter();
|
||||||
|
let backers = losers.clone();
|
||||||
|
|
||||||
T::SlashingHandler::punish_against_valid(session_index, CANDIDATE_HASH, losers);
|
T::SlashingHandler::punish_for_invalid(session_index, CANDIDATE_HASH, losers, backers);
|
||||||
|
|
||||||
let unapplied = <UnappliedSlashes<T>>::get(session_index, CANDIDATE_HASH);
|
let unapplied = <UnappliedSlashes<T>>::get(session_index, CANDIDATE_HASH);
|
||||||
assert_eq!(unapplied.unwrap().keys.len(), 1);
|
assert_eq!(unapplied.unwrap().keys.len(), 1);
|
||||||
@@ -123,7 +124,7 @@ fn dispute_proof(
|
|||||||
validator_id: ValidatorId,
|
validator_id: ValidatorId,
|
||||||
validator_index: ValidatorIndex,
|
validator_index: ValidatorIndex,
|
||||||
) -> DisputeProof {
|
) -> DisputeProof {
|
||||||
let kind = SlashingOffenceKind::AgainstValid;
|
let kind = SlashingOffenceKind::ForInvalid;
|
||||||
let time_slot = DisputesTimeSlot::new(session_index, CANDIDATE_HASH);
|
let time_slot = DisputesTimeSlot::new(session_index, CANDIDATE_HASH);
|
||||||
|
|
||||||
DisputeProof { time_slot, kind, validator_index, validator_id }
|
DisputeProof { time_slot, kind, validator_index, validator_id }
|
||||||
@@ -134,7 +135,7 @@ benchmarks! {
|
|||||||
where T: Config<KeyOwnerProof = MembershipProof>,
|
where T: Config<KeyOwnerProof = MembershipProof>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// in this setup we have a single `AgainstValid` dispute
|
// in this setup we have a single `ForInvalid` dispute
|
||||||
// submitted for a past session
|
// submitted for a past session
|
||||||
report_dispute_lost {
|
report_dispute_lost {
|
||||||
let n in 4..<<T as super::Config>::BenchmarkingConfig as BenchmarkingConfiguration>::MAX_VALIDATORS;
|
let n in 4..<<T as super::Config>::BenchmarkingConfig as BenchmarkingConfiguration>::MAX_VALIDATORS;
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ use crate::{
|
|||||||
disputes::DisputesHandler,
|
disputes::DisputesHandler,
|
||||||
mock::{
|
mock::{
|
||||||
new_test_ext, AccountId, AllPalletsWithSystem, Initializer, MockGenesisConfig, System,
|
new_test_ext, AccountId, AllPalletsWithSystem, Initializer, MockGenesisConfig, System,
|
||||||
Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, REWARD_VALIDATORS,
|
Test, PUNISH_BACKERS_FOR, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR,
|
||||||
|
REWARD_VALIDATORS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use frame_support::{
|
use frame_support::{
|
||||||
@@ -30,6 +31,10 @@ use frame_support::{
|
|||||||
use primitives::BlockNumber;
|
use primitives::BlockNumber;
|
||||||
use sp_core::{crypto::CryptoType, Pair};
|
use sp_core::{crypto::CryptoType, Pair};
|
||||||
|
|
||||||
|
const VOTE_FOR: VoteKind = VoteKind::ExplicitValid;
|
||||||
|
const VOTE_AGAINST: VoteKind = VoteKind::Invalid;
|
||||||
|
const VOTE_BACKING: VoteKind = VoteKind::Backing;
|
||||||
|
|
||||||
fn filter_dispute_set(stmts: MultiDisputeStatementSet) -> CheckedMultiDisputeStatementSet {
|
fn filter_dispute_set(stmts: MultiDisputeStatementSet) -> CheckedMultiDisputeStatementSet {
|
||||||
let config = <configuration::Pallet<Test>>::config();
|
let config = <configuration::Pallet<Test>>::config();
|
||||||
let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period;
|
let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period;
|
||||||
@@ -138,22 +143,26 @@ fn test_import_new_participant() {
|
|||||||
start: 0,
|
start: 0,
|
||||||
concluded_at: None,
|
concluded_at: None,
|
||||||
},
|
},
|
||||||
|
BTreeSet::new(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_err!(
|
assert_err!(
|
||||||
importer.import(ValidatorIndex(9), true),
|
importer.import(ValidatorIndex(9), VOTE_FOR),
|
||||||
VoteImportError::ValidatorIndexOutOfBounds,
|
VoteImportError::ValidatorIndexOutOfBounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_err!(importer.import(ValidatorIndex(0), true), VoteImportError::DuplicateStatement);
|
assert_err!(importer.import(ValidatorIndex(0), VOTE_FOR), VoteImportError::DuplicateStatement);
|
||||||
assert_ok!(importer.import(ValidatorIndex(0), false));
|
assert_ok!(importer.import(ValidatorIndex(0), VOTE_AGAINST));
|
||||||
|
|
||||||
assert_ok!(importer.import(ValidatorIndex(2), true));
|
assert_ok!(importer.import(ValidatorIndex(2), VOTE_FOR));
|
||||||
assert_err!(importer.import(ValidatorIndex(2), true), VoteImportError::DuplicateStatement);
|
assert_err!(importer.import(ValidatorIndex(2), VOTE_FOR), VoteImportError::DuplicateStatement);
|
||||||
|
|
||||||
assert_ok!(importer.import(ValidatorIndex(2), false));
|
assert_ok!(importer.import(ValidatorIndex(2), VOTE_AGAINST));
|
||||||
assert_err!(importer.import(ValidatorIndex(2), false), VoteImportError::DuplicateStatement);
|
assert_err!(
|
||||||
|
importer.import(ValidatorIndex(2), VOTE_AGAINST),
|
||||||
|
VoteImportError::DuplicateStatement
|
||||||
|
);
|
||||||
|
|
||||||
let summary = importer.finish();
|
let summary = importer.finish();
|
||||||
assert_eq!(summary.new_flags, DisputeStateFlags::default());
|
assert_eq!(summary.new_flags, DisputeStateFlags::default());
|
||||||
@@ -180,10 +189,11 @@ fn test_import_prev_participant_confirmed() {
|
|||||||
start: 0,
|
start: 0,
|
||||||
concluded_at: None,
|
concluded_at: None,
|
||||||
},
|
},
|
||||||
|
BTreeSet::new(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_ok!(importer.import(ValidatorIndex(2), true));
|
assert_ok!(importer.import(ValidatorIndex(2), VOTE_FOR));
|
||||||
|
|
||||||
let summary = importer.finish();
|
let summary = importer.finish();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -211,15 +221,16 @@ fn test_import_prev_participant_confirmed_slash_for() {
|
|||||||
start: 0,
|
start: 0,
|
||||||
concluded_at: None,
|
concluded_at: None,
|
||||||
},
|
},
|
||||||
|
BTreeSet::new(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_ok!(importer.import(ValidatorIndex(2), true));
|
assert_ok!(importer.import(ValidatorIndex(2), VOTE_FOR));
|
||||||
assert_ok!(importer.import(ValidatorIndex(2), false));
|
assert_ok!(importer.import(ValidatorIndex(2), VOTE_AGAINST));
|
||||||
assert_ok!(importer.import(ValidatorIndex(3), false));
|
assert_ok!(importer.import(ValidatorIndex(3), VOTE_AGAINST));
|
||||||
assert_ok!(importer.import(ValidatorIndex(4), false));
|
assert_ok!(importer.import(ValidatorIndex(4), VOTE_AGAINST));
|
||||||
assert_ok!(importer.import(ValidatorIndex(5), false));
|
assert_ok!(importer.import(ValidatorIndex(5), VOTE_AGAINST));
|
||||||
assert_ok!(importer.import(ValidatorIndex(6), false));
|
assert_ok!(importer.import(ValidatorIndex(6), VOTE_AGAINST));
|
||||||
|
|
||||||
let summary = importer.finish();
|
let summary = importer.finish();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -250,14 +261,15 @@ fn test_import_slash_against() {
|
|||||||
start: 0,
|
start: 0,
|
||||||
concluded_at: None,
|
concluded_at: None,
|
||||||
},
|
},
|
||||||
|
BTreeSet::new(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_ok!(importer.import(ValidatorIndex(3), true));
|
assert_ok!(importer.import(ValidatorIndex(3), VOTE_FOR));
|
||||||
assert_ok!(importer.import(ValidatorIndex(4), true));
|
assert_ok!(importer.import(ValidatorIndex(4), VOTE_FOR));
|
||||||
assert_ok!(importer.import(ValidatorIndex(5), false));
|
assert_ok!(importer.import(ValidatorIndex(5), VOTE_AGAINST));
|
||||||
assert_ok!(importer.import(ValidatorIndex(6), true));
|
assert_ok!(importer.import(ValidatorIndex(6), VOTE_FOR));
|
||||||
assert_ok!(importer.import(ValidatorIndex(7), true));
|
assert_ok!(importer.import(ValidatorIndex(7), VOTE_FOR));
|
||||||
|
|
||||||
let summary = importer.finish();
|
let summary = importer.finish();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -275,6 +287,48 @@ fn test_import_slash_against() {
|
|||||||
assert_eq!(summary.new_flags, DisputeStateFlags::FOR_SUPERMAJORITY);
|
assert_eq!(summary.new_flags, DisputeStateFlags::FOR_SUPERMAJORITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_import_backing_votes() {
|
||||||
|
let mut importer = DisputeStateImporter::new(
|
||||||
|
DisputeState {
|
||||||
|
validators_for: bitvec![u8, BitOrderLsb0; 1, 0, 1, 0, 0, 0, 0, 0],
|
||||||
|
validators_against: bitvec![u8, BitOrderLsb0; 0, 1, 0, 0, 0, 0, 0, 0],
|
||||||
|
start: 0,
|
||||||
|
concluded_at: None,
|
||||||
|
},
|
||||||
|
BTreeSet::from_iter([ValidatorIndex(0)]),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_ok!(importer.import(ValidatorIndex(3), VOTE_FOR));
|
||||||
|
assert_ok!(importer.import(ValidatorIndex(3), VOTE_BACKING));
|
||||||
|
assert_ok!(importer.import(ValidatorIndex(3), VOTE_AGAINST));
|
||||||
|
assert_ok!(importer.import(ValidatorIndex(6), VOTE_FOR));
|
||||||
|
assert_ok!(importer.import(ValidatorIndex(7), VOTE_BACKING));
|
||||||
|
// Don't import backing vote twice
|
||||||
|
assert_err!(
|
||||||
|
importer.import(ValidatorIndex(0), VOTE_BACKING),
|
||||||
|
VoteImportError::DuplicateStatement,
|
||||||
|
);
|
||||||
|
// Don't import explicit votes after backing
|
||||||
|
assert_err!(importer.import(ValidatorIndex(7), VOTE_FOR), VoteImportError::MaliciousBacker,);
|
||||||
|
|
||||||
|
let summary = importer.finish();
|
||||||
|
assert_eq!(
|
||||||
|
summary.state,
|
||||||
|
DisputeState {
|
||||||
|
validators_for: bitvec![u8, BitOrderLsb0; 1, 0, 1, 1, 0, 0, 1, 1],
|
||||||
|
validators_against: bitvec![u8, BitOrderLsb0; 0, 1, 0, 1, 0, 0, 0, 0],
|
||||||
|
start: 0,
|
||||||
|
concluded_at: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
summary.backers,
|
||||||
|
BTreeSet::from_iter([ValidatorIndex(0), ValidatorIndex(3), ValidatorIndex(7),]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Test that dispute timeout is handled correctly.
|
// Test that dispute timeout is handled correctly.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dispute_timeout() {
|
fn test_dispute_timeout() {
|
||||||
@@ -329,6 +383,7 @@ fn test_dispute_timeout() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let inclusion_parent = sp_core::H256::repeat_byte(0xff);
|
||||||
|
|
||||||
// v0 and v1 vote for 3, v2 against. We need f+1 votes (3) so that the dispute is
|
// v0 and v1 vote for 3, v2 against. We need f+1 votes (3) so that the dispute is
|
||||||
// confirmed. Otherwise It will be filtered out.
|
// confirmed. Otherwise It will be filtered out.
|
||||||
@@ -338,16 +393,13 @@ fn test_dispute_timeout() {
|
|||||||
session,
|
session,
|
||||||
statements: vec![
|
statements: vec![
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
ValidatorIndex(0),
|
ValidatorIndex(0),
|
||||||
v0.sign(
|
v0.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
&ExplicitDisputeStatement {
|
&SigningContext { session_index: start - 1, parent_hash: inclusion_parent },
|
||||||
valid: true,
|
)),
|
||||||
candidate_hash: candidate_hash.clone(),
|
|
||||||
session: start - 1,
|
|
||||||
}
|
|
||||||
.signing_payload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
||||||
@@ -495,21 +547,20 @@ fn test_provide_multi_dispute_is_providing() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let inclusion_parent = sp_core::H256::repeat_byte(0xff);
|
||||||
|
let session = 1;
|
||||||
let stmts = vec![DisputeStatementSet {
|
let stmts = vec![DisputeStatementSet {
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 1,
|
session,
|
||||||
statements: vec![
|
statements: vec![
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
ValidatorIndex(0),
|
ValidatorIndex(0),
|
||||||
v0.sign(
|
v0.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
&ExplicitDisputeStatement {
|
&SigningContext { session_index: session, parent_hash: inclusion_parent },
|
||||||
valid: true,
|
)),
|
||||||
candidate_hash: candidate_hash.clone(),
|
|
||||||
session: 1,
|
|
||||||
}
|
|
||||||
.signing_payload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
@@ -518,7 +569,7 @@ fn test_provide_multi_dispute_is_providing() {
|
|||||||
&ExplicitDisputeStatement {
|
&ExplicitDisputeStatement {
|
||||||
valid: false,
|
valid: false,
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 1,
|
session,
|
||||||
}
|
}
|
||||||
.signing_payload(),
|
.signing_payload(),
|
||||||
),
|
),
|
||||||
@@ -538,6 +589,70 @@ fn test_provide_multi_dispute_is_providing() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_disputes_with_missing_backing_votes_are_rejected() {
|
||||||
|
new_test_ext(Default::default()).execute_with(|| {
|
||||||
|
let v0 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v1 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
|
||||||
|
run_to_block(3, |b| {
|
||||||
|
// a new session at each block
|
||||||
|
if b == 1 {
|
||||||
|
Some((
|
||||||
|
true,
|
||||||
|
b,
|
||||||
|
vec![(&0, v0.public()), (&1, v1.public())],
|
||||||
|
Some(vec![(&0, v0.public()), (&1, v1.public())]),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Some((true, b, vec![(&1, v1.public())], Some(vec![(&1, v1.public())])))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let session = 1;
|
||||||
|
|
||||||
|
let stmts = vec![DisputeStatementSet {
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(0),
|
||||||
|
v0.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: true,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(1),
|
||||||
|
v1.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
assert!(Pallet::<Test>::process_checked_multi_dispute_data(
|
||||||
|
stmts
|
||||||
|
.into_iter()
|
||||||
|
.map(CheckedDisputeStatementSet::unchecked_from_unchecked)
|
||||||
|
.collect()
|
||||||
|
)
|
||||||
|
.is_err(),);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_freeze_on_note_included() {
|
fn test_freeze_on_note_included() {
|
||||||
new_test_ext(Default::default()).execute_with(|| {
|
new_test_ext(Default::default()).execute_with(|| {
|
||||||
@@ -555,6 +670,8 @@ fn test_freeze_on_note_included() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let inclusion_parent = sp_core::H256::repeat_byte(0xff);
|
||||||
|
let session = 3;
|
||||||
|
|
||||||
// v0 votes for 3
|
// v0 votes for 3
|
||||||
let stmts = vec![DisputeStatementSet {
|
let stmts = vec![DisputeStatementSet {
|
||||||
@@ -586,16 +703,13 @@ fn test_freeze_on_note_included() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
ValidatorIndex(1),
|
ValidatorIndex(1),
|
||||||
v1.sign(
|
v0.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
&ExplicitDisputeStatement {
|
&SigningContext { session_index: session, parent_hash: inclusion_parent },
|
||||||
valid: true,
|
)),
|
||||||
candidate_hash: candidate_hash.clone(),
|
|
||||||
session: 3,
|
|
||||||
}
|
|
||||||
.signing_payload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
@@ -629,11 +743,13 @@ fn test_freeze_provided_against_supermajority_for_included() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let inclusion_parent = sp_core::H256::repeat_byte(0xff);
|
||||||
|
let session = 3;
|
||||||
|
|
||||||
// v0 votes for 3
|
// v0 votes for 3
|
||||||
let stmts = vec![DisputeStatementSet {
|
let stmts = vec![DisputeStatementSet {
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 3,
|
session,
|
||||||
statements: vec![
|
statements: vec![
|
||||||
(
|
(
|
||||||
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
@@ -642,7 +758,7 @@ fn test_freeze_provided_against_supermajority_for_included() {
|
|||||||
&ExplicitDisputeStatement {
|
&ExplicitDisputeStatement {
|
||||||
valid: false,
|
valid: false,
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 3,
|
session,
|
||||||
}
|
}
|
||||||
.signing_payload(),
|
.signing_payload(),
|
||||||
),
|
),
|
||||||
@@ -654,22 +770,19 @@ fn test_freeze_provided_against_supermajority_for_included() {
|
|||||||
&ExplicitDisputeStatement {
|
&ExplicitDisputeStatement {
|
||||||
valid: false,
|
valid: false,
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 3,
|
session,
|
||||||
}
|
}
|
||||||
.signing_payload(),
|
.signing_payload(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
ValidatorIndex(1),
|
ValidatorIndex(1),
|
||||||
v1.sign(
|
v0.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
&ExplicitDisputeStatement {
|
&SigningContext { session_index: session, parent_hash: inclusion_parent },
|
||||||
valid: true,
|
)),
|
||||||
candidate_hash: candidate_hash.clone(),
|
|
||||||
session: 3,
|
|
||||||
}
|
|
||||||
.signing_payload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
@@ -851,23 +964,22 @@ fn test_provide_multi_dispute_success_and_other() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let inclusion_parent = sp_core::H256::repeat_byte(0xff);
|
||||||
|
let session = 3;
|
||||||
|
|
||||||
// v0 and v1 vote for 3, v6 votes against
|
// v0 and v1 vote for 3, v6 votes against
|
||||||
let stmts = vec![DisputeStatementSet {
|
let stmts = vec![DisputeStatementSet {
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 3,
|
session,
|
||||||
statements: vec![
|
statements: vec![
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
ValidatorIndex(0),
|
ValidatorIndex(0),
|
||||||
v0.sign(
|
v0.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
&ExplicitDisputeStatement {
|
&SigningContext { session_index: session, parent_hash: inclusion_parent },
|
||||||
valid: true,
|
)),
|
||||||
candidate_hash: candidate_hash.clone(),
|
|
||||||
session: 3,
|
|
||||||
}
|
|
||||||
.signing_payload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
@@ -876,7 +988,7 @@ fn test_provide_multi_dispute_success_and_other() {
|
|||||||
&ExplicitDisputeStatement {
|
&ExplicitDisputeStatement {
|
||||||
valid: false,
|
valid: false,
|
||||||
candidate_hash: candidate_hash.clone(),
|
candidate_hash: candidate_hash.clone(),
|
||||||
session: 3,
|
session,
|
||||||
}
|
}
|
||||||
.signing_payload(),
|
.signing_payload(),
|
||||||
),
|
),
|
||||||
@@ -926,16 +1038,13 @@ fn test_provide_multi_dispute_success_and_other() {
|
|||||||
session: 5,
|
session: 5,
|
||||||
statements: vec![
|
statements: vec![
|
||||||
(
|
(
|
||||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit),
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
ValidatorIndex(5),
|
ValidatorIndex(5),
|
||||||
v3.sign(
|
v3.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
&ExplicitDisputeStatement {
|
&SigningContext { session_index: 5, parent_hash: inclusion_parent },
|
||||||
valid: true,
|
)),
|
||||||
candidate_hash: candidate_hash.clone(),
|
|
||||||
session: 5,
|
|
||||||
}
|
|
||||||
.signing_payload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
@@ -1141,6 +1250,211 @@ fn test_provide_multi_dispute_success_and_other() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// In this setup we have only one dispute concluding AGAINST.
|
||||||
|
/// There are some votes imported post dispute conclusion.
|
||||||
|
/// We make sure these votes are accounted for in punishment.
|
||||||
|
#[test]
|
||||||
|
fn test_punish_post_conclusion() {
|
||||||
|
new_test_ext(Default::default()).execute_with(|| {
|
||||||
|
// supermajority threshold is 5
|
||||||
|
let v0 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v1 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v2 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v3 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v4 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v5 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
let v6 = <ValidatorId as CryptoType>::Pair::generate().0;
|
||||||
|
// Mapping between key pair and `ValidatorIndex`
|
||||||
|
// v0 -> 0
|
||||||
|
// v1 -> 3
|
||||||
|
// v2 -> 6
|
||||||
|
// v3 -> 5
|
||||||
|
// v4 -> 1
|
||||||
|
// v5 -> 4
|
||||||
|
// v6 -> 2
|
||||||
|
|
||||||
|
run_to_block(6, |b| {
|
||||||
|
// a new session at each block
|
||||||
|
Some((
|
||||||
|
true,
|
||||||
|
b,
|
||||||
|
vec![
|
||||||
|
(&0, v0.public()),
|
||||||
|
(&1, v1.public()),
|
||||||
|
(&2, v2.public()),
|
||||||
|
(&3, v3.public()),
|
||||||
|
(&4, v4.public()),
|
||||||
|
(&5, v5.public()),
|
||||||
|
(&6, v6.public()),
|
||||||
|
],
|
||||||
|
Some(vec![
|
||||||
|
(&0, v0.public()),
|
||||||
|
(&1, v1.public()),
|
||||||
|
(&2, v2.public()),
|
||||||
|
(&3, v3.public()),
|
||||||
|
(&4, v4.public()),
|
||||||
|
(&5, v5.public()),
|
||||||
|
(&6, v6.public()),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1));
|
||||||
|
let inclusion_parent = sp_core::H256::repeat_byte(0xff);
|
||||||
|
let session = 3;
|
||||||
|
|
||||||
|
let stmts = vec![DisputeStatementSet {
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
|
ValidatorIndex(0),
|
||||||
|
v0.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
|
&SigningContext { session_index: session, parent_hash: inclusion_parent },
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(1),
|
||||||
|
v4.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(2),
|
||||||
|
v6.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(6),
|
||||||
|
v2.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(4),
|
||||||
|
v5.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(5),
|
||||||
|
v3.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking),
|
||||||
|
ValidatorIndex(3),
|
||||||
|
v1.sign(&ApprovalVote(candidate_hash).signing_payload(session)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let stmts = filter_dispute_set(stmts);
|
||||||
|
assert_ok!(
|
||||||
|
Pallet::<Test>::process_checked_multi_dispute_data(stmts),
|
||||||
|
vec![(session, candidate_hash)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PUNISH_VALIDATORS_FOR.with(|r| r.borrow().clone()),
|
||||||
|
vec![(session, vec![ValidatorIndex(0), ValidatorIndex(3)]),],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PUNISH_BACKERS_FOR.with(|r| r.borrow().clone()),
|
||||||
|
vec![(session, vec![ValidatorIndex(0)]),],
|
||||||
|
);
|
||||||
|
|
||||||
|
// someone reveals 3 backing vote, 6 votes against
|
||||||
|
let stmts = vec![DisputeStatementSet {
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(
|
||||||
|
inclusion_parent,
|
||||||
|
)),
|
||||||
|
ValidatorIndex(3),
|
||||||
|
v1.sign(&CompactStatement::Valid(candidate_hash).signing_payload(
|
||||||
|
&SigningContext { session_index: session, parent_hash: inclusion_parent },
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit),
|
||||||
|
ValidatorIndex(6),
|
||||||
|
v2.sign(
|
||||||
|
&ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash: candidate_hash.clone(),
|
||||||
|
session,
|
||||||
|
}
|
||||||
|
.signing_payload(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let stmts = filter_dispute_set(stmts);
|
||||||
|
assert_ok!(Pallet::<Test>::process_checked_multi_dispute_data(stmts), vec![],);
|
||||||
|
|
||||||
|
// Ensure punishment for is called
|
||||||
|
assert_eq!(
|
||||||
|
PUNISH_VALIDATORS_FOR.with(|r| r.borrow().clone()),
|
||||||
|
vec![
|
||||||
|
(session, vec![ValidatorIndex(0), ValidatorIndex(3)]),
|
||||||
|
(session, vec![ValidatorIndex(3)]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PUNISH_BACKERS_FOR.with(|r| r.borrow().clone()),
|
||||||
|
vec![
|
||||||
|
(session, vec![ValidatorIndex(0)]),
|
||||||
|
(session, vec![ValidatorIndex(0), ValidatorIndex(3)])
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PUNISH_VALIDATORS_AGAINST.with(|r| r.borrow().clone()),
|
||||||
|
vec![(session, vec![]), (session, vec![]),],
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_revert_and_freeze() {
|
fn test_revert_and_freeze() {
|
||||||
new_test_ext(Default::default()).execute_with(|| {
|
new_test_ext(Default::default()).execute_with(|| {
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ thread_local! {
|
|||||||
pub static REWARD_VALIDATORS: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
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_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_AGAINST: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
||||||
|
pub static PUNISH_BACKERS_FOR: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::disputes::RewardValidators for Test {
|
impl crate::disputes::RewardValidators for Test {
|
||||||
@@ -269,14 +270,18 @@ impl crate::disputes::SlashingHandler<BlockNumber> for Test {
|
|||||||
session: SessionIndex,
|
session: SessionIndex,
|
||||||
_: CandidateHash,
|
_: CandidateHash,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
PUNISH_VALIDATORS_FOR.with(|r| r.borrow_mut().push((session, losers.into_iter().collect())))
|
PUNISH_VALIDATORS_FOR
|
||||||
|
.with(|r| r.borrow_mut().push((session, losers.into_iter().collect())));
|
||||||
|
PUNISH_BACKERS_FOR.with(|r| r.borrow_mut().push((session, backers.into_iter().collect())));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn punish_against_valid(
|
fn punish_against_valid(
|
||||||
session: SessionIndex,
|
session: SessionIndex,
|
||||||
_: CandidateHash,
|
_: CandidateHash,
|
||||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
|
_backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||||
) {
|
) {
|
||||||
PUNISH_VALIDATORS_AGAINST
|
PUNISH_VALIDATORS_AGAINST
|
||||||
.with(|r| r.borrow_mut().push((session, losers.into_iter().collect())))
|
.with(|r| r.borrow_mut().push((session, losers.into_iter().collect())))
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ eve: reports parachain_candidate_disputes_total is at least 10 within 15 seconds
|
|||||||
eve: reports parachain_candidate_dispute_concluded{validity="valid"} is at least 10 within 15 seconds
|
eve: reports parachain_candidate_dispute_concluded{validity="valid"} is at least 10 within 15 seconds
|
||||||
eve: reports parachain_candidate_dispute_concluded{validity="invalid"} is 0 within 15 seconds
|
eve: reports parachain_candidate_dispute_concluded{validity="invalid"} is 0 within 15 seconds
|
||||||
|
|
||||||
# Check there is an offence report
|
# As of <https://github.com/paritytech/polkadot/pull/6249>, we don't slash on disputes
|
||||||
alice: system event contains "There is an offence reported" within 60 seconds
|
# with `valid` outcome, so there is no offence reported.
|
||||||
|
# alice: system event contains "There is an offence reported" within 60 seconds
|
||||||
|
|
||||||
# Check lag - approval
|
# Check lag - approval
|
||||||
alice: reports polkadot_parachain_approval_checking_finality_lag is 0
|
alice: reports polkadot_parachain_approval_checking_finality_lag is 0
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ honest-validator-0: parachain 2000 block height is at least 2 within 180 seconds
|
|||||||
honest-validator-1: parachain 2001 block height is at least 2 within 180 seconds
|
honest-validator-1: parachain 2001 block height is at least 2 within 180 seconds
|
||||||
honest-validator-2: parachain 2002 block height is at least 2 within 180 seconds
|
honest-validator-2: parachain 2002 block height is at least 2 within 180 seconds
|
||||||
|
|
||||||
|
# Check there is an offence report after dispute conclusion
|
||||||
|
honest-validator-0: system event contains "There is an offence reported" within 180 seconds
|
||||||
|
honest-validator-1: system event contains "There is an offence reported" within 180 seconds
|
||||||
|
honest-validator-2: system event contains "There is an offence reported" within 180 seconds
|
||||||
|
|
||||||
# Check for chain reversion after dispute conclusion.
|
# Check for chain reversion after dispute conclusion.
|
||||||
honest-validator-0: log line contains "reverted due to a bad parachain block" within 180 seconds
|
honest-validator-0: log line contains "reverted due to a bad parachain block" within 180 seconds
|
||||||
honest-validator-1: log line contains "reverted due to a bad parachain block" within 180 seconds
|
honest-validator-1: log line contains "reverted due to a bad parachain block" within 180 seconds
|
||||||
|
|||||||
Reference in New Issue
Block a user