Skip to content

Commit

Permalink
do not block finality for "disabled" disputes (#3358)
Browse files Browse the repository at this point in the history
- [x] test with zombienet-sdk
- [x] prdoc

Relevant Issues:
#3314 (connected to the
cause)
#3345 (solves)

---------

Co-authored-by: Kian Paimani <[email protected]>
  • Loading branch information
2 people authored and eskimor committed Feb 19, 2024
1 parent 2fe3145 commit 32f9442
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 51 deletions.
44 changes: 34 additions & 10 deletions polkadot/node/core/dispute-coordinator/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ pub struct CandidateEnvironment<'a> {
executor_params: &'a ExecutorParams,
/// Validator indices controlled by this node.
controlled_indices: HashSet<ValidatorIndex>,
/// Indices of disabled validators at the `relay_parent`.
/// Indices of on-chain disabled validators at the `relay_parent` combined
/// with the off-chain state.
disabled_indices: HashSet<ValidatorIndex>,
}

Expand All @@ -67,16 +68,15 @@ impl<'a> CandidateEnvironment<'a> {
runtime_info: &'a mut RuntimeInfo,
session_index: SessionIndex,
relay_parent: Hash,
disabled_offchain: impl IntoIterator<Item = ValidatorIndex>,
) -> Option<CandidateEnvironment<'a>> {
let disabled_indices = runtime_info
let disabled_onchain = runtime_info
.get_disabled_validators(ctx.sender(), relay_parent)
.await
.unwrap_or_else(|err| {
gum::info!(target: LOG_TARGET, ?err, "Failed to get disabled validators");
Vec::new()
})
.into_iter()
.collect();
});

let (session, executor_params) = match runtime_info
.get_session_info_by_index(ctx.sender(), relay_parent, session_index)
Expand All @@ -87,6 +87,24 @@ impl<'a> CandidateEnvironment<'a> {
Err(_) => return None,
};

let n_validators = session.validators.len();
let byzantine_threshold = polkadot_primitives::byzantine_threshold(n_validators);
// combine on-chain with off-chain disabled validators
// process disabled validators in the following order:
// - on-chain disabled validators
// - prioritized order of off-chain disabled validators
// deduplicate the list and take at most `byzantine_threshold` validators
let disabled_indices = {
let mut d: HashSet<ValidatorIndex> = HashSet::new();
for v in disabled_onchain.into_iter().chain(disabled_offchain.into_iter()) {
if d.len() == byzantine_threshold {
break
}
d.insert(v);
}
d
};

let controlled_indices = find_controlled_validator_indices(keystore, &session.validators);
Some(Self { session_index, session, executor_params, controlled_indices, disabled_indices })
}
Expand Down Expand Up @@ -116,7 +134,7 @@ impl<'a> CandidateEnvironment<'a> {
&self.controlled_indices
}

