Dispute spam protection (#4134)

* Mostly notes.

* Better error messages.

* Introduce Fatal/NonFatal + drop back channel participation

- Fatal/NonFatal - in order to make it easier to use utility functions.
- We drop the back channel in dispute participation as it won't be
needed any more.

* Better error messages.

* Utility function for receiving `CandidateEvent`s.

* Ordering module typechecks.

* cargo fmt

* Prepare spam slots module.

* Implement SpamSlots mechanism.

* Implement queues.

* cargo fmt

* Participation.

* Participation taking shape.

* Finish participation.

* cargo fmt

* Cleanup.

* WIP: Cleanup + Integration.

* Make `RollingSessionWindow` initialized by default.

* Make approval voting typecheck.

* Get rid of lazy_static & fix approval voting tests

* Move `SessionWindowSize` to node primitives.

* Implement dispute coordinator initialization.

* cargo fmt

* Make queues return error instead of boolean.

* Initialized: WIP

* Introduce chain api for getting finalized block.

* Fix ordering to only prune candidates on finalized events.

* Pruning of old sessions in spam slots.

* New import logic.

* Make everything typecheck.

* Fix warnings.

* Get rid of obsolete dispute-participation.

* Fixes.

* Add back accidentelly deleted Cargo.lock

* Deliver disputes in an ordered fashion.

* Add module docs for errors

* Use type synonym.

* hidden docs.

* Fix overseer tests.

* Ordering provider taking `CandidateReceipt`.

... To be kicked on one next commit.

* Fix ordering to use relay_parent

as included block is not unique per candidate.

* Add comment in ordering.rs.

* Take care of duplicate entries in queues.

* Better spam slots.

* Review remarks + docs.

* Fix db tests.

* Participation tests.

* Also scrape votes on first leaf for good measure.

* Make tests typecheck.

* Spelling.

* Only participate in actual disputes, not on every import.

* Don't account backing votes to spam slots.

* Fix more tests.

* Don't participate if we don't have keys.

* Fix tests, typos and warnings.

* Fix merge error.

* Spelling fixes.

* Add missing docs.

* Queue tests.

* More tests.

* Add metrics + don't short circuit import.

* Basic test for ordering provider.

* Import fix.

* Remove dead link.

* One more dead link.

Co-authored-by: Lldenaurois <Ljdenaurois@gmail.com>
This commit is contained in:
Robert Klotzner
2021-11-19 18:08:21 +01:00
committed by GitHub
parent ef3addb6a2
commit 25974f2076
45 changed files with 4099 additions and 2621 deletions
+2 -17
View File
@@ -6368,22 +6368,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "polkadot-node-core-dispute-participation"
version = "0.9.13"
dependencies = [
"assert_matches",
"futures 0.3.17",
"parity-scale-codec",
"polkadot-node-primitives",
"polkadot-node-subsystem",
"polkadot-node-subsystem-test-helpers",
"polkadot-primitives",
"sp-core",
"thiserror",
"tracing",
]
[[package]]
name = "polkadot-node-core-parachains-inherent"
version = "0.9.13"
@@ -6591,6 +6575,7 @@ dependencies = [
"env_logger 0.9.0",
"futures 0.3.17",
"itertools",
"lazy_static",
"log",
"lru 0.7.0",
"metered-channel",
@@ -6599,6 +6584,7 @@ dependencies = [
"polkadot-node-jaeger",
"polkadot-node-metrics",
"polkadot-node-network-protocol",
"polkadot-node-primitives",
"polkadot-node-subsystem",
"polkadot-node-subsystem-test-helpers",
"polkadot-overseer",
@@ -6955,7 +6941,6 @@ dependencies = [
"polkadot-node-core-chain-api",
"polkadot-node-core-chain-selection",
"polkadot-node-core-dispute-coordinator",
"polkadot-node-core-dispute-participation",
"polkadot-node-core-parachains-inherent",
"polkadot-node-core-provisioner",
"polkadot-node-core-runtime-api",
-1
View File
@@ -56,7 +56,6 @@ members = [
"node/core/chain-api",
"node/core/chain-selection",
"node/core/dispute-coordinator",
"node/core/dispute-participation",
"node/core/parachains-inherent",
"node/core/provisioner",
"node/core/pvf",
@@ -76,7 +76,7 @@ struct ImportedBlockInfo {
}
struct ImportedBlockInfoEnv<'a> {
session_window: &'a RollingSessionWindow,
session_window: &'a Option<RollingSessionWindow>,
assignment_criteria: &'a (dyn AssignmentCriteria + Send + Sync),
keystore: &'a LocalKeystore,
}
@@ -133,7 +133,11 @@ async fn imported_block_info(
Err(_) => return Ok(None),
};
if env.session_window.earliest_session().map_or(true, |e| session_index < e) {
if env
.session_window
.as_ref()
.map_or(true, |s| session_index < s.earliest_session())
{
tracing::debug!(
target: LOG_TARGET,
"Block {} is from ancient session {}. Skipping",
@@ -180,7 +184,8 @@ async fn imported_block_info(
}
};
let session_info = match env.session_window.session_info(session_index) {
let session_info = match env.session_window.as_ref().and_then(|s| s.session_info(session_index))
{
Some(s) => s,
None => {
tracing::debug!(
@@ -324,7 +329,7 @@ pub(crate) async fn handle_new_head(
}
};
match state.session_window.cache_session_info_for_head(ctx, head).await {
match state.cache_session_info_for_head(ctx, head).await {
Err(e) => {
tracing::debug!(
target: LOG_TARGET,
@@ -335,7 +340,7 @@ pub(crate) async fn handle_new_head(
return Ok(Vec::new())
},
Ok(a @ SessionWindowUpdate::Advanced { .. }) => {
Ok(Some(a @ SessionWindowUpdate::Advanced { .. })) => {
tracing::info!(
target: LOG_TARGET,
update = ?a,
@@ -431,8 +436,9 @@ pub(crate) async fn handle_new_head(
let session_info = state
.session_window
.session_info(session_index)
.expect("imported_block_info requires session to be available; qed");
.as_ref()
.and_then(|s| s.session_info(session_index))
.expect("imported_block_info requires session info to be available; qed");
let (block_tick, no_show_duration) = {
let block_tick = slot_number_to_tick(state.slot_duration_millis, slot);
@@ -608,7 +614,7 @@ pub(crate) mod tests {
fn blank_state() -> State {
State {
session_window: RollingSessionWindow::new(APPROVAL_SESSIONS),
session_window: None,
keystore: Arc::new(LocalKeystore::in_memory()),
slot_duration_millis: 6_000,
clock: Box::new(MockClock::default()),
@@ -618,11 +624,11 @@ pub(crate) mod tests {
fn single_session_state(index: SessionIndex, info: SessionInfo) -> State {
State {
session_window: RollingSessionWindow::with_session_info(
session_window: Some(RollingSessionWindow::with_session_info(
APPROVAL_SESSIONS,
index,
vec![info],
),
)),
..blank_state()
}
}
@@ -740,7 +746,7 @@ pub(crate) mod tests {
let header = header.clone();
Box::pin(async move {
let env = ImportedBlockInfoEnv {
session_window: &session_window,
session_window: &Some(session_window),
assignment_criteria: &MockAssignmentCriteria,
keystore: &LocalKeystore::in_memory(),
};
@@ -849,7 +855,7 @@ pub(crate) mod tests {
let header = header.clone();
Box::pin(async move {
let env = ImportedBlockInfoEnv {
session_window: &session_window,
session_window: &Some(session_window),
assignment_criteria: &MockAssignmentCriteria,
keystore: &LocalKeystore::in_memory(),
};
@@ -942,7 +948,7 @@ pub(crate) mod tests {
.collect::<Vec<_>>();
let test_fut = {
let session_window = RollingSessionWindow::new(APPROVAL_SESSIONS);
let session_window = None;
let header = header.clone();
Box::pin(async move {
@@ -1037,11 +1043,11 @@ pub(crate) mod tests {
.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
.collect::<Vec<_>>();
let session_window = RollingSessionWindow::with_session_info(
let session_window = Some(RollingSessionWindow::with_session_info(
APPROVAL_SESSIONS,
session,
vec![session_info],
);
));
let header = header.clone();
Box::pin(async move {
+30 -5
View File
@@ -44,7 +44,10 @@ use polkadot_node_subsystem::{
};
use polkadot_node_subsystem_util::{
metrics::{self, prometheus},
rolling_session_window::RollingSessionWindow,
rolling_session_window::{
new_session_window_size, RollingSessionWindow, SessionWindowSize, SessionWindowUpdate,
SessionsUnavailable,
},
TimeoutExt,
};
use polkadot_primitives::v1::{
@@ -92,7 +95,8 @@ use crate::{
#[cfg(test)]
mod tests;
const APPROVAL_SESSIONS: SessionIndex = 6;
pub const APPROVAL_SESSIONS: SessionWindowSize = new_session_window_size!(6);
const APPROVAL_CHECKING_TIMEOUT: Duration = Duration::from_secs(120);
const APPROVAL_CACHE_SIZE: usize = 1024;
const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds.
@@ -568,7 +572,7 @@ impl CurrentlyCheckingSet {
}
struct State {
session_window: RollingSessionWindow,
session_window: Option<RollingSessionWindow>,
keystore: Arc<LocalKeystore>,
slot_duration_millis: u64,
clock: Box<dyn Clock + Send + Sync>,
@@ -577,9 +581,30 @@ struct State {
impl State {
fn session_info(&self, i: SessionIndex) -> Option<&SessionInfo> {
self.session_window.session_info(i)
self.session_window.as_ref().and_then(|w| w.session_info(i))
}
/// Bring `session_window` up to date.
pub async fn cache_session_info_for_head(
&mut self,
ctx: &mut (impl SubsystemContext + overseer::SubsystemContext),
head: Hash,
) -> Result<Option<SessionWindowUpdate>, SessionsUnavailable> {
let session_window = self.session_window.take();
match session_window {
None => {
self.session_window =
Some(RollingSessionWindow::new(ctx, APPROVAL_SESSIONS, head).await?);
Ok(None)
},
Some(mut session_window) => {
let r =
session_window.cache_session_info_for_head(ctx, head).await.map(Option::Some);
self.session_window = Some(session_window);
r
},
}
}
// Compute the required tranches for approval for this block and candidate combo.
// Fails if there is no approval entry for the block under the candidate or no candidate entry
// under the block, or if the session is out of bounds.
@@ -671,7 +696,7 @@ where
B: Backend,
{
let mut state = State {
session_window: RollingSessionWindow::new(APPROVAL_SESSIONS),
session_window: None,
keystore: subsystem.keystore,
slot_duration_millis: subsystem.slot_duration_millis,
clock,
@@ -24,6 +24,8 @@ struct MetricsInner {
votes: prometheus::CounterVec<prometheus::U64>,
/// Conclusion across all disputes.
concluded: prometheus::CounterVec<prometheus::U64>,
/// Number of participations that have been queued.
queued_participations: prometheus::CounterVec<prometheus::U64>,
}
/// Candidate validation metrics.
@@ -61,6 +63,18 @@ impl Metrics {
metrics.concluded.with_label_values(&["invalid"]).inc();
}
}
pub(crate) fn on_queued_priority_participation(&self) {
if let Some(metrics) = &self.0 {
metrics.queued_participations.with_label_values(&["priority"]).inc();
}
}
pub(crate) fn on_queued_best_effort_participation(&self) {
if let Some(metrics) = &self.0 {
metrics.queued_participations.with_label_values(&["best-effort"]).inc();
}
}
}
impl metrics::Metrics for Metrics {
@@ -93,6 +107,16 @@ impl metrics::Metrics for Metrics {
)?,
registry,
)?,
queued_participations: prometheus::register(
prometheus::CounterVec::new(
prometheus::Opts::new(
"parachain_dispute_participations",
"Total number of queued participations, grouped by priority and best-effort. (Not every queueing will necessarily lead to an actual participation because of duplicates.)",
),
&["priority"],
)?,
registry,
)?,
};
Ok(Metrics(Some(metrics)))
}
@@ -26,7 +26,10 @@ use polkadot_primitives::v1::{CandidateHash, SessionIndex};
use std::collections::HashMap;
use super::db::v1::{CandidateVotes, RecentDisputes};
use super::{
db::v1::{CandidateVotes, RecentDisputes},
error::FatalResult,
};
#[derive(Debug)]
pub enum BackendWriteOp {
@@ -53,7 +56,7 @@ pub trait Backend {
/// Atomically writes the list of operations, with later operations taking precedence over
/// prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
fn write<I>(&mut self, ops: I) -> FatalResult<()>
where
I: IntoIterator<Item = BackendWriteOp>;
}
@@ -27,12 +27,11 @@ use std::sync::Arc;
use kvdb::{DBTransaction, KeyValueDB};
use parity_scale_codec::{Decode, Encode};
use crate::{
real::{
backend::{Backend, BackendWriteOp, OverlayedBackend},
DISPUTE_WINDOW,
},
DisputeStatus,
use crate::real::{
backend::{Backend, BackendWriteOp, OverlayedBackend},
error::{Fatal, FatalResult},
status::DisputeStatus,
DISPUTE_WINDOW,
};
const RECENT_DISPUTES_KEY: &[u8; 15] = b"recent-disputes";
@@ -72,7 +71,7 @@ impl Backend for DbBackend {
/// Atomically writes the list of operations, with later operations taking precedence over
/// prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
fn write<I>(&mut self, ops: I) -> FatalResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
@@ -98,7 +97,7 @@ impl Backend for DbBackend {
}
}
self.inner.write(tx).map_err(Into::into)
self.inner.write(tx).map_err(Fatal::DbWriteFailed)
}
}
@@ -214,7 +213,7 @@ pub(crate) fn note_current_session(
overlay_db: &mut OverlayedBackend<'_, impl Backend>,
current_session: SessionIndex,
) -> SubsystemResult<()> {
let new_earliest = current_session.saturating_sub(DISPUTE_WINDOW);
let new_earliest = current_session.saturating_sub(DISPUTE_WINDOW.get());
match overlay_db.load_earliest_session()? {
None => {
// First launch - write new-earliest.
@@ -421,7 +420,7 @@ mod tests {
let prev_earliest_session = 0;
let new_earliest_session = 5;
let current_session = 5 + DISPUTE_WINDOW;
let current_session = 5 + DISPUTE_WINDOW.get();
let very_old = 3;
let slightly_old = 4;
@@ -0,0 +1,167 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use futures::channel::oneshot;
use thiserror::Error;
use polkadot_node_subsystem::{
errors::{ChainApiError, RuntimeApiError},
SubsystemError,
};
use polkadot_node_subsystem_util::{rolling_session_window::SessionsUnavailable, runtime};
use super::{db, participation};
use crate::real::{CodecError, LOG_TARGET};
/// Errors for this subsystem.
#[derive(Debug, Error)]
#[error(transparent)]
pub enum Error {
/// All fatal errors.
Fatal(#[from] Fatal),
/// All nonfatal/potentially recoverable errors.
NonFatal(#[from] NonFatal),
}
/// General `Result` type for dispute coordinator.
pub type Result<R> = std::result::Result<R, Error>;
/// Result type with only fatal errors.
pub type FatalResult<R> = std::result::Result<R, Fatal>;
/// Result type with only non fatal errors.
pub type NonFatalResult<R> = std::result::Result<R, NonFatal>;
impl From<runtime::Error> for Error {
fn from(o: runtime::Error) -> Self {
match o {
runtime::Error::Fatal(f) => Self::Fatal(Fatal::Runtime(f)),
runtime::Error::NonFatal(f) => Self::NonFatal(NonFatal::Runtime(f)),
}
}
}
impl From<SubsystemError> for Error {
fn from(o: SubsystemError) -> Self {
match o {
SubsystemError::Context(msg) => Self::Fatal(Fatal::SubsystemContext(msg)),
_ => Self::NonFatal(NonFatal::Subsystem(o)),
}
}
}
/// Fatal errors of this subsystem.
#[derive(Debug, Error)]
pub enum Fatal {
/// Errors coming from runtime::Runtime.
#[error("Error while accessing runtime information {0}")]
Runtime(#[from] runtime::Fatal),
/// We received a legacy `SubystemError::Context` error which is considered fatal.
#[error("SubsystemError::Context error: {0}")]
SubsystemContext(String),
/// `ctx.spawn` failed with an error.
#[error("Spawning a task failed: {0}")]
SpawnFailed(SubsystemError),
#[error("Participation worker receiver exhausted.")]
ParticipationWorkerReceiverExhausted,
/// Receiving subsystem message from overseer failed.
#[error("Receiving message from overseer failed: {0}")]
SubsystemReceive(#[source] SubsystemError),
#[error("Writing to database failed: {0}")]
DbWriteFailed(std::io::Error),
#[error("Oneshow for receiving block number from chain API got cancelled")]
CanceledBlockNumber,
#[error("Retrieving block number from chain API failed with error: {0}")]
ChainApiBlockNumber(ChainApiError),
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum NonFatal {
#[error(transparent)]
RuntimeApi(#[from] RuntimeApiError),
#[error(transparent)]
ChainApi(#[from] ChainApiError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error("Dispute import confirmation send failed (receiver canceled)")]
DisputeImportOneshotSend,
#[error(transparent)]
Subsystem(SubsystemError),
#[error(transparent)]
Codec(#[from] CodecError),
/// `RollingSessionWindow` was not able to retrieve `SessionInfo`s.
#[error("Sessions unavailable in `RollingSessionWindow`: {0}")]
RollingSessionWindow(#[from] SessionsUnavailable),
/// Errors coming from runtime::Runtime.
#[error("Error while accessing runtime information: {0}")]
Runtime(#[from] runtime::NonFatal),
#[error(transparent)]
QueueError(#[from] participation::QueueError),
}
impl From<db::v1::Error> for Error {
fn from(err: db::v1::Error) -> Self {
match err {
db::v1::Error::Io(io) => Self::NonFatal(NonFatal::Io(io)),
db::v1::Error::Codec(e) => Self::NonFatal(NonFatal::Codec(e)),
}
}
}
/// Utility for eating top level errors and log them.
///
/// We basically always want to try and continue on error. This utility function is meant to
/// consume top-level errors by simply logging them
pub fn log_error(result: Result<()>) -> std::result::Result<(), Fatal> {
match result {
Err(Error::Fatal(f)) => Err(f),
Err(Error::NonFatal(error)) => {
error.log();
Ok(())
},
Ok(()) => Ok(()),
}
}
impl NonFatal {
/// Log a `NonFatal`.
pub fn log(self) {
match self {
// don't spam the log with spurious errors
Self::RuntimeApi(_) | Self::Oneshot(_) =>
tracing::debug!(target: LOG_TARGET, error = ?self),
// it's worth reporting otherwise
_ => tracing::warn!(target: LOG_TARGET, error = ?self),
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,219 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::{
cmp::{Ord, Ordering, PartialOrd},
collections::{BTreeMap, HashSet},
};
use futures::channel::oneshot;
use polkadot_node_subsystem::{
messages::ChainApiMessage, ActivatedLeaf, ActiveLeavesUpdate, SubsystemSender,
};
use polkadot_node_subsystem_util::runtime::get_candidate_events;
use polkadot_primitives::v1::{BlockNumber, CandidateEvent, CandidateHash, CandidateReceipt, Hash};
use super::{
error::{Fatal, FatalResult, Result},
LOG_TARGET,
};
#[cfg(test)]
mod tests;
/// Provider of `CandidateComparator` for candidates.
pub struct OrderingProvider {
/// All candidates we have seen included, which not yet have been finalized.
included_candidates: HashSet<CandidateHash>,
/// including block -> `CandidateHash`
///
/// We need this to clean up `included_candidates` on `ActiveLeavesUpdate`.
candidates_by_block_number: BTreeMap<BlockNumber, HashSet<CandidateHash>>,
}
/// `Comparator` for ordering of disputes for candidates.
///
/// This `comparator` makes it possible to order disputes based on age and to ensure some fairness
/// between chains in case of equally old disputes.
///
/// Objective ordering between nodes is important in case of lots disputes, so nodes will pull in
/// the same direction and work on resolving the same disputes first. This ensures that we will
/// conclude some disputes, even if there are lots of them. While any objective ordering would
/// suffice for this goal, ordering by age ensures we are not only resolving disputes, but also
/// resolve the oldest one first, which are also the most urgent and important ones to resolve.
///
/// Note: That by `oldest` we mean oldest in terms of relay chain block number, for any block
/// number that has not yet been finalized. If a block has been finalized already it should be
/// treated as low priority when it comes to disputes, as even in the case of a negative outcome,
/// we are already too late. The ordering mechanism here serves to prevent this from happening in
/// the first place.
#[derive(Copy, Clone)]
pub struct CandidateComparator {
/// Block number of the relay parent.
///
/// Important, so we will be participating in oldest disputes first.
///
/// Note: In theory it would make more sense to use the `BlockNumber` of the including
/// block, as inclusion time is the actual relevant event when it comes to ordering. The
/// problem is, that a candidate can get included multiple times on forks, so the `BlockNumber`
/// of the including block is not unique. We could theoretically work around that problem, by
/// just using the lowest `BlockNumber` of all available including blocks - the problem is,
/// that is not stable. If a new fork appears after the fact, we would start ordering the same
/// candidate differently, which would result in the same candidate getting queued twice.
relay_parent_block_number: BlockNumber,
/// By adding the `CandidateHash`, we can guarantee a unique ordering across candidates.
candidate_hash: CandidateHash,
}
impl PartialEq for CandidateComparator {
fn eq(&self, other: &CandidateComparator) -> bool {
Ordering::Equal == self.cmp(other)
}
}
impl Eq for CandidateComparator {}
impl PartialOrd for CandidateComparator {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CandidateComparator {
fn cmp(&self, other: &Self) -> Ordering {
match self.relay_parent_block_number.cmp(&other.relay_parent_block_number) {
Ordering::Equal => (),
o => return o,
}
self.candidate_hash.cmp(&other.candidate_hash)
}
}
impl CandidateComparator {
/// Create a candidate comparator based on given (fake) values.
///
/// Useful for testing.
#[cfg(test)]
pub fn new_dummy(block_number: BlockNumber, candidate_hash: CandidateHash) -> Self {
Self { relay_parent_block_number: block_number, candidate_hash }
}
/// Check whether the given candidate hash belongs to this comparator.
pub fn matches_candidate(&self, candidate_hash: &CandidateHash) -> bool {
&self.candidate_hash == candidate_hash
}
}
impl OrderingProvider {
/// Create a properly initialized `OrderingProvider`.
pub async fn new<Sender: SubsystemSender>(
sender: &mut Sender,
initial_head: ActivatedLeaf,
) -> Result<Self> {
let mut s = Self {
included_candidates: HashSet::new(),
candidates_by_block_number: BTreeMap::new(),
};
let update =
ActiveLeavesUpdate { activated: Some(initial_head), deactivated: Default::default() };
s.process_active_leaves_update(sender, &update).await?;
Ok(s)
}
/// Retrieve a candidate `comparator` if available.
///
/// If not available, we can treat disputes concerning this candidate with low priority and
/// should use spam slots for such disputes.
pub async fn candidate_comparator<'a>(
&mut self,
sender: &mut impl SubsystemSender,
candidate: &CandidateReceipt,
) -> FatalResult<Option<CandidateComparator>> {
let candidate_hash = candidate.hash();
if !self.included_candidates.contains(&candidate_hash) {
return Ok(None)
}
let n = match get_block_number(sender, candidate.descriptor().relay_parent).await? {
None => {
tracing::warn!(
target: LOG_TARGET,
candidate_hash = ?candidate.hash(),
"Candidate's relay_parent could not be found via chain API, but we saw candidate included?!"
);
return Ok(None)
},
Some(n) => n,
};
Ok(Some(CandidateComparator { relay_parent_block_number: n, candidate_hash }))
}
/// Query active leaves for any candidate `CandidateEvent::CandidateIncluded` events.
///
/// and updates current heads, so we can query candidates for all non finalized blocks.
pub async fn process_active_leaves_update<Sender: SubsystemSender>(
&mut self,
sender: &mut Sender,
update: &ActiveLeavesUpdate,
) -> Result<()> {
if let Some(activated) = update.activated.as_ref() {
// Get included events:
let included = get_candidate_events(sender, activated.hash)
.await?
.into_iter()
.filter_map(|ev| match ev {
CandidateEvent::CandidateIncluded(receipt, _, _, _) => Some(receipt),
_ => None,
});
for receipt in included {
let candidate_hash = receipt.hash();
self.included_candidates.insert(candidate_hash);
self.candidates_by_block_number
.entry(activated.number)
.or_default()
.insert(candidate_hash);
}
}
Ok(())
}
/// Prune finalized candidates.
///
/// Once a candidate lives in a relay chain block that's behind the finalized chain/got
/// finalized, we can treat it as low priority.
pub fn process_finalized_block(&mut self, finalized: &BlockNumber) {
let not_finalized = self.candidates_by_block_number.split_off(finalized);
let finalized = std::mem::take(&mut self.candidates_by_block_number);
self.candidates_by_block_number = not_finalized;
// Clean up finalized:
for finalized_candidate in finalized.into_values().flatten() {
self.included_candidates.remove(&finalized_candidate);
}
}
}
async fn get_block_number(
sender: &mut impl SubsystemSender,
relay_parent: Hash,
) -> FatalResult<Option<BlockNumber>> {
let (tx, rx) = oneshot::channel();
sender.send_message(ChainApiMessage::BlockNumber(relay_parent, tx).into()).await;
rx.await
.map_err(|_| Fatal::CanceledBlockNumber)?
.map_err(Fatal::ChainApiBlockNumber)
}
@@ -0,0 +1,165 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::sync::Arc;
use assert_matches::assert_matches;
use futures::FutureExt;
use parity_scale_codec::Encode;
use sp_core::testing::TaskExecutor;
use polkadot_node_subsystem::{
jaeger,
messages::{
AllMessages, ChainApiMessage, DisputeCoordinatorMessage, RuntimeApiMessage,
RuntimeApiRequest,
},
ActivatedLeaf, ActiveLeavesUpdate, LeafStatus,
};
use polkadot_node_subsystem_test_helpers::{
make_subsystem_context, TestSubsystemContext, TestSubsystemContextHandle,
};
use polkadot_node_subsystem_util::reexports::SubsystemContext;
use polkadot_primitives::v1::{
BlakeTwo256, BlockNumber, CandidateEvent, CandidateReceipt, CoreIndex, GroupIndex, Hash, HashT,
HeadData,
};
use super::OrderingProvider;
type VirtualOverseer = TestSubsystemContextHandle<DisputeCoordinatorMessage>;
struct TestState {
next_block_number: BlockNumber,
ordering: OrderingProvider,
ctx: TestSubsystemContext<DisputeCoordinatorMessage, TaskExecutor>,
}
impl TestState {
async fn new() -> Self {
let (mut ctx, ctx_handle) = make_subsystem_context(TaskExecutor::new());
let leaf = get_activated_leaf(1);
launch_virtual_overseer(&mut ctx, ctx_handle);
Self {
next_block_number: 2,
ordering: OrderingProvider::new(ctx.sender(), leaf).await.unwrap(),
ctx,
}
}
/// Get a new leaf.
fn next_leaf(&mut self) -> ActivatedLeaf {
let r = get_activated_leaf(self.next_block_number);
self.next_block_number += 1;
r
}
async fn process_active_leaves_update(&mut self) {
let update = self.next_leaf();
self.ordering
.process_active_leaves_update(
self.ctx.sender(),
&ActiveLeavesUpdate::start_work(update),
)
.await
.unwrap();
}
}
/// Simulate other subsystems:
fn launch_virtual_overseer(ctx: &mut impl SubsystemContext, ctx_handle: VirtualOverseer) {
ctx.spawn(
"serve-active-leaves-update",
async move { virtual_overseer(ctx_handle).await }.boxed(),
)
.unwrap();
}
async fn virtual_overseer(mut ctx_handle: VirtualOverseer) {
let ev = vec![CandidateEvent::CandidateIncluded(
CandidateReceipt::default(),
HeadData::default(),
CoreIndex::from(0),
GroupIndex::from(0),
)];
assert_matches!(
ctx_handle.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::CandidateEvents(
tx,
)
)) => {
tx.send(Ok(Vec::new())).unwrap();
}
);
assert_matches!(
ctx_handle.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::CandidateEvents(
tx,
)
)) => {
tx.send(Ok(ev)).unwrap();
}
);
assert_matches!(
ctx_handle.recv().await,
AllMessages::ChainApi(ChainApiMessage::BlockNumber(_, tx)) => {
tx.send(Ok(Some(1))).unwrap();
}
);
}
/// Get a dummy `ActivatedLeaf` for a given block number.
fn get_activated_leaf(n: BlockNumber) -> ActivatedLeaf {
ActivatedLeaf {
hash: get_block_number_hash(n),
number: n,
status: LeafStatus::Fresh,
span: Arc::new(jaeger::Span::Disabled),
}
}
/// Get a dummy relay parent hash for dummy block number.
fn get_block_number_hash(n: BlockNumber) -> Hash {
BlakeTwo256::hash(&n.encode())
}
#[test]
fn ordering_provider_provides_ordering_when_initialized() {
futures::executor::block_on(async {
let mut state = TestState::new().await;
let r = state
.ordering
.candidate_comparator(state.ctx.sender(), &CandidateReceipt::default())
.await
.unwrap();
assert!(r.is_none());
// After next active leaves update we should have a comparator:
state.process_active_leaves_update().await;
let r = state
.ordering
.candidate_comparator(state.ctx.sender(), &CandidateReceipt::default())
.await
.unwrap();
assert!(r.is_some());
assert_eq!(r.unwrap().relay_parent_block_number, 1);
});
}
@@ -0,0 +1,438 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashSet;
use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt,
};
use polkadot_node_primitives::{ValidationResult, APPROVAL_EXECUTION_TIMEOUT};
use polkadot_node_subsystem::{
messages::{AvailabilityRecoveryMessage, AvailabilityStoreMessage, CandidateValidationMessage},
ActiveLeavesUpdate, RecoveryError, SubsystemContext, SubsystemSender,
};
use polkadot_node_subsystem_util::runtime::get_validation_code_by_hash;
use polkadot_primitives::v1::{BlockNumber, CandidateHash, CandidateReceipt, Hash, SessionIndex};
use crate::real::LOG_TARGET;
use super::{
error::{Fatal, FatalResult, NonFatal, Result},
ordering::CandidateComparator,
};
#[cfg(test)]
mod tests;
#[cfg(test)]
pub use tests::{participation_full_happy_path, participation_missing_availability};
mod queues;
use queues::Queues;
pub use queues::{Error as QueueError, ParticipationRequest};
/// How many participation processes do we want to run in parallel the most.
///
/// This should be a relatively low value, while we might have a speedup once we fetched the data,
/// due to multi core architectures, but the fetching itself can not be improved by parallel
/// requests. This means that higher numbers make it harder for a single dispute to resolve fast.
const MAX_PARALLEL_PARTICIPATIONS: usize = 3;
/// Keep track of disputes we need to participate in.
///
/// - Prioritize and queue participations
/// - Dequeue participation requests in order and launch participation worker.
pub struct Participation {
/// Participations currently being processed.
running_participations: HashSet<CandidateHash>,
/// Priority and best effort queues.
queue: Queues,
/// Sender to be passed to worker tasks.
worker_sender: WorkerMessageSender,
/// Some recent block for retrieving validation code from chain.
recent_block: Option<(BlockNumber, Hash)>,
}
/// Message from worker tasks.
#[derive(Debug)]
pub struct WorkerMessage(ParticipationStatement);
/// Sender use by worker tasks.
pub type WorkerMessageSender = mpsc::Sender<WorkerMessage>;
/// Receiver to receive messages from worker tasks.
pub type WorkerMessageReceiver = mpsc::Receiver<WorkerMessage>;
/// Statement as result of the validation process.
#[derive(Debug)]
pub struct ParticipationStatement {
/// Relevant session.
pub session: SessionIndex,
/// The candidate the worker has been spawned for.
pub candidate_hash: CandidateHash,
/// Used receipt.
pub candidate_receipt: CandidateReceipt,
/// Actual result.
pub outcome: ParticipationOutcome,
}
/// Outcome of the validation process.
#[derive(Copy, Clone, Debug)]
pub enum ParticipationOutcome {
/// Candidate was found to be valid.
Valid,
/// Candidate was found to be invalid.
Invalid,
/// Candidate was found to be unavailable.
Unavailable,
/// Something went wrong (bug), details can be found in the logs.
Error,
}
impl ParticipationOutcome {
/// If validation was successful, get whether the candidate was valid or invalid.
pub fn validity(self) -> Option<bool> {
match self {
Self::Valid => Some(true),
Self::Invalid => Some(false),
Self::Unavailable | Self::Error => None,
}
}
}
impl WorkerMessage {
fn from_request(req: ParticipationRequest, outcome: ParticipationOutcome) -> Self {
let session = req.session();
let (candidate_hash, candidate_receipt) = req.into_candidate_info();
Self(ParticipationStatement { session, candidate_hash, candidate_receipt, outcome })
}
}
impl Participation {
/// Get ready for managing dispute participation requests.
///
/// The passed in sender will be used by background workers to communicate back their results.
/// The calling context should make sure to call `Participation::on_worker_message()` for the
/// received messages.
pub fn new(sender: WorkerMessageSender) -> Self {
Self {
running_participations: HashSet::new(),
queue: Queues::new(),
worker_sender: sender,
recent_block: None,
}
}
/// Queue a dispute for the node to participate in.
///
/// If capacity is available right now and we already got some relay chain head via
/// `on_active_leaves_update`, the participation will be launched right away.
///
/// Returns: false, if queues are already full.
pub async fn queue_participation<Context: SubsystemContext>(
&mut self,
ctx: &mut Context,
comparator: Option<CandidateComparator>,
req: ParticipationRequest,
) -> Result<()> {
// Participation already running - we can ignore that request:
if self.running_participations.contains(req.candidate_hash()) {
return Ok(())
}
// Available capacity - participate right away (if we already have a recent block):
if let Some((_, h)) = self.recent_block {
if self.running_participations.len() < MAX_PARALLEL_PARTICIPATIONS {
self.fork_participation(ctx, req, h)?;
return Ok(())
}
}
// Out of capacity/no recent block yet - queue:
Ok(self.queue.queue(comparator, req).map_err(NonFatal::QueueError)?)
}
/// Message from a worker task was received - get the outcome.
///
/// Call this function to keep participations going and to receive `ParticipationStatement`s.
///
/// This message has to be called for each received worker message, in order to make sure
/// enough participation processes are running at any given time.
///
/// Returns: The received `ParticipationStatement` or a fatal error, in case
/// something went wrong when dequeuing more requests (tasks could not be spawned).
pub async fn get_participation_result<Context: SubsystemContext>(
&mut self,
ctx: &mut Context,
msg: WorkerMessage,
) -> FatalResult<ParticipationStatement> {
let WorkerMessage(statement) = msg;
self.running_participations.remove(&statement.candidate_hash);
let recent_block = self.recent_block.expect("We never ever reset recent_block to `None` and we already received a result, so it must have been set before. qed.");
self.dequeue_until_capacity(ctx, recent_block.1).await?;
Ok(statement)
}
/// Process active leaves update.
///
/// Make sure we to dequeue participations if that became possible and update most recent
/// block.
pub async fn process_active_leaves_update<Context: SubsystemContext>(
&mut self,
ctx: &mut Context,
update: &ActiveLeavesUpdate,
) -> FatalResult<()> {
if let Some(activated) = &update.activated {
match self.recent_block {
None => {
self.recent_block = Some((activated.number, activated.hash));
// Work got potentially unblocked:
self.dequeue_until_capacity(ctx, activated.hash).await?;
},
Some((number, _)) if activated.number > number => {
self.recent_block = Some((activated.number, activated.hash));
},
Some(_) => {},
}
}
Ok(())
}
/// Dequeue until `MAX_PARALLEL_PARTICIPATIONS` is reached.
async fn dequeue_until_capacity<Context: SubsystemContext>(
&mut self,
ctx: &mut Context,
recent_head: Hash,
) -> FatalResult<()> {
while self.running_participations.len() < MAX_PARALLEL_PARTICIPATIONS {
if let Some(req) = self.queue.dequeue() {
self.fork_participation(ctx, req, recent_head)?;
} else {
break
}
}
Ok(())
}
/// Fork a participation task in the background.
fn fork_participation<Context: SubsystemContext>(
&mut self,
ctx: &mut Context,
req: ParticipationRequest,
recent_head: Hash,
) -> FatalResult<()> {
if self.running_participations.insert(req.candidate_hash().clone()) {
let sender = ctx.sender().clone();
ctx.spawn(
"participation-worker",
participate(self.worker_sender.clone(), sender, recent_head, req).boxed(),
)
.map_err(Fatal::SpawnFailed)?;
}
Ok(())
}
}
async fn participate(
mut result_sender: WorkerMessageSender,
mut sender: impl SubsystemSender,
block_hash: Hash,
req: ParticipationRequest,
) {
// in order to validate a candidate we need to start by recovering the
// available data
let (recover_available_data_tx, recover_available_data_rx) = oneshot::channel();
sender
.send_message(
AvailabilityRecoveryMessage::RecoverAvailableData(
req.candidate_receipt().clone(),
req.session(),
None,
recover_available_data_tx,
)
.into(),
)
.await;
let available_data = match recover_available_data_rx.await {
Err(oneshot::Canceled) => {
tracing::warn!(
target: LOG_TARGET,
"`Oneshot` got cancelled when recovering available data {:?}",
req.candidate_hash(),
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return
},
Ok(Ok(data)) => data,
Ok(Err(RecoveryError::Invalid)) => {
// the available data was recovered but it is invalid, therefore we'll
// vote negatively for the candidate dispute
send_result(&mut result_sender, req, ParticipationOutcome::Invalid).await;
return
},
Ok(Err(RecoveryError::Unavailable)) => {
send_result(&mut result_sender, req, ParticipationOutcome::Unavailable).await;
return
},
};
// we also need to fetch the validation code which we can reference by its
// hash as taken from the candidate descriptor
let validation_code = match get_validation_code_by_hash(
&mut sender,
block_hash,
req.candidate_receipt().descriptor.validation_code_hash,
)
.await
{
Ok(Some(code)) => code,
Ok(None) => {
tracing::warn!(
target: LOG_TARGET,
"Validation code unavailable for code hash {:?} in the state of block {:?}",
req.candidate_receipt().descriptor.validation_code_hash,
block_hash,
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return
},
Err(err) => {
tracing::warn!(target: LOG_TARGET, ?err, "Error when fetching validation code.");
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return
},
};
// we dispatch a request to store the available data for the candidate. We
// want to maximize data availability for other potential checkers involved
// in the dispute
let (store_available_data_tx, store_available_data_rx) = oneshot::channel();
sender
.send_message(
AvailabilityStoreMessage::StoreAvailableData {
candidate_hash: *req.candidate_hash(),
n_validators: req.n_validators() as u32,
available_data: available_data.clone(),
tx: store_available_data_tx,
}
.into(),
)
.await;
match store_available_data_rx.await {
Err(oneshot::Canceled) => {
tracing::warn!(
target: LOG_TARGET,
"`Oneshot` got cancelled when storing available data {:?}",
req.candidate_hash(),
);
},
Ok(Err(err)) => {
tracing::warn!(
target: LOG_TARGET,
?err,
"Failed to store available data for candidate {:?}",
req.candidate_hash(),
);
},
Ok(Ok(())) => {},
}
// Issue a request to validate the candidate with the provided exhaustive
// parameters
//
// We use the approval execution timeout because this is intended to
// be run outside of backing and therefore should be subject to the
// same level of leeway.
let (validation_tx, validation_rx) = oneshot::channel();
sender
.send_message(
CandidateValidationMessage::ValidateFromExhaustive(
available_data.validation_data,
validation_code,
req.candidate_receipt().descriptor.clone(),
available_data.pov,
APPROVAL_EXECUTION_TIMEOUT,
validation_tx,
)
.into(),
)
.await;
// we cast votes (either positive or negative) depending on the outcome of
// the validation and if valid, whether the commitments hash matches
match validation_rx.await {
Err(oneshot::Canceled) => {
tracing::warn!(
target: LOG_TARGET,
"`Oneshot` got cancelled when validating candidate {:?}",
req.candidate_hash(),
);
send_result(&mut result_sender, req, ParticipationOutcome::Error).await;
return
},
Ok(Err(err)) => {
tracing::warn!(
target: LOG_TARGET,
"Candidate {:?} validation failed with: {:?}",
req.candidate_hash(),
err,
);
send_result(&mut result_sender, req, ParticipationOutcome::Invalid).await;
},
Ok(Ok(ValidationResult::Invalid(invalid))) => {
tracing::warn!(
target: LOG_TARGET,
"Candidate {:?} considered invalid: {:?}",
req.candidate_hash(),
invalid,
);
send_result(&mut result_sender, req, ParticipationOutcome::Invalid).await;
},
Ok(Ok(ValidationResult::Valid(commitments, _))) => {
if commitments.hash() != req.candidate_receipt().commitments_hash {
tracing::warn!(
target: LOG_TARGET,
expected = ?req.candidate_receipt().commitments_hash,
got = ?commitments.hash(),
"Candidate is valid but commitments hash doesn't match",
);
send_result(&mut result_sender, req, ParticipationOutcome::Invalid).await;
} else {
send_result(&mut result_sender, req, ParticipationOutcome::Valid).await;
}
},
}
}
/// Helper function for sending the result back and report any error.
async fn send_result(
sender: &mut WorkerMessageSender,
req: ParticipationRequest,
outcome: ParticipationOutcome,
) {
if let Err(err) = sender.feed(WorkerMessage::from_request(req, outcome)).await {
tracing::error!(
target: LOG_TARGET,
?err,
"Sending back participation result failed. Dispute coordinator not working properly!"
);
}
}
@@ -0,0 +1,210 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::collections::{BTreeMap, HashMap};
use thiserror::Error;
use polkadot_primitives::v1::{CandidateHash, CandidateReceipt, SessionIndex};
use crate::real::ordering::CandidateComparator;
#[cfg(test)]
mod tests;
/// How many potential garbage disputes we want to queue, before starting to drop requests.
#[cfg(not(test))]
const BEST_EFFORT_QUEUE_SIZE: usize = 100;
#[cfg(test)]
const BEST_EFFORT_QUEUE_SIZE: usize = 3;
/// How many priority disputes can be queued.
///
/// Once the queue exceeds that size, we will start to drop the newest participation requests in
/// the queue. Note that for each vote import the request will be re-added, if there is free
/// capacity. This limit just serves as a safe guard, it is not expected to ever really be reached.
///
/// For 100 parachains, this would allow for every single candidate in 100 blocks on
/// two forks to get disputed, which should be plenty to deal with any realistic attack.
#[cfg(not(test))]
const PRIORITY_QUEUE_SIZE: usize = 20_000;
#[cfg(test)]
const PRIORITY_QUEUE_SIZE: usize = 2;
/// Type for counting how often a candidate was added to the best effort queue.
type BestEffortCount = u32;
/// Queues for dispute participation.
pub struct Queues {
/// Set of best effort participation requests.
///
/// Note that as size is limited to `BEST_EFFORT_QUEUE_SIZE` we simply do a linear search for
/// the entry with the highest `added_count` to determine what dispute to participate next in.
///
/// This mechanism leads to an amplifying effect - the more validators already participated,
/// the more likely it becomes that more validators will participate soon, which should lead to
/// a quick resolution of disputes, even in the best effort queue.
best_effort: HashMap<CandidateHash, BestEffortEntry>,
/// Priority queue.
///
/// In the priority queue, we have a strict ordering of candidates and participation will
/// happen in that order.
priority: BTreeMap<CandidateComparator, ParticipationRequest>,
}
/// A dispute participation request that can be queued.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ParticipationRequest {
candidate_hash: CandidateHash,
candidate_receipt: CandidateReceipt,
session: SessionIndex,
n_validators: usize,
}
/// Entry for the best effort queue.
struct BestEffortEntry {
req: ParticipationRequest,
/// How often was the above request added to the queue.
added_count: BestEffortCount,
}
/// What can go wrong when queuing a request.
#[derive(Debug, Error)]
pub enum Error {
#[error("Request could not be queued, because best effort queue was already full.")]
BestEffortFull,
#[error("Request could not be queued, because priority queue was already full.")]
PriorityFull,
}
impl ParticipationRequest {
/// Create a new `ParticipationRequest` to be queued.
pub fn new(
candidate_receipt: CandidateReceipt,
session: SessionIndex,
n_validators: usize,
) -> Self {
Self { candidate_hash: candidate_receipt.hash(), candidate_receipt, session, n_validators }
}
pub fn candidate_receipt(&'_ self) -> &'_ CandidateReceipt {
&self.candidate_receipt
}
pub fn candidate_hash(&'_ self) -> &'_ CandidateHash {
&self.candidate_hash
}
pub fn session(&self) -> SessionIndex {
self.session
}
pub fn n_validators(&self) -> usize {
self.n_validators
}
pub fn into_candidate_info(self) -> (CandidateHash, CandidateReceipt) {
let Self { candidate_hash, candidate_receipt, .. } = self;
(candidate_hash, candidate_receipt)
}
}
impl Queues {
/// Create new `Queues`.
pub fn new() -> Self {
Self { best_effort: HashMap::new(), priority: BTreeMap::new() }
}
/// Will put message in queue, either priority or best effort depending on whether a
/// `CandidateComparator` was provided or not.
///
/// If the message was already previously present on best effort, it will be moved to priority
/// if a `CandidateComparator` has been passed now, otherwise the `added_count` on the best
/// effort queue will be bumped.
///
/// Returns error in case a queue was found full already.
pub fn queue(
&mut self,
comparator: Option<CandidateComparator>,
req: ParticipationRequest,
) -> Result<(), Error> {
debug_assert!(comparator
.map(|c| c.matches_candidate(req.candidate_hash()))
.unwrap_or(true));
if let Some(comparator) = comparator {
if self.priority.len() >= PRIORITY_QUEUE_SIZE {
return Err(Error::PriorityFull)
}
// Remove any best effort entry:
self.best_effort.remove(&req.candidate_hash);
self.priority.insert(comparator, req);
} else {
if self.best_effort.len() >= BEST_EFFORT_QUEUE_SIZE {
return Err(Error::BestEffortFull)
}
// Note: The request might have been added to priority in a previous call already, we
// take care of that case in `dequeue` (more efficient).
self.best_effort
.entry(req.candidate_hash)
.or_insert(BestEffortEntry { req, added_count: 0 })
.added_count += 1;
}
Ok(())
}
/// Get the next best request for dispute participation
///
/// if any. Priority queue is always considered first, then the best effort queue based on
/// `added_count`.
pub fn dequeue(&mut self) -> Option<ParticipationRequest> {
if let Some(req) = self.pop_priority() {
// In case a candidate became best effort over time, we might have it also queued in
// the best effort queue - get rid of any such entry:
self.best_effort.remove(req.candidate_hash());
return Some(req)
}
self.pop_best_effort()
}
/// Get the next best from the best effort queue.
///
/// If there are multiple best - just pick one.
fn pop_best_effort(&mut self) -> Option<ParticipationRequest> {
let best = self.best_effort.iter().reduce(|(hash1, entry1), (hash2, entry2)| {
if entry1.added_count > entry2.added_count {
(hash1, entry1)
} else {
(hash2, entry2)
}
});
if let Some((best_hash, _)) = best {
let best_hash = best_hash.clone();
self.best_effort.remove(&best_hash).map(|e| e.req)
} else {
None
}
}
/// Get best priority queue entry.
fn pop_priority(&mut self) -> Option<ParticipationRequest> {
// Once https://github.com/rust-lang/rust/issues/62924 is there, we can use a simple:
// priority.pop_first().
if let Some((comparator, _)) = self.priority.iter().next() {
let comparator = comparator.clone();
self.priority.remove(&comparator)
} else {
None
}
}
}
@@ -0,0 +1,131 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use assert_matches::assert_matches;
use polkadot_primitives::v1::{BlockNumber, CandidateReceipt, Hash};
use crate::real::ordering::CandidateComparator;
use super::{Error, ParticipationRequest, Queues};
/// Make a `ParticipationRequest` based on the given commitments hash.
fn make_participation_request(hash: Hash) -> ParticipationRequest {
let mut receipt = CandidateReceipt::default();
// make it differ:
receipt.commitments_hash = hash;
ParticipationRequest::new(receipt, 1, 100)
}
/// Make dummy comparator for request, based on the given block number.
fn make_dummy_comparator(
req: &ParticipationRequest,
relay_parent: BlockNumber,
) -> CandidateComparator {
CandidateComparator::new_dummy(relay_parent, *req.candidate_hash())
}
/// Check that dequeuing acknowledges order.
///
/// Any priority item will be dequeued before any best effort items, priority items will be
/// processed in order. Best effort items, based on how often they have been added.
#[test]
fn ordering_works_as_expected() {
let mut queue = Queues::new();
let req1 = make_participation_request(Hash::repeat_byte(0x01));
let req_prio = make_participation_request(Hash::repeat_byte(0x02));
let req3 = make_participation_request(Hash::repeat_byte(0x03));
let req_prio_2 = make_participation_request(Hash::repeat_byte(0x04));
let req5 = make_participation_request(Hash::repeat_byte(0x05));
let req_full = make_participation_request(Hash::repeat_byte(0x06));
let req_prio_full = make_participation_request(Hash::repeat_byte(0x07));
queue.queue(None, req1.clone()).unwrap();
queue
.queue(Some(make_dummy_comparator(&req_prio, 1)), req_prio.clone())
.unwrap();
queue.queue(None, req3.clone()).unwrap();
queue
.queue(Some(make_dummy_comparator(&req_prio_2, 2)), req_prio_2.clone())
.unwrap();
queue.queue(None, req3.clone()).unwrap();
queue.queue(None, req5.clone()).unwrap();
assert_matches!(
queue.queue(Some(make_dummy_comparator(&req_prio_full, 3)), req_prio_full),
Err(Error::PriorityFull)
);
assert_matches!(queue.queue(None, req_full), Err(Error::BestEffortFull));
assert_eq!(queue.dequeue(), Some(req_prio));
assert_eq!(queue.dequeue(), Some(req_prio_2));
assert_eq!(queue.dequeue(), Some(req3));
assert_matches!(
queue.dequeue(),
Some(r) => { assert!(r == req1 || r == req5) }
);
assert_matches!(
queue.dequeue(),
Some(r) => { assert!(r == req1 || r == req5) }
);
assert_matches!(queue.dequeue(), None);
}
/// No matter how often a candidate gets queued, it should only ever get dequeued once.
#[test]
fn candidate_is_only_dequeued_once() {
let mut queue = Queues::new();
let req1 = make_participation_request(Hash::repeat_byte(0x01));
let req_prio = make_participation_request(Hash::repeat_byte(0x02));
let req_best_effort_then_prio = make_participation_request(Hash::repeat_byte(0x03));
let req_prio_then_best_effort = make_participation_request(Hash::repeat_byte(0x04));
queue.queue(None, req1.clone()).unwrap();
queue
.queue(Some(make_dummy_comparator(&req_prio, 1)), req_prio.clone())
.unwrap();
// Insert same best effort again:
queue.queue(None, req1.clone()).unwrap();
// insert same prio again:
queue
.queue(Some(make_dummy_comparator(&req_prio, 1)), req_prio.clone())
.unwrap();
// Insert first as best effort:
queue.queue(None, req_best_effort_then_prio.clone()).unwrap();
// Then as prio:
queue
.queue(
Some(make_dummy_comparator(&req_best_effort_then_prio, 2)),
req_best_effort_then_prio.clone(),
)
.unwrap();
// Make space in prio:
assert_eq!(queue.dequeue(), Some(req_prio));
// Insert first as prio:
queue
.queue(
Some(make_dummy_comparator(&req_prio_then_best_effort, 3)),
req_prio_then_best_effort.clone(),
)
.unwrap();
// Then as best effort:
queue.queue(None, req_prio_then_best_effort.clone()).unwrap();
assert_eq!(queue.dequeue(), Some(req_best_effort_then_prio));
assert_eq!(queue.dequeue(), Some(req_prio_then_best_effort));
assert_eq!(queue.dequeue(), Some(req1));
assert_eq!(queue.dequeue(), None);
}
@@ -0,0 +1,550 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use assert_matches::assert_matches;
use futures::StreamExt;
use polkadot_node_subsystem_util::TimeoutExt;
use std::{sync::Arc, time::Duration};
use sp_core::testing::TaskExecutor;
use super::*;
use parity_scale_codec::Encode;
use polkadot_node_primitives::{AvailableData, BlockData, InvalidCandidate, PoV};
use polkadot_node_subsystem::{
jaeger,
messages::{
AllMessages, DisputeCoordinatorMessage, RuntimeApiMessage, RuntimeApiRequest,
ValidationFailed,
},
ActivatedLeaf, ActiveLeavesUpdate, LeafStatus,
};
use polkadot_node_subsystem_test_helpers::{
make_subsystem_context, TestSubsystemContext, TestSubsystemContextHandle,
};
use polkadot_primitives::v1::{BlakeTwo256, CandidateCommitments, HashT, Header, ValidationCode};
type VirtualOverseer = TestSubsystemContextHandle<DisputeCoordinatorMessage>;
pub fn make_our_subsystem_context<S>(
spawn: S,
) -> (
TestSubsystemContext<DisputeCoordinatorMessage, S>,
TestSubsystemContextHandle<DisputeCoordinatorMessage>,
) {
make_subsystem_context(spawn)
}
async fn participate(
ctx: &mut impl SubsystemContext,
participation: &mut Participation,
) -> Result<()> {
let commitments = CandidateCommitments::default();
participate_with_commitments_hash(ctx, participation, commitments.hash()).await
}
async fn participate_with_commitments_hash(
ctx: &mut impl SubsystemContext,
participation: &mut Participation,
commitments_hash: Hash,
) -> Result<()> {
let candidate_receipt = {
let mut receipt = CandidateReceipt::default();
receipt.commitments_hash = commitments_hash;
receipt
};
let session = 1;
let n_validators = 10;
let req = ParticipationRequest::new(candidate_receipt, session, n_validators);
participation.queue_participation(ctx, None, req).await
}
async fn activate_leaf(
ctx: &mut impl SubsystemContext,
participation: &mut Participation,
block_number: BlockNumber,
) -> FatalResult<()> {
let block_header = Header {
parent_hash: BlakeTwo256::hash(&block_number.encode()),
number: block_number,
digest: Default::default(),
state_root: Default::default(),
extrinsics_root: Default::default(),
};
let block_hash = block_header.hash();
participation
.process_active_leaves_update(
ctx,
&ActiveLeavesUpdate::start_work(ActivatedLeaf {
hash: block_hash,
span: Arc::new(jaeger::Span::Disabled),
number: block_number,
status: LeafStatus::Fresh,
}),
)
.await
}
/// Full participation happy path as seen via the overseer.
pub async fn participation_full_happy_path(ctx_handle: &mut VirtualOverseer) {
recover_available_data(ctx_handle).await;
fetch_validation_code(ctx_handle).await;
store_available_data(ctx_handle, true).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Ok(ValidationResult::Valid(Default::default(), Default::default()))).unwrap();
},
"overseer did not receive candidate validation message",
);
}
/// Full participation with failing availability recovery.
pub async fn participation_missing_availability(ctx_handle: &mut VirtualOverseer) {
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
}
async fn recover_available_data(virtual_overseer: &mut VirtualOverseer) {
let pov_block = PoV { block_data: BlockData(Vec::new()) };
let available_data =
AvailableData { pov: Arc::new(pov_block), validation_data: Default::default() };
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Ok(available_data)).unwrap();
},
"overseer did not receive recover available data message",
);
}
/// Handles validation code fetch, returns the received relay parent hash.
async fn fetch_validation_code(virtual_overseer: &mut VirtualOverseer) -> Hash {
let validation_code = ValidationCode(Vec::new());
assert_matches!(
virtual_overseer.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
hash,
RuntimeApiRequest::ValidationCodeByHash(
_,
tx,
)
)) => {
tx.send(Ok(Some(validation_code))).unwrap();
hash
},
"overseer did not receive runtime API request for validation code",
)
}
async fn store_available_data(virtual_overseer: &mut VirtualOverseer, success: bool) {
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityStore(AvailabilityStoreMessage::StoreAvailableData { tx, .. }) => {
if success {
tx.send(Ok(())).unwrap();
} else {
tx.send(Err(())).unwrap();
}
},
"overseer did not receive store available data request",
);
}
#[test]
fn same_req_wont_get_queued_if_participation_is_already_running() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
for _ in 0..MAX_PARALLEL_PARTICIPATIONS {
participate(&mut ctx, &mut participation).await.unwrap();
}
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Unavailable => {}
);
// we should not have any further results nor recovery requests:
assert_matches!(ctx_handle.recv().timeout(Duration::from_millis(10)).await, None);
assert_matches!(worker_receiver.next().timeout(Duration::from_millis(10)).await, None);
})
}
#[test]
fn reqs_get_queued_when_out_of_capacity() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
for i in 0..MAX_PARALLEL_PARTICIPATIONS {
participate_with_commitments_hash(
&mut ctx,
&mut participation,
Hash::repeat_byte(i as _),
)
.await
.unwrap();
}
for _ in 0..MAX_PARALLEL_PARTICIPATIONS + 1 {
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Unavailable => {}
);
}
// we should not have any further results nor recovery requests:
assert_matches!(ctx_handle.recv().timeout(Duration::from_millis(10)).await, None);
assert_matches!(worker_receiver.next().timeout(Duration::from_millis(10)).await, None);
})
}
#[test]
fn reqs_get_queued_on_no_recent_block() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, _worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
participate(&mut ctx, &mut participation).await.unwrap();
assert!(ctx_handle.recv().timeout(Duration::from_millis(10)).await.is_none());
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
// after activating at least one leaf the recent block
// state should be available which should lead to trying
// to participate by first trying to recover the available
// data
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(AvailabilityRecoveryMessage::RecoverAvailableData(
..
)),
"overseer did not receive recover available data message",
);
})
}
#[test]
fn cannot_participate_if_cannot_recover_available_data() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Unavailable => {}
);
})
}
#[test]
fn cannot_participate_if_cannot_recover_validation_code() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::ValidationCodeByHash(
_,
tx,
)
)) => {
tx.send(Ok(None)).unwrap();
},
"overseer did not receive runtime API request for validation code",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Error => {}
);
})
}
#[test]
fn cast_invalid_vote_if_available_data_is_invalid() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
assert_matches!(
ctx_handle.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Invalid)).unwrap();
},
"overseer did not receive recover available data message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
#[test]
fn cast_invalid_vote_if_validation_fails_or_is_invalid() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
store_available_data(&mut ctx_handle, true).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Ok(ValidationResult::Invalid(InvalidCandidate::Timeout))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
#[test]
fn cast_invalid_vote_if_validation_passes_but_commitments_dont_match() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
store_available_data(&mut ctx_handle, true).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
let mut commitments = CandidateCommitments::default();
// this should lead to a commitments hash mismatch
commitments.processed_downward_messages = 42;
tx.send(Ok(ValidationResult::Valid(commitments, Default::default()))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
#[test]
fn cast_valid_vote_if_validation_passes() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
store_available_data(&mut ctx_handle, true).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Ok(ValidationResult::Valid(Default::default(), Default::default()))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Valid => {}
);
})
}
#[test]
fn failure_to_store_available_data_does_not_preclude_participation() {
futures::executor::block_on(async {
let (mut ctx, mut ctx_handle) = make_our_subsystem_context(TaskExecutor::new());
let (sender, mut worker_receiver) = mpsc::channel(1);
let mut participation = Participation::new(sender);
activate_leaf(&mut ctx, &mut participation, 10).await.unwrap();
participate(&mut ctx, &mut participation).await.unwrap();
recover_available_data(&mut ctx_handle).await;
assert_eq!(
fetch_validation_code(&mut ctx_handle).await,
participation.recent_block.unwrap().1
);
// the store available data request should fail:
store_available_data(&mut ctx_handle, false).await;
assert_matches!(
ctx_handle.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Err(ValidationFailed("fail".to_string()))).unwrap();
},
"overseer did not receive candidate validation message",
);
let result = participation
.get_participation_result(&mut ctx, worker_receiver.next().await.unwrap())
.await
.unwrap();
assert_matches!(
result.outcome,
ParticipationOutcome::Invalid => {}
);
})
}
@@ -0,0 +1,123 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::collections::{HashMap, HashSet};
use polkadot_primitives::v1::{CandidateHash, SessionIndex, ValidatorIndex};
use crate::real::LOG_TARGET;
/// Type used for counting potential spam votes.
type SpamCount = u32;
/// How many unconfirmed disputes a validator is allowed to be a participant in (per session).
///
/// Unconfirmed means: Node has not seen the candidate be included on any chain, it has not cast a
/// vote itself on that dispute, the dispute has not yet reached more than a third of
/// validator's votes and the including relay chain block has not yet been finalized.
///
/// Exact number of `MAX_SPAM_VOTES` is not that important here. It is important that the number is
/// low enough to not cause resource exhaustion, if multiple validators spend their limits. Also
/// if things are working properly, this number cannot really be too low either, as all relevant
/// disputes _should_ have been seen as included my enough validators. (Otherwise the candidate
/// would not have been available in the first place and could not have been included.) So this is
/// really just a fallback mechanism if things go terribly wrong.
const MAX_SPAM_VOTES: SpamCount = 50;
/// Spam slots for raised disputes concerning unknown candidates.
pub struct SpamSlots {
/// Counts per validator and session.
///
/// Must not exceed `MAX_SPAM_VOTES`.
slots: HashMap<(SessionIndex, ValidatorIndex), SpamCount>,
/// All unconfirmed candidates we are aware of right now.
unconfirmed: UnconfirmedDisputes,
}
/// Unconfirmed disputes to be passed at initialization.
pub type UnconfirmedDisputes = HashMap<(SessionIndex, CandidateHash), HashSet<ValidatorIndex>>;
impl SpamSlots {
/// Recover `SpamSlots` from state on startup.
///
/// Initialize based on already existing active disputes.
pub fn recover_from_state(unconfirmed_disputes: UnconfirmedDisputes) -> Self {
let mut slots: HashMap<(SessionIndex, ValidatorIndex), SpamCount> = HashMap::new();
for ((session, _), validators) in unconfirmed_disputes.iter() {
for validator in validators {
let e = slots.entry((*session, *validator)).or_default();
*e += 1;
if *e > MAX_SPAM_VOTES {
tracing::debug!(
target: LOG_TARGET,
?session,
?validator,
count = ?e,
"Import exceeded spam slot for validator"
);
}
}
}
Self { slots, unconfirmed: unconfirmed_disputes }
}
/// Add an unconfirmed dispute if free slots are available.
pub fn add_unconfirmed(
&mut self,
session: SessionIndex,
candidate: CandidateHash,
validator: ValidatorIndex,
) -> bool {
let c = self.slots.entry((session, validator)).or_default();
if *c >= MAX_SPAM_VOTES {
return false
}
let validators = self.unconfirmed.entry((session, candidate)).or_default();
if validators.insert(validator) {
*c += 1;
true
} else {
false
}
}
/// Clear out spam slots for a given candiate in a session.
///
/// This effectively reduces the spam slot count for all validators participating in a dispute
/// for that candidate. You should call this function once a dispute became obsolete or got
/// confirmed and thus votes for it should no longer be treated as potential spam.
pub fn clear(&mut self, key: &(SessionIndex, CandidateHash)) {
if let Some(validators) = self.unconfirmed.remove(key) {
let (session, _) = key;
for validator in validators {
if let Some(c) = self.slots.remove(&(*session, validator)) {
let new = c - 1;
if new > 0 {
self.slots.insert((*session, validator), new);
}
}
}
}
}
/// Prune all spam slots for sessions older than the given index.
pub fn prune_old(&mut self, oldest_index: SessionIndex) {
self.unconfirmed.retain(|(session, _), _| *session >= oldest_index);
self.slots.retain(|(session, _), _| *session >= oldest_index);
}
}
@@ -0,0 +1,165 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::time::{SystemTime, UNIX_EPOCH};
use parity_scale_codec::{Decode, Encode};
use polkadot_primitives::v1::{CandidateHash, SessionIndex};
use crate::real::LOG_TARGET;
/// The choice here is fairly arbitrary. But any dispute that concluded more than a few minutes ago
/// is not worth considering anymore. Changing this value has little to no bearing on consensus,
/// and really only affects the work that the node might do on startup during periods of many
/// disputes.
pub const ACTIVE_DURATION_SECS: Timestamp = 180;
/// Timestamp based on the 1 Jan 1970 UNIX base, which is persistent across node restarts and OS reboots.
pub type Timestamp = u64;
/// The status of dispute. This is a state machine which can be altered by the
/// helper methods.
#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq)]
pub enum DisputeStatus {
/// The dispute is active and unconcluded.
#[codec(index = 0)]
Active,
/// The dispute has been concluded in favor of the candidate
/// since the given timestamp.
#[codec(index = 1)]
ConcludedFor(Timestamp),
/// The dispute has been concluded against the candidate
/// since the given timestamp.
///
/// This takes precedence over `ConcludedFor` in the case that
/// both are true, which is impossible unless a large amount of
/// validators are participating on both sides.
#[codec(index = 2)]
ConcludedAgainst(Timestamp),
/// Dispute has been confirmed (more than `byzantine_threshold` have already participated/ or
/// we have seen the candidate included already/participated successfully ourselves).
#[codec(index = 3)]
Confirmed,
}
impl DisputeStatus {
/// Initialize the status to the active state.
pub fn active() -> DisputeStatus {
DisputeStatus::Active
}
/// Move status to confirmed status, if not yet concluded/confirmed already.
pub fn confirm(self) -> DisputeStatus {
match self {
DisputeStatus::Active => DisputeStatus::Confirmed,
DisputeStatus::Confirmed => DisputeStatus::Confirmed,
DisputeStatus::ConcludedFor(_) | DisputeStatus::ConcludedAgainst(_) => self,
}
}
/// Check whether the dispute is not a spam dispute.
pub fn is_confirmed_concluded(&self) -> bool {
match self {
&DisputeStatus::Confirmed |
&DisputeStatus::ConcludedFor(_) |
DisputeStatus::ConcludedAgainst(_) => true,
&DisputeStatus::Active => false,
}
}
/// 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.
pub fn concluded_for(self, now: Timestamp) -> DisputeStatus {
match self {
DisputeStatus::Active | DisputeStatus::Confirmed => DisputeStatus::ConcludedFor(now),
DisputeStatus::ConcludedFor(at) => DisputeStatus::ConcludedFor(std::cmp::min(at, now)),
against => against,
}
}
/// 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.
pub fn concluded_against(self, now: Timestamp) -> DisputeStatus {
match self {
DisputeStatus::Active | DisputeStatus::Confirmed =>
DisputeStatus::ConcludedAgainst(now),
DisputeStatus::ConcludedFor(at) =>
DisputeStatus::ConcludedAgainst(std::cmp::min(at, now)),
DisputeStatus::ConcludedAgainst(at) =>
DisputeStatus::ConcludedAgainst(std::cmp::min(at, now)),
}
}
/// Whether the disputed candidate is possibly invalid.
pub fn is_possibly_invalid(&self) -> bool {
match self {
DisputeStatus::Active |
DisputeStatus::Confirmed |
DisputeStatus::ConcludedAgainst(_) => true,
DisputeStatus::ConcludedFor(_) => false,
}
}
/// Yields the timestamp this dispute concluded at, if any.
pub fn concluded_at(&self) -> Option<Timestamp> {
match self {
DisputeStatus::Active | DisputeStatus::Confirmed => None,
DisputeStatus::ConcludedFor(at) | DisputeStatus::ConcludedAgainst(at) => Some(*at),
}
}
}
/// Get active disputes as iterator, preserving its `DisputeStatus`.
pub fn get_active_with_status(
recent_disputes: impl Iterator<Item = ((SessionIndex, CandidateHash), DisputeStatus)>,
now: Timestamp,
) -> impl Iterator<Item = ((SessionIndex, CandidateHash), DisputeStatus)> {
recent_disputes.filter_map(move |(disputed, status)| {
status
.concluded_at()
.filter(|at| *at + ACTIVE_DURATION_SECS < now)
.map_or(Some((disputed, status)), |_| None)
})
}
pub trait Clock: Send + Sync {
fn now(&self) -> Timestamp;
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Timestamp {
// `SystemTime` is notoriously non-monotonic, so our timers might not work
// exactly as expected.
//
// Regardless, disputes are considered active based on an order of minutes,
// so a few seconds of slippage in either direction shouldn't affect the
// amount of work the node is doing significantly.
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(e) => {
tracing::warn!(
target: LOG_TARGET,
err = ?e,
"Current time is before unix epoch. Validation will not work correctly."
);
0
},
}
}
}
@@ -14,30 +14,60 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashMap;
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering as AtomicOrdering},
Arc,
},
time::Duration,
};
use super::*;
use assert_matches::assert_matches;
use futures::{
channel::oneshot,
future::{self, BoxFuture},
};
use overseer::TimeoutExt;
use kvdb::KeyValueDB;
use parity_scale_codec::Encode;
use polkadot_node_primitives::SignedDisputeStatement;
use polkadot_node_subsystem::{
messages::{DisputeCoordinatorMessage, DisputeDistributionMessage, ImportStatementsResult},
overseer::FromOverseer,
OverseerSignal,
};
use polkadot_node_subsystem_util::TimeoutExt;
use sc_keystore::LocalKeystore;
use sp_core::testing::TaskExecutor;
use sp_keyring::Sr25519Keyring;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use polkadot_node_subsystem::{
jaeger,
messages::{AllMessages, BlockDescription, RuntimeApiMessage, RuntimeApiRequest},
ActivatedLeaf, ActiveLeavesUpdate, LeafStatus,
};
use polkadot_node_subsystem_test_helpers::{make_subsystem_context, TestSubsystemContextHandle};
use polkadot_primitives::v1::{BlakeTwo256, HashT, Header, SessionInfo, ValidatorId};
use sp_core::testing::TaskExecutor;
use sp_keyring::Sr25519Keyring;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use polkadot_primitives::v1::{
BlakeTwo256, BlockNumber, CandidateCommitments, CandidateHash, CandidateReceipt, Hash, HashT,
Header, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidatorId, ValidatorIndex,
};
use std::{
sync::atomic::{AtomicU64, Ordering as AtomicOrdering},
time::Duration,
use crate::{
metrics::Metrics,
real::{
backend::Backend,
participation::{participation_full_happy_path, participation_missing_availability},
status::ACTIVE_DURATION_SECS,
},
Config, DisputeCoordinatorSubsystem,
};
use super::{
db::v1::DbBackend,
status::{Clock, Timestamp},
};
const TEST_TIMEOUT: Duration = Duration::from_secs(2);
@@ -204,13 +234,23 @@ impl TestState {
)
}
assert_matches!(
virtual_overseer.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_new_leaf,
RuntimeApiRequest::CandidateEvents(tx),
)) => {
tx.send(Ok(Vec::new())).unwrap();
}
);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_new_leaf,
RuntimeApiRequest::FetchOnChainVotes(tx),
)) => {
// add some `BackedCandidates` or resolved disputes here as needed
//add some `BackedCandidates` or resolved disputes here as needed
tx.send(Ok(Some(ScrapedOnChainVotes::default()))).unwrap();
}
)
@@ -222,12 +262,12 @@ impl TestState {
session: SessionIndex,
) {
let leaves: Vec<Hash> = self.headers.keys().cloned().collect();
for leaf in leaves.iter() {
for (n, leaf) in leaves.iter().enumerate() {
virtual_overseer
.send(FromOverseer::Signal(OverseerSignal::ActiveLeaves(
ActiveLeavesUpdate::start_work(ActivatedLeaf {
hash: *leaf,
number: 1,
number: n as u32,
span: Arc::new(jaeger::Span::Disabled),
status: LeafStatus::Fresh,
}),
@@ -286,7 +326,7 @@ impl TestState {
Metrics::default(),
);
let backend = DbBackend::new(self.db.clone(), self.config.column_config());
let subsystem_task = run(subsystem, ctx, backend, Box::new(self.clock.clone()));
let subsystem_task = subsystem.run(ctx, backend, Box::new(self.clock.clone()));
let test_task = test(self, ctx_handle);
let (_, state) = futures::executor::block_on(future::join(subsystem_task, test_task));
@@ -301,6 +341,33 @@ where
TestState::default().resume(test)
}
/// Handle participation messages.
async fn participation_with_distribution(
virtual_overseer: &mut VirtualOverseer,
candidate_hash: &CandidateHash,
) {
participation_full_happy_path(virtual_overseer).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeDistribution(
DisputeDistributionMessage::SendDispute(msg)
) => {
assert_eq!(&msg.candidate_receipt().hash(), candidate_hash);
}
);
}
fn make_valid_candidate_receipt() -> CandidateReceipt {
let mut candidate_receipt = CandidateReceipt::default();
candidate_receipt.commitments_hash = CandidateCommitments::default().hash();
candidate_receipt
}
fn make_invalid_candidate_receipt() -> CandidateReceipt {
// Commitments hash will be 0, which is not correct:
CandidateReceipt::default()
}
#[test]
fn conflicting_votes_lead_to_dispute_participation() {
test_harness(|mut test_state, mut virtual_overseer| {
@@ -309,7 +376,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -338,22 +405,8 @@ fn conflicting_votes_lead_to_dispute_participation() {
},
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(DisputeParticipationMessage::Participate {
candidate_hash: c_hash,
candidate_receipt: c_receipt,
session: s,
n_validators,
report_availability,
}) => {
assert_eq!(c_hash, candidate_hash);
assert_eq!(c_receipt, candidate_receipt);
assert_eq!(s, session);
assert_eq!(n_validators, test_state.validators.len() as u32);
report_availability.send(true).unwrap();
}
);
participation_with_distribution(&mut virtual_overseer, &candidate_hash).await;
{
let (tx, rx) = oneshot::channel();
@@ -376,7 +429,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
.await;
let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.invalid.len(), 1);
}
@@ -405,7 +458,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
.await;
let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.invalid.len(), 2);
}
@@ -427,7 +480,7 @@ fn positive_votes_dont_trigger_participation() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -532,7 +585,7 @@ fn wrong_validator_index_is_ignored() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -602,7 +655,7 @@ fn finality_votes_ignore_disputed_candidates() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -629,17 +682,7 @@ fn finality_votes_ignore_disputed_candidates() {
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
report_availability,
..
}
) => {
report_availability.send(true).unwrap();
}
);
participation_with_distribution(&mut virtual_overseer, &candidate_hash).await;
{
let (tx, rx) = oneshot::channel();
@@ -705,7 +748,7 @@ fn supermajority_valid_dispute_may_be_finalized() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -735,23 +778,7 @@ fn supermajority_valid_dispute_may_be_finalized() {
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
candidate_hash: c_hash,
candidate_receipt: c_receipt,
session: s,
report_availability,
..
}
) => {
assert_eq!(candidate_hash, c_hash);
assert_eq!(candidate_receipt, c_receipt);
assert_eq!(session, s);
report_availability.send(true).unwrap();
}
);
participation_with_distribution(&mut virtual_overseer, &candidate_hash).await;
let mut statements = Vec::new();
for i in (0..supermajority_threshold - 1).map(|i| i + 3) {
@@ -838,7 +865,7 @@ fn concluded_supermajority_for_non_active_after_time() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -868,20 +895,11 @@ fn concluded_supermajority_for_non_active_after_time() {
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
report_availability,
..
}
) => {
report_availability.send(true).unwrap();
}
);
participation_with_distribution(&mut virtual_overseer, &candidate_hash).await;
let mut statements = Vec::new();
for i in (0..supermajority_threshold - 1).map(|i| i + 3) {
// -2: 1 for already imported vote and one for local vote (which is valid).
for i in (0..supermajority_threshold - 2).map(|i| i + 3) {
let vote =
test_state.issue_statement_with_index(i, candidate_hash, session, true).await;
@@ -941,7 +959,8 @@ fn concluded_supermajority_against_non_active_after_time() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_invalid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -955,7 +974,7 @@ fn concluded_supermajority_against_non_active_after_time() {
let invalid_vote =
test_state.issue_statement_with_index(1, candidate_hash, session, false).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
let (pending_confirmation, confirmation_rx) = oneshot::channel();
virtual_overseer
.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
@@ -970,21 +989,15 @@ fn concluded_supermajority_against_non_active_after_time() {
},
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
report_availability,
..
}
) => {
report_availability.send(true).unwrap();
}
assert_matches!(confirmation_rx.await.unwrap(),
ImportStatementsResult::ValidImport => {}
);
participation_with_distribution(&mut virtual_overseer, &candidate_hash).await;
let mut statements = Vec::new();
for i in (0..supermajority_threshold - 1).map(|i| i + 3) {
// minus 2, because of local vote and one previously imported invalid vote.
for i in (0..supermajority_threshold - 2).map(|i| i + 3) {
let vote =
test_state.issue_statement_with_index(i, candidate_hash, session, false).await;
@@ -1029,85 +1042,11 @@ fn concluded_supermajority_against_non_active_after_time() {
}
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
assert!(virtual_overseer.try_recv().await.is_none());
test_state
})
});
}
#[test]
fn fresh_dispute_ignored_if_unavailable() {
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 = CandidateReceipt::default();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
let valid_vote =
test_state.issue_statement_with_index(2, candidate_hash, session, true).await;
let invalid_vote =
test_state.issue_statement_with_index(1, candidate_hash, session, false).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer
.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
candidate_receipt: candidate_receipt.clone(),
session,
statements: vec![
(valid_vote, ValidatorIndex(2)),
(invalid_vote, ValidatorIndex(1)),
],
pending_confirmation,
},
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
report_availability,
..
}
) => {
report_availability.send(false).unwrap();
}
virtual_overseer.try_recv().await,
None => {}
);
{
let (tx, rx) = oneshot::channel();
virtual_overseer
.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
})
.await;
assert!(rx.await.unwrap().is_empty());
let (tx, rx) = oneshot::channel();
virtual_overseer
.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::RecentDisputes(tx),
})
.await;
assert!(rx.await.unwrap().is_empty());
}
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
assert!(virtual_overseer.try_recv().await.is_none());
test_state
})
});
@@ -1121,7 +1060,7 @@ fn resume_dispute_without_local_statement() {
Box::pin(async move {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -1148,17 +1087,8 @@ fn resume_dispute_without_local_statement() {
})
.await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
report_availability,
..
}
) => {
report_availability.send(true).unwrap();
}
);
// Missing availability -> No local vote.
participation_missing_availability(&mut virtual_overseer).await;
assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));
@@ -1186,26 +1116,10 @@ fn resume_dispute_without_local_statement() {
Box::pin(async move {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
candidate_hash: c_hash,
candidate_receipt: c_receipt,
session: s,
report_availability,
..
}
) => {
assert_eq!(candidate_hash, c_hash);
assert_eq!(candidate_receipt, c_receipt);
assert_eq!(session, s);
report_availability.send(true).unwrap();
}
);
participation_with_distribution(&mut virtual_overseer, &candidate_hash).await;
let valid_vote0 =
test_state.issue_statement_with_index(0, candidate_hash, session, true).await;
@@ -1266,7 +1180,7 @@ fn resume_dispute_with_local_statement() {
Box::pin(async move {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -1344,7 +1258,7 @@ fn resume_dispute_without_local_statement_or_local_key() {
Box::pin(async move {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -1386,7 +1300,10 @@ fn resume_dispute_without_local_statement_or_local_key() {
}
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
assert!(virtual_overseer.try_recv().await.is_none());
assert_matches!(
virtual_overseer.try_recv().await,
None => {}
);
test_state
})
@@ -1417,7 +1334,7 @@ fn resume_dispute_with_local_statement_without_local_key() {
Box::pin(async move {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -1505,7 +1422,7 @@ fn issue_local_statement_does_cause_distribution_but_not_duplicate_participation
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_valid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -1552,7 +1469,7 @@ fn issue_local_statement_does_cause_distribution_but_not_duplicate_participation
}
);
// Make sure we don't get a `DisputeParticiationMessage`.
// Make sure we won't participate:
assert!(virtual_overseer.recv().timeout(TEST_TIMEOUT).await.is_none());
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
@@ -1571,7 +1488,7 @@ fn negative_issue_local_statement_only_triggers_import() {
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
let candidate_receipt = CandidateReceipt::default();
let candidate_receipt = make_invalid_candidate_receipt();
let candidate_hash = candidate_receipt.hash();
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
@@ -1596,7 +1513,7 @@ fn negative_issue_local_statement_only_triggers_import() {
let disputes = backend.load_recent_disputes().unwrap();
assert_eq!(disputes, None);
// Assert that subsystem is not sending Participation messages:
// Assert that subsystem is not participating.
assert!(virtual_overseer.recv().timeout(TEST_TIMEOUT).await.is_none());
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
@@ -1,20 +0,0 @@
[package]
name = "polkadot-node-core-dispute-participation"
version = "0.9.13"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
futures = "0.3.17"
thiserror = "1.0.30"
tracing = "0.1.29"
polkadot-node-primitives = { path = "../../primitives" }
polkadot-node-subsystem = { path = "../../subsystem" }
polkadot-primitives = { path = "../../../primitives" }
[dev-dependencies]
assert_matches = "1.5.0"
parity-scale-codec = "2.3.1"
polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers"}
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
@@ -1,372 +0,0 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Implements the dispute participation subsystem.
//!
//! This subsystem is responsible for actually participating in disputes: when
//! notified of a dispute, we recover the candidate data, validate the
//! candidate, and cast our vote in the dispute.
use futures::{channel::oneshot, prelude::*};
use polkadot_node_primitives::{ValidationResult, APPROVAL_EXECUTION_TIMEOUT};
use polkadot_node_subsystem::{
errors::{RecoveryError, RuntimeApiError},
messages::{
AvailabilityRecoveryMessage, AvailabilityStoreMessage, CandidateValidationMessage,
DisputeCoordinatorMessage, DisputeParticipationMessage, RuntimeApiMessage,
RuntimeApiRequest,
},
overseer, ActiveLeavesUpdate, FromOverseer, OverseerSignal, SpawnedSubsystem, SubsystemContext,
SubsystemError,
};
use polkadot_primitives::v1::{BlockNumber, CandidateHash, CandidateReceipt, Hash, SessionIndex};
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "parachain::dispute-participation";
struct State {
recent_block: Option<(BlockNumber, Hash)>,
}
/// An implementation of the dispute participation subsystem.
pub struct DisputeParticipationSubsystem;
impl DisputeParticipationSubsystem {
/// Create a new instance of the subsystem.
pub fn new() -> Self {
DisputeParticipationSubsystem
}
}
impl<Context> overseer::Subsystem<Context, SubsystemError> for DisputeParticipationSubsystem
where
Context: SubsystemContext<Message = DisputeParticipationMessage>,
Context: overseer::SubsystemContext<Message = DisputeParticipationMessage>,
{
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = run(ctx).map(|_| Ok(())).boxed();
SpawnedSubsystem { name: "dispute-participation-subsystem", future }
}
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
RuntimeApi(#[from] RuntimeApiError),
#[error(transparent)]
Subsystem(#[from] SubsystemError),
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error("Oneshot receiver died")]
OneshotSendFailed,
#[error(transparent)]
Participation(#[from] ParticipationError),
}
#[derive(Debug, thiserror::Error)]
pub enum ParticipationError {
#[error("Missing recent block state to participate in dispute")]
MissingRecentBlockState,
#[error("Failed to recover available data for candidate {0}")]
MissingAvailableData(CandidateHash),
#[error("Failed to recover validation code for candidate {0}")]
MissingValidationCode(CandidateHash),
}
impl Error {
fn trace(&self) {
match self {
// don't spam the log with spurious errors
Self::RuntimeApi(_) | Self::Oneshot(_) => {
tracing::debug!(target: LOG_TARGET, err = ?self)
},
// it's worth reporting otherwise
_ => tracing::warn!(target: LOG_TARGET, err = ?self),
}
}
}
async fn run<Context>(mut ctx: Context)
where
Context: SubsystemContext<Message = DisputeParticipationMessage>,
Context: overseer::SubsystemContext<Message = DisputeParticipationMessage>,
{
let mut state = State { recent_block: None };
loop {
match ctx.recv().await {
Err(_) => return,
Ok(FromOverseer::Signal(OverseerSignal::Conclude)) => {
tracing::info!(target: LOG_TARGET, "Received `Conclude` signal, exiting");
return
},
Ok(FromOverseer::Signal(OverseerSignal::BlockFinalized(_, _))) => {},
Ok(FromOverseer::Signal(OverseerSignal::ActiveLeaves(update))) => {
update_state(&mut state, update);
},
Ok(FromOverseer::Communication { msg }) => {
if let Err(err) = handle_incoming(&mut ctx, &mut state, msg).await {
err.trace();
if let Error::Subsystem(SubsystemError::Context(_)) = err {
return
}
}
},
}
}
}
fn update_state(state: &mut State, update: ActiveLeavesUpdate) {
for active in update.activated {
if state.recent_block.map_or(true, |s| active.number > s.0) {
state.recent_block = Some((active.number, active.hash));
}
}
}
async fn handle_incoming(
ctx: &mut impl SubsystemContext,
state: &mut State,
message: DisputeParticipationMessage,
) -> Result<(), Error> {
match message {
DisputeParticipationMessage::Participate {
candidate_hash,
candidate_receipt,
session,
n_validators,
report_availability,
} =>
if let Some((_, block_hash)) = state.recent_block {
participate(
ctx,
block_hash,
candidate_hash,
candidate_receipt,
session,
n_validators,
report_availability,
)
.await
} else {
return Err(ParticipationError::MissingRecentBlockState.into())
},
}
}
async fn participate(
ctx: &mut impl SubsystemContext,
block_hash: Hash,
candidate_hash: CandidateHash,
candidate_receipt: CandidateReceipt,
session: SessionIndex,
n_validators: u32,
report_availability: oneshot::Sender<bool>,
) -> Result<(), Error> {
let (recover_available_data_tx, recover_available_data_rx) = oneshot::channel();
let (code_tx, code_rx) = oneshot::channel();
let (store_available_data_tx, store_available_data_rx) = oneshot::channel();
let (validation_tx, validation_rx) = oneshot::channel();
// in order to validate a candidate we need to start by recovering the
// available data
ctx.send_message(AvailabilityRecoveryMessage::RecoverAvailableData(
candidate_receipt.clone(),
session,
None,
recover_available_data_tx,
))
.await;
let available_data = match recover_available_data_rx.await? {
Ok(data) => {
report_availability.send(true).map_err(|_| Error::OneshotSendFailed)?;
data
},
Err(RecoveryError::Invalid) => {
report_availability.send(true).map_err(|_| Error::OneshotSendFailed)?;
// the available data was recovered but it is invalid, therefore we'll
// vote negatively for the candidate dispute
cast_invalid_vote(ctx, candidate_hash, candidate_receipt, session).await;
return Ok(())
},
Err(RecoveryError::Unavailable) => {
report_availability.send(false).map_err(|_| Error::OneshotSendFailed)?;
return Err(ParticipationError::MissingAvailableData(candidate_hash).into())
},
};
// we also need to fetch the validation code which we can reference by its
// hash as taken from the candidate descriptor
ctx.send_message(RuntimeApiMessage::Request(
block_hash,
RuntimeApiRequest::ValidationCodeByHash(
candidate_receipt.descriptor.validation_code_hash,
code_tx,
),
))
.await;
let validation_code = match code_rx.await?? {
Some(code) => code,
None => {
tracing::warn!(
target: LOG_TARGET,
"Validation code unavailable for code hash {:?} in the state of block {:?}",
candidate_receipt.descriptor.validation_code_hash,
block_hash,
);
return Err(ParticipationError::MissingValidationCode(candidate_hash).into())
},
};
// we dispatch a request to store the available data for the candidate. we
// want to maximize data availability for other potential checkers involved
// in the dispute
ctx.send_message(AvailabilityStoreMessage::StoreAvailableData {
candidate_hash,
n_validators,
available_data: available_data.clone(),
tx: store_available_data_tx,
})
.await;
match store_available_data_rx.await? {
Err(_) => {
tracing::warn!(
target: LOG_TARGET,
"Failed to store available data for candidate {:?}",
candidate_hash,
);
},
Ok(()) => {},
}
// we issue a request to validate the candidate with the provided exhaustive
// parameters
//
// We use the approval execution timeout because this is intended to
// be run outside of backing and therefore should be subject to the
// same level of leeway.
ctx.send_message(CandidateValidationMessage::ValidateFromExhaustive(
available_data.validation_data,
validation_code,
candidate_receipt.descriptor.clone(),
available_data.pov,
APPROVAL_EXECUTION_TIMEOUT,
validation_tx,
))
.await;
// we cast votes (either positive or negative) depending on the outcome of
// the validation and if valid, whether the commitments hash matches
match validation_rx.await? {
Err(err) => {
tracing::warn!(
target: LOG_TARGET,
"Candidate {:?} validation failed with: {:?}",
candidate_receipt.hash(),
err,
);
cast_invalid_vote(ctx, candidate_hash, candidate_receipt, session).await;
},
Ok(ValidationResult::Invalid(invalid)) => {
tracing::warn!(
target: LOG_TARGET,
"Candidate {:?} considered invalid: {:?}",
candidate_hash,
invalid,
);
cast_invalid_vote(ctx, candidate_hash, candidate_receipt, session).await;
},
Ok(ValidationResult::Valid(commitments, _)) => {
if commitments.hash() != candidate_receipt.commitments_hash {
tracing::warn!(
target: LOG_TARGET,
expected = ?candidate_receipt.commitments_hash,
got = ?commitments.hash(),
"Candidate is valid but commitments hash doesn't match",
);
cast_invalid_vote(ctx, candidate_hash, candidate_receipt, session).await;
} else {
cast_valid_vote(ctx, candidate_hash, candidate_receipt, session).await;
}
},
}
Ok(())
}
async fn cast_valid_vote(
ctx: &mut impl SubsystemContext,
candidate_hash: CandidateHash,
candidate_receipt: CandidateReceipt,
session: SessionIndex,
) {
tracing::info!(
target: LOG_TARGET,
"Casting valid vote in dispute for candidate {:?}",
candidate_hash,
);
issue_local_statement(ctx, candidate_hash, candidate_receipt, session, true).await;
}
async fn cast_invalid_vote(
ctx: &mut impl SubsystemContext,
candidate_hash: CandidateHash,
candidate_receipt: CandidateReceipt,
session: SessionIndex,
) {
tracing::info!(
target: LOG_TARGET,
"Casting invalid vote in dispute for candidate {:?}",
candidate_hash,
);
issue_local_statement(ctx, candidate_hash, candidate_receipt, session, false).await;
}
async fn issue_local_statement(
ctx: &mut impl SubsystemContext,
candidate_hash: CandidateHash,
candidate_receipt: CandidateReceipt,
session: SessionIndex,
valid: bool,
) {
ctx.send_message(DisputeCoordinatorMessage::IssueLocalStatement(
session,
candidate_hash,
candidate_receipt,
valid,
))
.await
}
@@ -1,432 +0,0 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use assert_matches::assert_matches;
use futures::future::{self, BoxFuture};
use std::sync::Arc;
use sp_core::testing::TaskExecutor;
use super::*;
use parity_scale_codec::Encode;
use polkadot_node_primitives::{AvailableData, BlockData, InvalidCandidate, PoV};
use polkadot_node_subsystem::{
jaeger,
messages::{AllMessages, ValidationFailed},
overseer::Subsystem,
ActivatedLeaf, ActiveLeavesUpdate, LeafStatus,
};
use polkadot_node_subsystem_test_helpers::{make_subsystem_context, TestSubsystemContextHandle};
use polkadot_primitives::v1::{BlakeTwo256, CandidateCommitments, HashT, Header, ValidationCode};
type VirtualOverseer = TestSubsystemContextHandle<DisputeParticipationMessage>;
fn test_harness<F>(test: F)
where
F: FnOnce(VirtualOverseer) -> BoxFuture<'static, VirtualOverseer>,
{
let (ctx, ctx_handle) = make_subsystem_context(TaskExecutor::new());
let subsystem = DisputeParticipationSubsystem::new();
let spawned_subsystem = subsystem.start(ctx);
let test_future = test(ctx_handle);
let (subsystem_result, _) =
futures::executor::block_on(future::join(spawned_subsystem.future, async move {
let mut ctx_handle = test_future.await;
ctx_handle.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
// no further request is received by the overseer which means that
// no further attempt to participate was made
assert!(ctx_handle.try_recv().await.is_none());
}));
subsystem_result.unwrap();
}
async fn activate_leaf(virtual_overseer: &mut VirtualOverseer, block_number: BlockNumber) {
let block_header = Header {
parent_hash: BlakeTwo256::hash(&block_number.encode()),
number: block_number,
digest: Default::default(),
state_root: Default::default(),
extrinsics_root: Default::default(),
};
let block_hash = block_header.hash();
virtual_overseer
.send(FromOverseer::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work(
ActivatedLeaf {
hash: block_hash,
span: Arc::new(jaeger::Span::Disabled),
number: block_number,
status: LeafStatus::Fresh,
},
))))
.await;
}
async fn participate(virtual_overseer: &mut VirtualOverseer) -> oneshot::Receiver<bool> {
let commitments = CandidateCommitments::default();
let candidate_receipt = {
let mut receipt = CandidateReceipt::default();
receipt.commitments_hash = commitments.hash();
receipt
};
let candidate_hash = candidate_receipt.hash();
let session = 1;
let n_validators = 10;
let (report_availability, receive_availability) = oneshot::channel();
virtual_overseer
.send(FromOverseer::Communication {
msg: DisputeParticipationMessage::Participate {
candidate_hash,
candidate_receipt: candidate_receipt.clone(),
session,
n_validators,
report_availability,
},
})
.await;
receive_availability
}
async fn recover_available_data(
virtual_overseer: &mut VirtualOverseer,
receive_availability: oneshot::Receiver<bool>,
) {
let pov_block = PoV { block_data: BlockData(Vec::new()) };
let available_data =
AvailableData { pov: Arc::new(pov_block), validation_data: Default::default() };
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Ok(available_data)).unwrap();
},
"overseer did not receive recover available data message",
);
assert_eq!(receive_availability.await.expect("Availability should get reported"), true);
}
async fn fetch_validation_code(virtual_overseer: &mut VirtualOverseer) {
let validation_code = ValidationCode(Vec::new());
assert_matches!(
virtual_overseer.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::ValidationCodeByHash(
_,
tx,
)
)) => {
tx.send(Ok(Some(validation_code))).unwrap();
},
"overseer did not receive runtime API request for validation code",
);
}
async fn store_available_data(virtual_overseer: &mut VirtualOverseer, success: bool) {
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityStore(AvailabilityStoreMessage::StoreAvailableData { tx, .. }) => {
if success {
tx.send(Ok(())).unwrap();
} else {
tx.send(Err(())).unwrap();
}
},
"overseer did not receive store available data request",
);
}
#[test]
fn cannot_participate_when_recent_block_state_is_missing() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
let _ = participate(&mut virtual_overseer).await;
virtual_overseer
})
});
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let _ = participate(&mut virtual_overseer).await;
// after activating at least one leaf the recent block
// state should be available which should lead to trying
// to participate by first trying to recover the available
// data
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(..)
),
"overseer did not receive recover available data message",
);
virtual_overseer
})
});
}
#[test]
fn cannot_participate_if_cannot_recover_available_data() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Unavailable)).unwrap();
},
"overseer did not receive recover available data message",
);
assert_eq!(
receive_availability.await.expect("Availability should get reported"),
false
);
virtual_overseer
})
});
}
#[test]
fn cannot_participate_if_cannot_recover_validation_code() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::ValidationCodeByHash(
_,
tx,
)
)) => {
tx.send(Ok(None)).unwrap();
},
"overseer did not receive runtime API request for validation code",
);
virtual_overseer
})
});
}
#[test]
fn cast_invalid_vote_if_available_data_is_invalid() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::AvailabilityRecovery(
AvailabilityRecoveryMessage::RecoverAvailableData(_, _, _, tx)
) => {
tx.send(Err(RecoveryError::Invalid)).unwrap();
},
"overseer did not receive recover available data message",
);
assert_eq!(receive_availability.await.expect("Availability should get reported"), true);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::IssueLocalStatement(
_,
_,
_,
false,
)),
"overseer did not receive issue local statement message",
);
virtual_overseer
})
});
}
#[test]
fn cast_invalid_vote_if_validation_fails_or_is_invalid() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
store_available_data(&mut virtual_overseer, true).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Ok(ValidationResult::Invalid(InvalidCandidate::Timeout))).unwrap();
},
"overseer did not receive candidate validation message",
);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::IssueLocalStatement(
_,
_,
_,
false,
)),
"overseer did not receive issue local statement message",
);
virtual_overseer
})
});
}
#[test]
fn cast_invalid_vote_if_validation_passes_but_commitments_dont_match() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
store_available_data(&mut virtual_overseer, true).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
let mut commitments = CandidateCommitments::default();
// this should lead to a commitments hash mismatch
commitments.processed_downward_messages = 42;
tx.send(Ok(ValidationResult::Valid(commitments, Default::default()))).unwrap();
},
"overseer did not receive candidate validation message",
);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::IssueLocalStatement(
_,
_,
_,
false,
)),
"overseer did not receive issue local statement message",
);
virtual_overseer
})
});
}
#[test]
fn cast_valid_vote_if_validation_passes() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
store_available_data(&mut virtual_overseer, true).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Ok(ValidationResult::Valid(Default::default(), Default::default()))).unwrap();
},
"overseer did not receive candidate validation message",
);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::IssueLocalStatement(
_,
_,
_,
true,
)),
"overseer did not receive issue local statement message",
);
virtual_overseer
})
});
}
#[test]
fn failure_to_store_available_data_does_not_preclude_participation() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
// the store available data request should fail
store_available_data(&mut virtual_overseer, false).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::CandidateValidation(
CandidateValidationMessage::ValidateFromExhaustive(_, _, _, _, timeout, tx)
) if timeout == APPROVAL_EXECUTION_TIMEOUT => {
tx.send(Err(ValidationFailed("fail".to_string()))).unwrap();
},
"overseer did not receive candidate validation message",
);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::IssueLocalStatement(
_,
_,
_,
false,
)),
"overseer did not receive issue local statement message",
);
virtual_overseer
})
});
}
@@ -49,18 +49,18 @@ impl From<runtime::Error> for Error {
#[derive(Debug, Error)]
pub enum Fatal {
/// Spawning a running task failed.
#[error("Spawning subsystem task failed")]
#[error("Spawning subsystem task failed: {0}")]
SpawnTask(#[source] SubsystemError),
/// Requester stream exhausted.
#[error("Erasure chunk requester stream exhausted")]
RequesterExhausted,
#[error("Receive channel closed")]
#[error("Receive channel closed: {0}")]
IncomingMessageChannel(#[source] SubsystemError),
/// Errors coming from runtime::Runtime.
#[error("Error while accessing runtime information")]
#[error("Error while accessing runtime information: {0}")]
Runtime(#[from] runtime::Fatal),
}
@@ -84,7 +84,7 @@ pub enum NonFatal {
SendResponse,
/// Fetching PoV failed with `RequestError`.
#[error("FetchPoV request error")]
#[error("FetchPoV request error: {0}")]
FetchPoV(#[source] RequestError),
/// Fetching PoV failed as the received PoV did not match the expected hash.
@@ -99,7 +99,7 @@ pub enum NonFatal {
InvalidValidatorIndex,
/// Errors coming from runtime::Runtime.
#[error("Error while accessing runtime information")]
#[error("Error while accessing runtime information: {0}")]
Runtime(#[from] runtime::NonFatal),
}
@@ -1225,8 +1225,6 @@ fn spread_event_to_subsystems_is_up_to_date() {
cnt += 1;
},
AllMessages::DisputeCoordinator(_) => unreachable!("Not interested in network events"),
AllMessages::DisputeParticipation(_) =>
unreachable!("Not interested in network events"),
AllMessages::DisputeDistribution(_) => unreachable!("Not interested in network events"),
AllMessages::ChainSelection(_) => unreachable!("Not interested in network events"),
// Add variants here as needed, `{ cnt += 1; }` for those that need to be
@@ -145,7 +145,7 @@ where
) -> Self {
let runtime = RuntimeInfo::new_with_config(runtime::Config {
keystore: Some(keystore),
session_cache_lru_size: DISPUTE_WINDOW as usize,
session_cache_lru_size: DISPUTE_WINDOW.get() as usize,
});
let (tx, sender_rx) = mpsc::channel(1);
let disputes_sender = DisputeSender::new(tx, metrics.clone());
@@ -145,7 +145,7 @@ where
) -> Self {
let runtime = RuntimeInfo::new_with_config(runtime::Config {
keystore: None,
session_cache_lru_size: DISPUTE_WINDOW as usize,
session_cache_lru_size: DISPUTE_WINDOW.get() as usize,
});
Self {
runtime,
-4
View File
@@ -89,7 +89,6 @@ pub fn dummy_overseer_builder<'a, Spawner, SupportsParachains>(
DummySubsystem,
DummySubsystem,
DummySubsystem,
DummySubsystem,
>,
SubsystemError,
>
@@ -130,7 +129,6 @@ pub fn one_for_all_overseer_builder<'a, Spawner, SupportsParachains, Sub>(
Sub,
Sub,
Sub,
Sub,
>,
SubsystemError,
>
@@ -156,7 +154,6 @@ where
+ Subsystem<OverseerSubsystemContext<ApprovalVotingMessage>, SubsystemError>
+ Subsystem<OverseerSubsystemContext<GossipSupportMessage>, SubsystemError>
+ Subsystem<OverseerSubsystemContext<DisputeCoordinatorMessage>, SubsystemError>
+ Subsystem<OverseerSubsystemContext<DisputeParticipationMessage>, SubsystemError>
+ Subsystem<OverseerSubsystemContext<DisputeDistributionMessage>, SubsystemError>
+ Subsystem<OverseerSubsystemContext<ChainSelectionMessage>, SubsystemError>,
{
@@ -181,7 +178,6 @@ where
.approval_voting(subsystem.clone())
.gossip_support(subsystem.clone())
.dispute_coordinator(subsystem.clone())
.dispute_participation(subsystem.clone())
.dispute_distribution(subsystem.clone())
.chain_selection(subsystem)
.activation_external_listeners(Default::default())
+3 -6
View File
@@ -80,9 +80,9 @@ use polkadot_node_subsystem_types::messages::{
AvailabilityRecoveryMessage, AvailabilityStoreMessage, BitfieldDistributionMessage,
BitfieldSigningMessage, CandidateBackingMessage, CandidateValidationMessage, ChainApiMessage,
ChainSelectionMessage, CollationGenerationMessage, CollatorProtocolMessage,
DisputeCoordinatorMessage, DisputeDistributionMessage, DisputeParticipationMessage,
GossipSupportMessage, NetworkBridgeEvent, NetworkBridgeMessage, ProvisionerMessage,
RuntimeApiMessage, StatementDistributionMessage,
DisputeCoordinatorMessage, DisputeDistributionMessage, GossipSupportMessage,
NetworkBridgeEvent, NetworkBridgeMessage, ProvisionerMessage, RuntimeApiMessage,
StatementDistributionMessage,
};
pub use polkadot_node_subsystem_types::{
errors::{SubsystemError, SubsystemResult},
@@ -462,9 +462,6 @@ pub struct Overseer<SupportsParachains> {
#[subsystem(no_dispatch, DisputeCoordinatorMessage)]
dispute_coordinator: DisputeCoordinator,
#[subsystem(no_dispatch, DisputeParticipationMessage)]
dispute_participation: DisputeParticipation,
#[subsystem(no_dispatch, DisputeDistributionMessage)]
dispute_distribution: DisputeDistribution,
+1 -19
View File
@@ -888,17 +888,6 @@ fn test_dispute_coordinator_msg() -> DisputeCoordinatorMessage {
DisputeCoordinatorMessage::RecentDisputes(sender)
}
fn test_dispute_participation_msg() -> DisputeParticipationMessage {
let (sender, _) = oneshot::channel();
DisputeParticipationMessage::Participate {
candidate_hash: Default::default(),
candidate_receipt: Default::default(),
session: 0,
n_validators: 0,
report_availability: sender,
}
}
fn test_dispute_distribution_msg() -> DisputeDistributionMessage {
let dummy_dispute_message = UncheckedDisputeMessage {
candidate_receipt: Default::default(),
@@ -930,7 +919,7 @@ fn test_chain_selection_msg() -> ChainSelectionMessage {
// Checks that `stop`, `broadcast_signal` and `broadcast_message` are implemented correctly.
#[test]
fn overseer_all_subsystems_receive_signals_and_messages() {
const NUM_SUBSYSTEMS: usize = 21;
const NUM_SUBSYSTEMS: usize = 20;
// -3 for BitfieldSigning, GossipSupport and AvailabilityDistribution
const NUM_SUBSYSTEMS_MESSAGED: usize = NUM_SUBSYSTEMS - 3;
@@ -1009,9 +998,6 @@ fn overseer_all_subsystems_receive_signals_and_messages() {
handle
.send_msg_anon(AllMessages::DisputeCoordinator(test_dispute_coordinator_msg()))
.await;
handle
.send_msg_anon(AllMessages::DisputeParticipation(test_dispute_participation_msg()))
.await;
handle
.send_msg_anon(AllMessages::DisputeDistribution(test_dispute_distribution_msg()))
.await;
@@ -1069,7 +1055,6 @@ fn context_holds_onto_message_until_enough_signals_received() {
let (approval_voting_bounded_tx, _) = metered::channel(CHANNEL_CAPACITY);
let (gossip_support_bounded_tx, _) = metered::channel(CHANNEL_CAPACITY);
let (dispute_coordinator_bounded_tx, _) = metered::channel(CHANNEL_CAPACITY);
let (dispute_participation_bounded_tx, _) = metered::channel(CHANNEL_CAPACITY);
let (dispute_distribution_bounded_tx, _) = metered::channel(CHANNEL_CAPACITY);
let (chain_selection_bounded_tx, _) = metered::channel(CHANNEL_CAPACITY);
@@ -1091,7 +1076,6 @@ fn context_holds_onto_message_until_enough_signals_received() {
let (approval_voting_unbounded_tx, _) = metered::unbounded();
let (gossip_support_unbounded_tx, _) = metered::unbounded();
let (dispute_coordinator_unbounded_tx, _) = metered::unbounded();
let (dispute_participation_unbounded_tx, _) = metered::unbounded();
let (dispute_distribution_unbounded_tx, _) = metered::unbounded();
let (chain_selection_unbounded_tx, _) = metered::unbounded();
@@ -1114,7 +1098,6 @@ fn context_holds_onto_message_until_enough_signals_received() {
approval_voting: approval_voting_bounded_tx.clone(),
gossip_support: gossip_support_bounded_tx.clone(),
dispute_coordinator: dispute_coordinator_bounded_tx.clone(),
dispute_participation: dispute_participation_bounded_tx.clone(),
dispute_distribution: dispute_distribution_bounded_tx.clone(),
chain_selection: chain_selection_bounded_tx.clone(),
@@ -1136,7 +1119,6 @@ fn context_holds_onto_message_until_enough_signals_received() {
approval_voting_unbounded: approval_voting_unbounded_tx.clone(),
gossip_support_unbounded: gossip_support_unbounded_tx.clone(),
dispute_coordinator_unbounded: dispute_coordinator_unbounded_tx.clone(),
dispute_participation_unbounded: dispute_participation_unbounded_tx.clone(),
dispute_distribution_unbounded: dispute_distribution_unbounded_tx.clone(),
chain_selection_unbounded: chain_selection_unbounded_tx.clone(),
};
+53 -6
View File
@@ -65,12 +65,6 @@ pub const VALIDATION_CODE_BOMB_LIMIT: usize = (MAX_CODE_SIZE * 4u32) as usize;
/// The bomb limit for decompressing PoV blobs.
pub const POV_BOMB_LIMIT: usize = (MAX_POV_SIZE * 4u32) as usize;
/// It would be nice to draw this from the chain state, but we have no tools for it right now.
/// On Polkadot this is 1 day, and on Kusama it's 6 hours.
///
/// Number of sessions we want to consider in disputes.
pub const DISPUTE_WINDOW: SessionIndex = 6;
/// The amount of time to spend on execution during backing.
pub const BACKING_EXECUTION_TIMEOUT: Duration = Duration::from_secs(2);
@@ -82,6 +76,59 @@ pub const BACKING_EXECUTION_TIMEOUT: Duration = Duration::from_secs(2);
/// dispute participants.
pub const APPROVAL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(6);
/// Type of a session window size.
///
/// We are not using `NonZeroU32` here because `expect` and `unwrap` are not yet const, so global
/// constants of `SessionWindowSize` would require `lazy_static` in that case.
///
/// See: https://github.com/rust-lang/rust/issues/67441
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct SessionWindowSize(SessionIndex);
#[macro_export]
/// Create a new checked `SessionWindowSize`
///
/// which cannot be 0.
macro_rules! new_session_window_size {
(0) => {
compile_error!("Must be non zero");
};
(0_u32) => {
compile_error!("Must be non zero");
};
(0 as u32) => {
compile_error!("Must be non zero");
};
(0 as _) => {
compile_error!("Must be non zero");
};
($l:literal) => {
SessionWindowSize::unchecked_new($l as _)
};
}
/// It would be nice to draw this from the chain state, but we have no tools for it right now.
/// On Polkadot this is 1 day, and on Kusama it's 6 hours.
///
/// Number of sessions we want to consider in disputes.
pub const DISPUTE_WINDOW: SessionWindowSize = new_session_window_size!(6);
impl SessionWindowSize {
/// Get the value as `SessionIndex` for doing comparisons with those.
pub fn get(self) -> SessionIndex {
self.0
}
/// Helper function for `new_session_window_size`.
///
/// Don't use it. The only reason it is public, is because otherwise the
/// `new_session_window_size` macro would not work outside of this module.
#[doc(hidden)]
pub const fn unchecked_new(size: SessionIndex) -> Self {
Self(size)
}
}
/// The cumulative weight of a block in a fork-choice rule.
pub type BlockWeight = u32;
-2
View File
@@ -108,7 +108,6 @@ polkadot-node-core-candidate-validation = { path = "../core/candidate-validation
polkadot-node-core-chain-api = { path = "../core/chain-api", optional = true }
polkadot-node-core-chain-selection = { path = "../core/chain-selection", optional = true }
polkadot-node-core-dispute-coordinator = { path = "../core/dispute-coordinator", optional = true }
polkadot-node-core-dispute-participation = { path = "../core/dispute-participation", optional = true }
polkadot-node-core-provisioner = { path = "../core/provisioner", optional = true }
polkadot-node-core-runtime-api = { path = "../core/runtime-api", optional = true }
polkadot-statement-distribution = { path = "../network/statement-distribution", optional = true }
@@ -145,7 +144,6 @@ full-node = [
"polkadot-node-core-chain-api",
"polkadot-node-core-chain-selection",
"polkadot-node-core-dispute-coordinator",
"polkadot-node-core-dispute-participation",
"polkadot-node-core-provisioner",
"polkadot-node-core-runtime-api",
"polkadot-statement-distribution",
-3
View File
@@ -59,7 +59,6 @@ pub use polkadot_node_core_candidate_validation::CandidateValidationSubsystem;
pub use polkadot_node_core_chain_api::ChainApiSubsystem;
pub use polkadot_node_core_chain_selection::ChainSelectionSubsystem;
pub use polkadot_node_core_dispute_coordinator::DisputeCoordinatorSubsystem;
pub use polkadot_node_core_dispute_participation::DisputeParticipationSubsystem;
pub use polkadot_node_core_provisioner::ProvisioningSubsystem as ProvisionerSubsystem;
pub use polkadot_node_core_runtime_api::RuntimeApiSubsystem;
pub use polkadot_statement_distribution::StatementDistribution as StatementDistributionSubsystem;
@@ -159,7 +158,6 @@ pub fn prepared_overseer_builder<'a, Spawner, RuntimeClient>(
ApprovalVotingSubsystem,
GossipSupportSubsystem<AuthorityDiscoveryService>,
DisputeCoordinatorSubsystem,
DisputeParticipationSubsystem,
DisputeDistributionSubsystem<AuthorityDiscoveryService>,
ChainSelectionSubsystem,
>,
@@ -259,7 +257,6 @@ where
keystore.clone(),
Metrics::register(registry)?,
))
.dispute_participation(DisputeParticipationSubsystem::new())
.dispute_distribution(DisputeDistributionSubsystem::new(
keystore.clone(),
dispute_req_receiver,
@@ -275,25 +275,6 @@ pub enum ImportStatementsResult {
ValidImport,
}
/// Messages received by the dispute participation subsystem.
#[derive(Debug)]
pub enum DisputeParticipationMessage {
/// Validate a candidate for the purposes of participating in a dispute.
Participate {
/// The hash of the candidate
candidate_hash: CandidateHash,
/// The candidate receipt itself.
candidate_receipt: CandidateReceipt,
/// The session the candidate appears in.
session: SessionIndex,
/// The number of validators in the session.
n_validators: u32,
/// Give immediate feedback on whether the candidate was available or
/// not.
report_availability: oneshot::Sender<bool>,
},
}
/// Messages going to the dispute distribution subsystem.
#[derive(Debug)]
pub enum DisputeDistributionMessage {
+2
View File
@@ -22,6 +22,7 @@ polkadot-node-jaeger = { path = "../jaeger" }
polkadot-node-metrics = { path = "../metrics" }
polkadot-node-network-protocol = { path = "../network/protocol" }
polkadot-primitives = { path = "../../primitives" }
polkadot-node-primitives = { path = "../primitives" }
polkadot-overseer = { path = "../overseer" }
metered-channel = { path = "../metered-channel" }
@@ -35,3 +36,4 @@ env_logger = "0.9.0"
futures = { version = "0.3.17", features = ["thread-pool"] }
log = "0.4.13"
polkadot-node-subsystem-test-helpers = { path = "../subsystem-test-helpers" }
lazy_static = "1.4.0"
+1
View File
@@ -209,6 +209,7 @@ specialize_requests! {
fn request_assumed_validation_data(para_id: ParaId, expected_persisted_validation_data_hash: Hash) -> Option<(PersistedValidationData, ValidationCodeHash)>; AssumedValidationData;
fn request_session_index_for_child() -> SessionIndex; SessionIndexForChild;
fn request_validation_code(para_id: ParaId, assumption: OccupiedCoreAssumption) -> Option<ValidationCode>; ValidationCode;
fn request_validation_code_by_hash(validation_code_hash: ValidationCodeHash) -> Option<ValidationCode>; ValidationCodeByHash;
fn request_candidate_pending_availability(para_id: ParaId) -> Option<CommittedCandidateReceipt>; CandidatePendingAvailability;
fn request_candidate_events() -> Vec<CandidateEvent>; CandidateEvents;
fn request_session_info(index: SessionIndex) -> Option<SessionInfo>; SessionInfo;
@@ -19,6 +19,7 @@
//! This is useful for consensus components which need to stay up-to-date about recent sessions but don't
//! care about the state of particular blocks.
pub use polkadot_node_primitives::{new_session_window_size, SessionWindowSize};
use polkadot_primitives::v1::{Hash, SessionIndex, SessionInfo};
use futures::channel::oneshot;
@@ -27,6 +28,7 @@ use polkadot_node_subsystem::{
messages::{RuntimeApiMessage, RuntimeApiRequest},
overseer, SubsystemContext,
};
use thiserror::Error;
/// Sessions unavailable in state to cache.
#[derive(Debug)]
@@ -51,7 +53,7 @@ pub struct SessionsUnavailableInfo {
}
/// Sessions were unavailable to fetch from the state for some reason.
#[derive(Debug)]
#[derive(Debug, Error)]
pub struct SessionsUnavailable {
/// The error kind.
kind: SessionsUnavailableKind,
@@ -59,16 +61,15 @@ pub struct SessionsUnavailable {
info: Option<SessionsUnavailableInfo>,
}
impl core::fmt::Display for SessionsUnavailable {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
write!(f, "Sessions unavailable: {:?}, info: {:?}", self.kind, self.info)
}
}
/// An indicated update of the rolling session window.
#[derive(Debug, PartialEq, Clone)]
pub enum SessionWindowUpdate {
/// The session window was just initialized to the current values.
Initialized {
/// The start of the window (inclusive).
window_start: SessionIndex,
/// The end of the window (inclusive).
window_end: SessionIndex,
},
/// The session window was just advanced from one range to a new one.
Advanced {
/// The previous start of the window (inclusive).
@@ -85,49 +86,63 @@ pub enum SessionWindowUpdate {
}
/// A rolling window of sessions and cached session info.
#[derive(Default)]
pub struct RollingSessionWindow {
earliest_session: Option<SessionIndex>,
earliest_session: SessionIndex,
session_info: Vec<SessionInfo>,
window_size: SessionIndex,
window_size: SessionWindowSize,
}
impl RollingSessionWindow {
/// Initialize a new session info cache with the given window size.
pub fn new(window_size: SessionIndex) -> Self {
RollingSessionWindow { earliest_session: None, session_info: Vec::new(), window_size }
pub async fn new(
ctx: &mut (impl SubsystemContext + overseer::SubsystemContext),
window_size: SessionWindowSize,
block_hash: Hash,
) -> Result<Self, SessionsUnavailable> {
let session_index = get_session_index_for_head(ctx, block_hash).await?;
let window_start = session_index.saturating_sub(window_size.get() - 1);
match load_all_sessions(ctx, block_hash, window_start, session_index).await {
Err(kind) => Err(SessionsUnavailable {
kind,
info: Some(SessionsUnavailableInfo {
window_start,
window_end: session_index,
block_hash,
}),
}),
Ok(s) => Ok(Self { earliest_session: window_start, session_info: s, window_size }),
}
}
/// Initialize a new session info cache with the given window size and
/// initial data.
pub fn with_session_info(
window_size: SessionIndex,
window_size: SessionWindowSize,
earliest_session: SessionIndex,
session_info: Vec<SessionInfo>,
) -> Self {
RollingSessionWindow { earliest_session: Some(earliest_session), session_info, window_size }
RollingSessionWindow { earliest_session, session_info, window_size }
}
/// Access the session info for the given session index, if stored within the window.
pub fn session_info(&self, index: SessionIndex) -> Option<&SessionInfo> {
self.earliest_session.and_then(|earliest| {
if index < earliest {
None
} else {
self.session_info.get((index - earliest) as usize)
}
})
if index < self.earliest_session {
None
} else {
self.session_info.get((index - self.earliest_session) as usize)
}
}
/// Access the index of the earliest session, if the window is not empty.
pub fn earliest_session(&self) -> Option<SessionIndex> {
self.earliest_session.clone()
}
/// Access the index of the latest session, if the window is not empty.
pub fn latest_session(&self) -> Option<SessionIndex> {
/// Access the index of the earliest session.
pub fn earliest_session(&self) -> SessionIndex {
self.earliest_session
.map(|earliest| earliest + (self.session_info.len() as SessionIndex).saturating_sub(1))
}
/// Access the index of the latest session.
pub fn latest_session(&self) -> SessionIndex {
self.earliest_session + (self.session_info.len() as SessionIndex).saturating_sub(1)
}
/// When inspecting a new import notification, updates the session info cache to match
@@ -142,116 +157,86 @@ impl RollingSessionWindow {
ctx: &mut (impl SubsystemContext + overseer::SubsystemContext),
block_hash: Hash,
) -> Result<SessionWindowUpdate, SessionsUnavailable> {
if self.window_size == 0 {
let session_index = get_session_index_for_head(ctx, block_hash).await?;
let old_window_start = self.earliest_session;
let latest = self.latest_session();
// Either cached or ancient.
if session_index <= latest {
return Ok(SessionWindowUpdate::Unchanged)
}
let session_index = {
let (s_tx, s_rx) = oneshot::channel();
let old_window_end = latest;
// We're requesting session index of a child to populate the cache in advance.
ctx.send_message(RuntimeApiMessage::Request(
block_hash,
RuntimeApiRequest::SessionIndexForChild(s_tx),
))
.await;
let window_start = session_index.saturating_sub(self.window_size.get() - 1);
match s_rx.await {
Ok(Ok(s)) => s,
Ok(Err(e)) =>
return Err(SessionsUnavailable {
kind: SessionsUnavailableKind::RuntimeApi(e),
info: None,
}),
Err(e) =>
return Err(SessionsUnavailable {
kind: SessionsUnavailableKind::RuntimeApiUnavailable(e),
info: None,
}),
}
};
// keep some of the old window, if applicable.
let overlap_start = window_start.saturating_sub(old_window_start);
match self.earliest_session {
None => {
// First block processed on start-up.
let fresh_start = if latest < window_start { window_start } else { latest + 1 };
let window_start = session_index.saturating_sub(self.window_size - 1);
match load_all_sessions(ctx, block_hash, fresh_start, session_index).await {
Err(kind) => Err(SessionsUnavailable {
kind,
info: Some(SessionsUnavailableInfo {
window_start: fresh_start,
window_end: session_index,
block_hash,
}),
}),
Ok(s) => {
let update = SessionWindowUpdate::Advanced {
prev_window_start: old_window_start,
prev_window_end: old_window_end,
new_window_start: window_start,
new_window_end: session_index,
};
match load_all_sessions(ctx, block_hash, window_start, session_index).await {
Err(kind) => Err(SessionsUnavailable {
kind,
info: Some(SessionsUnavailableInfo {
window_start,
window_end: session_index,
block_hash,
}),
}),
Ok(s) => {
let update = SessionWindowUpdate::Initialized {
window_start,
window_end: session_index,
};
let outdated = std::cmp::min(overlap_start as usize, self.session_info.len());
self.session_info.drain(..outdated);
self.session_info.extend(s);
// we need to account for this case:
// window_start ................................... session_index
// old_window_start ........... latest
let new_earliest = std::cmp::max(window_start, old_window_start);
self.earliest_session = new_earliest;
self.earliest_session = Some(window_start);
self.session_info = s;
Ok(update)
},
}
},
Some(old_window_start) => {
let latest =
self.latest_session().expect("latest always exists if earliest does; qed");
// Either cached or ancient.
if session_index <= latest {
return Ok(SessionWindowUpdate::Unchanged)
}
let old_window_end = latest;
let window_start = session_index.saturating_sub(self.window_size - 1);
// keep some of the old window, if applicable.
let overlap_start = window_start.saturating_sub(old_window_start);
let fresh_start = if latest < window_start { window_start } else { latest + 1 };
match load_all_sessions(ctx, block_hash, fresh_start, session_index).await {
Err(kind) => Err(SessionsUnavailable {
kind,
info: Some(SessionsUnavailableInfo {
window_start: fresh_start,
window_end: session_index,
block_hash,
}),
}),
Ok(s) => {
let update = SessionWindowUpdate::Advanced {
prev_window_start: old_window_start,
prev_window_end: old_window_end,
new_window_start: window_start,
new_window_end: session_index,
};
let outdated =
std::cmp::min(overlap_start as usize, self.session_info.len());
self.session_info.drain(..outdated);
self.session_info.extend(s);
// we need to account for this case:
// window_start ................................... session_index
// old_window_start ........... latest
let new_earliest = std::cmp::max(window_start, old_window_start);
self.earliest_session = Some(new_earliest);
Ok(update)
},
}
Ok(update)
},
}
}
}
async fn get_session_index_for_head(
ctx: &mut (impl SubsystemContext + overseer::SubsystemContext),
block_hash: Hash,
) -> Result<SessionIndex, SessionsUnavailable> {
let (s_tx, s_rx) = oneshot::channel();
// We're requesting session index of a child to populate the cache in advance.
ctx.send_message(RuntimeApiMessage::Request(
block_hash,
RuntimeApiRequest::SessionIndexForChild(s_tx),
))
.await;
match s_rx.await {
Ok(Ok(s)) => Ok(s),
Ok(Err(e)) =>
return Err(SessionsUnavailable {
kind: SessionsUnavailableKind::RuntimeApi(e),
info: None,
}),
Err(e) =>
return Err(SessionsUnavailable {
kind: SessionsUnavailableKind::RuntimeApiUnavailable(e),
info: None,
}),
}
}
async fn load_all_sessions(
ctx: &mut (impl SubsystemContext + overseer::SubsystemContext),
block_hash: Hash,
@@ -289,7 +274,7 @@ mod tests {
use polkadot_primitives::v1::Header;
use sp_core::testing::TaskExecutor;
const TEST_WINDOW_SIZE: SessionIndex = 6;
pub const TEST_WINDOW_SIZE: SessionWindowSize = new_session_window_size!(6);
fn dummy_session_info(index: SessionIndex) -> SessionInfo {
SessionInfo {
@@ -309,7 +294,7 @@ mod tests {
fn cache_session_info_test(
expected_start_session: SessionIndex,
session: SessionIndex,
mut window: RollingSessionWindow,
window: Option<RollingSessionWindow>,
expect_requests_from: SessionIndex,
) {
let header = Header {
@@ -328,9 +313,15 @@ mod tests {
let test_fut = {
Box::pin(async move {
window.cache_session_info_for_head(&mut ctx, hash).await.unwrap();
assert_eq!(window.earliest_session, Some(expected_start_session));
let window = match window {
None =>
RollingSessionWindow::new(&mut ctx, TEST_WINDOW_SIZE, hash).await.unwrap(),
Some(mut window) => {
window.cache_session_info_for_head(&mut ctx, hash).await.unwrap();
window
},
};
assert_eq!(window.earliest_session, expected_start_session);
assert_eq!(
window.session_info,
(expected_start_session..=session).map(dummy_session_info).collect::<Vec<_>>(),
@@ -370,34 +361,34 @@ mod tests {
#[test]
fn cache_session_info_first_early() {
cache_session_info_test(0, 1, RollingSessionWindow::new(TEST_WINDOW_SIZE), 0);
cache_session_info_test(0, 1, None, 0);
}
#[test]
fn cache_session_info_does_not_underflow() {
let window = RollingSessionWindow {
earliest_session: Some(1),
earliest_session: 1,
session_info: vec![dummy_session_info(1)],
window_size: TEST_WINDOW_SIZE,
};
cache_session_info_test(1, 2, window, 2);
cache_session_info_test(1, 2, Some(window), 2);
}
#[test]
fn cache_session_info_first_late() {
cache_session_info_test(
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE.get() - 1),
100,
RollingSessionWindow::new(TEST_WINDOW_SIZE),
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
None,
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE.get() - 1),
);
}
#[test]
fn cache_session_info_jump() {
let window = RollingSessionWindow {
earliest_session: Some(50),
earliest_session: 50,
session_info: vec![
dummy_session_info(50),
dummy_session_info(51),
@@ -407,43 +398,43 @@ mod tests {
};
cache_session_info_test(
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE.get() - 1),
100,
window,
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
Some(window),
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE.get() - 1),
);
}
#[test]
fn cache_session_info_roll_full() {
let start = 99 - (TEST_WINDOW_SIZE - 1);
let start = 99 - (TEST_WINDOW_SIZE.get() - 1);
let window = RollingSessionWindow {
earliest_session: Some(start),
earliest_session: start,
session_info: (start..=99).map(dummy_session_info).collect(),
window_size: TEST_WINDOW_SIZE,
};
cache_session_info_test(
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE.get() - 1),
100,
window,
Some(window),
100, // should only make one request.
);
}
#[test]
fn cache_session_info_roll_many_full() {
let start = 97 - (TEST_WINDOW_SIZE - 1);
let start = 97 - (TEST_WINDOW_SIZE.get() - 1);
let window = RollingSessionWindow {
earliest_session: Some(start),
earliest_session: start,
session_info: (start..=97).map(dummy_session_info).collect(),
window_size: TEST_WINDOW_SIZE,
};
cache_session_info_test(
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE.get() - 1),
100,
window,
Some(window),
98,
);
}
@@ -452,13 +443,16 @@ mod tests {
fn cache_session_info_roll_early() {
let start = 0;
let window = RollingSessionWindow {
earliest_session: Some(start),
earliest_session: start,
session_info: (0..=1).map(dummy_session_info).collect(),
window_size: TEST_WINDOW_SIZE,
};
cache_session_info_test(
0, 2, window, 2, // should only make one request.
0,
2,
Some(window),
2, // should only make one request.
);
}
@@ -466,18 +460,18 @@ mod tests {
fn cache_session_info_roll_many_early() {
let start = 0;
let window = RollingSessionWindow {
earliest_session: Some(start),
earliest_session: start,
session_info: (0..=1).map(dummy_session_info).collect(),
window_size: TEST_WINDOW_SIZE,
};
cache_session_info_test(0, 3, window, 2);
cache_session_info_test(0, 3, Some(window), 2);
}
#[test]
fn any_session_unavailable_for_caching_means_no_change() {
let session: SessionIndex = 6;
let start_session = session.saturating_sub(TEST_WINDOW_SIZE - 1);
let start_session = session.saturating_sub(TEST_WINDOW_SIZE.get() - 1);
let header = Header {
digest: Default::default(),
@@ -490,13 +484,11 @@ mod tests {
let pool = TaskExecutor::new();
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
let mut window = RollingSessionWindow::new(TEST_WINDOW_SIZE);
let hash = header.hash();
let test_fut = {
Box::pin(async move {
let res = window.cache_session_info_for_head(&mut ctx, hash).await;
let res = RollingSessionWindow::new(&mut ctx, TEST_WINDOW_SIZE, hash).await;
assert!(res.is_err());
})
};
@@ -551,14 +543,14 @@ mod tests {
let pool = TaskExecutor::new();
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
let mut window = RollingSessionWindow::new(TEST_WINDOW_SIZE);
let hash = header.hash();
let test_fut = {
Box::pin(async move {
window.cache_session_info_for_head(&mut ctx, hash).await.unwrap();
let window =
RollingSessionWindow::new(&mut ctx, TEST_WINDOW_SIZE, hash).await.unwrap();
assert_eq!(window.earliest_session, Some(session));
assert_eq!(window.earliest_session, session);
assert_eq!(window.session_info, vec![dummy_session_info(session)]);
})
};
@@ -48,11 +48,11 @@ pub enum Fatal {
pub enum NonFatal {
/// Some request to the runtime failed.
/// For example if we prune a block we're requesting info about.
#[error("Runtime API error")]
#[error("Runtime API error {0}")]
RuntimeRequest(RuntimeApiError),
/// We tried fetching a session info which was not available.
#[error("There was no session with the given index")]
#[error("There was no session with the given index {0}")]
NoSuchSession(SessionIndex),
}
@@ -27,13 +27,14 @@ use sp_keystore::{CryptoStore, SyncCryptoStorePtr};
use polkadot_node_subsystem::{SubsystemContext, SubsystemSender};
use polkadot_primitives::v1::{
CoreState, EncodeAs, GroupIndex, GroupRotationInfo, Hash, OccupiedCore, SessionIndex,
SessionInfo, Signed, SigningContext, UncheckedSigned, ValidatorId, ValidatorIndex,
CandidateEvent, CoreState, EncodeAs, GroupIndex, GroupRotationInfo, Hash, OccupiedCore,
SessionIndex, SessionInfo, Signed, SigningContext, UncheckedSigned, ValidationCode,
ValidationCodeHash, ValidatorId, ValidatorIndex,
};
use crate::{
request_availability_cores, request_session_index_for_child, request_session_info,
request_validator_groups,
request_availability_cores, request_candidate_events, request_session_index_for_child,
request_session_info, request_validation_code_by_hash, request_validator_groups,
};
/// Errors that can happen on runtime fetches.
@@ -300,3 +301,27 @@ where
recv_runtime(request_validator_groups(relay_parent, ctx.sender()).await).await?;
Ok(info)
}
/// Get `CandidateEvent`s for the given `relay_parent`.
pub async fn get_candidate_events<Sender>(
sender: &mut Sender,
relay_parent: Hash,
) -> Result<Vec<CandidateEvent>>
where
Sender: SubsystemSender,
{
recv_runtime(request_candidate_events(relay_parent, sender).await).await
}
/// Fetch `ValidationCode` by hash from the runtime.
pub async fn get_validation_code_by_hash<Sender>(
sender: &mut Sender,
relay_parent: Hash,
validation_code_hash: ValidationCodeHash,
) -> Result<Option<ValidationCode>>
where
Sender: SubsystemSender,
{
recv_runtime(request_validation_code_by_hash(relay_parent, validation_code_hash, sender).await)
.await
}
+11
View File
@@ -1277,6 +1277,17 @@ impl DisputeStatement {
DisputeStatement::Invalid(_) => true,
}
}
/// Statement is backing statement.
pub fn is_backing(&self) -> bool {
match *self {
Self::Valid(ValidDisputeStatementKind::BackingSeconded(_)) |
Self::Valid(ValidDisputeStatementKind::BackingValid(_)) => true,
Self::Valid(ValidDisputeStatementKind::Explicit) |
Self::Valid(ValidDisputeStatementKind::ApprovalChecking) |
Self::Invalid(_) => false,
}
}
}
/// Different kinds of statements of validity on a candidate.
@@ -54,7 +54,6 @@
- [Approval Distribution](node/approval/approval-distribution.md)
- [Disputes Subsystems](node/disputes/README.md)
- [Dispute Coordinator](node/disputes/dispute-coordinator.md)
- [Dispute Participation](node/disputes/dispute-participation.md)
- [Dispute Distribution](node/disputes/dispute-distribution.md)
- [Utility Subsystems](node/utility/README.md)
- [Availability Store](node/utility/availability-store.md)
@@ -4,4 +4,4 @@ The approval subsystems implement the node-side of the [Approval Protocol](../..
We make a divide between the [assignment/voting logic](approval-voting.md) and the [distribution logic](approval-distribution.md) that distributes assignment certifications and approval votes. The logic in the assignment and voting also informs the GRANDPA voting rule on how to vote.
These subsystems are intended to flag issues and begin [participating in live disputes](../disputes/dispute-participation.md). Dispute subsystems also track all observed votes (backing, approval, and dispute-specific) by all validators on all candidates.
These subsystems are intended to flag issues and begin participating in live disputes. Dispute subsystems also track all observed votes (backing, approval, and dispute-specific) by all validators on all candidates.
@@ -2,7 +2,7 @@
This is the central subsystem of the node-side components which participate in disputes. This subsystem wraps a database which tracks all statements observed by all validators over some window of sessions. Votes older than this session window are pruned.
This subsystem will be the point which produce dispute votes, either positive or negative, based on locally-observed validation results as well as a sink for votes received by other subsystems. When importing a dispute vote from another node, this will trigger the [dispute participation](dispute-participation.md) subsystem to recover and validate the block and call back to this subsystem.
This subsystem will be the point which produce dispute votes, either positive or negative, based on locally-observed validation results as well as a sink for votes received by other subsystems. When importing a dispute vote from another node, this will trigger participation in the dispute.
## Database Schema
@@ -56,11 +56,10 @@ Input: [`DisputeCoordinatorMessage`][DisputeCoordinatorMessage]
Output:
- [`RuntimeApiMessage`][RuntimeApiMessage]
- [`DisputeParticipationMessage`][DisputeParticipationMessage]
## Functionality
This assumes a constant `DISPUTE_WINDOW: SessionIndex`. This should correspond to at least 1 day.
This assumes a constant `DISPUTE_WINDOW: SessionWindowSize`. This should correspond to at least 1 day.
Ephemeral in-memory state:
@@ -75,8 +74,7 @@ struct State {
Check DB for recorded votes for non concluded disputes we have not yet
recorded a local statement for.
For all of those send `DisputeParticipationMessage::Participate` message to
dispute participation subsystem.
For all of those initiate dispute participation.
### On `OverseerSignal::ActiveLeavesUpdate`
@@ -171,4 +169,3 @@ Do nothing.
[DisputeStatement]: ../../types/disputes.md#disputestatement
[DisputeCoordinatorMessage]: ../../types/overseer-protocol.md#dispute-coordinator-message
[RuntimeApiMessage]: ../../types/overseer-protocol.md#runtime-api-message
[DisputeParticipationMessage]: ../../types/overseer-protocol.md#dispute-participation-message
@@ -21,9 +21,9 @@ This design should result in a protocol that is:
### Output
- [`DisputeCoordinatorMessage::ActiveDisputes`][DisputeParticipationMessage]
- [`DisputeCoordinatorMessage::ImportStatements`][DisputeParticipationMessage]
- [`DisputeCoordinatorMessage::QueryCandidateVotes`][DisputeParticipationMessage]
- [`DisputeCoordinatorMessage::ActiveDisputes`][DisputeCoordinatorMessage]
- [`DisputeCoordinatorMessage::ImportStatements`][DisputeCoordinatorMessage]
- [`DisputeCoordinatorMessage::QueryCandidateVotes`][DisputeCoordinatorMessage]
- [`RuntimeApiMessage`][RuntimeApiMessage]
### Wire format
@@ -357,4 +357,3 @@ no real harm done: There was no serious attack to begin with.
[DisputeDistributionMessage]: ../../types/overseer-protocol.md#dispute-distribution-message
[RuntimeApiMessage]: ../../types/overseer-protocol.md#runtime-api-message
[DisputeParticipationMessage]: ../../types/overseer-protocol.md#dispute-participation-message
@@ -1,68 +0,0 @@
# Dispute Participation
This subsystem is responsible for actually participating in disputes: when notified of a dispute, we need to recover the candidate data, validate the candidate, and cast our vote in the dispute.
Fortunately, most of that work is handled by other subsystems; this subsystem is just a small glue component for tying other subsystems together and issuing statements based on their validity.
## Protocol
Input: [`DisputeParticipationMessage`][DisputeParticipationMessage]
Output:
- [`RuntimeApiMessage`][RuntimeApiMessage]
- [`CandidateValidationMessage`][CandidateValidationMessage]
- [`AvailabilityRecoveryMessage`][AvailabilityRecoveryMessage]
- [`AvailabilityStoreMessage`][AvailabilityStoreMessage]
- [`ChainApiMessage`][ChainApiMessage]
## Functionality
In-memory state:
```rust
struct State {
recent_block_hash: Option<(BlockNumber, Hash)>
}
```
### On `OverseerSignal::ActiveLeavesUpdate`
Update `recent_block` in in-memory state according to the highest observed active leaf.
### On `OverseerSignal::BlockFinalized`
Do nothing.
### On `OverseerSignal::Conclude`
Conclude.
### On `DisputeParticipationMessage::Participate`
* Decompose into parts: `{ candidate_hash, candidate_receipt, session, voted_indices }`
* Issue an [`AvailabilityRecoveryMessage::RecoverAvailableData`][AvailabilityRecoveryMessage]
* Report back availability result to the `AvailabilityRecoveryMessage` sender
via the `report_availability` oneshot.
* If the result is `Unavailable`, return.
* If the result is `Invalid`, [cast invalid votes](#cast-votes) and return.
* If the data is recovered, dispatch a [`RuntimeApiMessage::ValidationCodeByHash`][RuntimeApiMessage] with the parameters `(candidate_receipt.descriptor.validation_code_hash)` at `state.recent_block.hash`.
* Dispatch a [`AvailabilityStoreMessage::StoreAvailableData`][AvailabilityStoreMessage] with the data.
* If the code is not fetched from the chain, return. This should be impossible with correct relay chain configuration, at least if chain synchronization is working correctly.
* Dispatch a [`CandidateValidationMessage::ValidateFromExhaustive`][CandidateValidationMessage] with the available data and the validation code and `APPROVAL_EXECUTION_TIMEOUT` as the timeout parameter.
* If the validation result is `Invalid`, [cast invalid votes](#cast-votes) and return.
* If the validation fails, [cast invalid votes](#cast-votes) and return.
* If the validation succeeds, compute the `CandidateCommitments` based on the validation result and compare against the candidate receipt's `commitments_hash`. If they match, [cast valid votes](#cast-votes) and if not, [cast invalid votes](#cast-votes).
### Cast Votes
This requires the parameters `{ candidate_receipt, candidate_hash, session, voted_indices }` as well as a choice of either `Valid` or `Invalid`.
Invoke [`DisputeCoordinatorMessage::IssueLocalStatement`][DisputeCoordinatorMessage] with `is_valid` according to the parameterization,.
[RuntimeApiMessage]: ../../types/overseer-protocol.md#runtime-api-message
[DisputeParticipationMessage]: ../../types/overseer-protocol.md#dispute-participation-message
[DisputeCoordinatorMessage]: ../../types/overseer-protocol.md#dispute-coordinator-message
[CandidateValidationMessage]: ../../types/overseer-protocol.md#candidate-validation-message
[AvailabilityRecoveryMessage]: ../../types/overseer-protocol.md#availability-recovery-message
[ChainApiMessage]: ../../types/overseer-protocol.md#chain-api-message
[AvailabilityStoreMessage]: ../../types/overseer-protocol.md#availability-store-message
@@ -69,7 +69,6 @@ enum AllMessages {
ApprovalDistribution(ApprovalDistributionMessage),
GossipSupport(GossipSupportMessage),
DisputeCoordinator(DisputeCoordinatorMessage),
DisputeParticipation(DisputeParticipationMessage),
ChainSelection(ChainSelectionMessage),
}
```
@@ -473,30 +472,6 @@ pub enum ImportStatementsResult {
}
```
## Dispute Participation Message
Messages received by the [Dispute Participation subsystem](../node/disputes/dispute-participation.md)
This subsystem simply executes requests to evaluate a candidate.
```rust
enum DisputeParticipationMessage {
/// Validate a candidate for the purposes of participating in a dispute.
Participate {
/// The hash of the candidate
candidate_hash: CandidateHash,
/// The candidate receipt itself.
candidate_receipt: CandidateReceipt,
/// The session the candidate appears in.
session: SessionIndex,
/// The number of validators in the session.
n_validators: u32,
/// Give immediate feedback on whether the candidate was available or
/// not.
report_availability: oneshot::Sender<bool>,
}
}
```
## Dispute Distribution Message