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:
eskimor
2023-01-10 12:04:05 +01:00
committed by GitHub
parent 44fd95661c
commit cc650fe53d
16 changed files with 778 additions and 642 deletions
@@ -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.
+211 -56
View File
@@ -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);