mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-01 10:07:56 +00:00
Fix cycle dispute-coordinator <-> dispute-distribution (#6489)
* First iteration of message sender. * dyn Fn variant (no cloning) * Full implementation + Clone, without allocs on `Send` * Further clarifications/cleanup. * MessageSender -> NestingSender * Doc update/clarification. * dispute-coordinator: Send disputes on startup. + Some fixes, cleanup. * Fix whitespace. * Dispute distribution fixes, cleanup. * Cargo.lock * Fix spaces. * More format fixes. What is cargo fmt doing actually? * More fmt fixes. * Fix nesting sender. * Fixes. * Whitespace * Enable logging. * Guide update. * Fmt fixes, typos. * Remove unused function. * Simplifications, doc fixes. * Update roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md Co-authored-by: Marcin S. <marcin@bytedude.com> * Fmt + doc example fix. Co-authored-by: eskimor <eskimor@no-such-url.com> Co-authored-by: Marcin S. <marcin@bytedude.com>
This commit is contained in:
@@ -31,6 +31,7 @@ assert_matches = "1.4.0"
|
||||
test-helpers = { package = "polkadot-primitives-test-helpers", path = "../../../primitives/test-helpers" }
|
||||
futures-timer = "3.0.2"
|
||||
sp-application-crypto = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
sp-tracing = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
|
||||
[features]
|
||||
# If not enabled, the dispute coordinator will do nothing.
|
||||
|
||||
@@ -87,60 +87,69 @@ impl<'a> CandidateEnvironment<'a> {
|
||||
|
||||
/// Whether or not we already issued some statement about a candidate.
|
||||
pub enum OwnVoteState {
|
||||
/// We already voted/issued a statement for the candidate.
|
||||
Voted,
|
||||
/// We already voted/issued a statement for the candidate and it was an approval vote.
|
||||
/// Our votes, if any.
|
||||
Voted(Vec<(ValidatorIndex, (DisputeStatement, ValidatorSignature))>),
|
||||
|
||||
/// We are not a parachain validator in the session.
|
||||
///
|
||||
/// Needs special treatment as we have to make sure to propagate it to peers, to guarantee the
|
||||
/// dispute can conclude.
|
||||
VotedApproval(Vec<(ValidatorIndex, ValidatorSignature)>),
|
||||
/// We not yet voted for the dispute.
|
||||
NoVote,
|
||||
/// Hence we cannot vote.
|
||||
CannotVote,
|
||||
}
|
||||
|
||||
impl OwnVoteState {
|
||||
fn new<'a>(votes: &CandidateVotes, env: &CandidateEnvironment<'a>) -> Self {
|
||||
let mut our_valid_votes = env
|
||||
.controlled_indices()
|
||||
let controlled_indices = env.controlled_indices();
|
||||
if controlled_indices.is_empty() {
|
||||
return Self::CannotVote
|
||||
}
|
||||
|
||||
let our_valid_votes = controlled_indices
|
||||
.iter()
|
||||
.filter_map(|i| votes.valid.raw().get_key_value(i))
|
||||
.peekable();
|
||||
let mut our_invalid_votes =
|
||||
env.controlled_indices.iter().filter_map(|i| votes.invalid.get_key_value(i));
|
||||
let has_valid_votes = our_valid_votes.peek().is_some();
|
||||
let has_invalid_votes = our_invalid_votes.next().is_some();
|
||||
let our_approval_votes: Vec<_> = our_valid_votes
|
||||
.filter_map(|(index, (k, sig))| {
|
||||
if let ValidDisputeStatementKind::ApprovalChecking = k {
|
||||
Some((*index, sig.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.map(|(index, (kind, sig))| (*index, (DisputeStatement::Valid(*kind), sig.clone())));
|
||||
let our_invalid_votes = controlled_indices
|
||||
.iter()
|
||||
.filter_map(|i| votes.invalid.get_key_value(i))
|
||||
.map(|(index, (kind, sig))| (*index, (DisputeStatement::Invalid(*kind), sig.clone())));
|
||||
|
||||
if !our_approval_votes.is_empty() {
|
||||
return Self::VotedApproval(our_approval_votes)
|
||||
}
|
||||
if has_valid_votes || has_invalid_votes {
|
||||
return Self::Voted
|
||||
}
|
||||
Self::NoVote
|
||||
Self::Voted(our_valid_votes.chain(our_invalid_votes).collect())
|
||||
}
|
||||
|
||||
/// Whether or not we issued a statement for the candidate already.
|
||||
fn voted(&self) -> bool {
|
||||
/// Is a vote from us missing but we are a validator able to vote?
|
||||
fn vote_missing(&self) -> bool {
|
||||
match self {
|
||||
Self::Voted | Self::VotedApproval(_) => true,
|
||||
Self::NoVote => false,
|
||||
Self::Voted(votes) if votes.is_empty() => true,
|
||||
Self::Voted(_) | Self::CannotVote => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get own approval votes, if any.
|
||||
fn approval_votes(&self) -> Option<&Vec<(ValidatorIndex, ValidatorSignature)>> {
|
||||
///
|
||||
/// Empty iterator means, no approval votes. `None` means, there will never be any (we cannot
|
||||
/// vote).
|
||||
fn approval_votes(
|
||||
&self,
|
||||
) -> Option<impl Iterator<Item = (ValidatorIndex, &ValidatorSignature)>> {
|
||||
match self {
|
||||
Self::VotedApproval(votes) => Some(&votes),
|
||||
_ => None,
|
||||
Self::Voted(votes) => Some(votes.iter().filter_map(|(index, (kind, sig))| {
|
||||
if let DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking) = kind {
|
||||
Some((*index, sig))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
Self::CannotVote => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get our votes if there are any.
|
||||
///
|
||||
/// Empty iterator means, no votes. `None` means, there will never be any (we cannot
|
||||
/// vote).
|
||||
fn votes(&self) -> Option<&Vec<(ValidatorIndex, (DisputeStatement, ValidatorSignature))>> {
|
||||
match self {
|
||||
Self::Voted(votes) => Some(&votes),
|
||||
Self::CannotVote => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +179,7 @@ impl CandidateVoteState<CandidateVotes> {
|
||||
valid: ValidCandidateVotes::new(),
|
||||
invalid: BTreeMap::new(),
|
||||
};
|
||||
Self { votes, own_vote: OwnVoteState::NoVote, dispute_status: None }
|
||||
Self { votes, own_vote: OwnVoteState::CannotVote, dispute_status: None }
|
||||
}
|
||||
|
||||
/// Create a new `CandidateVoteState` from already existing votes.
|
||||
@@ -327,16 +336,25 @@ impl<V> CandidateVoteState<V> {
|
||||
self.dispute_status.map_or(false, |s| s.is_confirmed_concluded())
|
||||
}
|
||||
|
||||
/// This machine already cast some vote in that dispute/for that candidate.
|
||||
pub fn has_own_vote(&self) -> bool {
|
||||
self.own_vote.voted()
|
||||
/// Are we a validator in the session, but have not yet voted?
|
||||
pub fn own_vote_missing(&self) -> bool {
|
||||
self.own_vote.vote_missing()
|
||||
}
|
||||
|
||||
/// Own approval votes if any:
|
||||
pub fn own_approval_votes(&self) -> Option<&Vec<(ValidatorIndex, ValidatorSignature)>> {
|
||||
pub fn own_approval_votes(
|
||||
&self,
|
||||
) -> Option<impl Iterator<Item = (ValidatorIndex, &ValidatorSignature)>> {
|
||||
self.own_vote.approval_votes()
|
||||
}
|
||||
|
||||
/// Get own votes if there are any.
|
||||
pub fn own_votes(
|
||||
&self,
|
||||
) -> Option<&Vec<(ValidatorIndex, (DisputeStatement, ValidatorSignature))>> {
|
||||
self.own_vote.votes()
|
||||
}
|
||||
|
||||
/// Whether or not there is a dispute and it has already enough valid votes to conclude.
|
||||
pub fn has_concluded_for(&self) -> bool {
|
||||
self.dispute_status.map_or(false, |s| s.has_concluded_for())
|
||||
|
||||
@@ -26,8 +26,7 @@ use futures::{
|
||||
use sc_keystore::LocalKeystore;
|
||||
|
||||
use polkadot_node_primitives::{
|
||||
disputes::ValidCandidateVotes, CandidateVotes, DisputeMessage, DisputeMessageCheckError,
|
||||
DisputeStatus, SignedDisputeStatement, Timestamp,
|
||||
disputes::ValidCandidateVotes, CandidateVotes, DisputeStatus, SignedDisputeStatement, Timestamp,
|
||||
};
|
||||
use polkadot_node_subsystem::{
|
||||
messages::{
|
||||
@@ -48,6 +47,7 @@ use polkadot_primitives::v2::{
|
||||
use crate::{
|
||||
error::{log_error, Error, FatalError, FatalResult, JfyiError, JfyiResult, Result},
|
||||
import::{CandidateEnvironment, CandidateVoteState},
|
||||
is_potential_spam,
|
||||
metrics::Metrics,
|
||||
status::{get_active_with_status, Clock},
|
||||
DisputeCoordinatorSubsystem, LOG_TARGET,
|
||||
@@ -55,7 +55,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
backend::Backend,
|
||||
db,
|
||||
db, make_dispute_message,
|
||||
participation::{
|
||||
self, Participation, ParticipationPriority, ParticipationRequest, ParticipationStatement,
|
||||
WorkerMessageReceiver,
|
||||
@@ -396,19 +396,19 @@ impl Initialized {
|
||||
CompactStatement::Valid(_) =>
|
||||
ValidDisputeStatementKind::BackingValid(relay_parent),
|
||||
};
|
||||
debug_assert!(
|
||||
SignedDisputeStatement::new_checked(
|
||||
debug_assert!(
|
||||
SignedDisputeStatement::new_checked(
|
||||
DisputeStatement::Valid(valid_statement_kind),
|
||||
candidate_hash,
|
||||
session,
|
||||
validator_public.clone(),
|
||||
validator_signature.clone(),
|
||||
).is_ok(),
|
||||
"Scraped backing votes had invalid signature! candidate: {:?}, session: {:?}, validator_public: {:?}",
|
||||
candidate_hash,
|
||||
session,
|
||||
validator_public,
|
||||
);
|
||||
).is_ok(),
|
||||
"Scraped backing votes had invalid signature! candidate: {:?}, session: {:?}, validator_public: {:?}",
|
||||
candidate_hash,
|
||||
session,
|
||||
validator_public,
|
||||
);
|
||||
let signed_dispute_statement =
|
||||
SignedDisputeStatement::new_unchecked_from_trusted_source(
|
||||
DisputeStatement::Valid(valid_statement_kind),
|
||||
@@ -492,20 +492,20 @@ impl Initialized {
|
||||
})
|
||||
.cloned()?;
|
||||
|
||||
debug_assert!(
|
||||
SignedDisputeStatement::new_checked(
|
||||
debug_assert!(
|
||||
SignedDisputeStatement::new_checked(
|
||||
dispute_statement.clone(),
|
||||
candidate_hash,
|
||||
session,
|
||||
validator_public.clone(),
|
||||
validator_signature.clone(),
|
||||
).is_ok(),
|
||||
"Scraped dispute votes had invalid signature! candidate: {:?}, session: {:?}, dispute_statement: {:?}, validator_public: {:?}",
|
||||
candidate_hash,
|
||||
session,
|
||||
).is_ok(),
|
||||
"Scraped dispute votes had invalid signature! candidate: {:?}, session: {:?}, dispute_statement: {:?}, validator_public: {:?}",
|
||||
candidate_hash,
|
||||
session,
|
||||
dispute_statement,
|
||||
validator_public,
|
||||
);
|
||||
validator_public,
|
||||
);
|
||||
|
||||
Some((
|
||||
SignedDisputeStatement::new_unchecked_from_trusted_source(
|
||||
@@ -845,18 +845,16 @@ impl Initialized {
|
||||
|
||||
let is_included = self.scraper.is_candidate_included(&candidate_hash);
|
||||
let is_backed = self.scraper.is_candidate_backed(&candidate_hash);
|
||||
let has_own_vote = new_state.has_own_vote();
|
||||
let own_vote_missing = new_state.own_vote_missing();
|
||||
let is_disputed = new_state.is_disputed();
|
||||
let has_controlled_indices = !env.controlled_indices().is_empty();
|
||||
let is_confirmed = new_state.is_confirmed();
|
||||
let potential_spam =
|
||||
!is_included && !is_backed && !new_state.is_confirmed() && !new_state.has_own_vote();
|
||||
// We participate only in disputes which are included, backed or confirmed
|
||||
let allow_participation = is_included || is_backed || is_confirmed;
|
||||
let potential_spam = is_potential_spam(&self.scraper, &new_state, &candidate_hash);
|
||||
// We participate only in disputes which are not potential spam.
|
||||
let allow_participation = !potential_spam;
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
has_own_vote = ?new_state.has_own_vote(),
|
||||
?own_vote_missing,
|
||||
?potential_spam,
|
||||
?is_included,
|
||||
?candidate_hash,
|
||||
@@ -903,7 +901,7 @@ impl Initialized {
|
||||
// - `is_included` lands in prioritised queue
|
||||
// - `is_confirmed` | `is_backed` lands in best effort queue
|
||||
// We don't participate in disputes on finalized candidates.
|
||||
if !has_own_vote && is_disputed && has_controlled_indices && allow_participation {
|
||||
if own_vote_missing && is_disputed && allow_participation {
|
||||
let priority = ParticipationPriority::with_priority_if(is_included);
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
@@ -930,9 +928,8 @@ impl Initialized {
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
?is_confirmed,
|
||||
?has_own_vote,
|
||||
?own_vote_missing,
|
||||
?is_disputed,
|
||||
?has_controlled_indices,
|
||||
?allow_participation,
|
||||
?is_included,
|
||||
?is_backed,
|
||||
@@ -946,10 +943,9 @@ impl Initialized {
|
||||
|
||||
// Also send any already existing approval vote on new disputes:
|
||||
if import_result.is_freshly_disputed() {
|
||||
let no_votes = Vec::new();
|
||||
let our_approval_votes = new_state.own_approval_votes().unwrap_or(&no_votes);
|
||||
let our_approval_votes = new_state.own_approval_votes().into_iter().flatten();
|
||||
for (validator_index, sig) in our_approval_votes {
|
||||
let pub_key = match env.validators().get(*validator_index) {
|
||||
let pub_key = match env.validators().get(validator_index) {
|
||||
None => {
|
||||
gum::error!(
|
||||
target: LOG_TARGET,
|
||||
@@ -979,7 +975,7 @@ impl Initialized {
|
||||
env.session_info(),
|
||||
&new_state.votes(),
|
||||
statement,
|
||||
*validator_index,
|
||||
validator_index,
|
||||
) {
|
||||
Err(err) => {
|
||||
gum::error!(
|
||||
@@ -1150,9 +1146,9 @@ impl Initialized {
|
||||
Ok(None) => {},
|
||||
Err(e) => {
|
||||
gum::error!(
|
||||
target: LOG_TARGET,
|
||||
err = ?e,
|
||||
"Encountered keystore error while signing dispute statement",
|
||||
target: LOG_TARGET,
|
||||
err = ?e,
|
||||
"Encountered keystore error while signing dispute statement",
|
||||
);
|
||||
},
|
||||
}
|
||||
@@ -1251,74 +1247,6 @@ impl MaybeCandidateReceipt {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum DisputeMessageCreationError {
|
||||
#[error("There was no opposite vote available")]
|
||||
NoOppositeVote,
|
||||
#[error("Found vote had an invalid validator index that could not be found")]
|
||||
InvalidValidatorIndex,
|
||||
#[error("Statement found in votes had invalid signature.")]
|
||||
InvalidStoredStatement,
|
||||
#[error(transparent)]
|
||||
InvalidStatementCombination(DisputeMessageCheckError),
|
||||
}
|
||||
|
||||
fn make_dispute_message(
|
||||
info: &SessionInfo,
|
||||
votes: &CandidateVotes,
|
||||
our_vote: SignedDisputeStatement,
|
||||
our_index: ValidatorIndex,
|
||||
) -> std::result::Result<DisputeMessage, DisputeMessageCreationError> {
|
||||
let validators = &info.validators;
|
||||
|
||||
let (valid_statement, valid_index, invalid_statement, invalid_index) =
|
||||
if let DisputeStatement::Valid(_) = our_vote.statement() {
|
||||
let (validator_index, (statement_kind, validator_signature)) =
|
||||
votes.invalid.iter().next().ok_or(DisputeMessageCreationError::NoOppositeVote)?;
|
||||
let other_vote = SignedDisputeStatement::new_checked(
|
||||
DisputeStatement::Invalid(*statement_kind),
|
||||
*our_vote.candidate_hash(),
|
||||
our_vote.session_index(),
|
||||
validators
|
||||
.get(*validator_index)
|
||||
.ok_or(DisputeMessageCreationError::InvalidValidatorIndex)?
|
||||
.clone(),
|
||||
validator_signature.clone(),
|
||||
)
|
||||
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
|
||||
(our_vote, our_index, other_vote, *validator_index)
|
||||
} else {
|
||||
let (validator_index, (statement_kind, validator_signature)) = votes
|
||||
.valid
|
||||
.raw()
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or(DisputeMessageCreationError::NoOppositeVote)?;
|
||||
let other_vote = SignedDisputeStatement::new_checked(
|
||||
DisputeStatement::Valid(*statement_kind),
|
||||
*our_vote.candidate_hash(),
|
||||
our_vote.session_index(),
|
||||
validators
|
||||
.get(*validator_index)
|
||||
.ok_or(DisputeMessageCreationError::InvalidValidatorIndex)?
|
||||
.clone(),
|
||||
validator_signature.clone(),
|
||||
)
|
||||
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
|
||||
(other_vote, *validator_index, our_vote, our_index)
|
||||
};
|
||||
|
||||
DisputeMessage::from_signed_statements(
|
||||
valid_statement,
|
||||
valid_index,
|
||||
invalid_statement,
|
||||
invalid_index,
|
||||
votes.candidate_receipt.clone(),
|
||||
info,
|
||||
)
|
||||
.map_err(DisputeMessageCreationError::InvalidStatementCombination)
|
||||
}
|
||||
|
||||
/// Determine the best block and its block number.
|
||||
/// Assumes `block_descriptions` are sorted from the one
|
||||
/// with the lowest `BlockNumber` to the highest.
|
||||
|
||||
@@ -28,17 +28,21 @@ use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
|
||||
use gum::CandidateHash;
|
||||
use sc_keystore::LocalKeystore;
|
||||
|
||||
use polkadot_node_primitives::CandidateVotes;
|
||||
use polkadot_node_primitives::{
|
||||
CandidateVotes, DisputeMessage, DisputeMessageCheckError, SignedDisputeStatement,
|
||||
};
|
||||
use polkadot_node_subsystem::{
|
||||
overseer, ActivatedLeaf, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError,
|
||||
messages::DisputeDistributionMessage, overseer, ActivatedLeaf, FromOrchestra, OverseerSignal,
|
||||
SpawnedSubsystem, SubsystemError,
|
||||
};
|
||||
use polkadot_node_subsystem_util::{
|
||||
database::Database,
|
||||
rolling_session_window::{DatabaseParams, RollingSessionWindow},
|
||||
};
|
||||
use polkadot_primitives::v2::{ScrapedOnChainVotes, ValidatorIndex, ValidatorPair};
|
||||
use polkadot_primitives::v2::{DisputeStatement, ScrapedOnChainVotes, SessionInfo, ValidatorIndex};
|
||||
|
||||
use crate::{
|
||||
error::{FatalResult, JfyiError, Result},
|
||||
@@ -50,6 +54,7 @@ use db::v1::DbBackend;
|
||||
use fatality::Split;
|
||||
|
||||
use self::{
|
||||
import::{CandidateEnvironment, CandidateVoteState},
|
||||
participation::{ParticipationPriority, ParticipationRequest},
|
||||
spam_slots::{SpamSlots, UnconfirmedDisputes},
|
||||
};
|
||||
@@ -274,10 +279,13 @@ impl DisputeCoordinatorSubsystem {
|
||||
// Prune obsolete disputes:
|
||||
db::v1::note_earliest_session(overlay_db, rolling_session_window.earliest_session())?;
|
||||
|
||||
let now = clock.now();
|
||||
|
||||
let active_disputes = match overlay_db.load_recent_disputes() {
|
||||
Ok(Some(disputes)) =>
|
||||
get_active_with_status(disputes.into_iter(), clock.now()).collect(),
|
||||
Ok(None) => Vec::new(),
|
||||
Ok(disputes) => disputes
|
||||
.map(|disputes| get_active_with_status(disputes.into_iter(), now))
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
Err(e) => {
|
||||
gum::error!(target: LOG_TARGET, "Failed initial load of recent disputes: {:?}", e);
|
||||
return Err(e.into())
|
||||
@@ -285,9 +293,23 @@ impl DisputeCoordinatorSubsystem {
|
||||
};
|
||||
|
||||
let mut participation_requests = Vec::new();
|
||||
let mut unconfirmed_disputes: UnconfirmedDisputes = UnconfirmedDisputes::new();
|
||||
let mut spam_disputes: UnconfirmedDisputes = UnconfirmedDisputes::new();
|
||||
let (scraper, votes) = ChainScraper::new(ctx.sender(), initial_head).await?;
|
||||
for ((session, ref candidate_hash), status) in active_disputes {
|
||||
for ((session, ref candidate_hash), _) in active_disputes {
|
||||
let env =
|
||||
match CandidateEnvironment::new(&self.keystore, &rolling_session_window, session) {
|
||||
None => {
|
||||
gum::warn!(
|
||||
target: LOG_TARGET,
|
||||
session,
|
||||
"We are lacking a `SessionInfo` for handling db votes on startup."
|
||||
);
|
||||
|
||||
continue
|
||||
},
|
||||
Some(env) => env,
|
||||
};
|
||||
|
||||
let votes: CandidateVotes =
|
||||
match overlay_db.load_candidate_votes(session, candidate_hash) {
|
||||
Ok(Some(votes)) => votes.into(),
|
||||
@@ -301,60 +323,52 @@ impl DisputeCoordinatorSubsystem {
|
||||
continue
|
||||
},
|
||||
};
|
||||
let vote_state = CandidateVoteState::new(votes, &env, now);
|
||||
|
||||
let validators = match rolling_session_window.session_info(session) {
|
||||
None => {
|
||||
gum::warn!(
|
||||
let potential_spam = is_potential_spam(&scraper, &vote_state, candidate_hash);
|
||||
let is_included =
|
||||
scraper.is_candidate_included(&vote_state.votes().candidate_receipt.hash());
|
||||
|
||||
if potential_spam {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?session,
|
||||
?candidate_hash,
|
||||
"Found potential spam dispute on startup"
|
||||
);
|
||||
spam_disputes
|
||||
.insert((session, *candidate_hash), vote_state.votes().voted_indices());
|
||||
} else {
|
||||
// Participate if need be:
|
||||
if vote_state.own_vote_missing() {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
session,
|
||||
"Missing info for session which has an active dispute",
|
||||
?session,
|
||||
?candidate_hash,
|
||||
"Found valid dispute, with no vote from us on startup - participating."
|
||||
);
|
||||
continue
|
||||
},
|
||||
Some(info) => info.validators.clone(),
|
||||
};
|
||||
|
||||
let voted_indices = votes.voted_indices();
|
||||
|
||||
// Determine if there are any missing local statements for this dispute. Validators are
|
||||
// filtered if:
|
||||
// 1) their statement already exists, or
|
||||
// 2) the validator key is not in the local keystore (i.e. the validator is remote).
|
||||
// The remaining set only contains local validators that are also missing statements.
|
||||
let missing_local_statement = validators
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, validator)| (ValidatorIndex(index as _), validator))
|
||||
.any(|(index, validator)| {
|
||||
!voted_indices.contains(&index) &&
|
||||
self.keystore
|
||||
.key_pair::<ValidatorPair>(validator)
|
||||
.ok()
|
||||
.map_or(false, |v| v.is_some())
|
||||
});
|
||||
|
||||
let is_included = scraper.is_candidate_included(&votes.candidate_receipt.hash());
|
||||
|
||||
if !status.is_confirmed_concluded() && !is_included {
|
||||
unconfirmed_disputes.insert((session, *candidate_hash), voted_indices);
|
||||
}
|
||||
|
||||
// Participate for all non-concluded disputes which do not have a
|
||||
// recorded local statement.
|
||||
if missing_local_statement {
|
||||
participation_requests.push((
|
||||
ParticipationPriority::with_priority_if(is_included),
|
||||
ParticipationRequest::new(votes.candidate_receipt.clone(), session),
|
||||
));
|
||||
participation_requests.push((
|
||||
ParticipationPriority::with_priority_if(is_included),
|
||||
ParticipationRequest::new(
|
||||
vote_state.votes().candidate_receipt.clone(),
|
||||
session,
|
||||
),
|
||||
));
|
||||
}
|
||||
// Else make sure our own vote is distributed:
|
||||
else {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?session,
|
||||
?candidate_hash,
|
||||
"Found valid dispute, with vote from us on startup - send vote."
|
||||
);
|
||||
send_dispute_messages(ctx, &env, &vote_state).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
participation_requests,
|
||||
votes,
|
||||
SpamSlots::recover_from_state(unconfirmed_disputes),
|
||||
scraper,
|
||||
))
|
||||
Ok((participation_requests, votes, SpamSlots::recover_from_state(spam_disputes), scraper))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,3 +421,144 @@ async fn wait_for_first_leaf<Context>(ctx: &mut Context) -> Result<Option<Activa
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check wheter a dispute for the given candidate could be spam.
|
||||
///
|
||||
/// That is the candidate could be made up.
|
||||
pub fn is_potential_spam<V>(
|
||||
scraper: &ChainScraper,
|
||||
vote_state: &CandidateVoteState<V>,
|
||||
candidate_hash: &CandidateHash,
|
||||
) -> bool {
|
||||
let is_disputed = vote_state.is_disputed();
|
||||
let is_included = scraper.is_candidate_included(candidate_hash);
|
||||
let is_backed = scraper.is_candidate_backed(candidate_hash);
|
||||
let is_confirmed = vote_state.is_confirmed();
|
||||
|
||||
is_disputed && !is_included && !is_backed && !is_confirmed
|
||||
}
|
||||
|
||||
/// Tell dispute-distribution to send all our votes.
|
||||
///
|
||||
/// Should be called on startup for all active disputes where there are votes from us already.
|
||||
#[overseer::contextbounds(DisputeCoordinator, prefix = self::overseer)]
|
||||
async fn send_dispute_messages<Context>(
|
||||
ctx: &mut Context,
|
||||
env: &CandidateEnvironment<'_>,
|
||||
vote_state: &CandidateVoteState<CandidateVotes>,
|
||||
) {
|
||||
for own_vote in vote_state.own_votes().into_iter().flatten() {
|
||||
let (validator_index, (kind, sig)) = own_vote;
|
||||
let public_key = if let Some(key) = env.session_info().validators.get(*validator_index) {
|
||||
key.clone()
|
||||
} else {
|
||||
gum::error!(
|
||||
target: LOG_TARGET,
|
||||
?validator_index,
|
||||
session_index = ?env.session_index(),
|
||||
"Could not find our own key in `SessionInfo`"
|
||||
);
|
||||
continue
|
||||
};
|
||||
let our_vote_signed = SignedDisputeStatement::new_checked(
|
||||
kind.clone(),
|
||||
vote_state.votes().candidate_receipt.hash(),
|
||||
env.session_index(),
|
||||
public_key,
|
||||
sig.clone(),
|
||||
);
|
||||
let our_vote_signed = match our_vote_signed {
|
||||
Ok(signed) => signed,
|
||||
Err(()) => {
|
||||
gum::error!(
|
||||
target: LOG_TARGET,
|
||||
"Checking our own signature failed - db corruption?"
|
||||
);
|
||||
continue
|
||||
},
|
||||
};
|
||||
let dispute_message = match make_dispute_message(
|
||||
env.session_info(),
|
||||
vote_state.votes(),
|
||||
our_vote_signed,
|
||||
*validator_index,
|
||||
) {
|
||||
Err(err) => {
|
||||
gum::debug!(target: LOG_TARGET, ?err, "Creating dispute message failed.");
|
||||
continue
|
||||
},
|
||||
Ok(dispute_message) => dispute_message,
|
||||
};
|
||||
|
||||
ctx.send_message(DisputeDistributionMessage::SendDispute(dispute_message)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DisputeMessageCreationError {
|
||||
#[error("There was no opposite vote available")]
|
||||
NoOppositeVote,
|
||||
#[error("Found vote had an invalid validator index that could not be found")]
|
||||
InvalidValidatorIndex,
|
||||
#[error("Statement found in votes had invalid signature.")]
|
||||
InvalidStoredStatement,
|
||||
#[error(transparent)]
|
||||
InvalidStatementCombination(DisputeMessageCheckError),
|
||||
}
|
||||
|
||||
/// Create a `DisputeMessage` to be sent to `DisputeDistribution`.
|
||||
pub fn make_dispute_message(
|
||||
info: &SessionInfo,
|
||||
votes: &CandidateVotes,
|
||||
our_vote: SignedDisputeStatement,
|
||||
our_index: ValidatorIndex,
|
||||
) -> std::result::Result<DisputeMessage, DisputeMessageCreationError> {
|
||||
let validators = &info.validators;
|
||||
|
||||
let (valid_statement, valid_index, invalid_statement, invalid_index) =
|
||||
if let DisputeStatement::Valid(_) = our_vote.statement() {
|
||||
let (validator_index, (statement_kind, validator_signature)) =
|
||||
votes.invalid.iter().next().ok_or(DisputeMessageCreationError::NoOppositeVote)?;
|
||||
let other_vote = SignedDisputeStatement::new_checked(
|
||||
DisputeStatement::Invalid(*statement_kind),
|
||||
*our_vote.candidate_hash(),
|
||||
our_vote.session_index(),
|
||||
validators
|
||||
.get(*validator_index)
|
||||
.ok_or(DisputeMessageCreationError::InvalidValidatorIndex)?
|
||||
.clone(),
|
||||
validator_signature.clone(),
|
||||
)
|
||||
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
|
||||
(our_vote, our_index, other_vote, *validator_index)
|
||||
} else {
|
||||
let (validator_index, (statement_kind, validator_signature)) = votes
|
||||
.valid
|
||||
.raw()
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or(DisputeMessageCreationError::NoOppositeVote)?;
|
||||
let other_vote = SignedDisputeStatement::new_checked(
|
||||
DisputeStatement::Valid(*statement_kind),
|
||||
*our_vote.candidate_hash(),
|
||||
our_vote.session_index(),
|
||||
validators
|
||||
.get(*validator_index)
|
||||
.ok_or(DisputeMessageCreationError::InvalidValidatorIndex)?
|
||||
.clone(),
|
||||
validator_signature.clone(),
|
||||
)
|
||||
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
|
||||
(other_vote, *validator_index, our_vote, our_index)
|
||||
};
|
||||
|
||||
DisputeMessage::from_signed_statements(
|
||||
valid_statement,
|
||||
valid_index,
|
||||
invalid_statement,
|
||||
invalid_index,
|
||||
votes.candidate_receipt.clone(),
|
||||
info,
|
||||
)
|
||||
.map_err(DisputeMessageCreationError::InvalidStatementCombination)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ use futures::{
|
||||
use polkadot_node_subsystem_util::database::Database;
|
||||
|
||||
use polkadot_node_primitives::{
|
||||
DisputeStatus, SignedDisputeStatement, SignedFullStatement, Statement,
|
||||
DisputeMessage, DisputeStatus, SignedDisputeStatement, SignedFullStatement, Statement,
|
||||
};
|
||||
use polkadot_node_subsystem::{
|
||||
messages::{
|
||||
@@ -291,6 +291,7 @@ impl TestState {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Returns any sent `DisputeMessage`s.
|
||||
async fn handle_sync_queries(
|
||||
&mut self,
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
@@ -298,7 +299,7 @@ impl TestState {
|
||||
block_number: BlockNumber,
|
||||
session: SessionIndex,
|
||||
candidate_events: Vec<CandidateEvent>,
|
||||
) {
|
||||
) -> Vec<DisputeMessage> {
|
||||
// Order of messages is not fixed (different on initializing):
|
||||
#[derive(Debug)]
|
||||
struct FinishedSteps {
|
||||
@@ -316,6 +317,7 @@ impl TestState {
|
||||
}
|
||||
|
||||
let mut finished_steps = FinishedSteps::new();
|
||||
let mut sent_disputes = Vec::new();
|
||||
|
||||
while !finished_steps.is_done() {
|
||||
let recv = overseer_recv(virtual_overseer).await;
|
||||
@@ -400,6 +402,9 @@ impl TestState {
|
||||
let block_num = self.headers.get(&hash).map(|header| header.number);
|
||||
tx.send(Ok(block_num)).unwrap();
|
||||
},
|
||||
AllMessages::DisputeDistribution(DisputeDistributionMessage::SendDispute(msg)) => {
|
||||
sent_disputes.push(msg);
|
||||
},
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_new_leaf,
|
||||
RuntimeApiRequest::CandidateEvents(tx),
|
||||
@@ -439,14 +444,25 @@ impl TestState {
|
||||
},
|
||||
}
|
||||
}
|
||||
return sent_disputes
|
||||
}
|
||||
|
||||
async fn handle_resume_sync(
|
||||
&mut self,
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
session: SessionIndex,
|
||||
) {
|
||||
) -> Vec<DisputeMessage> {
|
||||
self.handle_resume_sync_with_events(virtual_overseer, session, Vec::new()).await
|
||||
}
|
||||
|
||||
async fn handle_resume_sync_with_events(
|
||||
&mut self,
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
session: SessionIndex,
|
||||
mut initial_events: Vec<CandidateEvent>,
|
||||
) -> Vec<DisputeMessage> {
|
||||
let leaves: Vec<Hash> = self.headers.keys().cloned().collect();
|
||||
let mut messages = Vec::new();
|
||||
for (n, leaf) in leaves.iter().enumerate() {
|
||||
gum::debug!(
|
||||
block_number= ?n,
|
||||
@@ -463,15 +479,14 @@ impl TestState {
|
||||
)))
|
||||
.await;
|
||||
|
||||
self.handle_sync_queries(
|
||||
virtual_overseer,
|
||||
*leaf,
|
||||
n as BlockNumber,
|
||||
session,
|
||||
Vec::new(),
|
||||
)
|
||||
.await;
|
||||
let events = if n == 1 { std::mem::take(&mut initial_events) } else { Vec::new() };
|
||||
|
||||
let mut new_messages = self
|
||||
.handle_sync_queries(virtual_overseer, *leaf, n as BlockNumber, session, events)
|
||||
.await;
|
||||
messages.append(&mut new_messages);
|
||||
}
|
||||
messages
|
||||
}
|
||||
|
||||
fn session_info(&self) -> SessionInfo {
|
||||
@@ -2148,6 +2163,7 @@ fn concluded_supermajority_against_non_active_after_time() {
|
||||
|
||||
#[test]
|
||||
fn resume_dispute_without_local_statement() {
|
||||
sp_tracing::init_for_tests();
|
||||
let session = 1;
|
||||
|
||||
test_harness(|mut test_state, mut virtual_overseer| {
|
||||
@@ -2188,10 +2204,8 @@ fn resume_dispute_without_local_statement() {
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash, HashMap::new())
|
||||
.await;
|
||||
|
||||
// Missing availability -> No local vote.
|
||||
// Participation won't happen here because the dispute is neither backed, not confirmed
|
||||
// nor the candidate is included. Or in other words - we'll refrain from participation.
|
||||
|
||||
assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));
|
||||
|
||||
{
|
||||
@@ -2216,7 +2230,17 @@ fn resume_dispute_without_local_statement() {
|
||||
// local statement for the active dispute.
|
||||
.resume(|mut test_state, mut virtual_overseer| {
|
||||
Box::pin(async move {
|
||||
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
// Candidate is now backed:
|
||||
let dispute_messages = test_state
|
||||
.handle_resume_sync_with_events(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(dispute_messages.len(), 0, "We don't expect any messages sent here.");
|
||||
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
@@ -2282,6 +2306,7 @@ fn resume_dispute_without_local_statement() {
|
||||
|
||||
#[test]
|
||||
fn resume_dispute_with_local_statement() {
|
||||
sp_tracing::init_for_tests();
|
||||
let session = 1;
|
||||
|
||||
test_harness(|mut test_state, mut virtual_overseer| {
|
||||
@@ -2359,10 +2384,19 @@ fn resume_dispute_with_local_statement() {
|
||||
})
|
||||
})
|
||||
// Alice should not send a DisputeParticiationMessage::Participate on restart since she has a
|
||||
// local statement for the active dispute.
|
||||
// local statement for the active dispute, instead she should try to (re-)send her vote.
|
||||
.resume(|mut test_state, mut virtual_overseer| {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
Box::pin(async move {
|
||||
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
|
||||
let messages = test_state
|
||||
.handle_resume_sync_with_events(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(messages.len(), 1, "A message should have gone out.");
|
||||
|
||||
// Assert that subsystem is not sending Participation messages because we issued a local statement
|
||||
assert!(virtual_overseer.recv().timeout(TEST_TIMEOUT).await.is_none());
|
||||
@@ -2390,7 +2424,12 @@ fn resume_dispute_without_local_statement_or_local_key() {
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_included_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (valid_vote, invalid_vote) = generate_opposing_votes_pair(
|
||||
@@ -2464,101 +2503,6 @@ fn resume_dispute_without_local_statement_or_local_key() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_dispute_with_local_statement_without_local_key() {
|
||||
let session = 1;
|
||||
|
||||
let test_state = TestState::default();
|
||||
let mut test_state = test_state.resume(|mut test_state, mut virtual_overseer| {
|
||||
Box::pin(async move {
|
||||
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 local_valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
ValidatorIndex(0),
|
||||
candidate_hash,
|
||||
session,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (valid_vote, invalid_vote) = generate_opposing_votes_pair(
|
||||
&test_state,
|
||||
ValidatorIndex(1),
|
||||
ValidatorIndex(2),
|
||||
candidate_hash,
|
||||
session,
|
||||
VoteType::Explicit,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (pending_confirmation, confirmation_rx) = oneshot::channel();
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt: candidate_receipt.clone(),
|
||||
session,
|
||||
statements: vec![
|
||||
(local_valid_vote, ValidatorIndex(0)),
|
||||
(valid_vote, ValidatorIndex(1)),
|
||||
(invalid_vote, ValidatorIndex(2)),
|
||||
],
|
||||
pending_confirmation: Some(pending_confirmation),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash, HashMap::new())
|
||||
.await;
|
||||
|
||||
assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));
|
||||
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(rx.await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
assert!(virtual_overseer.try_recv().await.is_none());
|
||||
|
||||
test_state
|
||||
})
|
||||
});
|
||||
|
||||
// No keys:
|
||||
test_state.subsystem_keystore =
|
||||
make_keystore(vec![Sr25519Keyring::Two.to_seed()].into_iter()).into();
|
||||
// Two should not send a DisputeParticiationMessage::Participate on restart since we gave
|
||||
// her a non existing key.
|
||||
test_state.resume(|mut test_state, mut virtual_overseer| {
|
||||
Box::pin(async move {
|
||||
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
|
||||
|
||||
// Assert that subsystem is not sending Participation messages because we don't
|
||||
// have a key.
|
||||
assert!(virtual_overseer.recv().timeout(TEST_TIMEOUT).await.is_none());
|
||||
|
||||
virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
assert!(virtual_overseer.try_recv().await.is_none());
|
||||
|
||||
test_state
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_valid_local_statement_does_cause_distribution_but_not_duplicate_participation() {
|
||||
issue_local_statement_does_cause_distribution_but_not_duplicate_participation(true);
|
||||
|
||||
Reference in New Issue
Block a user