mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 08:11:04 +00:00
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:
Generated
+2
-17
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user