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>
This commit is contained in:
eskimor
2022-11-14 12:41:45 +01:00
committed by GitHub
parent e4eb6266f8
commit 97dddbab93
5 changed files with 166 additions and 107 deletions
@@ -28,7 +28,7 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use polkadot_node_primitives::{CandidateVotes, SignedDisputeStatement}; use polkadot_node_primitives::{CandidateVotes, DisputeStatus, SignedDisputeStatement, Timestamp};
use polkadot_node_subsystem_util::rolling_session_window::RollingSessionWindow; use polkadot_node_subsystem_util::rolling_session_window::RollingSessionWindow;
use polkadot_primitives::v2::{ use polkadot_primitives::v2::{
CandidateReceipt, DisputeStatement, IndexedVec, SessionIndex, SessionInfo, CandidateReceipt, DisputeStatement, IndexedVec, SessionIndex, SessionInfo,
@@ -154,22 +154,8 @@ pub struct CandidateVoteState<Votes> {
/// Information about own votes: /// Information about own votes:
own_vote: OwnVoteState, own_vote: OwnVoteState,
/// Whether or not the dispute concluded invalid. /// Current dispute status, if there is any.
concluded_invalid: bool, dispute_status: Option<DisputeStatus>,
/// Whether or not the dispute concluded valid.
///
/// Note: Due to equivocations it is technically possible for a dispute to conclude both valid
/// and invalid. In that case the invalid result takes precedence.
concluded_valid: bool,
/// There is an ongoing dispute and we reached f+1 votes -> the dispute is confirmed
///
/// as at least one honest validator cast a vote for the candidate.
is_confirmed: bool,
/// Whether or not we have an ongoing dispute.
is_disputed: bool,
} }
impl CandidateVoteState<CandidateVotes> { impl CandidateVoteState<CandidateVotes> {
@@ -179,18 +165,11 @@ impl CandidateVoteState<CandidateVotes> {
pub fn new_from_receipt(candidate_receipt: CandidateReceipt) -> Self { pub fn new_from_receipt(candidate_receipt: CandidateReceipt) -> Self {
let votes = let votes =
CandidateVotes { candidate_receipt, valid: BTreeMap::new(), invalid: BTreeMap::new() }; CandidateVotes { candidate_receipt, valid: BTreeMap::new(), invalid: BTreeMap::new() };
Self { Self { votes, own_vote: OwnVoteState::NoVote, dispute_status: None }
votes,
own_vote: OwnVoteState::NoVote,
concluded_invalid: false,
concluded_valid: false,
is_confirmed: false,
is_disputed: false,
}
} }
/// Create a new `CandidateVoteState` from already existing votes. /// Create a new `CandidateVoteState` from already existing votes.
pub fn new<'a>(votes: CandidateVotes, env: &CandidateEnvironment<'a>) -> Self { pub fn new<'a>(votes: CandidateVotes, env: &CandidateEnvironment<'a>, now: Timestamp) -> Self {
let own_vote = OwnVoteState::new(&votes, env); let own_vote = OwnVoteState::new(&votes, env);
let n_validators = env.validators().len(); let n_validators = env.validators().len();
@@ -198,16 +177,31 @@ impl CandidateVoteState<CandidateVotes> {
let supermajority_threshold = let supermajority_threshold =
polkadot_primitives::v2::supermajority_threshold(n_validators); polkadot_primitives::v2::supermajority_threshold(n_validators);
let concluded_invalid = votes.invalid.len() >= supermajority_threshold;
let concluded_valid = votes.valid.len() >= supermajority_threshold;
// We have a dispute, if we have votes on both sides: // We have a dispute, if we have votes on both sides:
let is_disputed = !votes.invalid.is_empty() && !votes.valid.is_empty(); let is_disputed = !votes.invalid.is_empty() && !votes.valid.is_empty();
let byzantine_threshold = polkadot_primitives::v2::byzantine_threshold(n_validators); let dispute_status = if is_disputed {
let is_confirmed = votes.voted_indices().len() > byzantine_threshold && is_disputed; let mut status = DisputeStatus::active();
let byzantine_threshold = polkadot_primitives::v2::byzantine_threshold(n_validators);
let is_confirmed = votes.voted_indices().len() > byzantine_threshold;
if is_confirmed {
status = status.confirm();
};
let concluded_for = votes.valid.len() >= supermajority_threshold;
if concluded_for {
status = status.conclude_for(now);
};
Self { votes, own_vote, concluded_invalid, concluded_valid, is_confirmed, is_disputed } let concluded_against = votes.invalid.len() >= supermajority_threshold;
if concluded_against {
status = status.conclude_against(now);
};
Some(status)
} else {
None
};
Self { votes, own_vote, dispute_status }
} }
/// Import fresh statements. /// Import fresh statements.
@@ -217,6 +211,7 @@ impl CandidateVoteState<CandidateVotes> {
self, self,
env: &CandidateEnvironment, env: &CandidateEnvironment,
statements: Vec<(SignedDisputeStatement, ValidatorIndex)>, statements: Vec<(SignedDisputeStatement, ValidatorIndex)>,
now: Timestamp,
) -> ImportResult { ) -> ImportResult {
let (mut votes, old_state) = self.into_old_state(); let (mut votes, old_state) = self.into_old_state();
@@ -294,7 +289,7 @@ impl CandidateVoteState<CandidateVotes> {
} }
} }
let new_state = Self::new(votes, env); let new_state = Self::new(votes, env, now);
ImportResult { ImportResult {
old_state, old_state,
@@ -313,32 +308,15 @@ impl CandidateVoteState<CandidateVotes> {
/// Extract `CandidateVotes` for handling import of new statements. /// Extract `CandidateVotes` for handling import of new statements.
fn into_old_state(self) -> (CandidateVotes, CandidateVoteState<()>) { fn into_old_state(self) -> (CandidateVotes, CandidateVoteState<()>) {
let CandidateVoteState { let CandidateVoteState { votes, own_vote, dispute_status } = self;
votes, (votes, CandidateVoteState { votes: (), own_vote, dispute_status })
own_vote,
concluded_invalid,
concluded_valid,
is_confirmed,
is_disputed,
} = self;
(
votes,
CandidateVoteState {
votes: (),
own_vote,
concluded_invalid,
concluded_valid,
is_confirmed,
is_disputed,
},
)
} }
} }
impl<V> CandidateVoteState<V> { impl<V> CandidateVoteState<V> {
/// Whether or not we have an ongoing dispute. /// Whether or not we have an ongoing dispute.
pub fn is_disputed(&self) -> bool { pub fn is_disputed(&self) -> bool {
self.is_disputed self.dispute_status.is_some()
} }
/// Whether there is an ongoing confirmed dispute. /// Whether there is an ongoing confirmed dispute.
@@ -346,7 +324,7 @@ impl<V> CandidateVoteState<V> {
/// This checks whether there is a dispute ongoing and we have more than byzantine threshold /// This checks whether there is a dispute ongoing and we have more than byzantine threshold
/// votes. /// votes.
pub fn is_confirmed(&self) -> bool { pub fn is_confirmed(&self) -> bool {
self.is_confirmed self.dispute_status.map_or(false, |s| s.is_confirmed_concluded())
} }
/// This machine already cast some vote in that dispute/for that candidate. /// This machine already cast some vote in that dispute/for that candidate.
@@ -359,14 +337,19 @@ impl<V> CandidateVoteState<V> {
self.own_vote.approval_votes() self.own_vote.approval_votes()
} }
/// Whether or not this dispute has already enough valid votes to conclude. /// Whether or not there is a dispute and it has already enough valid votes to conclude.
pub fn is_concluded_valid(&self) -> bool { pub fn has_concluded_for(&self) -> bool {
self.concluded_valid self.dispute_status.map_or(false, |s| s.has_concluded_for())
} }
/// Whether or not this dispute has already enough invalid votes to conclude. /// Whether or not there is a dispute and it has already enough invalid votes to conclude.
pub fn is_concluded_invalid(&self) -> bool { pub fn has_concluded_against(&self) -> bool {
self.concluded_invalid self.dispute_status.map_or(false, |s| s.has_concluded_against())
}
/// Get access to the dispute status, in case there is one.
pub fn dispute_status(&self) -> &Option<DisputeStatus> {
&self.dispute_status
} }
/// Access to underlying votes. /// Access to underlying votes.
@@ -451,18 +434,18 @@ impl ImportResult {
} }
/// Whether or not any dispute just concluded valid due to the import. /// Whether or not any dispute just concluded valid due to the import.
pub fn is_freshly_concluded_valid(&self) -> bool { pub fn is_freshly_concluded_for(&self) -> bool {
!self.old_state().is_concluded_valid() && self.new_state().is_concluded_valid() !self.old_state().has_concluded_for() && self.new_state().has_concluded_for()
} }
/// Whether or not any dispute just concluded invalid due to the import. /// Whether or not any dispute just concluded invalid due to the import.
pub fn is_freshly_concluded_invalid(&self) -> bool { pub fn is_freshly_concluded_against(&self) -> bool {
!self.old_state().is_concluded_invalid() && self.new_state().is_concluded_invalid() !self.old_state().has_concluded_against() && self.new_state().has_concluded_against()
} }
/// Whether or not any dispute just concluded either invalid or valid due to the import. /// Whether or not any dispute just concluded either invalid or valid due to the import.
pub fn is_freshly_concluded(&self) -> bool { pub fn is_freshly_concluded(&self) -> bool {
self.is_freshly_concluded_invalid() || self.is_freshly_concluded_valid() self.is_freshly_concluded_against() || self.is_freshly_concluded_for()
} }
/// Modify this `ImportResult`s, by importing additional approval votes. /// Modify this `ImportResult`s, by importing additional approval votes.
@@ -473,6 +456,7 @@ impl ImportResult {
self, self,
env: &CandidateEnvironment, env: &CandidateEnvironment,
approval_votes: HashMap<ValidatorIndex, ValidatorSignature>, approval_votes: HashMap<ValidatorIndex, ValidatorSignature>,
now: Timestamp,
) -> Self { ) -> Self {
let Self { let Self {
old_state, old_state,
@@ -508,7 +492,7 @@ impl ImportResult {
} }
} }
let new_state = CandidateVoteState::new(votes, env); let new_state = CandidateVoteState::new(votes, env, now);
Self { Self {
old_state, old_state,
@@ -748,7 +748,7 @@ impl Initialized {
.load_candidate_votes(session, &candidate_hash)? .load_candidate_votes(session, &candidate_hash)?
.map(CandidateVotes::from) .map(CandidateVotes::from)
{ {
Some(votes) => CandidateVoteState::new(votes, &env), Some(votes) => CandidateVoteState::new(votes, &env, now),
None => None =>
if let MaybeCandidateReceipt::Provides(candidate_receipt) = candidate_receipt { if let MaybeCandidateReceipt::Provides(candidate_receipt) = candidate_receipt {
CandidateVoteState::new_from_receipt(candidate_receipt) CandidateVoteState::new_from_receipt(candidate_receipt)
@@ -766,7 +766,7 @@ impl Initialized {
gum::trace!(target: LOG_TARGET, ?candidate_hash, ?session, "Loaded votes"); gum::trace!(target: LOG_TARGET, ?candidate_hash, ?session, "Loaded votes");
let import_result = { let import_result = {
let intermediate_result = old_state.import_statements(&env, statements); let intermediate_result = old_state.import_statements(&env, statements, now);
// Handle approval vote import: // Handle approval vote import:
// //
@@ -803,7 +803,7 @@ impl Initialized {
); );
intermediate_result intermediate_result
}, },
Ok(votes) => intermediate_result.import_approval_votes(&env, votes), Ok(votes) => intermediate_result.import_approval_votes(&env, votes, now),
} }
} else { } else {
gum::trace!( gum::trace!(
@@ -977,43 +977,34 @@ impl Initialized {
} }
// All good, update recent disputes if state has changed: // All good, update recent disputes if state has changed:
if import_result.dispute_state_changed() { if let Some(new_status) = new_state.dispute_status() {
let mut recent_disputes = overlay_db.load_recent_disputes()?.unwrap_or_default(); // Only bother with db access, if there was an actual change.
if import_result.dispute_state_changed() {
let mut recent_disputes = overlay_db.load_recent_disputes()?.unwrap_or_default();
let status = recent_disputes.entry((session, candidate_hash)).or_insert_with(|| { let status =
gum::info!( recent_disputes.entry((session, candidate_hash)).or_insert_with(|| {
gum::info!(
target: LOG_TARGET,
?candidate_hash,
session,
"New dispute initiated for candidate.",
);
DisputeStatus::active()
});
*status = *new_status;
gum::trace!(
target: LOG_TARGET, target: LOG_TARGET,
?candidate_hash, ?candidate_hash,
session, ?status,
"New dispute initiated for candidate.", has_concluded_for = ?new_state.has_concluded_for(),
has_concluded_against = ?new_state.has_concluded_against(),
"Writing recent disputes with updates for candidate"
); );
DisputeStatus::active() overlay_db.write_recent_disputes(recent_disputes);
});
if new_state.is_confirmed() {
*status = status.confirm();
} }
// Note: concluded-invalid overwrites concluded-valid,
// so we do this check first. Dispute state machine is
// non-commutative.
if new_state.is_concluded_valid() {
*status = status.concluded_for(now);
}
if new_state.is_concluded_invalid() {
*status = status.concluded_against(now);
}
gum::trace!(
target: LOG_TARGET,
?candidate_hash,
?status,
is_concluded_valid = ?new_state.is_concluded_valid(),
is_concluded_invalid = ?new_state.is_concluded_invalid(),
"Writing recent disputes with updates for candidate"
);
overlay_db.write_recent_disputes(recent_disputes);
} }
// Update metrics: // Update metrics:
@@ -1036,7 +1027,7 @@ impl Initialized {
); );
self.metrics.on_approval_votes(import_result.imported_approval_votes()); self.metrics.on_approval_votes(import_result.imported_approval_votes());
if import_result.is_freshly_concluded_valid() { if import_result.is_freshly_concluded_for() {
gum::info!( gum::info!(
target: LOG_TARGET, target: LOG_TARGET,
?candidate_hash, ?candidate_hash,
@@ -1045,7 +1036,7 @@ impl Initialized {
); );
self.metrics.on_concluded_valid(); self.metrics.on_concluded_valid();
} }
if import_result.is_freshly_concluded_invalid() { if import_result.is_freshly_concluded_against() {
gum::info!( gum::info!(
target: LOG_TARGET, target: LOG_TARGET,
?candidate_hash, ?candidate_hash,
@@ -2917,6 +2917,68 @@ fn redundant_votes_ignored() {
}); });
} }
#[test]
/// Make sure no disputes are recorded when there are no opposing votes, even if we reached supermajority.
fn no_onesided_disputes() {
test_harness(|mut test_state, mut virtual_overseer| {
Box::pin(async move {
let session = 1;
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
.await;
let mut statements = Vec::new();
for index in 1..10 {
statements.push((
test_state
.issue_backing_statement_with_index(
ValidatorIndex(index),
candidate_hash,
session,
)
.await,
ValidatorIndex(index),
));
}
let (pending_confirmation, confirmation_rx) = oneshot::channel();
virtual_overseer
.send(FromOrchestra::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_receipt: candidate_receipt.clone(),
session,
statements,
pending_confirmation: Some(pending_confirmation),
},
})
.await;
assert_matches!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));
// We should not have any active disputes now.
let (tx, rx) = oneshot::channel();
virtual_overseer
.send(FromOrchestra::Communication {
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
})
.await;
assert!(rx.await.unwrap().is_empty());
virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
// No more messages expected:
assert!(virtual_overseer.try_recv().await.is_none());
test_state
})
});
}
#[test] #[test]
fn refrain_from_participation() { fn refrain_from_participation() {
test_harness(|mut test_state, mut virtual_overseer| { test_harness(|mut test_state, mut virtual_overseer| {
@@ -33,6 +33,9 @@ use polkadot_primitives::v2::{
/// ///
/// And most likely has been constructed correctly. This is used with /// And most likely has been constructed correctly. This is used with
/// `DisputeDistributionMessage::SendDispute` for sending out votes. /// `DisputeDistributionMessage::SendDispute` for sending out votes.
///
/// NOTE: This is sent over the wire, any changes are a change in protocol and need to be
/// versioned.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DisputeMessage(UncheckedDisputeMessage); pub struct DisputeMessage(UncheckedDisputeMessage);
@@ -19,8 +19,12 @@ use parity_scale_codec::{Decode, Encode};
/// Timestamp based on the 1 Jan 1970 UNIX base, which is persistent across node restarts and OS reboots. /// Timestamp based on the 1 Jan 1970 UNIX base, which is persistent across node restarts and OS reboots.
pub type Timestamp = u64; pub type Timestamp = u64;
/// The status of dispute. This is a state machine which can be altered by the /// The status of dispute.
/// helper methods. ///
/// As managed by the dispute coordinator.
///
/// NOTE: This status is persisted to the database, any changes have to be versioned and a db
/// migration will be needed.
#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq)] #[derive(Debug, Clone, Copy, Encode, Decode, PartialEq)]
pub enum DisputeStatus { pub enum DisputeStatus {
/// The dispute is active and unconcluded. /// The dispute is active and unconcluded.
@@ -69,9 +73,24 @@ impl DisputeStatus {
} }
} }
/// Concluded valid?
pub fn has_concluded_for(&self) -> bool {
match self {
&DisputeStatus::ConcludedFor(_) => true,
_ => false,
}
}
/// Concluded invalid?
pub fn has_concluded_against(&self) -> bool {
match self {
&DisputeStatus::ConcludedAgainst(_) => true,
_ => false,
}
}
/// Transition the status to a new status after observing the dispute has concluded for the candidate. /// Transition the status to a new status after observing the dispute has concluded for the candidate.
/// This may be a no-op if the status was already concluded. /// This may be a no-op if the status was already concluded.
pub fn concluded_for(self, now: Timestamp) -> DisputeStatus { pub fn conclude_for(self, now: Timestamp) -> DisputeStatus {
match self { match self {
DisputeStatus::Active | DisputeStatus::Confirmed => DisputeStatus::ConcludedFor(now), DisputeStatus::Active | DisputeStatus::Confirmed => DisputeStatus::ConcludedFor(now),
DisputeStatus::ConcludedFor(at) => DisputeStatus::ConcludedFor(std::cmp::min(at, now)), DisputeStatus::ConcludedFor(at) => DisputeStatus::ConcludedFor(std::cmp::min(at, now)),
@@ -81,7 +100,7 @@ impl DisputeStatus {
/// Transition the status to a new status after observing the dispute has concluded against the candidate. /// Transition the status to a new status after observing the dispute has concluded against the candidate.
/// This may be a no-op if the status was already concluded. /// This may be a no-op if the status was already concluded.
pub fn concluded_against(self, now: Timestamp) -> DisputeStatus { pub fn conclude_against(self, now: Timestamp) -> DisputeStatus {
match self { match self {
DisputeStatus::Active | DisputeStatus::Confirmed => DisputeStatus::Active | DisputeStatus::Confirmed =>
DisputeStatus::ConcludedAgainst(now), DisputeStatus::ConcludedAgainst(now),