Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

do not block finality for "disabled" disputes #3358

Merged
merged 15 commits into from
Feb 17, 2024
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());
Overkillus marked this conversation as resolved.
Show resolved Hide resolved

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused. We are disabling validators 3 and 4, but the votes are coming from 1 and 2? Ok, so this should not affect the import, make sense, but why should it affect participation further down?

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
10 changes: 10 additions & 0 deletions prdoc/pr_3358.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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.
ordian marked this conversation as resolved.
Show resolved Hide resolved

crates: [ ]
Loading