/// Indices of disabled validators at the `relay_parent`.
/// Indices of off-chain and on-chain disabled validators.
pub fn disabled_indices(&'a self) -> &'a HashSet<ValidatorIndex> {
&self.disabled_indices
}
Expand Down Expand Up @@ -237,13 +255,19 @@ impl CandidateVoteState<CandidateVotes> {

let supermajority_threshold = polkadot_primitives::supermajority_threshold(n_validators);

// We have a dispute, if we have votes on both sides:
let is_disputed = !votes.invalid.is_empty() && !votes.valid.raw().is_empty();
// We have a dispute, if we have votes on both sides, with at least one invalid vote
// from non-disabled validator or with votes on both sides and confirmed.
let has_non_disabled_invalid_votes =
votes.invalid.keys().any(|i| !env.disabled_indices().contains(i));
let byzantine_threshold = polkadot_primitives::byzantine_threshold(n_validators);
let votes_on_both_sides = !votes.valid.raw().is_empty() && !votes.invalid.is_empty();
let is_confirmed =
votes_on_both_sides && (votes.voted_indices().len() > byzantine_threshold);
let is_disputed =
is_confirmed || (has_non_disabled_invalid_votes && !votes.valid.raw().is_empty());

let (dispute_status, byzantine_threshold_against) = if is_disputed {
let mut status = DisputeStatus::active();
let byzantine_threshold = polkadot_primitives::byzantine_threshold(n_validators);
let is_confirmed = votes.voted_indices().len() > byzantine_threshold;
if is_confirmed {
status = status.confirm();
};
Expand Down
34 changes: 7 additions & 27 deletions polkadot/node/core/dispute-coordinator/src/initialized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
//! Dispute coordinator subsystem in initialized state (after first active leaf is received).

use std::{
collections::{BTreeMap, HashSet, VecDeque},
collections::{BTreeMap, VecDeque},
sync::Arc,
};

Expand Down Expand Up @@ -970,6 +970,7 @@ impl Initialized {
&mut self.runtime_info,
session,
relay_parent,
self.offchain_disabled_validators.iter(session),
)
.await
{
Expand Down Expand Up @@ -1099,36 +1100,14 @@ impl Initialized {

let new_state = import_result.new_state();

let byzantine_threshold = polkadot_primitives::byzantine_threshold(n_validators);
// combine on-chain with off-chain disabled validators
// process disabled validators in the following order:
// - on-chain disabled validators
// - prioritized order of off-chain disabled validators
// deduplicate the list and take at most `byzantine_threshold` validators
let disabled_validators = {
let mut d: HashSet<ValidatorIndex> = HashSet::new();
for v in env
.disabled_indices()
.iter()
.cloned()
.chain(self.offchain_disabled_validators.iter(session))
{
if d.len() == byzantine_threshold {
break
}
d.insert(v);
}
d
};

let is_included = self.scraper.is_candidate_included(&candidate_hash);
let is_backed = self.scraper.is_candidate_backed(&candidate_hash);
let own_vote_missing = new_state.own_vote_missing();
let is_disputed = new_state.is_disputed();
let is_confirmed = new_state.is_confirmed();
let potential_spam = is_potential_spam(&self.scraper, &new_state, &candidate_hash, |v| {
disabled_validators.contains(v)
});
let is_disabled = |v: &ValidatorIndex| env.disabled_indices().contains(v);
let potential_spam =
is_potential_spam(&self.scraper, &new_state, &candidate_hash, is_disabled);
let allow_participation = !potential_spam;

gum::trace!(
Expand All @@ -1139,7 +1118,7 @@ impl Initialized {
?candidate_hash,
confirmed = ?new_state.is_confirmed(),
has_invalid_voters = ?!import_result.new_invalid_voters().is_empty(),
n_disabled_validators = ?disabled_validators.len(),
n_disabled_validators = ?env.disabled_indices().len(),
"Is spam?"
);

Expand Down Expand Up @@ -1439,6 +1418,7 @@ impl Initialized {
&mut self.runtime_info,
session,
candidate_receipt.descriptor.relay_parent,
self.offchain_disabled_validators.iter(session),
)
.await
{
Expand Down
9 changes: 5 additions & 4 deletions polkadot/node/core/dispute-coordinator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ impl DisputeCoordinatorSubsystem {
runtime_info,
highest_session,
leaf_hash,
// on startup we don't have any off-chain disabled state
std::iter::empty(),
)
.await
{
Expand Down Expand Up @@ -370,10 +372,9 @@ impl DisputeCoordinatorSubsystem {
},
};
let vote_state = CandidateVoteState::new(votes, &env, now);
let onchain_disabled = env.disabled_indices();
let potential_spam = is_potential_spam(&scraper, &vote_state, candidate_hash, |v| {
onchain_disabled.contains(v)
});
let is_disabled = |v: &ValidatorIndex| env.disabled_indices().contains(v);
let potential_spam =
is_potential_spam(&scraper, &vote_state, candidate_hash, is_disabled);
let is_included =
scraper.is_candidate_included(&vote_state.votes().candidate_receipt.hash());

Expand Down
45 changes: 35 additions & 10 deletions polkadot/node/core/dispute-coordinator/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2645,14 +2645,23 @@ fn participation_with_onchain_disabling_unconfirmed() {
.await;

handle_disabled_validators_queries(&mut virtual_overseer, vec![disabled_index]).await;
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash, HashMap::new())
.await;

assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));

// we should not participate due to disabled indices on chain
assert!(virtual_overseer.recv().timeout(TEST_TIMEOUT).await.is_none());

{
// make sure the dispute is not active
let (tx, rx) = oneshot::channel();
virtual_overseer
.send(FromOrchestra::Communication {
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
})
.await;

assert_eq!(rx.await.unwrap().len(), 0);
}

// Scenario 2: unconfirmed dispute with non-disabled validator against.
// Expectation: even if the dispute is unconfirmed, we should participate
// once we receive an invalid vote from a non-disabled validator.
Expand All @@ -2679,6 +2688,9 @@ fn participation_with_onchain_disabling_unconfirmed() {
})
.await;

handle_approval_vote_request(&mut virtual_overseer, &candidate_hash, HashMap::new())
.await;

assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));

participation_with_distribution(
Expand Down Expand Up @@ -2710,7 +2722,7 @@ fn participation_with_onchain_disabling_unconfirmed() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.raw().len(), 2); // 3+1 => we have participated
assert_eq!(votes.valid.raw().len(), 2); // 1+1 => we have participated
assert_eq!(votes.invalid.len(), 2);
}

Expand Down Expand Up @@ -2832,6 +2844,7 @@ fn participation_with_onchain_disabling_confirmed() {

#[test]
fn participation_with_offchain_disabling() {
sp_tracing::init_for_tests();
test_harness(|mut test_state, mut virtual_overseer| {
Box::pin(async move {
let session = 1;
Expand Down Expand Up @@ -2968,17 +2981,23 @@ fn participation_with_offchain_disabling() {
// let's disable validators 3, 4 on chain, but this should not affect this import
let disabled_validators = vec![ValidatorIndex(3), ValidatorIndex(4)];
handle_disabled_validators_queries(&mut virtual_overseer, disabled_validators).await;
handle_approval_vote_request(
&mut virtual_overseer,
&another_candidate_hash,
HashMap::new(),
)
.await;
assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));

// we should not participate since due to offchain disabling
assert!(virtual_overseer.recv().timeout(TEST_TIMEOUT).await.is_none());

{
// make sure the new dispute is not active
let (tx, rx) = oneshot::channel();
virtual_overseer
.send(FromOrchestra::Communication {
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
})
.await;

assert_eq!(rx.await.unwrap().len(), 1);
}

// now import enough votes for dispute confirmation
// even though all of these votes are from (on chain) disabled validators
let mut statements = Vec::new();
Expand Down Expand Up @@ -3007,6 +3026,12 @@ fn participation_with_offchain_disabling() {
})
.await;

handle_approval_vote_request(
&mut virtual_overseer,
&another_candidate_hash,
HashMap::new(),
)
.await;
assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));

participation_with_distribution(
Expand Down
11 changes: 11 additions & 0 deletions prdoc/pr_3358.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title: Do not stall finality on spam disputes

doc:
- audience: Node Operator
description: |
This PR fixes the issue that periodically caused
finality stalls on Kusama due to disputes happening
there in combination with disputes spam protection mechanism.
See: https://github.com/paritytech/polkadot-sdk/issues/3345

crates: [ ]

0 comments on commit 32f9442

Please sign in to comment.