feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "pezkuwi-dispute-distribution"
|
||||
version = "7.0.0"
|
||||
description = "Pezkuwi Dispute Distribution subsystem, which ensures all concerned validators are aware of a dispute and have the relevant votes."
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["std"], workspace = true, default-features = true }
|
||||
fatality = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-timer = { workspace = true }
|
||||
gum = { workspace = true, default-features = true }
|
||||
indexmap = { workspace = true }
|
||||
pezkuwi-node-network-protocol = { workspace = true, default-features = true }
|
||||
pezkuwi-node-primitives = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
|
||||
pezkuwi-primitives = { workspace = true, default-features = true }
|
||||
sc-network = { workspace = true, default-features = true }
|
||||
sp-application-crypto = { workspace = true, default-features = true }
|
||||
sp-keystore = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
pezkuwi-node-subsystem-test-helpers = { workspace = true }
|
||||
pezkuwi-primitives-test-helpers = { workspace = true }
|
||||
sc-keystore = { workspace = true, default-features = true }
|
||||
sp-keyring = { workspace = true, default-features = true }
|
||||
sp-tracing = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"gum/runtime-benchmarks",
|
||||
"pezkuwi-node-network-protocol/runtime-benchmarks",
|
||||
"pezkuwi-node-primitives/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem-util/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem/runtime-benchmarks",
|
||||
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"sc-network/runtime-benchmarks",
|
||||
"sp-keyring/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
//! Error handling related code and Error/Result definitions.
|
||||
|
||||
use pezkuwi_node_subsystem::SubsystemError;
|
||||
use pezkuwi_node_subsystem_util::runtime;
|
||||
|
||||
use crate::{sender, LOG_TARGET};
|
||||
|
||||
use fatality::Nested;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[fatality::fatality(splitable)]
|
||||
pub enum Error {
|
||||
/// Receiving subsystem message from overseer failed.
|
||||
#[fatal]
|
||||
#[error("Receiving message from overseer failed")]
|
||||
SubsystemReceive(#[source] SubsystemError),
|
||||
|
||||
/// Spawning a running task failed.
|
||||
#[fatal]
|
||||
#[error("Spawning subsystem task failed")]
|
||||
SpawnTask(#[source] SubsystemError),
|
||||
|
||||
/// `DisputeSender` mpsc receiver exhausted.
|
||||
#[fatal]
|
||||
#[error("Erasure chunk requester stream exhausted")]
|
||||
SenderExhausted,
|
||||
|
||||
/// Errors coming from `runtime::Runtime`.
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information")]
|
||||
Runtime(#[from] runtime::Error),
|
||||
|
||||
/// Errors coming from `DisputeSender`
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information")]
|
||||
Sender(#[from] sender::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub type FatalResult<T> = std::result::Result<T, FatalError>;
|
||||
|
||||
/// 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<()>, ctx: &'static str) -> std::result::Result<(), FatalError> {
|
||||
match result.into_nested()? {
|
||||
Err(jfyi) => {
|
||||
gum::warn!(target: LOG_TARGET, error = ?jfyi, ctx);
|
||||
Ok(())
|
||||
},
|
||||
Ok(()) => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! # Sending and receiving of `DisputeRequest`s.
|
||||
//!
|
||||
//! This subsystem essentially consists of two parts:
|
||||
//!
|
||||
//! - a sender
|
||||
//! - and a receiver
|
||||
//!
|
||||
//! The sender is responsible for getting our vote out, see `sender`. The receiver handles
|
||||
//! incoming [`DisputeRequest`](v1::DisputeRequest)s and offers spam protection, see `receiver`.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::{channel::mpsc, FutureExt, StreamExt, TryFutureExt};
|
||||
|
||||
use pezkuwi_node_network_protocol::authority_discovery::AuthorityDiscovery;
|
||||
use pezkuwi_node_subsystem_util::nesting_sender::NestingSender;
|
||||
use sp_keystore::KeystorePtr;
|
||||
|
||||
use pezkuwi_node_network_protocol::request_response::{incoming::IncomingRequestReceiver, v1};
|
||||
use pezkuwi_node_primitives::DISPUTE_WINDOW;
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::DisputeDistributionMessage, overseer, FromOrchestra, OverseerSignal,
|
||||
SpawnedSubsystem, SubsystemError,
|
||||
};
|
||||
use pezkuwi_node_subsystem_util::{runtime, runtime::RuntimeInfo};
|
||||
|
||||
/// ## The sender [`DisputeSender`]
|
||||
///
|
||||
/// The sender (`DisputeSender`) keeps track of live disputes and makes sure our vote gets out for
|
||||
/// each one of those. The sender is responsible for sending our vote to each validator
|
||||
/// participating in the dispute and to each authority currently authoring blocks. The sending can
|
||||
/// be initiated by sending `DisputeDistributionMessage::SendDispute` message to this subsystem.
|
||||
///
|
||||
/// In addition the `DisputeSender` will query the coordinator for active disputes on each
|
||||
/// [`DisputeSender::update_leaves`] call and will initiate sending (start a `SendTask`) for every,
|
||||
/// to this subsystem, unknown dispute. This is to make sure, we get our vote out, even on
|
||||
/// restarts.
|
||||
///
|
||||
/// The actual work of sending and keeping track of transmission attempts to each validator for a
|
||||
/// particular dispute are done by [`SendTask`]. The purpose of the `DisputeSender` is to keep
|
||||
/// track of all ongoing disputes and start and clean up `SendTask`s accordingly.
|
||||
mod sender;
|
||||
use self::sender::{DisputeSender, DisputeSenderMessage};
|
||||
|
||||
/// ## The receiver [`DisputesReceiver`]
|
||||
///
|
||||
/// The receiving side is implemented as `DisputesReceiver` and is run as a separate long running
|
||||
/// task within this subsystem ([`DisputesReceiver::run`]).
|
||||
///
|
||||
/// Conceptually all the receiver has to do, is waiting for incoming requests which are passed in
|
||||
/// via a dedicated channel and forwarding them to the dispute coordinator via
|
||||
/// `DisputeCoordinatorMessage::ImportStatements`. Being the interface to the network and untrusted
|
||||
/// nodes, the reality is not that simple of course. Before importing statements the receiver will
|
||||
/// batch up imports as well as possible for efficient imports while maintaining timely dispute
|
||||
/// resolution and handling of spamming validators:
|
||||
///
|
||||
/// - Drop all messages from non validator nodes, for this it requires the [`AuthorityDiscovery`]
|
||||
/// service.
|
||||
/// - Drop messages from a node, if it sends at a too high rate.
|
||||
/// - Filter out duplicate messages (over some period of time).
|
||||
/// - Drop any obviously invalid votes (invalid signatures for example).
|
||||
/// - Ban peers whose votes were deemed invalid.
|
||||
///
|
||||
/// In general dispute-distribution works on limiting the work the dispute-coordinator will have to
|
||||
/// do, while at the same time making it aware of new disputes as fast as possible.
|
||||
///
|
||||
/// For successfully imported votes, we will confirm the receipt of the message back to the sender.
|
||||
/// This way a received confirmation guarantees, that the vote has been stored to disk by the
|
||||
/// receiver.
|
||||
mod receiver;
|
||||
use self::receiver::DisputesReceiver;
|
||||
|
||||
/// Error and [`Result`] type for this subsystem.
|
||||
mod error;
|
||||
use error::{log_error, Error, FatalError, FatalResult, Result};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod metrics;
|
||||
//// Prometheus `Metrics` for dispute distribution.
|
||||
pub use metrics::Metrics;
|
||||
|
||||
const LOG_TARGET: &'static str = "teyrchain::dispute-distribution";
|
||||
|
||||
/// Rate limit on the `receiver` side.
|
||||
///
|
||||
/// If messages from one peer come in at a higher rate than every `RECEIVE_RATE_LIMIT` on average,
|
||||
/// we start dropping messages from that peer to enforce that limit.
|
||||
pub const RECEIVE_RATE_LIMIT: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Rate limit on the `sender` side.
|
||||
///
|
||||
/// In order to not hit the `RECEIVE_RATE_LIMIT` on the receiving side, we limit out sending rate as
|
||||
/// well.
|
||||
///
|
||||
/// We add 50ms extra, just to have some save margin to the `RECEIVE_RATE_LIMIT`.
|
||||
pub const SEND_RATE_LIMIT: Duration = RECEIVE_RATE_LIMIT.saturating_add(Duration::from_millis(50));
|
||||
|
||||
/// The dispute distribution subsystem.
|
||||
pub struct DisputeDistributionSubsystem<AD> {
|
||||
/// Easy and efficient runtime access for this subsystem.
|
||||
runtime: RuntimeInfo,
|
||||
|
||||
/// Sender for our dispute requests.
|
||||
disputes_sender: DisputeSender<DisputeSenderMessage>,
|
||||
|
||||
/// Receive messages from `DisputeSender` background tasks.
|
||||
sender_rx: mpsc::Receiver<DisputeSenderMessage>,
|
||||
|
||||
/// Receiver for incoming requests.
|
||||
req_receiver: Option<IncomingRequestReceiver<v1::DisputeRequest>>,
|
||||
|
||||
/// Authority discovery service.
|
||||
authority_discovery: AD,
|
||||
|
||||
/// Metrics for this subsystem.
|
||||
metrics: Metrics,
|
||||
}
|
||||
|
||||
#[overseer::subsystem(DisputeDistribution, error = SubsystemError, prefix = self::overseer)]
|
||||
impl<Context, AD> DisputeDistributionSubsystem<AD>
|
||||
where
|
||||
<Context as overseer::DisputeDistributionContextTrait>::Sender:
|
||||
overseer::DisputeDistributionSenderTrait + Sync + Send,
|
||||
AD: AuthorityDiscovery + Clone,
|
||||
{
|
||||
fn start(self, ctx: Context) -> SpawnedSubsystem {
|
||||
let future = self
|
||||
.run(ctx)
|
||||
.map_err(|e| SubsystemError::with_origin("dispute-distribution", e))
|
||||
.boxed();
|
||||
|
||||
SpawnedSubsystem { name: "dispute-distribution-subsystem", future }
|
||||
}
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(DisputeDistribution, prefix = self::overseer)]
|
||||
impl<AD> DisputeDistributionSubsystem<AD>
|
||||
where
|
||||
AD: AuthorityDiscovery + Clone,
|
||||
{
|
||||
/// Create a new instance of the dispute distribution.
|
||||
pub fn new(
|
||||
keystore: KeystorePtr,
|
||||
req_receiver: IncomingRequestReceiver<v1::DisputeRequest>,
|
||||
authority_discovery: AD,
|
||||
metrics: Metrics,
|
||||
) -> Self {
|
||||
let runtime = RuntimeInfo::new_with_config(runtime::Config {
|
||||
keystore: Some(keystore),
|
||||
session_cache_lru_size: DISPUTE_WINDOW.get(),
|
||||
});
|
||||
let (tx, sender_rx) = NestingSender::new_root(1);
|
||||
let disputes_sender = DisputeSender::new(tx, metrics.clone());
|
||||
Self {
|
||||
runtime,
|
||||
disputes_sender,
|
||||
sender_rx,
|
||||
req_receiver: Some(req_receiver),
|
||||
authority_discovery,
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start processing work as passed on from the Overseer.
|
||||
async fn run<Context>(mut self, mut ctx: Context) -> std::result::Result<(), FatalError> {
|
||||
let receiver = DisputesReceiver::new(
|
||||
ctx.sender().clone(),
|
||||
self.req_receiver
|
||||
.take()
|
||||
.expect("Must be provided on `new` and we take ownership here. qed."),
|
||||
self.authority_discovery.clone(),
|
||||
self.metrics.clone(),
|
||||
);
|
||||
ctx.spawn("disputes-receiver", receiver.run().boxed())
|
||||
.map_err(FatalError::SpawnTask)?;
|
||||
|
||||
// Process messages for sending side.
|
||||
//
|
||||
// Note: We want the sender to be rate limited and we are currently taking advantage of the
|
||||
// fact that the root task of this subsystem is only concerned with sending: Functions of
|
||||
// `DisputeSender` might back pressure if the rate limit is hit, which will slow down this
|
||||
// loop. If this fact ever changes, we will likely need another task.
|
||||
loop {
|
||||
let message = MuxedMessage::receive(&mut ctx, &mut self.sender_rx).await;
|
||||
match message {
|
||||
MuxedMessage::Subsystem(result) => {
|
||||
let result = match result? {
|
||||
FromOrchestra::Signal(signal) => {
|
||||
match self.handle_signals(&mut ctx, signal).await {
|
||||
Ok(SignalResult::Conclude) => return Ok(()),
|
||||
Ok(SignalResult::Continue) => Ok(()),
|
||||
Err(f) => Err(f),
|
||||
}
|
||||
},
|
||||
FromOrchestra::Communication { msg } =>
|
||||
self.handle_subsystem_message(&mut ctx, msg).await,
|
||||
};
|
||||
log_error(result, "on FromOrchestra")?;
|
||||
},
|
||||
MuxedMessage::Sender(result) => {
|
||||
let result = self
|
||||
.disputes_sender
|
||||
.on_message(
|
||||
&mut ctx,
|
||||
&mut self.runtime,
|
||||
result.ok_or(FatalError::SenderExhausted)?,
|
||||
)
|
||||
.await
|
||||
.map_err(Error::Sender);
|
||||
log_error(result, "on_message")?;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle overseer signals.
|
||||
async fn handle_signals<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
signal: OverseerSignal,
|
||||
) -> Result<SignalResult> {
|
||||
match signal {
|
||||
OverseerSignal::Conclude => return Ok(SignalResult::Conclude),
|
||||
OverseerSignal::ActiveLeaves(update) => {
|
||||
self.disputes_sender.update_leaves(ctx, &mut self.runtime, update).await?;
|
||||
},
|
||||
OverseerSignal::BlockFinalized(_, _) => {},
|
||||
};
|
||||
Ok(SignalResult::Continue)
|
||||
}
|
||||
|
||||
/// Handle `DisputeDistributionMessage`s.
|
||||
async fn handle_subsystem_message<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
msg: DisputeDistributionMessage,
|
||||
) -> Result<()> {
|
||||
match msg {
|
||||
DisputeDistributionMessage::SendDispute(dispute_msg) =>
|
||||
self.disputes_sender.start_sender(ctx, &mut self.runtime, dispute_msg).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Messages to be handled in this subsystem.
|
||||
#[derive(Debug)]
|
||||
enum MuxedMessage {
|
||||
/// Messages from other subsystems.
|
||||
Subsystem(FatalResult<FromOrchestra<DisputeDistributionMessage>>),
|
||||
/// Messages from spawned sender background tasks.
|
||||
Sender(Option<DisputeSenderMessage>),
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(DisputeDistribution, prefix = self::overseer)]
|
||||
impl MuxedMessage {
|
||||
async fn receive<Context>(
|
||||
ctx: &mut Context,
|
||||
from_sender: &mut mpsc::Receiver<DisputeSenderMessage>,
|
||||
) -> Self {
|
||||
// We are only fusing here to make `select` happy, in reality we will quit if the stream
|
||||
// ends.
|
||||
let from_overseer = ctx.recv().fuse();
|
||||
futures::pin_mut!(from_overseer, from_sender);
|
||||
// We select biased to make sure we finish up loose ends, before starting new work.
|
||||
futures::select_biased!(
|
||||
msg = from_sender.next() => MuxedMessage::Sender(msg),
|
||||
msg = from_overseer => MuxedMessage::Subsystem(msg.map_err(FatalError::SubsystemReceive)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of handling signal from overseer.
|
||||
enum SignalResult {
|
||||
/// Overseer asked us to conclude.
|
||||
Conclude,
|
||||
/// We can continue processing events.
|
||||
Continue,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use pezkuwi_node_subsystem_util::{
|
||||
metrics,
|
||||
metrics::{
|
||||
prometheus,
|
||||
prometheus::{Counter, CounterVec, Opts, PrometheusError, Registry, U64},
|
||||
},
|
||||
};
|
||||
|
||||
/// Label for success counters.
|
||||
pub const SUCCEEDED: &'static str = "succeeded";
|
||||
|
||||
/// Label for fail counters.
|
||||
pub const FAILED: &'static str = "failed";
|
||||
|
||||
/// Dispute Distribution metrics.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Metrics(Option<MetricsInner>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MetricsInner {
|
||||
/// Number of sent dispute requests (succeeded and failed).
|
||||
sent_requests: CounterVec<U64>,
|
||||
|
||||
/// Number of requests received.
|
||||
///
|
||||
/// This is all requests coming in, regardless of whether they are processed or dropped.
|
||||
received_requests: Counter<U64>,
|
||||
|
||||
/// Number of requests for which `ImportStatements` returned.
|
||||
///
|
||||
/// We both have successful imports and failed imports here.
|
||||
imported_requests: CounterVec<U64>,
|
||||
|
||||
/// The duration of issued dispute request to response.
|
||||
time_dispute_request: prometheus::Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
/// Create new dummy metrics, not reporting anything.
|
||||
pub fn new_dummy() -> Self {
|
||||
Metrics(None)
|
||||
}
|
||||
|
||||
/// Increment counter on finished request sending.
|
||||
pub fn on_sent_request(&self, label: &'static str) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.sent_requests.with_label_values(&[label]).inc()
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment counter on served disputes.
|
||||
pub fn on_received_request(&self) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.received_requests.inc()
|
||||
}
|
||||
}
|
||||
|
||||
/// Statements have been imported.
|
||||
pub fn on_imported(&self, label: &'static str, num_requests: usize) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics
|
||||
.imported_requests
|
||||
.with_label_values(&[label])
|
||||
.inc_by(num_requests as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a timer to time request/response duration.
|
||||
pub fn time_dispute_request(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.time_dispute_request.start_timer())
|
||||
}
|
||||
}
|
||||
|
||||
impl metrics::Metrics for Metrics {
|
||||
fn try_register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
let metrics = MetricsInner {
|
||||
sent_requests: prometheus::register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"pezkuwi_teyrchain_dispute_distribution_sent_requests",
|
||||
"Total number of sent requests.",
|
||||
),
|
||||
&["success"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
received_requests: prometheus::register(
|
||||
Counter::new(
|
||||
"pezkuwi_teyrchain_dispute_distribution_received_requests",
|
||||
"Total number of received dispute requests.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
imported_requests: prometheus::register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"pezkuwi_teyrchain_dispute_distribution_imported_requests",
|
||||
"Total number of imported requests.",
|
||||
),
|
||||
&["success"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
time_dispute_request: prometheus::register(
|
||||
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_dispute_distribution_time_dispute_request",
|
||||
"Time needed for dispute votes to get confirmed/fail getting transmitted.",
|
||||
))?,
|
||||
registry,
|
||||
)?,
|
||||
};
|
||||
Ok(Metrics(Some(metrics)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use gum::CandidateHash;
|
||||
use pezkuwi_node_network_protocol::{
|
||||
request_response::{incoming::OutgoingResponseSender, v1::DisputeRequest},
|
||||
PeerId,
|
||||
};
|
||||
use pezkuwi_node_primitives::SignedDisputeStatement;
|
||||
use pezkuwi_primitives::{CandidateReceiptV2 as CandidateReceipt, ValidatorIndex};
|
||||
|
||||
use crate::receiver::{BATCH_COLLECTING_INTERVAL, MIN_KEEP_BATCH_ALIVE_VOTES};
|
||||
|
||||
use super::MAX_BATCH_LIFETIME;
|
||||
|
||||
/// A batch of votes to be imported into the `dispute-coordinator`.
|
||||
///
|
||||
/// Vote imports are way more efficient when performed in batches, hence we batch together incoming
|
||||
/// votes until the rate of incoming votes falls below a threshold, then we import into the dispute
|
||||
/// coordinator.
|
||||
///
|
||||
/// A `Batch` keeps track of the votes to be imported and the current incoming rate, on rate update
|
||||
/// it will "flush" in case the incoming rate dropped too low, preparing the import.
|
||||
pub struct Batch {
|
||||
/// The actual candidate this batch is concerned with.
|
||||
candidate_receipt: CandidateReceipt,
|
||||
|
||||
/// Cache of `CandidateHash` (candidate_receipt.hash()).
|
||||
candidate_hash: CandidateHash,
|
||||
|
||||
/// All valid votes received in this batch so far.
|
||||
///
|
||||
/// We differentiate between valid and invalid votes, so we can detect (and drop) duplicates,
|
||||
/// while still allowing validators to equivocate.
|
||||
///
|
||||
/// Detecting and rejecting duplicates is crucial in order to effectively enforce
|
||||
/// `MIN_KEEP_BATCH_ALIVE_VOTES` per `BATCH_COLLECTING_INTERVAL`. If we would count duplicates
|
||||
/// here, the mechanism would be broken.
|
||||
valid_votes: HashMap<ValidatorIndex, SignedDisputeStatement>,
|
||||
|
||||
/// All invalid votes received in this batch so far.
|
||||
invalid_votes: HashMap<ValidatorIndex, SignedDisputeStatement>,
|
||||
|
||||
/// How many votes have been batched since the last tick/creation.
|
||||
votes_batched_since_last_tick: u32,
|
||||
|
||||
/// Expiry time for the batch.
|
||||
///
|
||||
/// By this time the latest this batch will get flushed.
|
||||
best_before: Instant,
|
||||
|
||||
/// Requesters waiting for a response.
|
||||
requesters: Vec<(PeerId, OutgoingResponseSender<DisputeRequest>)>,
|
||||
}
|
||||
|
||||
/// Result of checking a batch every `BATCH_COLLECTING_INTERVAL`.
|
||||
pub(super) enum TickResult {
|
||||
/// Batch is still alive, please call `tick` again at the given `Instant`.
|
||||
Alive(Batch, Instant),
|
||||
/// Batch is done, ready for import!
|
||||
Done(PreparedImport),
|
||||
}
|
||||
|
||||
/// Ready for import.
|
||||
pub struct PreparedImport {
|
||||
pub candidate_receipt: CandidateReceipt,
|
||||
pub statements: Vec<(SignedDisputeStatement, ValidatorIndex)>,
|
||||
/// Information about original requesters.
|
||||
pub requesters: Vec<(PeerId, OutgoingResponseSender<DisputeRequest>)>,
|
||||
}
|
||||
|
||||
impl From<Batch> for PreparedImport {
|
||||
fn from(batch: Batch) -> Self {
|
||||
let Batch {
|
||||
candidate_receipt,
|
||||
valid_votes,
|
||||
invalid_votes,
|
||||
requesters: pending_responses,
|
||||
..
|
||||
} = batch;
|
||||
|
||||
let statements = valid_votes
|
||||
.into_iter()
|
||||
.chain(invalid_votes.into_iter())
|
||||
.map(|(index, statement)| (statement, index))
|
||||
.collect();
|
||||
|
||||
Self { candidate_receipt, statements, requesters: pending_responses }
|
||||
}
|
||||
}
|
||||
|
||||
impl Batch {
|
||||
/// Create a new empty batch based on the given `CandidateReceipt`.
|
||||
///
|
||||
/// To create a `Batch` use Batches::find_batch`.
|
||||
///
|
||||
/// Arguments:
|
||||
///
|
||||
/// * `candidate_receipt` - The candidate this batch is meant to track votes for.
|
||||
/// * `now` - current time stamp for calculating the first tick.
|
||||
///
|
||||
/// Returns: A batch and the first `Instant` you are supposed to call `tick`.
|
||||
pub(super) fn new(candidate_receipt: CandidateReceipt, now: Instant) -> (Self, Instant) {
|
||||
let s = Self {
|
||||
candidate_hash: candidate_receipt.hash(),
|
||||
candidate_receipt,
|
||||
valid_votes: HashMap::new(),
|
||||
invalid_votes: HashMap::new(),
|
||||
votes_batched_since_last_tick: 0,
|
||||
best_before: Instant::now() + MAX_BATCH_LIFETIME,
|
||||
requesters: Vec::new(),
|
||||
};
|
||||
let next_tick = s.calculate_next_tick(now);
|
||||
(s, next_tick)
|
||||
}
|
||||
|
||||
/// Receipt of the candidate this batch is batching votes for.
|
||||
pub fn candidate_receipt(&self) -> &CandidateReceipt {
|
||||
&self.candidate_receipt
|
||||
}
|
||||
|
||||
/// Add votes from a validator into the batch.
|
||||
///
|
||||
/// The statements are supposed to be the valid and invalid statements received in a
|
||||
/// `DisputeRequest`.
|
||||
///
|
||||
/// The given `pending_response` is the corresponding response sender for responding to `peer`.
|
||||
/// If at least one of the votes is new as far as this batch is concerned we record the
|
||||
/// pending_response, for later use. In case both votes are known already, we return the
|
||||
/// response sender as an `Err` value.
|
||||
pub fn add_votes(
|
||||
&mut self,
|
||||
valid_vote: (SignedDisputeStatement, ValidatorIndex),
|
||||
invalid_vote: (SignedDisputeStatement, ValidatorIndex),
|
||||
peer: PeerId,
|
||||
pending_response: OutgoingResponseSender<DisputeRequest>,
|
||||
) -> Result<(), OutgoingResponseSender<DisputeRequest>> {
|
||||
debug_assert!(valid_vote.0.candidate_hash() == invalid_vote.0.candidate_hash());
|
||||
debug_assert!(valid_vote.0.candidate_hash() == &self.candidate_hash);
|
||||
|
||||
let mut duplicate = true;
|
||||
|
||||
if self.valid_votes.insert(valid_vote.1, valid_vote.0).is_none() {
|
||||
self.votes_batched_since_last_tick += 1;
|
||||
duplicate = false;
|
||||
}
|
||||
if self.invalid_votes.insert(invalid_vote.1, invalid_vote.0).is_none() {
|
||||
self.votes_batched_since_last_tick += 1;
|
||||
duplicate = false;
|
||||
}
|
||||
|
||||
if duplicate {
|
||||
Err(pending_response)
|
||||
} else {
|
||||
self.requesters.push((peer, pending_response));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check batch for liveness.
|
||||
///
|
||||
/// This function is supposed to be called at instants given at construction and as returned as
|
||||
/// part of `TickResult`.
|
||||
pub(super) fn tick(mut self, now: Instant) -> TickResult {
|
||||
if self.votes_batched_since_last_tick >= MIN_KEEP_BATCH_ALIVE_VOTES &&
|
||||
now < self.best_before
|
||||
{
|
||||
// Still good:
|
||||
let next_tick = self.calculate_next_tick(now);
|
||||
// Reset counter:
|
||||
self.votes_batched_since_last_tick = 0;
|
||||
TickResult::Alive(self, next_tick)
|
||||
} else {
|
||||
TickResult::Done(PreparedImport::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate when the next tick should happen.
|
||||
///
|
||||
/// This will usually return `now + BATCH_COLLECTING_INTERVAL`, except if the lifetime of this
|
||||
/// batch would exceed `MAX_BATCH_LIFETIME`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `now` - The current time.
|
||||
fn calculate_next_tick(&self, now: Instant) -> Instant {
|
||||
let next_tick = now + BATCH_COLLECTING_INTERVAL;
|
||||
if next_tick < self.best_before {
|
||||
next_tick
|
||||
} else {
|
||||
self.best_before
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{hash_map, HashMap},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures::future::pending;
|
||||
|
||||
use pezkuwi_node_network_protocol::request_response::DISPUTE_REQUEST_TIMEOUT;
|
||||
use pezkuwi_primitives::{CandidateHash, CandidateReceiptV2 as CandidateReceipt};
|
||||
|
||||
use crate::{
|
||||
receiver::batches::{batch::TickResult, waiting_queue::PendingWake},
|
||||
LOG_TARGET,
|
||||
};
|
||||
|
||||
pub use self::batch::{Batch, PreparedImport};
|
||||
use self::waiting_queue::WaitingQueue;
|
||||
|
||||
use super::{
|
||||
error::{JfyiError, JfyiResult},
|
||||
BATCH_COLLECTING_INTERVAL,
|
||||
};
|
||||
|
||||
/// A single batch (per candidate) as managed by `Batches`.
|
||||
mod batch;
|
||||
|
||||
/// Queue events in time and wait for them to become ready.
|
||||
mod waiting_queue;
|
||||
|
||||
/// Safe-guard in case votes trickle in real slow.
|
||||
///
|
||||
/// If the batch life time exceeded the time the sender is willing to wait for a confirmation, we
|
||||
/// would trigger pointless re-sends.
|
||||
const MAX_BATCH_LIFETIME: Duration = DISPUTE_REQUEST_TIMEOUT.saturating_sub(Duration::from_secs(2));
|
||||
|
||||
/// Limit the number of batches that can be alive at any given time.
|
||||
///
|
||||
/// Reasoning for this number, see guide.
|
||||
pub const MAX_BATCHES: usize = 1000;
|
||||
|
||||
/// Manage batches.
|
||||
///
|
||||
/// - Batches can be found via `find_batch()` in order to add votes to them/check they exist.
|
||||
/// - Batches can be checked for being ready for flushing in order to import contained votes.
|
||||
pub struct Batches {
|
||||
/// The batches we manage.
|
||||
///
|
||||
/// Kept invariants:
|
||||
/// For each entry in `batches`, there exists an entry in `waiting_queue` as well - we wait on
|
||||
/// all batches!
|
||||
batches: HashMap<CandidateHash, Batch>,
|
||||
/// Waiting queue for waiting for batches to become ready for `tick`.
|
||||
///
|
||||
/// Kept invariants by `Batches`:
|
||||
/// For each entry in the `waiting_queue` there exists a corresponding entry in `batches`.
|
||||
waiting_queue: WaitingQueue<CandidateHash>,
|
||||
}
|
||||
|
||||
/// A found batch is either really found or got created so it can be found.
|
||||
pub enum FoundBatch<'a> {
|
||||
/// Batch just got created.
|
||||
Created(&'a mut Batch),
|
||||
/// Batch already existed.
|
||||
Found(&'a mut Batch),
|
||||
}
|
||||
|
||||
impl Batches {
|
||||
/// Create new empty `Batches`.
|
||||
pub fn new() -> Self {
|
||||
debug_assert!(
|
||||
MAX_BATCH_LIFETIME > BATCH_COLLECTING_INTERVAL,
|
||||
"Unexpectedly low `MAX_BATCH_LIFETIME`, please check parameters."
|
||||
);
|
||||
Self { batches: HashMap::new(), waiting_queue: WaitingQueue::new() }
|
||||
}
|
||||
|
||||
/// Find a particular batch.
|
||||
///
|
||||
/// That is either find it, or we create it as reflected by the result `FoundBatch`.
|
||||
pub fn find_batch(
|
||||
&mut self,
|
||||
candidate_hash: CandidateHash,
|
||||
candidate_receipt: CandidateReceipt,
|
||||
) -> JfyiResult<FoundBatch<'_>> {
|
||||
if self.batches.len() >= MAX_BATCHES {
|
||||
return Err(JfyiError::MaxBatchLimitReached);
|
||||
}
|
||||
debug_assert!(candidate_hash == candidate_receipt.hash());
|
||||
let result = match self.batches.entry(candidate_hash) {
|
||||
hash_map::Entry::Vacant(vacant) => {
|
||||
let now = Instant::now();
|
||||
let (created, ready_at) = Batch::new(candidate_receipt, now);
|
||||
let pending_wake = PendingWake { payload: candidate_hash, ready_at };
|
||||
self.waiting_queue.push(pending_wake);
|
||||
FoundBatch::Created(vacant.insert(created))
|
||||
},
|
||||
hash_map::Entry::Occupied(occupied) => FoundBatch::Found(occupied.into_mut()),
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Wait for the next `tick` to check for ready batches.
|
||||
///
|
||||
/// This function blocks (returns `Poll::Pending`) until at least one batch can be
|
||||
/// checked for readiness meaning that `BATCH_COLLECTING_INTERVAL` has passed since the last
|
||||
/// check for that batch or it reached end of life.
|
||||
///
|
||||
/// If this `Batches` instance is empty (does not actually contain any batches), then this
|
||||
/// function will always return `Poll::Pending`.
|
||||
///
|
||||
/// Returns: A `Vec` of all `PreparedImport`s from batches that became ready.
|
||||
pub async fn check_batches(&mut self) -> Vec<PreparedImport> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mut imports = Vec::new();
|
||||
|
||||
// Wait for at least one batch to become ready:
|
||||
self.waiting_queue.wait_ready(now).await;
|
||||
|
||||
// Process all ready entries:
|
||||
while let Some(wake) = self.waiting_queue.pop_ready(now) {
|
||||
let batch = self.batches.remove(&wake.payload);
|
||||
debug_assert!(
|
||||
batch.is_some(),
|
||||
"Entries referenced in `waiting_queue` are supposed to exist!"
|
||||
);
|
||||
let batch = match batch {
|
||||
None => return pending().await,
|
||||
Some(batch) => batch,
|
||||
};
|
||||
match batch.tick(now) {
|
||||
TickResult::Done(import) => {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
candidate_hash = ?wake.payload,
|
||||
"Batch became ready."
|
||||
);
|
||||
imports.push(import);
|
||||
},
|
||||
TickResult::Alive(old_batch, next_tick) => {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
candidate_hash = ?wake.payload,
|
||||
"Batch found to be still alive on check."
|
||||
);
|
||||
let pending_wake = PendingWake { payload: wake.payload, ready_at: next_tick };
|
||||
self.waiting_queue.push(pending_wake);
|
||||
self.batches.insert(wake.payload, old_batch);
|
||||
},
|
||||
}
|
||||
}
|
||||
imports
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{cmp::Ordering, collections::BinaryHeap, time::Instant};
|
||||
|
||||
use futures::future::pending;
|
||||
use futures_timer::Delay;
|
||||
|
||||
/// Wait asynchronously for given `Instant`s one after the other.
|
||||
///
|
||||
/// `PendingWake`s can be inserted and `WaitingQueue` makes `wait_ready()` to always wait for the
|
||||
/// next `Instant` in the queue.
|
||||
pub struct WaitingQueue<Payload> {
|
||||
/// All pending wakes we are supposed to wait on in order.
|
||||
pending_wakes: BinaryHeap<PendingWake<Payload>>,
|
||||
/// Wait for next `PendingWake`.
|
||||
timer: Option<Delay>,
|
||||
}
|
||||
|
||||
/// Represents some event waiting to be processed at `ready_at`.
|
||||
///
|
||||
/// This is an event in `WaitingQueue`. It provides an `Ord` instance, that sorts descending with
|
||||
/// regard to `Instant` (so we get a `min-heap` with the earliest `Instant` at the top).
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct PendingWake<Payload> {
|
||||
pub payload: Payload,
|
||||
pub ready_at: Instant,
|
||||
}
|
||||
|
||||
impl<Payload: Eq + Ord> WaitingQueue<Payload> {
|
||||
/// Get a new empty `WaitingQueue`.
|
||||
///
|
||||
/// If you call `pop` on this queue immediately, it will always return `Poll::Pending`.
|
||||
pub fn new() -> Self {
|
||||
Self { pending_wakes: BinaryHeap::new(), timer: None }
|
||||
}
|
||||
|
||||
/// Push a `PendingWake`.
|
||||
///
|
||||
/// The next call to `wait_ready` will make sure to wake soon enough to process that new event
|
||||
/// in a timely manner.
|
||||
pub fn push(&mut self, wake: PendingWake<Payload>) {
|
||||
self.pending_wakes.push(wake);
|
||||
// Reset timer as it is potentially obsolete now:
|
||||
self.timer = None;
|
||||
}
|
||||
|
||||
/// Pop the next ready item.
|
||||
///
|
||||
/// This function does not wait, if nothing is ready right now as determined by the passed
|
||||
/// `now` time stamp, this function simply returns `None`.
|
||||
pub fn pop_ready(&mut self, now: Instant) -> Option<PendingWake<Payload>> {
|
||||
let is_ready = self.pending_wakes.peek().map_or(false, |p| p.ready_at <= now);
|
||||
if is_ready {
|
||||
Some(self.pending_wakes.pop().expect("We just peeked. qed."))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Don't pop, just wait until something is ready.
|
||||
///
|
||||
/// Once this function returns `Poll::Ready(())` `pop_ready()` will return `Some`, if passed
|
||||
/// the same `Instant`.
|
||||
///
|
||||
/// Whether ready or not is determined based on the passed time stamp `now` which should be the
|
||||
/// current time as returned by `Instant::now()`
|
||||
///
|
||||
/// This function waits asynchronously for an item to become ready. If there is no more item,
|
||||
/// this call will wait forever (return Poll::Pending without scheduling a wake).
|
||||
pub async fn wait_ready(&mut self, now: Instant) {
|
||||
if let Some(timer) = &mut self.timer {
|
||||
// Previous timer was not done yet.
|
||||
timer.await
|
||||
}
|
||||
|
||||
let next_waiting = self.pending_wakes.peek();
|
||||
let is_ready = next_waiting.map_or(false, |p| p.ready_at <= now);
|
||||
if is_ready {
|
||||
return;
|
||||
}
|
||||
|
||||
self.timer = next_waiting.map(|p| Delay::new(p.ready_at.duration_since(now)));
|
||||
match &mut self.timer {
|
||||
None => return pending().await,
|
||||
Some(timer) => timer.await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Payload: Eq + Ord> PartialOrd<PendingWake<Payload>> for PendingWake<Payload> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Payload: Ord> Ord for PendingWake<Payload> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// Reverse order for min-heap:
|
||||
match other.ready_at.cmp(&self.ready_at) {
|
||||
Ordering::Equal => other.payload.cmp(&self.payload),
|
||||
o => o,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
task::Poll,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use futures::{future::poll_fn, pin_mut, Future};
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
use super::{PendingWake, WaitingQueue};
|
||||
|
||||
#[test]
|
||||
fn wait_ready_waits_for_earliest_event_always() {
|
||||
sp_tracing::try_init_simple();
|
||||
let mut queue = WaitingQueue::new();
|
||||
let now = Instant::now();
|
||||
let start = now;
|
||||
queue.push(PendingWake { payload: 1u32, ready_at: now + Duration::from_millis(3) });
|
||||
// Push another one in order:
|
||||
queue.push(PendingWake { payload: 2u32, ready_at: now + Duration::from_millis(5) });
|
||||
// Push one out of order:
|
||||
queue.push(PendingWake { payload: 0u32, ready_at: now + Duration::from_millis(1) });
|
||||
// Push another one at same timestamp (should become ready at the same time)
|
||||
queue.push(PendingWake { payload: 10u32, ready_at: now + Duration::from_millis(1) });
|
||||
|
||||
futures::executor::block_on(async move {
|
||||
// No time passed yet - nothing should be ready.
|
||||
assert!(queue.pop_ready(now).is_none(), "No time has passed, nothing should be ready");
|
||||
|
||||
// Receive them in order at expected times:
|
||||
queue.wait_ready(now).await;
|
||||
gum::trace!(target: LOG_TARGET, "After first wait.");
|
||||
|
||||
let now = start + Duration::from_millis(1);
|
||||
assert!(Instant::now() - start >= Duration::from_millis(1));
|
||||
assert_eq!(queue.pop_ready(now).map(|p| p.payload), Some(0u32));
|
||||
// One more should be ready:
|
||||
assert_eq!(queue.pop_ready(now).map(|p| p.payload), Some(10u32));
|
||||
assert!(queue.pop_ready(now).is_none(), "No more entry expected to be ready.");
|
||||
|
||||
queue.wait_ready(now).await;
|
||||
gum::trace!(target: LOG_TARGET, "After second wait.");
|
||||
let now = start + Duration::from_millis(3);
|
||||
assert!(Instant::now() - start >= Duration::from_millis(3));
|
||||
assert_eq!(queue.pop_ready(now).map(|p| p.payload), Some(1u32));
|
||||
assert!(queue.pop_ready(now).is_none(), "No more entry expected to be ready.");
|
||||
|
||||
// Push in between wait:
|
||||
poll_fn(|cx| {
|
||||
let fut = queue.wait_ready(now);
|
||||
pin_mut!(fut);
|
||||
assert_matches!(fut.poll(cx), Poll::Pending);
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
queue.push(PendingWake { payload: 3u32, ready_at: start + Duration::from_millis(4) });
|
||||
|
||||
queue.wait_ready(now).await;
|
||||
// Newly pushed element should have become ready:
|
||||
gum::trace!(target: LOG_TARGET, "After third wait.");
|
||||
let now = start + Duration::from_millis(4);
|
||||
assert!(Instant::now() - start >= Duration::from_millis(4));
|
||||
assert_eq!(queue.pop_ready(now).map(|p| p.payload), Some(3u32));
|
||||
assert!(queue.pop_ready(now).is_none(), "No more entry expected to be ready.");
|
||||
|
||||
queue.wait_ready(now).await;
|
||||
gum::trace!(target: LOG_TARGET, "After fourth wait.");
|
||||
let now = start + Duration::from_millis(5);
|
||||
assert!(Instant::now() - start >= Duration::from_millis(5));
|
||||
assert_eq!(queue.pop_ready(now).map(|p| p.payload), Some(2u32));
|
||||
assert!(queue.pop_ready(now).is_none(), "No more entry expected to be ready.");
|
||||
|
||||
// queue empty - should wait forever now:
|
||||
poll_fn(|cx| {
|
||||
let fut = queue.wait_ready(now);
|
||||
pin_mut!(fut);
|
||||
assert_matches!(fut.poll(cx), Poll::Pending);
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
//! Error handling related code and Error/Result definitions.
|
||||
|
||||
use fatality::Nested;
|
||||
|
||||
use gum::CandidateHash;
|
||||
use pezkuwi_node_network_protocol::{request_response::incoming, PeerId};
|
||||
use pezkuwi_node_subsystem_util::runtime;
|
||||
use pezkuwi_primitives::AuthorityDiscoveryId;
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[fatality::fatality(splitable)]
|
||||
pub enum Error {
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information")]
|
||||
Runtime(#[from] runtime::Error),
|
||||
|
||||
#[fatal(forward)]
|
||||
#[error("Retrieving next incoming request failed.")]
|
||||
IncomingRequest(#[from] incoming::Error),
|
||||
|
||||
#[error("Sending back response to peers {0:#?} failed.")]
|
||||
SendResponses(Vec<PeerId>),
|
||||
|
||||
#[error("Changing peer's ({0}) reputation failed.")]
|
||||
SetPeerReputation(PeerId),
|
||||
|
||||
#[error("Dispute request with invalid signatures, from peer {0}.")]
|
||||
InvalidSignature(PeerId),
|
||||
|
||||
#[error("Received votes from peer {0} have been completely redundant.")]
|
||||
RedundantMessage(PeerId),
|
||||
|
||||
#[error("Import of dispute got canceled for candidate {0} - import failed for some reason.")]
|
||||
ImportCanceled(CandidateHash),
|
||||
|
||||
#[error("Peer {0} attempted to participate in dispute and is not a validator.")]
|
||||
NotAValidator(PeerId),
|
||||
|
||||
#[error("Force flush for batch that could not be found attempted, candidate hash: {0}")]
|
||||
ForceFlushBatchDoesNotExist(CandidateHash),
|
||||
|
||||
// Should never happen in practice:
|
||||
#[error("We needed to drop messages, because we reached limit on concurrent batches.")]
|
||||
MaxBatchLimitReached,
|
||||
|
||||
#[error("Authority {0} sent messages at a too high rate.")]
|
||||
AuthorityFlooding(AuthorityDiscoveryId),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub type JfyiResult<T> = std::result::Result<T, JfyiError>;
|
||||
|
||||
/// 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<(), FatalError> {
|
||||
match result.into_nested()? {
|
||||
Err(error @ JfyiError::ImportCanceled(_)) => {
|
||||
gum::debug!(target: LOG_TARGET, error = ?error);
|
||||
Ok(())
|
||||
},
|
||||
Err(JfyiError::NotAValidator(peer)) => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?peer,
|
||||
"Dropping message from peer (unknown authority id)"
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => {
|
||||
gum::warn!(target: LOG_TARGET, error = ?error);
|
||||
Ok(())
|
||||
},
|
||||
Ok(()) => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::poll_fn,
|
||||
pin_mut,
|
||||
stream::{FuturesUnordered, StreamExt},
|
||||
Future,
|
||||
};
|
||||
|
||||
use gum::CandidateHash;
|
||||
use pezkuwi_node_network_protocol::{
|
||||
authority_discovery::AuthorityDiscovery,
|
||||
request_response::{
|
||||
incoming::{self, OutgoingResponse, OutgoingResponseSender},
|
||||
v1::{DisputeRequest, DisputeResponse},
|
||||
IncomingRequest, IncomingRequestReceiver,
|
||||
},
|
||||
PeerId, UnifiedReputationChange as Rep,
|
||||
};
|
||||
use pezkuwi_node_primitives::DISPUTE_WINDOW;
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::{DisputeCoordinatorMessage, ImportStatementsResult},
|
||||
overseer,
|
||||
};
|
||||
use pezkuwi_node_subsystem_util::{runtime, runtime::RuntimeInfo};
|
||||
|
||||
use crate::{
|
||||
metrics::{FAILED, SUCCEEDED},
|
||||
Metrics, LOG_TARGET,
|
||||
};
|
||||
|
||||
mod error;
|
||||
|
||||
/// Rate limiting queues for incoming requests by peers.
|
||||
mod peer_queues;
|
||||
|
||||
/// Batch imports together.
|
||||
mod batches;
|
||||
|
||||
use self::{
|
||||
batches::{Batches, FoundBatch, PreparedImport},
|
||||
error::{log_error, JfyiError, JfyiResult, Result},
|
||||
peer_queues::PeerQueues,
|
||||
};
|
||||
|
||||
const COST_INVALID_REQUEST: Rep = Rep::CostMajor("Received message could not be decoded.");
|
||||
const COST_INVALID_SIGNATURE: Rep = Rep::Malicious("Signatures were invalid.");
|
||||
const COST_NOT_A_VALIDATOR: Rep = Rep::CostMajor("Reporting peer was not a validator.");
|
||||
|
||||
/// Invalid imports can be caused by flooding, e.g. by a disabled validator.
|
||||
const COST_INVALID_IMPORT: Rep =
|
||||
Rep::CostMinor("Import was deemed invalid by dispute-coordinator.");
|
||||
|
||||
/// How many votes must have arrived in the last `BATCH_COLLECTING_INTERVAL`
|
||||
///
|
||||
/// in order for a batch to stay alive and not get flushed/imported to the dispute-coordinator.
|
||||
///
|
||||
/// This ensures a timely import of batches.
|
||||
#[cfg(not(test))]
|
||||
pub const MIN_KEEP_BATCH_ALIVE_VOTES: u32 = 10;
|
||||
#[cfg(test)]
|
||||
pub const MIN_KEEP_BATCH_ALIVE_VOTES: u32 = 2;
|
||||
|
||||
/// Time we allow to pass for new votes to trickle in.
|
||||
///
|
||||
/// See `MIN_KEEP_BATCH_ALIVE_VOTES` above.
|
||||
/// Should be greater or equal to `RECEIVE_RATE_LIMIT` (there is no point in checking any faster).
|
||||
pub const BATCH_COLLECTING_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
/// State for handling incoming `DisputeRequest` messages.
|
||||
pub struct DisputesReceiver<Sender, AD> {
|
||||
/// Access to session information.
|
||||
runtime: RuntimeInfo,
|
||||
|
||||
/// Subsystem sender for communication with other subsystems.
|
||||
sender: Sender,
|
||||
|
||||
/// Channel to retrieve incoming requests from.
|
||||
receiver: IncomingRequestReceiver<DisputeRequest>,
|
||||
|
||||
/// Rate limiting queue for each peer (only authorities).
|
||||
peer_queues: PeerQueues,
|
||||
|
||||
/// Currently active batches of imports per candidate.
|
||||
batches: Batches,
|
||||
|
||||
/// Authority discovery service:
|
||||
authority_discovery: AD,
|
||||
|
||||
/// Imports currently being processed by the `dispute-coordinator`.
|
||||
pending_imports: FuturesUnordered<PendingImport>,
|
||||
|
||||
/// Log received requests.
|
||||
metrics: Metrics,
|
||||
}
|
||||
|
||||
/// Messages as handled by this receiver internally.
|
||||
enum MuxedMessage {
|
||||
/// An import got confirmed by the coordinator.
|
||||
///
|
||||
/// We need to handle those for two reasons:
|
||||
///
|
||||
/// - We need to make sure responses are actually sent (therefore we need to await futures
|
||||
/// promptly).
|
||||
/// - We need to punish peers whose import got rejected.
|
||||
ConfirmedImport(ImportResult),
|
||||
|
||||
/// A new request has arrived and should be handled.
|
||||
NewRequest(IncomingRequest<DisputeRequest>),
|
||||
|
||||
/// Rate limit timer hit - is time to process one row of messages.
|
||||
///
|
||||
/// This is the result of calling `self.peer_queues.pop_reqs()`.
|
||||
WakePeerQueuesPopReqs(Vec<IncomingRequest<DisputeRequest>>),
|
||||
|
||||
/// It is time to check batches.
|
||||
///
|
||||
/// Every `BATCH_COLLECTING_INTERVAL` we check whether less than `MIN_KEEP_BATCH_ALIVE_VOTES`
|
||||
/// new votes arrived, if so the batch is ready for import.
|
||||
///
|
||||
/// This is the result of calling `self.batches.check_batches()`.
|
||||
WakeCheckBatches(Vec<PreparedImport>),
|
||||
}
|
||||
|
||||
impl<Sender, AD> DisputesReceiver<Sender, AD>
|
||||
where
|
||||
AD: AuthorityDiscovery,
|
||||
Sender: overseer::DisputeDistributionSenderTrait,
|
||||
{
|
||||
/// Create a new receiver which can be `run`.
|
||||
pub fn new(
|
||||
sender: Sender,
|
||||
receiver: IncomingRequestReceiver<DisputeRequest>,
|
||||
authority_discovery: AD,
|
||||
metrics: Metrics,
|
||||
) -> Self {
|
||||
let runtime = RuntimeInfo::new_with_config(runtime::Config {
|
||||
keystore: None,
|
||||
session_cache_lru_size: DISPUTE_WINDOW.get(),
|
||||
});
|
||||
Self {
|
||||
runtime,
|
||||
sender,
|
||||
receiver,
|
||||
peer_queues: PeerQueues::new(),
|
||||
batches: Batches::new(),
|
||||
authority_discovery,
|
||||
pending_imports: FuturesUnordered::new(),
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get that receiver started.
|
||||
///
|
||||
/// This is an endless loop and should be spawned into its own task.
|
||||
pub async fn run(mut self) {
|
||||
loop {
|
||||
match log_error(self.run_inner().await) {
|
||||
Ok(()) => {},
|
||||
Err(fatal) => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
error = ?fatal,
|
||||
"Shutting down"
|
||||
);
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Actual work happening here in three phases:
|
||||
///
|
||||
/// 1. Receive and queue incoming messages until the rate limit timer hits.
|
||||
/// 2. Do import/batching for the head of all queues.
|
||||
/// 3. Check and flush any ready batches.
|
||||
async fn run_inner(&mut self) -> Result<()> {
|
||||
let msg = self.receive_message().await?;
|
||||
|
||||
match msg {
|
||||
MuxedMessage::NewRequest(req) => {
|
||||
// Phase 1:
|
||||
self.metrics.on_received_request();
|
||||
self.dispatch_to_queues(req).await?;
|
||||
},
|
||||
MuxedMessage::WakePeerQueuesPopReqs(reqs) => {
|
||||
// Phase 2:
|
||||
for req in reqs {
|
||||
// No early return - we cannot cancel imports of one peer, because the import of
|
||||
// another failed:
|
||||
match log_error(self.start_import_or_batch(req).await) {
|
||||
Ok(()) => {},
|
||||
Err(fatal) => return Err(fatal.into()),
|
||||
}
|
||||
}
|
||||
},
|
||||
MuxedMessage::WakeCheckBatches(ready_imports) => {
|
||||
// Phase 3:
|
||||
self.import_ready_batches(ready_imports).await;
|
||||
},
|
||||
MuxedMessage::ConfirmedImport(import_result) => {
|
||||
self.update_imported_requests_metrics(&import_result);
|
||||
// Confirm imports to requesters/punish them on invalid imports:
|
||||
send_responses_to_requesters(import_result).await?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive one `MuxedMessage`.
|
||||
///
|
||||
///
|
||||
/// Dispatching events to messages as they happen.
|
||||
async fn receive_message(&mut self) -> Result<MuxedMessage> {
|
||||
poll_fn(|ctx| {
|
||||
// In case of Ready(None), we want to wait for pending requests:
|
||||
if let Poll::Ready(Some(v)) = self.pending_imports.poll_next_unpin(ctx) {
|
||||
return Poll::Ready(Ok(MuxedMessage::ConfirmedImport(v?)));
|
||||
}
|
||||
|
||||
let rate_limited = self.peer_queues.pop_reqs();
|
||||
pin_mut!(rate_limited);
|
||||
// We poll rate_limit before batches, so we don't unnecessarily delay importing to
|
||||
// batches.
|
||||
if let Poll::Ready(reqs) = rate_limited.poll(ctx) {
|
||||
return Poll::Ready(Ok(MuxedMessage::WakePeerQueuesPopReqs(reqs)));
|
||||
}
|
||||
|
||||
let ready_batches = self.batches.check_batches();
|
||||
pin_mut!(ready_batches);
|
||||
if let Poll::Ready(ready_batches) = ready_batches.poll(ctx) {
|
||||
return Poll::Ready(Ok(MuxedMessage::WakeCheckBatches(ready_batches)));
|
||||
}
|
||||
|
||||
let next_req = self.receiver.recv(|| vec![COST_INVALID_REQUEST]);
|
||||
pin_mut!(next_req);
|
||||
if let Poll::Ready(r) = next_req.poll(ctx) {
|
||||
return match r {
|
||||
Err(e) => Poll::Ready(Err(incoming::Error::from(e).into())),
|
||||
Ok(v) => Poll::Ready(Ok(MuxedMessage::NewRequest(v))),
|
||||
};
|
||||
}
|
||||
Poll::Pending
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Process incoming requests.
|
||||
///
|
||||
/// - Check sender is authority
|
||||
/// - Dispatch message to corresponding queue in `peer_queues`.
|
||||
/// - If queue is full, drop message and change reputation of sender.
|
||||
async fn dispatch_to_queues(&mut self, req: IncomingRequest<DisputeRequest>) -> JfyiResult<()> {
|
||||
let peer = req.peer;
|
||||
// Only accept messages from validators, in case there are multiple `AuthorityId`s, we
|
||||
// just take the first one. On session boundaries this might allow validators to double
|
||||
// their rate limit for a short period of time, which seems acceptable.
|
||||
let authority_id = match self
|
||||
.authority_discovery
|
||||
.get_authority_ids_by_peer_id(peer)
|
||||
.await
|
||||
.and_then(|s| s.into_iter().next())
|
||||
{
|
||||
None => {
|
||||
req.send_outgoing_response(OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes: vec![COST_NOT_A_VALIDATOR],
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| JfyiError::SendResponses(vec![peer]))?;
|
||||
return Err(JfyiError::NotAValidator(peer).into());
|
||||
},
|
||||
Some(auth_id) => auth_id,
|
||||
};
|
||||
|
||||
// Queue request:
|
||||
if let Err((authority_id, req)) = self.peer_queues.push_req(authority_id, req) {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?authority_id,
|
||||
?peer,
|
||||
"Peer hit the rate limit - dropping message."
|
||||
);
|
||||
req.send_outgoing_response(OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes: vec![],
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| JfyiError::SendResponses(vec![peer]))?;
|
||||
return Err(JfyiError::AuthorityFlooding(authority_id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start importing votes for the given request or batch.
|
||||
///
|
||||
/// Signature check and in case we already have an existing batch we import to that batch,
|
||||
/// otherwise import to `dispute-coordinator` directly and open a batch.
|
||||
async fn start_import_or_batch(
|
||||
&mut self,
|
||||
incoming: IncomingRequest<DisputeRequest>,
|
||||
) -> Result<()> {
|
||||
let IncomingRequest { peer, payload, pending_response } = incoming;
|
||||
|
||||
let info = self
|
||||
.runtime
|
||||
.get_session_info_by_index(
|
||||
&mut self.sender,
|
||||
payload.0.candidate_receipt.descriptor.relay_parent(),
|
||||
payload.0.session_index,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let votes_result = payload.0.try_into_signed_votes(&info.session_info);
|
||||
|
||||
let (candidate_receipt, valid_vote, invalid_vote) = match votes_result {
|
||||
Err(()) => {
|
||||
// Signature invalid:
|
||||
pending_response
|
||||
.send_outgoing_response(OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes: vec![COST_INVALID_SIGNATURE],
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| JfyiError::SetPeerReputation(peer))?;
|
||||
|
||||
return Err(From::from(JfyiError::InvalidSignature(peer)));
|
||||
},
|
||||
Ok(votes) => votes,
|
||||
};
|
||||
|
||||
let candidate_hash = *valid_vote.0.candidate_hash();
|
||||
|
||||
match self.batches.find_batch(candidate_hash, candidate_receipt)? {
|
||||
FoundBatch::Created(batch) => {
|
||||
// There was no entry yet - start import immediately:
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
?peer,
|
||||
"No batch yet - triggering immediate import"
|
||||
);
|
||||
let import = PreparedImport {
|
||||
candidate_receipt: batch.candidate_receipt().clone(),
|
||||
statements: vec![valid_vote, invalid_vote],
|
||||
requesters: vec![(peer, pending_response)],
|
||||
};
|
||||
self.start_import(import).await;
|
||||
},
|
||||
FoundBatch::Found(batch) => {
|
||||
gum::trace!(target: LOG_TARGET, ?candidate_hash, "Batch exists - batching request");
|
||||
let batch_result =
|
||||
batch.add_votes(valid_vote, invalid_vote, peer, pending_response);
|
||||
|
||||
if let Err(pending_response) = batch_result {
|
||||
// We don't expect honest peers to send redundant votes within a single batch,
|
||||
// as the timeout for retry is much higher. Still we don't want to punish the
|
||||
// node as it might not be the node's fault. Some other (malicious) node could
|
||||
// have been faster sending the same votes in order to harm the reputation of
|
||||
// that honest node. Given that we already have a rate limit, if a validator
|
||||
// chooses to waste available rate with redundant votes - so be it. The actual
|
||||
// dispute resolution is unaffected.
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?peer,
|
||||
"Peer sent completely redundant votes within a single batch - that looks fishy!",
|
||||
);
|
||||
pending_response
|
||||
.send_outgoing_response(OutgoingResponse {
|
||||
// While we have seen duplicate votes, we cannot confirm as we don't
|
||||
// know yet whether the batch is going to be confirmed, so we assume
|
||||
// the worst. We don't want to push the pending response to the batch
|
||||
// either as that would be unbounded, only limited by the rate limit.
|
||||
result: Err(()),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| JfyiError::SendResponses(vec![peer]))?;
|
||||
return Err(From::from(JfyiError::RedundantMessage(peer)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Trigger import into the dispute-coordinator of ready batches (`PreparedImport`s).
|
||||
async fn import_ready_batches(&mut self, ready_imports: Vec<PreparedImport>) {
|
||||
for import in ready_imports {
|
||||
self.start_import(import).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start import and add response receiver to `pending_imports`.
|
||||
async fn start_import(&mut self, import: PreparedImport) {
|
||||
let PreparedImport { candidate_receipt, statements, requesters } = import;
|
||||
let (session_index, candidate_hash) = match statements.iter().next() {
|
||||
None => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
candidate_hash = ?candidate_receipt.hash(),
|
||||
"Not importing empty batch"
|
||||
);
|
||||
return;
|
||||
},
|
||||
Some(vote) => (vote.0.session_index(), *vote.0.candidate_hash()),
|
||||
};
|
||||
|
||||
let (pending_confirmation, confirmation_rx) = oneshot::channel();
|
||||
self.sender
|
||||
.send_message(DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt,
|
||||
session: session_index,
|
||||
statements,
|
||||
pending_confirmation: Some(pending_confirmation),
|
||||
})
|
||||
.await;
|
||||
|
||||
let pending =
|
||||
PendingImport { candidate_hash, requesters, pending_response: confirmation_rx };
|
||||
|
||||
self.pending_imports.push(pending);
|
||||
}
|
||||
|
||||
fn update_imported_requests_metrics(&self, result: &ImportResult) {
|
||||
let label = match result.result {
|
||||
ImportStatementsResult::ValidImport => SUCCEEDED,
|
||||
ImportStatementsResult::InvalidImport => FAILED,
|
||||
};
|
||||
self.metrics.on_imported(label, result.requesters.len());
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_responses_to_requesters(import_result: ImportResult) -> JfyiResult<()> {
|
||||
let ImportResult { requesters, result } = import_result;
|
||||
|
||||
let mk_response = match result {
|
||||
ImportStatementsResult::ValidImport => || OutgoingResponse {
|
||||
result: Ok(DisputeResponse::Confirmed),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
},
|
||||
ImportStatementsResult::InvalidImport => || OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes: vec![COST_INVALID_IMPORT],
|
||||
sent_feedback: None,
|
||||
},
|
||||
};
|
||||
|
||||
let mut sending_failed_for = Vec::new();
|
||||
for (peer, pending_response) in requesters {
|
||||
if let Err(()) = pending_response.send_outgoing_response(mk_response()) {
|
||||
sending_failed_for.push(peer);
|
||||
}
|
||||
}
|
||||
|
||||
if !sending_failed_for.is_empty() {
|
||||
Err(JfyiError::SendResponses(sending_failed_for))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A future that resolves into an `ImportResult` when ready.
|
||||
///
|
||||
/// This future is used on `dispute-coordinator` import messages for the oneshot response receiver
|
||||
/// to:
|
||||
/// - Keep track of concerned `CandidateHash` for reporting errors.
|
||||
/// - Keep track of requesting peers so we can confirm the import/punish them on invalid imports.
|
||||
struct PendingImport {
|
||||
candidate_hash: CandidateHash,
|
||||
requesters: Vec<(PeerId, OutgoingResponseSender<DisputeRequest>)>,
|
||||
pending_response: oneshot::Receiver<ImportStatementsResult>,
|
||||
}
|
||||
|
||||
/// A `PendingImport` becomes an `ImportResult` once done.
|
||||
struct ImportResult {
|
||||
/// Requesters of that import.
|
||||
requesters: Vec<(PeerId, OutgoingResponseSender<DisputeRequest>)>,
|
||||
/// Actual result of the import.
|
||||
result: ImportStatementsResult,
|
||||
}
|
||||
|
||||
impl PendingImport {
|
||||
async fn wait_for_result(&mut self) -> JfyiResult<ImportResult> {
|
||||
let result = (&mut self.pending_response)
|
||||
.await
|
||||
.map_err(|_| JfyiError::ImportCanceled(self.candidate_hash))?;
|
||||
Ok(ImportResult { requesters: std::mem::take(&mut self.requesters), result })
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for PendingImport {
|
||||
type Output = JfyiResult<ImportResult>;
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let fut = self.wait_for_result();
|
||||
pin_mut!(fut);
|
||||
fut.poll(cx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::collections::{hash_map::Entry, HashMap, VecDeque};
|
||||
|
||||
use futures::future::pending;
|
||||
use futures_timer::Delay;
|
||||
use pezkuwi_node_network_protocol::request_response::{v1::DisputeRequest, IncomingRequest};
|
||||
use pezkuwi_primitives::AuthorityDiscoveryId;
|
||||
|
||||
use crate::RECEIVE_RATE_LIMIT;
|
||||
|
||||
/// How many messages we are willing to queue per peer (validator).
|
||||
///
|
||||
/// The larger this value is, the larger bursts are allowed to be without us dropping messages. On
|
||||
/// the flip side this gets allocated per validator, so for a size of 10 this will result
|
||||
/// in `10_000 * size_of(IncomingRequest)` in the worst case.
|
||||
///
|
||||
/// `PEER_QUEUE_CAPACITY` must not be 0 for obvious reasons.
|
||||
#[cfg(not(test))]
|
||||
pub const PEER_QUEUE_CAPACITY: usize = 10;
|
||||
#[cfg(test)]
|
||||
pub const PEER_QUEUE_CAPACITY: usize = 2;
|
||||
|
||||
/// Queues for messages from authority peers for rate limiting.
|
||||
///
|
||||
/// Invariants ensured:
|
||||
///
|
||||
/// 1. No queue will ever have more than `PEER_QUEUE_CAPACITY` elements.
|
||||
/// 2. There are no empty queues. Whenever a queue gets empty, it is removed. This way checking
|
||||
/// whether there are any messages queued is cheap.
|
||||
/// 3. As long as not empty, `pop_reqs` will, if called in sequence, not return `Ready` more often
|
||||
/// than once for every `RECEIVE_RATE_LIMIT`, but it will always return Ready eventually.
|
||||
/// 4. If empty `pop_reqs` will never return `Ready`, but will always be `Pending`.
|
||||
pub struct PeerQueues {
|
||||
/// Actual queues.
|
||||
queues: HashMap<AuthorityDiscoveryId, VecDeque<IncomingRequest<DisputeRequest>>>,
|
||||
|
||||
/// Delay timer for establishing the rate limit.
|
||||
rate_limit_timer: Option<Delay>,
|
||||
}
|
||||
|
||||
impl PeerQueues {
|
||||
/// New empty `PeerQueues`.
|
||||
pub fn new() -> Self {
|
||||
Self { queues: HashMap::new(), rate_limit_timer: None }
|
||||
}
|
||||
|
||||
/// Push an incoming request for a given authority.
|
||||
///
|
||||
/// Returns: `Ok(())` if succeeded, `Err((args))` if capacity is reached.
|
||||
pub fn push_req(
|
||||
&mut self,
|
||||
peer: AuthorityDiscoveryId,
|
||||
req: IncomingRequest<DisputeRequest>,
|
||||
) -> Result<(), (AuthorityDiscoveryId, IncomingRequest<DisputeRequest>)> {
|
||||
let queue = match self.queues.entry(peer) {
|
||||
Entry::Vacant(vacant) => vacant.insert(VecDeque::new()),
|
||||
Entry::Occupied(occupied) => {
|
||||
if occupied.get().len() >= PEER_QUEUE_CAPACITY {
|
||||
return Err((occupied.key().clone(), req));
|
||||
}
|
||||
occupied.into_mut()
|
||||
},
|
||||
};
|
||||
queue.push_back(req);
|
||||
|
||||
// We have at least one element to process - rate limit `timer` needs to exist now:
|
||||
self.ensure_timer();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pop all heads and return them for processing.
|
||||
///
|
||||
/// This gets one message from each peer that has sent at least one.
|
||||
///
|
||||
/// This function is rate limited, if called in sequence it will not return more often than
|
||||
/// every `RECEIVE_RATE_LIMIT`.
|
||||
///
|
||||
/// NOTE: If empty this function will not return `Ready` at all, but will always be `Pending`.
|
||||
pub async fn pop_reqs(&mut self) -> Vec<IncomingRequest<DisputeRequest>> {
|
||||
self.wait_for_timer().await;
|
||||
|
||||
let mut heads = Vec::with_capacity(self.queues.len());
|
||||
let old_queues = std::mem::replace(&mut self.queues, HashMap::new());
|
||||
for (k, mut queue) in old_queues.into_iter() {
|
||||
let front = queue.pop_front();
|
||||
debug_assert!(front.is_some(), "Invariant that queues are never empty is broken.");
|
||||
|
||||
if let Some(front) = front {
|
||||
heads.push(front);
|
||||
}
|
||||
if !queue.is_empty() {
|
||||
self.queues.insert(k, queue);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.is_empty() {
|
||||
// Still not empty - we should get woken at some point.
|
||||
self.ensure_timer();
|
||||
}
|
||||
|
||||
heads
|
||||
}
|
||||
|
||||
/// Whether or not all queues are empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queues.is_empty()
|
||||
}
|
||||
|
||||
/// Ensure there is an active `timer`.
|
||||
///
|
||||
/// Checks whether one exists and if not creates one.
|
||||
fn ensure_timer(&mut self) -> &mut Delay {
|
||||
self.rate_limit_timer.get_or_insert(Delay::new(RECEIVE_RATE_LIMIT))
|
||||
}
|
||||
|
||||
/// Wait for `timer` if it exists, or be `Pending` forever.
|
||||
///
|
||||
/// Afterwards it gets set back to `None`.
|
||||
async fn wait_for_timer(&mut self) {
|
||||
match self.rate_limit_timer.as_mut() {
|
||||
None => pending().await,
|
||||
Some(timer) => timer.await,
|
||||
}
|
||||
self.rate_limit_timer = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
//! Error handling related code and Error/Result definitions.
|
||||
|
||||
use pezkuwi_node_primitives::disputes::DisputeMessageCheckError;
|
||||
use pezkuwi_node_subsystem::SubsystemError;
|
||||
use pezkuwi_node_subsystem_util::runtime;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[fatality::fatality(splitable)]
|
||||
pub enum Error {
|
||||
#[fatal]
|
||||
#[error("Spawning subsystem task failed")]
|
||||
SpawnTask(#[source] SubsystemError),
|
||||
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information")]
|
||||
Runtime(#[from] runtime::Error),
|
||||
|
||||
/// We need available active heads for finding relevant authorities.
|
||||
#[error("No active heads available - needed for finding relevant authorities.")]
|
||||
NoActiveHeads,
|
||||
|
||||
/// This error likely indicates a bug in the coordinator.
|
||||
#[error("Oneshot for asking dispute coordinator for active disputes got canceled.")]
|
||||
AskActiveDisputesCanceled,
|
||||
|
||||
/// This error likely indicates a bug in the coordinator.
|
||||
#[error("Oneshot for asking dispute coordinator for candidate votes got canceled.")]
|
||||
AskCandidateVotesCanceled,
|
||||
|
||||
/// This error does indicate a bug in the coordinator.
|
||||
///
|
||||
/// We were not able to successfully construct a `DisputeMessage` from disputes votes.
|
||||
#[error("Invalid dispute encountered")]
|
||||
InvalidDisputeFromCoordinator(#[source] DisputeMessageCheckError),
|
||||
|
||||
/// This error does indicate a bug in the coordinator.
|
||||
///
|
||||
/// We did not receive votes on both sides for `CandidateVotes` received from the coordinator.
|
||||
#[error("Missing votes for valid dispute")]
|
||||
MissingVotesFromCoordinator,
|
||||
|
||||
/// This error does indicate a bug in the coordinator.
|
||||
///
|
||||
/// `SignedDisputeStatement` could not be reconstructed from recorded statements.
|
||||
#[error("Invalid statements from coordinator")]
|
||||
InvalidStatementFromCoordinator,
|
||||
|
||||
/// This error does indicate a bug in the coordinator.
|
||||
///
|
||||
/// A statement's `ValidatorIndex` could not be looked up.
|
||||
#[error("ValidatorIndex of statement could not be found")]
|
||||
InvalidValidatorIndexFromCoordinator,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type JfyiErrorResult<T> = std::result::Result<T, JfyiError>;
|
||||
@@ -0,0 +1,392 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{channel::oneshot, future::poll_fn, Future};
|
||||
|
||||
use futures_timer::Delay;
|
||||
use indexmap::{map::Entry, IndexMap};
|
||||
use pezkuwi_node_network_protocol::request_response::v1::DisputeRequest;
|
||||
use pezkuwi_node_primitives::{DisputeMessage, DisputeStatus};
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::DisputeCoordinatorMessage, overseer, ActiveLeavesUpdate, SubsystemSender,
|
||||
};
|
||||
use pezkuwi_node_subsystem_util::{nesting_sender::NestingSender, runtime::RuntimeInfo};
|
||||
use pezkuwi_primitives::{CandidateHash, Hash, SessionIndex};
|
||||
|
||||
/// For each ongoing dispute we have a `SendTask` which takes care of it.
|
||||
///
|
||||
/// It is going to spawn real tasks as it sees fit for getting the votes of the particular dispute
|
||||
/// out.
|
||||
///
|
||||
/// As we assume disputes have a priority, we start sending for disputes in the order
|
||||
/// `start_sender` got called.
|
||||
mod send_task;
|
||||
use send_task::SendTask;
|
||||
pub use send_task::TaskFinish;
|
||||
|
||||
/// Error and [`Result`] type for sender.
|
||||
mod error;
|
||||
pub use error::{Error, FatalError, JfyiError, Result};
|
||||
|
||||
use self::error::JfyiErrorResult;
|
||||
use crate::{Metrics, LOG_TARGET, SEND_RATE_LIMIT};
|
||||
|
||||
/// Messages as sent by background tasks.
|
||||
#[derive(Debug)]
|
||||
pub enum DisputeSenderMessage {
|
||||
/// A task finished.
|
||||
TaskFinish(TaskFinish),
|
||||
/// A request for active disputes to the dispute-coordinator finished.
|
||||
ActiveDisputesReady(JfyiErrorResult<BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>>),
|
||||
}
|
||||
|
||||
/// The `DisputeSender` keeps track of all ongoing disputes we need to send statements out.
|
||||
///
|
||||
/// For each dispute a `SendTask` is responsible for sending to the concerned validators for that
|
||||
/// particular dispute. The `DisputeSender` keeps track of those tasks, informs them about new
|
||||
/// sessions/validator sets and cleans them up when they become obsolete.
|
||||
///
|
||||
/// The unit of work for the `DisputeSender` is a dispute, represented by `SendTask`s.
|
||||
pub struct DisputeSender<M> {
|
||||
/// All heads we currently consider active.
|
||||
active_heads: Vec<Hash>,
|
||||
|
||||
/// List of currently active sessions.
|
||||
///
|
||||
/// Value is the hash that was used for the query.
|
||||
active_sessions: HashMap<SessionIndex, Hash>,
|
||||
|
||||
/// All ongoing dispute sending this subsystem is aware of.
|
||||
///
|
||||
/// Using an `IndexMap` so items can be iterated in the order of insertion.
|
||||
disputes: IndexMap<CandidateHash, SendTask<M>>,
|
||||
|
||||
/// Sender to be cloned for `SendTask`s.
|
||||
tx: NestingSender<M, DisputeSenderMessage>,
|
||||
|
||||
/// `Some` if we are waiting for a response `DisputeCoordinatorMessage::ActiveDisputes`.
|
||||
waiting_for_active_disputes: Option<WaitForActiveDisputesState>,
|
||||
|
||||
/// Future for delaying too frequent creation of dispute sending tasks.
|
||||
rate_limit: RateLimit,
|
||||
|
||||
/// Metrics for reporting stats about sent requests.
|
||||
metrics: Metrics,
|
||||
}
|
||||
|
||||
/// State we keep while waiting for active disputes.
|
||||
///
|
||||
/// When we send `DisputeCoordinatorMessage::ActiveDisputes`, this is the state we keep while
|
||||
/// waiting for the response.
|
||||
struct WaitForActiveDisputesState {
|
||||
/// Have we seen any new sessions since last refresh?
|
||||
have_new_sessions: bool,
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(DisputeDistribution, prefix = self::overseer)]
|
||||
impl<M: 'static + Send + Sync> DisputeSender<M> {
|
||||
/// Create a new `DisputeSender` which can be used to start dispute sending.
|
||||
pub fn new(tx: NestingSender<M, DisputeSenderMessage>, metrics: Metrics) -> Self {
|
||||
Self {
|
||||
active_heads: Vec::new(),
|
||||
active_sessions: HashMap::new(),
|
||||
disputes: IndexMap::new(),
|
||||
tx,
|
||||
waiting_for_active_disputes: None,
|
||||
rate_limit: RateLimit::new(),
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `SendTask` for a particular new dispute.
|
||||
///
|
||||
/// This function is rate-limited by `SEND_RATE_LIMIT`. It will block if called too frequently
|
||||
/// in order to maintain the limit.
|
||||
pub async fn start_sender<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
msg: DisputeMessage,
|
||||
) -> Result<()> {
|
||||
let req: DisputeRequest = msg.into();
|
||||
let candidate_hash = req.0.candidate_receipt.hash();
|
||||
match self.disputes.entry(candidate_hash) {
|
||||
Entry::Occupied(_) => {
|
||||
gum::trace!(target: LOG_TARGET, ?candidate_hash, "Dispute sending already active.");
|
||||
return Ok(());
|
||||
},
|
||||
Entry::Vacant(vacant) => {
|
||||
self.rate_limit.limit("in start_sender", candidate_hash).await;
|
||||
|
||||
let send_task = SendTask::new(
|
||||
ctx,
|
||||
runtime,
|
||||
&self.active_sessions,
|
||||
NestingSender::new(self.tx.clone(), DisputeSenderMessage::TaskFinish),
|
||||
req,
|
||||
&self.metrics,
|
||||
)
|
||||
.await?;
|
||||
vacant.insert(send_task);
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive message from a background task.
|
||||
pub async fn on_message<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
msg: DisputeSenderMessage,
|
||||
) -> Result<()> {
|
||||
match msg {
|
||||
DisputeSenderMessage::TaskFinish(msg) => {
|
||||
let TaskFinish { candidate_hash, receiver, result } = msg;
|
||||
|
||||
self.metrics.on_sent_request(result.as_metrics_label());
|
||||
|
||||
let task = match self.disputes.get_mut(&candidate_hash) {
|
||||
None => {
|
||||
// Can happen when a dispute ends, with messages still in queue:
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?result,
|
||||
"Received `FromSendingTask::Finished` for non existing dispute."
|
||||
);
|
||||
return Ok(());
|
||||
},
|
||||
Some(task) => task,
|
||||
};
|
||||
task.on_finished_send(&receiver, result);
|
||||
},
|
||||
DisputeSenderMessage::ActiveDisputesReady(result) => {
|
||||
let state = self.waiting_for_active_disputes.take();
|
||||
let have_new_sessions = state.map(|s| s.have_new_sessions).unwrap_or(false);
|
||||
let active_disputes = result?;
|
||||
self.handle_new_active_disputes(ctx, runtime, active_disputes, have_new_sessions)
|
||||
.await?;
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Take care of a change in active leaves.
|
||||
///
|
||||
/// Update our knowledge on sessions and initiate fetching for new active disputes.
|
||||
pub async fn update_leaves<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
update: ActiveLeavesUpdate,
|
||||
) -> Result<()> {
|
||||
let ActiveLeavesUpdate { activated, deactivated } = update;
|
||||
let deactivated: HashSet<_> = deactivated.into_iter().collect();
|
||||
self.active_heads.retain(|h| !deactivated.contains(h));
|
||||
self.active_heads.extend(activated.into_iter().map(|l| l.hash));
|
||||
|
||||
let have_new_sessions = self.refresh_sessions(ctx, runtime).await?;
|
||||
|
||||
// Not yet waiting for data, request an update:
|
||||
match self.waiting_for_active_disputes.take() {
|
||||
None => {
|
||||
self.waiting_for_active_disputes =
|
||||
Some(WaitForActiveDisputesState { have_new_sessions });
|
||||
let mut sender = ctx.sender().clone();
|
||||
let mut tx = self.tx.clone();
|
||||
|
||||
let get_active_disputes_task = async move {
|
||||
let result = get_active_disputes(&mut sender).await;
|
||||
let result =
|
||||
tx.send_message(DisputeSenderMessage::ActiveDisputesReady(result)).await;
|
||||
if let Err(err) = result {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?err,
|
||||
"Sending `DisputeSenderMessage` from background task failed."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.spawn("get_active_disputes", Box::pin(get_active_disputes_task))
|
||||
.map_err(FatalError::SpawnTask)?;
|
||||
},
|
||||
Some(state) => {
|
||||
let have_new_sessions = state.have_new_sessions || have_new_sessions;
|
||||
let new_state = WaitForActiveDisputesState { have_new_sessions };
|
||||
self.waiting_for_active_disputes = Some(new_state);
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Dispute coordinator slow? We are still waiting for data on next active leaves update."
|
||||
);
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle new active disputes response.
|
||||
///
|
||||
/// - Initiate a retry of failed sends which are still active.
|
||||
/// - Get new authorities to send messages to.
|
||||
/// - Get rid of obsolete tasks and disputes.
|
||||
///
|
||||
/// This function ensures the `SEND_RATE_LIMIT`, therefore it might block.
|
||||
async fn handle_new_active_disputes<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
active_disputes: BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>,
|
||||
have_new_sessions: bool,
|
||||
) -> Result<()> {
|
||||
let active_disputes: HashSet<_> =
|
||||
active_disputes.into_iter().map(|((_, c), _)| c).collect();
|
||||
|
||||
// Cleanup obsolete senders (retain keeps order of remaining elements):
|
||||
self.disputes
|
||||
.retain(|candidate_hash, _| active_disputes.contains(candidate_hash));
|
||||
|
||||
// Iterates in order of insertion:
|
||||
let mut should_rate_limit = true;
|
||||
for (candidate_hash, dispute) in self.disputes.iter_mut() {
|
||||
if have_new_sessions || dispute.has_failed_sends() {
|
||||
if should_rate_limit {
|
||||
self.rate_limit
|
||||
.limit("while going through new sessions/failed sends", *candidate_hash)
|
||||
.await;
|
||||
}
|
||||
let sends_happened = dispute
|
||||
.refresh_sends(ctx, runtime, &self.active_sessions, &self.metrics)
|
||||
.await?;
|
||||
// Only rate limit if we actually sent something out _and_ it was not just because
|
||||
// of errors on previous sends.
|
||||
//
|
||||
// Reasoning: It would not be acceptable to slow down the whole subsystem, just
|
||||
// because of a few bad peers having problems. It is actually better to risk
|
||||
// running into their rate limit in that case and accept a minor reputation change.
|
||||
should_rate_limit = sends_happened && have_new_sessions;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Make active sessions correspond to currently active heads.
|
||||
///
|
||||
/// Returns: true if sessions changed.
|
||||
async fn refresh_sessions<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
) -> Result<bool> {
|
||||
let new_sessions = get_active_session_indices(ctx, runtime, &self.active_heads).await?;
|
||||
let new_sessions_raw: HashSet<_> = new_sessions.keys().collect();
|
||||
let old_sessions_raw: HashSet<_> = self.active_sessions.keys().collect();
|
||||
let updated = new_sessions_raw != old_sessions_raw;
|
||||
// Update in any case, so we use current heads for queries:
|
||||
self.active_sessions = new_sessions;
|
||||
Ok(updated)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rate limiting logic.
|
||||
///
|
||||
/// Suitable for the sending side.
|
||||
struct RateLimit {
|
||||
limit: Delay,
|
||||
}
|
||||
|
||||
impl RateLimit {
|
||||
/// Create new `RateLimit` that is immediately ready.
|
||||
fn new() -> Self {
|
||||
// Start with an empty duration, as there has not been any previous call.
|
||||
Self { limit: Delay::new(Duration::new(0, 0)) }
|
||||
}
|
||||
|
||||
/// Initialized with actual `SEND_RATE_LIMIT` duration.
|
||||
fn new_limit() -> Self {
|
||||
Self { limit: Delay::new(SEND_RATE_LIMIT) }
|
||||
}
|
||||
|
||||
/// Wait until ready and prepare for next call.
|
||||
///
|
||||
/// String given as occasion and candidate hash are logged in case the rate limit hit.
|
||||
async fn limit(&mut self, occasion: &'static str, candidate_hash: CandidateHash) {
|
||||
// Wait for rate limit and add some logging:
|
||||
let mut num_wakes: u32 = 0;
|
||||
poll_fn(|cx| {
|
||||
let old_limit = Pin::new(&mut self.limit);
|
||||
match old_limit.poll(cx) {
|
||||
Poll::Pending => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?occasion,
|
||||
?candidate_hash,
|
||||
?num_wakes,
|
||||
"Sending rate limit hit, slowing down requests"
|
||||
);
|
||||
num_wakes += 1;
|
||||
Poll::Pending
|
||||
},
|
||||
Poll::Ready(()) => Poll::Ready(()),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
*self = Self::new_limit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the currently active sessions.
|
||||
///
|
||||
/// List is all indices of all active sessions together with the head that was used for the query.
|
||||
#[overseer::contextbounds(DisputeDistribution, prefix = self::overseer)]
|
||||
async fn get_active_session_indices<Context>(
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
active_heads: &Vec<Hash>,
|
||||
) -> Result<HashMap<SessionIndex, Hash>> {
|
||||
let mut indices = HashMap::new();
|
||||
// Iterate all heads we track as active and fetch the child' session indices.
|
||||
for head in active_heads {
|
||||
let session_index = runtime.get_session_index_for_child(ctx.sender(), *head).await?;
|
||||
// Cache session info
|
||||
if let Err(err) =
|
||||
runtime.get_session_info_by_index(ctx.sender(), *head, session_index).await
|
||||
{
|
||||
gum::debug!(target: LOG_TARGET, ?err, ?session_index, "Can't cache SessionInfo");
|
||||
}
|
||||
indices.insert(session_index, *head);
|
||||
}
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
/// Retrieve Set of active disputes from the dispute coordinator.
|
||||
async fn get_active_disputes<Sender>(
|
||||
sender: &mut Sender,
|
||||
) -> JfyiErrorResult<BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>>
|
||||
where
|
||||
Sender: SubsystemSender<DisputeCoordinatorMessage>,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
sender.send_message(DisputeCoordinatorMessage::ActiveDisputes(tx)).await;
|
||||
rx.await.map_err(|_| JfyiError::AskActiveDisputesCanceled)
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use futures::{Future, FutureExt};
|
||||
|
||||
use pezkuwi_node_network_protocol::{
|
||||
request_response::{
|
||||
outgoing::RequestError,
|
||||
v1::{DisputeRequest, DisputeResponse},
|
||||
OutgoingRequest, OutgoingResult, Recipient, Requests,
|
||||
},
|
||||
IfDisconnected,
|
||||
};
|
||||
use pezkuwi_node_subsystem::{messages::NetworkBridgeTxMessage, overseer};
|
||||
use pezkuwi_node_subsystem_util::{metrics, nesting_sender::NestingSender, runtime::RuntimeInfo};
|
||||
use pezkuwi_primitives::{AuthorityDiscoveryId, CandidateHash, Hash, SessionIndex, ValidatorIndex};
|
||||
|
||||
use super::error::{FatalError, Result};
|
||||
|
||||
use crate::{
|
||||
metrics::{FAILED, SUCCEEDED},
|
||||
Metrics, LOG_TARGET,
|
||||
};
|
||||
|
||||
/// Delivery status for a particular dispute.
|
||||
///
|
||||
/// Keeps track of all the validators that have to be reached for a dispute.
|
||||
///
|
||||
/// The unit of work for a `SendTask` is an authority/validator.
|
||||
pub struct SendTask<M> {
|
||||
/// The request we are supposed to get out to all `teyrchain` validators of the dispute's
|
||||
/// session and to all current authorities.
|
||||
request: DisputeRequest,
|
||||
|
||||
/// The set of authorities we need to send our messages to. This set will change at session
|
||||
/// boundaries. It will always be at least the `teyrchain` validators of the session where the
|
||||
/// dispute happened and the authorities of the current sessions as determined by active heads.
|
||||
deliveries: HashMap<AuthorityDiscoveryId, DeliveryStatus>,
|
||||
|
||||
/// Whether we have any tasks failed since the last refresh.
|
||||
has_failed_sends: bool,
|
||||
|
||||
/// Sender to be cloned for tasks.
|
||||
tx: NestingSender<M, TaskFinish>,
|
||||
}
|
||||
|
||||
/// Status of a particular vote/statement delivery to a particular validator.
|
||||
enum DeliveryStatus {
|
||||
/// Request is still in flight.
|
||||
Pending,
|
||||
/// Succeeded - no need to send request to this peer anymore.
|
||||
Succeeded,
|
||||
}
|
||||
|
||||
/// A sending task finishes with this result:
|
||||
#[derive(Debug)]
|
||||
pub struct TaskFinish {
|
||||
/// The candidate this task was running for.
|
||||
pub candidate_hash: CandidateHash,
|
||||
/// The authority the request was sent to.
|
||||
pub receiver: AuthorityDiscoveryId,
|
||||
/// The result of the delivery attempt.
|
||||
pub result: TaskResult,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TaskResult {
|
||||
/// Task succeeded in getting the request to its peer.
|
||||
Succeeded,
|
||||
/// Task was not able to get the request out to its peer.
|
||||
///
|
||||
/// It should be retried in that case.
|
||||
Failed(RequestError),
|
||||
}
|
||||
|
||||
impl TaskResult {
|
||||
pub fn as_metrics_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Succeeded => SUCCEEDED,
|
||||
Self::Failed(_) => FAILED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(DisputeDistribution, prefix = self::overseer)]
|
||||
impl<M: 'static + Send + Sync> SendTask<M> {
|
||||
/// Initiates sending a dispute message to peers.
|
||||
///
|
||||
/// Creation of new `SendTask`s is subject to rate limiting. As each `SendTask` will trigger
|
||||
/// sending a message to each validator, hence for employing a per-peer rate limit, we need to
|
||||
/// limit the construction of new `SendTask`s.
|
||||
pub async fn new<Context>(
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
active_sessions: &HashMap<SessionIndex, Hash>,
|
||||
tx: NestingSender<M, TaskFinish>,
|
||||
request: DisputeRequest,
|
||||
metrics: &Metrics,
|
||||
) -> Result<Self> {
|
||||
let mut send_task =
|
||||
Self { request, deliveries: HashMap::new(), has_failed_sends: false, tx };
|
||||
send_task.refresh_sends(ctx, runtime, active_sessions, metrics).await?;
|
||||
Ok(send_task)
|
||||
}
|
||||
|
||||
/// Make sure we are sending to all relevant authorities.
|
||||
///
|
||||
/// This function is called at construction and should also be called whenever a session change
|
||||
/// happens and on a regular basis to ensure we are retrying failed attempts.
|
||||
///
|
||||
/// This might resend to validators and is thus subject to any rate limiting we might want.
|
||||
/// Calls to this function for different instances should be rate limited according to
|
||||
/// `SEND_RATE_LIMIT`.
|
||||
///
|
||||
/// Returns: `True` if this call resulted in new requests.
|
||||
pub async fn refresh_sends<Context>(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
active_sessions: &HashMap<SessionIndex, Hash>,
|
||||
metrics: &Metrics,
|
||||
) -> Result<bool> {
|
||||
let new_authorities = self.get_relevant_validators(ctx, runtime, active_sessions).await?;
|
||||
|
||||
// Note this will also contain all authorities for which sending failed previously:
|
||||
let add_authorities: Vec<_> = new_authorities
|
||||
.iter()
|
||||
.filter(|a| !self.deliveries.contains_key(a))
|
||||
.map(Clone::clone)
|
||||
.collect();
|
||||
|
||||
// Get rid of dead/irrelevant tasks/statuses:
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
already_running_deliveries = ?self.deliveries.len(),
|
||||
"Cleaning up deliveries"
|
||||
);
|
||||
self.deliveries.retain(|k, _| new_authorities.contains(k));
|
||||
|
||||
// Start any new tasks that are needed:
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
new_and_failed_authorities = ?add_authorities.len(),
|
||||
overall_authority_set_size = ?new_authorities.len(),
|
||||
already_running_deliveries = ?self.deliveries.len(),
|
||||
"Starting new send requests for authorities."
|
||||
);
|
||||
let new_statuses =
|
||||
send_requests(ctx, self.tx.clone(), add_authorities, self.request.clone(), metrics)
|
||||
.await?;
|
||||
|
||||
let was_empty = new_statuses.is_empty();
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
sent_requests = ?new_statuses.len(),
|
||||
"Requests dispatched."
|
||||
);
|
||||
|
||||
self.has_failed_sends = false;
|
||||
self.deliveries.extend(new_statuses.into_iter());
|
||||
Ok(!was_empty)
|
||||
}
|
||||
|
||||
/// Whether any sends have failed since the last refresh.
|
||||
pub fn has_failed_sends(&self) -> bool {
|
||||
self.has_failed_sends
|
||||
}
|
||||
|
||||
/// Handle a finished response waiting task.
|
||||
///
|
||||
/// Called by `DisputeSender` upon reception of the corresponding message from our spawned
|
||||
/// `wait_response_task`.
|
||||
pub fn on_finished_send(&mut self, authority: &AuthorityDiscoveryId, result: TaskResult) {
|
||||
match result {
|
||||
TaskResult::Failed(err) => {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?authority,
|
||||
candidate_hash = %self.request.0.candidate_receipt.hash(),
|
||||
%err,
|
||||
"Error sending dispute statements to node."
|
||||
);
|
||||
|
||||
self.has_failed_sends = true;
|
||||
// Remove state, so we know what to try again:
|
||||
self.deliveries.remove(authority);
|
||||
},
|
||||
TaskResult::Succeeded => {
|
||||
let status = match self.deliveries.get_mut(&authority) {
|
||||
None => {
|
||||
// Can happen when a sending became irrelevant while the response was
|
||||
// already queued.
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
candidate = ?self.request.0.candidate_receipt.hash(),
|
||||
?authority,
|
||||
?result,
|
||||
"Received `FromSendingTask::Finished` for non existing task."
|
||||
);
|
||||
return;
|
||||
},
|
||||
Some(status) => status,
|
||||
};
|
||||
// We are done here:
|
||||
*status = DeliveryStatus::Succeeded;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine all validators that should receive the given dispute requests.
|
||||
///
|
||||
/// This is all `teyrchain` validators of the session the candidate occurred and all authorities
|
||||
/// of all currently active sessions, determined by currently active heads.
|
||||
async fn get_relevant_validators<Context>(
|
||||
&self,
|
||||
ctx: &mut Context,
|
||||
runtime: &mut RuntimeInfo,
|
||||
active_sessions: &HashMap<SessionIndex, Hash>,
|
||||
) -> Result<HashSet<AuthorityDiscoveryId>> {
|
||||
let ref_head = self.request.0.candidate_receipt.descriptor.relay_parent();
|
||||
// Retrieve all authorities which participated in the teyrchain consensus of the session
|
||||
// in which the candidate was backed.
|
||||
let info = runtime
|
||||
.get_session_info_by_index(ctx.sender(), ref_head, self.request.0.session_index)
|
||||
.await?;
|
||||
let session_info = &info.session_info;
|
||||
let validator_count = session_info.validators.len();
|
||||
let mut authorities: HashSet<_> = session_info
|
||||
.discovery_keys
|
||||
.iter()
|
||||
.take(validator_count)
|
||||
.enumerate()
|
||||
.filter(|(i, _)| Some(ValidatorIndex(*i as _)) != info.validator_info.our_index)
|
||||
.map(|(_, v)| v.clone())
|
||||
.collect();
|
||||
|
||||
// Retrieve all authorities for the current session as indicated by the active
|
||||
// heads we are tracking.
|
||||
for (session_index, head) in active_sessions.iter() {
|
||||
let info =
|
||||
runtime.get_session_info_by_index(ctx.sender(), *head, *session_index).await?;
|
||||
let session_info = &info.session_info;
|
||||
let new_set = session_info
|
||||
.discovery_keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| Some(ValidatorIndex(*i as _)) != info.validator_info.our_index)
|
||||
.map(|(_, v)| v.clone());
|
||||
authorities.extend(new_set);
|
||||
}
|
||||
Ok(authorities)
|
||||
}
|
||||
}
|
||||
|
||||
/// Start sending of the given message to all given authorities.
|
||||
///
|
||||
/// And spawn tasks for handling the response.
|
||||
#[overseer::contextbounds(DisputeDistribution, prefix = self::overseer)]
|
||||
async fn send_requests<Context, M: 'static + Send + Sync>(
|
||||
ctx: &mut Context,
|
||||
tx: NestingSender<M, TaskFinish>,
|
||||
receivers: Vec<AuthorityDiscoveryId>,
|
||||
req: DisputeRequest,
|
||||
metrics: &Metrics,
|
||||
) -> Result<HashMap<AuthorityDiscoveryId, DeliveryStatus>> {
|
||||
let mut statuses = HashMap::with_capacity(receivers.len());
|
||||
let mut reqs = Vec::with_capacity(receivers.len());
|
||||
|
||||
for receiver in receivers {
|
||||
let (outgoing, pending_response) =
|
||||
OutgoingRequest::new(Recipient::Authority(receiver.clone()), req.clone());
|
||||
|
||||
reqs.push(Requests::DisputeSendingV1(outgoing));
|
||||
|
||||
let fut = wait_response_task(
|
||||
pending_response,
|
||||
req.0.candidate_receipt.hash(),
|
||||
receiver.clone(),
|
||||
tx.clone(),
|
||||
metrics.time_dispute_request(),
|
||||
);
|
||||
|
||||
ctx.spawn("dispute-sender", fut.boxed()).map_err(FatalError::SpawnTask)?;
|
||||
statuses.insert(receiver, DeliveryStatus::Pending);
|
||||
}
|
||||
|
||||
let msg = NetworkBridgeTxMessage::SendRequests(reqs, IfDisconnected::ImmediateError);
|
||||
ctx.send_message(msg).await;
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
/// Future to be spawned in a task for awaiting a response.
|
||||
async fn wait_response_task<M: 'static + Send + Sync>(
|
||||
pending_response: impl Future<Output = OutgoingResult<DisputeResponse>>,
|
||||
candidate_hash: CandidateHash,
|
||||
receiver: AuthorityDiscoveryId,
|
||||
mut tx: NestingSender<M, TaskFinish>,
|
||||
_timer: Option<metrics::prometheus::prometheus::HistogramTimer>,
|
||||
) {
|
||||
let result = pending_response.await;
|
||||
let msg = match result {
|
||||
Err(err) => TaskFinish { candidate_hash, receiver, result: TaskResult::Failed(err) },
|
||||
Ok(DisputeResponse::Confirmed) =>
|
||||
TaskFinish { candidate_hash, receiver, result: TaskResult::Succeeded },
|
||||
};
|
||||
if let Err(err) = tx.send_message(msg).await {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
%err,
|
||||
"Failed to notify subsystem about dispute sending result."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
//! Mock data and utility functions for unit tests in this subsystem.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, LazyLock},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use pezkuwi_node_network_protocol::{authority_discovery::AuthorityDiscovery, PeerId};
|
||||
use sc_keystore::LocalKeystore;
|
||||
use sp_application_crypto::AppCrypto;
|
||||
use sp_keyring::Sr25519Keyring;
|
||||
use sp_keystore::{Keystore, KeystorePtr};
|
||||
|
||||
use pezkuwi_node_primitives::{DisputeMessage, SignedDisputeStatement};
|
||||
use pezkuwi_primitives::{
|
||||
AuthorityDiscoveryId, CandidateHash, CandidateReceiptV2 as CandidateReceipt, Hash,
|
||||
SessionIndex, SessionInfo, ValidatorId, ValidatorIndex,
|
||||
};
|
||||
use pezkuwi_primitives_test_helpers::dummy_candidate_descriptor_v2;
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
pub const MOCK_SESSION_INDEX: SessionIndex = 1;
|
||||
pub const MOCK_NEXT_SESSION_INDEX: SessionIndex = 2;
|
||||
pub const MOCK_VALIDATORS: [Sr25519Keyring; 6] = [
|
||||
Sr25519Keyring::Ferdie,
|
||||
Sr25519Keyring::Alice,
|
||||
Sr25519Keyring::Bob,
|
||||
Sr25519Keyring::Charlie,
|
||||
Sr25519Keyring::Dave,
|
||||
Sr25519Keyring::Eve,
|
||||
];
|
||||
|
||||
pub const MOCK_AUTHORITIES_NEXT_SESSION: [Sr25519Keyring; 2] =
|
||||
[Sr25519Keyring::One, Sr25519Keyring::Two];
|
||||
|
||||
pub const FERDIE_INDEX: ValidatorIndex = ValidatorIndex(0);
|
||||
pub const ALICE_INDEX: ValidatorIndex = ValidatorIndex(1);
|
||||
pub const BOB_INDEX: ValidatorIndex = ValidatorIndex(2);
|
||||
pub const CHARLIE_INDEX: ValidatorIndex = ValidatorIndex(3);
|
||||
|
||||
/// Mocked `AuthorityDiscovery` service.
|
||||
pub static MOCK_AUTHORITY_DISCOVERY: LazyLock<MockAuthorityDiscovery> =
|
||||
LazyLock::new(|| MockAuthorityDiscovery::new());
|
||||
// Creating an innocent looking `SessionInfo` is really expensive in a debug build. Around
|
||||
// 700ms on my machine, We therefore cache those keys here:
|
||||
pub static MOCK_VALIDATORS_DISCOVERY_KEYS: LazyLock<HashMap<Sr25519Keyring, AuthorityDiscoveryId>> =
|
||||
LazyLock::new(|| {
|
||||
MOCK_VALIDATORS
|
||||
.iter()
|
||||
.chain(MOCK_AUTHORITIES_NEXT_SESSION.iter())
|
||||
.map(|v| (*v, v.public().into()))
|
||||
.collect()
|
||||
});
|
||||
pub static FERDIE_DISCOVERY_KEY: LazyLock<AuthorityDiscoveryId> =
|
||||
LazyLock::new(|| MOCK_VALIDATORS_DISCOVERY_KEYS.get(&Sr25519Keyring::Ferdie).unwrap().clone());
|
||||
|
||||
pub static MOCK_SESSION_INFO: LazyLock<SessionInfo> = LazyLock::new(|| SessionInfo {
|
||||
validators: MOCK_VALIDATORS.iter().take(4).map(|k| k.public().into()).collect(),
|
||||
discovery_keys: MOCK_VALIDATORS
|
||||
.iter()
|
||||
.map(|k| MOCK_VALIDATORS_DISCOVERY_KEYS.get(&k).unwrap().clone())
|
||||
.collect(),
|
||||
assignment_keys: vec![],
|
||||
validator_groups: Default::default(),
|
||||
n_cores: 0,
|
||||
zeroth_delay_tranche_width: 0,
|
||||
relay_vrf_modulo_samples: 0,
|
||||
n_delay_tranches: 0,
|
||||
no_show_slots: 0,
|
||||
needed_approvals: 0,
|
||||
active_validator_indices: vec![],
|
||||
dispute_period: 6,
|
||||
random_seed: [0u8; 32],
|
||||
});
|
||||
|
||||
/// `SessionInfo` for the second session. (No more validators, but two more authorities.
|
||||
pub static MOCK_NEXT_SESSION_INFO: LazyLock<SessionInfo> = LazyLock::new(|| SessionInfo {
|
||||
discovery_keys: MOCK_AUTHORITIES_NEXT_SESSION
|
||||
.iter()
|
||||
.map(|k| MOCK_VALIDATORS_DISCOVERY_KEYS.get(&k).unwrap().clone())
|
||||
.collect(),
|
||||
validators: Default::default(),
|
||||
assignment_keys: vec![],
|
||||
validator_groups: Default::default(),
|
||||
n_cores: 0,
|
||||
zeroth_delay_tranche_width: 0,
|
||||
relay_vrf_modulo_samples: 0,
|
||||
n_delay_tranches: 0,
|
||||
no_show_slots: 0,
|
||||
needed_approvals: 0,
|
||||
active_validator_indices: vec![],
|
||||
dispute_period: 6,
|
||||
random_seed: [0u8; 32],
|
||||
});
|
||||
|
||||
pub fn make_candidate_receipt(relay_parent: Hash) -> CandidateReceipt {
|
||||
CandidateReceipt {
|
||||
descriptor: dummy_candidate_descriptor_v2(relay_parent),
|
||||
commitments_hash: Hash::random(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_explicit_signed(
|
||||
validator: Sr25519Keyring,
|
||||
candidate_hash: CandidateHash,
|
||||
valid: bool,
|
||||
) -> SignedDisputeStatement {
|
||||
let keystore: KeystorePtr = Arc::new(LocalKeystore::in_memory());
|
||||
Keystore::sr25519_generate_new(&*keystore, ValidatorId::ID, Some(&validator.to_seed()))
|
||||
.expect("Insert key into keystore");
|
||||
|
||||
SignedDisputeStatement::sign_explicit(
|
||||
&keystore,
|
||||
valid,
|
||||
candidate_hash,
|
||||
MOCK_SESSION_INDEX,
|
||||
validator.public().into(),
|
||||
)
|
||||
.expect("Keystore should be fine.")
|
||||
.expect("Signing should work.")
|
||||
}
|
||||
|
||||
pub fn make_dispute_message(
|
||||
candidate: CandidateReceipt,
|
||||
valid_validator: ValidatorIndex,
|
||||
invalid_validator: ValidatorIndex,
|
||||
) -> DisputeMessage {
|
||||
let candidate_hash = candidate.hash();
|
||||
let before_request = Instant::now();
|
||||
let valid_vote =
|
||||
make_explicit_signed(MOCK_VALIDATORS[valid_validator.0 as usize], candidate_hash, true);
|
||||
gum::trace!(
|
||||
"Passed time for valid vote: {:#?}",
|
||||
Instant::now().saturating_duration_since(before_request)
|
||||
);
|
||||
let before_request = Instant::now();
|
||||
let invalid_vote =
|
||||
make_explicit_signed(MOCK_VALIDATORS[invalid_validator.0 as usize], candidate_hash, false);
|
||||
gum::trace!(
|
||||
"Passed time for invalid vote: {:#?}",
|
||||
Instant::now().saturating_duration_since(before_request)
|
||||
);
|
||||
DisputeMessage::from_signed_statements(
|
||||
valid_vote,
|
||||
valid_validator,
|
||||
invalid_vote,
|
||||
invalid_validator,
|
||||
candidate,
|
||||
&MOCK_SESSION_INFO,
|
||||
)
|
||||
.expect("DisputeMessage construction should work.")
|
||||
}
|
||||
|
||||
/// Dummy `AuthorityDiscovery` service.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockAuthorityDiscovery {
|
||||
peer_ids: HashMap<Sr25519Keyring, PeerId>,
|
||||
}
|
||||
|
||||
impl MockAuthorityDiscovery {
|
||||
pub fn new() -> Self {
|
||||
let mut peer_ids = HashMap::new();
|
||||
peer_ids.insert(Sr25519Keyring::Alice, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::Bob, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::Ferdie, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::Charlie, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::Dave, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::Eve, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::One, PeerId::random());
|
||||
peer_ids.insert(Sr25519Keyring::Two, PeerId::random());
|
||||
|
||||
Self { peer_ids }
|
||||
}
|
||||
|
||||
pub fn get_peer_id_by_authority(&self, authority: Sr25519Keyring) -> PeerId {
|
||||
*self.peer_ids.get(&authority).expect("Tester only picks valid authorities")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthorityDiscovery for MockAuthorityDiscovery {
|
||||
async fn get_addresses_by_authority_id(
|
||||
&mut self,
|
||||
_authority: pezkuwi_primitives::AuthorityDiscoveryId,
|
||||
) -> Option<HashSet<sc_network::Multiaddr>> {
|
||||
panic!("Not implemented");
|
||||
}
|
||||
|
||||
async fn get_authority_ids_by_peer_id(
|
||||
&mut self,
|
||||
peer_id: pezkuwi_node_network_protocol::PeerId,
|
||||
) -> Option<HashSet<pezkuwi_primitives::AuthorityDiscoveryId>> {
|
||||
for (a, p) in self.peer_ids.iter() {
|
||||
if p == &peer_id {
|
||||
let result =
|
||||
HashSet::from([MOCK_VALIDATORS_DISCOVERY_KEYS.get(&a).unwrap().clone()]);
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
%peer_id,
|
||||
?result,
|
||||
"Returning authority ids for peer id"
|
||||
);
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,901 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
//! Subsystem unit tests
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
task::Poll,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use codec::{Decode, Encode};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{poll_fn, ready},
|
||||
pin_mut, Future,
|
||||
};
|
||||
use futures_timer::Delay;
|
||||
|
||||
use sc_network::{config::RequestResponseConfig, ProtocolName};
|
||||
|
||||
use pezkuwi_node_network_protocol::{
|
||||
request_response::{v1::DisputeRequest, IncomingRequest, ReqProtocolNames},
|
||||
PeerId,
|
||||
};
|
||||
use sp_keyring::Sr25519Keyring;
|
||||
|
||||
use pezkuwi_node_network_protocol::{
|
||||
request_response::{v1::DisputeResponse, Recipient, Requests},
|
||||
IfDisconnected,
|
||||
};
|
||||
use pezkuwi_node_primitives::DisputeStatus;
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::{
|
||||
AllMessages, DisputeCoordinatorMessage, DisputeDistributionMessage, ImportStatementsResult,
|
||||
NetworkBridgeTxMessage, RuntimeApiMessage, RuntimeApiRequest,
|
||||
},
|
||||
ActiveLeavesUpdate, FromOrchestra, OverseerSignal,
|
||||
};
|
||||
use pezkuwi_node_subsystem_test_helpers::{
|
||||
mock::{make_ferdie_keystore, new_leaf},
|
||||
subsystem_test_harness, TestSubsystemContextHandle,
|
||||
};
|
||||
use pezkuwi_primitives::{
|
||||
AuthorityDiscoveryId, Block, CandidateHash, CandidateReceiptV2 as CandidateReceipt,
|
||||
ExecutorParams, Hash, NodeFeatures, SessionIndex, SessionInfo,
|
||||
};
|
||||
|
||||
use self::mock::{
|
||||
make_candidate_receipt, make_dispute_message, ALICE_INDEX, FERDIE_DISCOVERY_KEY, FERDIE_INDEX,
|
||||
MOCK_AUTHORITY_DISCOVERY, MOCK_NEXT_SESSION_INDEX, MOCK_NEXT_SESSION_INFO, MOCK_SESSION_INDEX,
|
||||
MOCK_SESSION_INFO,
|
||||
};
|
||||
use crate::{
|
||||
receiver::BATCH_COLLECTING_INTERVAL,
|
||||
tests::mock::{BOB_INDEX, CHARLIE_INDEX},
|
||||
DisputeDistributionSubsystem, Metrics, LOG_TARGET, SEND_RATE_LIMIT,
|
||||
};
|
||||
|
||||
/// Useful mock providers.
|
||||
pub mod mock;
|
||||
|
||||
#[test]
|
||||
fn send_dispute_sends_dispute() {
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>, _req_cfg| async move {
|
||||
let _ = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
send_dispute(&mut handle, candidate).await;
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_honors_rate_limit() {
|
||||
sp_tracing::try_init_simple();
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>, _req_cfg| async move {
|
||||
let _ = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let before_request = Instant::now();
|
||||
send_dispute(&mut handle, candidate).await;
|
||||
// First send should not be rate limited:
|
||||
gum::trace!("Passed time: {:#?}", Instant::now().saturating_duration_since(before_request));
|
||||
// This test would likely be flaky on CI:
|
||||
//assert!(Instant::now().saturating_duration_since(before_request) < SEND_RATE_LIMIT);
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
send_dispute(&mut handle, candidate).await;
|
||||
// Second send should be rate limited:
|
||||
gum::trace!(
|
||||
"Passed time for send_dispute: {:#?}",
|
||||
Instant::now().saturating_duration_since(before_request)
|
||||
);
|
||||
assert!(Instant::now() - before_request >= SEND_RATE_LIMIT);
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
/// Helper for sending a new dispute to dispute-distribution sender and handling resulting messages.
|
||||
async fn send_dispute(
|
||||
handle: &mut TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
candidate: CandidateReceipt,
|
||||
) {
|
||||
let before_request = Instant::now();
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
gum::trace!(
|
||||
"Passed time for making message: {:#?}",
|
||||
Instant::now().saturating_duration_since(before_request)
|
||||
);
|
||||
let before_request = Instant::now();
|
||||
handle
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeDistributionMessage::SendDispute(message.clone()),
|
||||
})
|
||||
.await;
|
||||
gum::trace!(
|
||||
"Passed time for sending message: {:#?}",
|
||||
Instant::now().saturating_duration_since(before_request)
|
||||
);
|
||||
|
||||
let expected_receivers = {
|
||||
let info = &MOCK_SESSION_INFO;
|
||||
info.discovery_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|a| a != &Sr25519Keyring::Ferdie.public().into())
|
||||
.collect()
|
||||
// All validators are also authorities in the first session, so we are
|
||||
// done here.
|
||||
};
|
||||
check_sent_requests(handle, expected_receivers, true).await;
|
||||
}
|
||||
|
||||
// Things to test:
|
||||
// x Request triggers import
|
||||
// x Subsequent imports get batched
|
||||
// x Batch gets flushed.
|
||||
// x Batch gets renewed.
|
||||
// x Non authority requests get dropped.
|
||||
// x Sending rate limit is honored.
|
||||
// x Receiving rate limit is honored.
|
||||
// x Duplicate requests on batch are dropped
|
||||
|
||||
#[test]
|
||||
fn received_non_authorities_are_dropped() {
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
mut req_cfg: RequestResponseConfig| async move {
|
||||
let req_tx = req_cfg.inbound_queue.as_mut().unwrap();
|
||||
let _ = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
|
||||
// Non validator request should get dropped:
|
||||
let rx_response =
|
||||
send_network_dispute_request(req_tx, PeerId::random(), message.clone().into()).await;
|
||||
|
||||
assert_matches!(
|
||||
rx_response.await,
|
||||
Ok(resp) => {
|
||||
let sc_network::config::OutgoingResponse {
|
||||
result: _,
|
||||
reputation_changes,
|
||||
sent_feedback: _,
|
||||
} = resp;
|
||||
// Peer should get punished:
|
||||
assert_eq!(reputation_changes.len(), 1);
|
||||
}
|
||||
);
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn received_request_triggers_import() {
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
mut req_cfg: RequestResponseConfig| async move {
|
||||
let req_tx = req_cfg.inbound_queue.as_mut().unwrap();
|
||||
let _ = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
|
||||
nested_network_dispute_request(
|
||||
&mut handle,
|
||||
req_tx,
|
||||
MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Alice),
|
||||
message.clone().into(),
|
||||
ImportStatementsResult::ValidImport,
|
||||
true,
|
||||
move |_handle, _req_tx, _message| ready(()),
|
||||
)
|
||||
.await;
|
||||
|
||||
gum::trace!(target: LOG_TARGET, "Concluding.");
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batching_works() {
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
mut req_cfg: RequestResponseConfig| async move {
|
||||
let req_tx = req_cfg.inbound_queue.as_mut().unwrap();
|
||||
let _ = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
|
||||
// Initial request should get forwarded immediately:
|
||||
nested_network_dispute_request(
|
||||
&mut handle,
|
||||
req_tx,
|
||||
MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Alice),
|
||||
message.clone().into(),
|
||||
ImportStatementsResult::ValidImport,
|
||||
true,
|
||||
move |_handle, _req_tx, _message| ready(()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut rx_responses = Vec::new();
|
||||
|
||||
let message = make_dispute_message(candidate.clone(), BOB_INDEX, FERDIE_INDEX);
|
||||
let peer = MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Bob);
|
||||
rx_responses.push(send_network_dispute_request(req_tx, peer, message.clone().into()).await);
|
||||
|
||||
let message = make_dispute_message(candidate.clone(), CHARLIE_INDEX, FERDIE_INDEX);
|
||||
let peer = MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Charlie);
|
||||
rx_responses.push(send_network_dispute_request(req_tx, peer, message.clone().into()).await);
|
||||
gum::trace!("Imported 3 votes into batch");
|
||||
|
||||
Delay::new(BATCH_COLLECTING_INTERVAL);
|
||||
gum::trace!("Batch should still be alive");
|
||||
// Batch should still be alive (2 new votes):
|
||||
// Let's import two more votes, but fully duplicates - should not extend batch live.
|
||||
gum::trace!("Importing duplicate votes");
|
||||
let mut rx_responses_duplicate = Vec::new();
|
||||
let message = make_dispute_message(candidate.clone(), BOB_INDEX, FERDIE_INDEX);
|
||||
let peer = MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Bob);
|
||||
rx_responses_duplicate
|
||||
.push(send_network_dispute_request(req_tx, peer, message.clone().into()).await);
|
||||
|
||||
let message = make_dispute_message(candidate.clone(), CHARLIE_INDEX, FERDIE_INDEX);
|
||||
let peer = MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Charlie);
|
||||
rx_responses_duplicate
|
||||
.push(send_network_dispute_request(req_tx, peer, message.clone().into()).await);
|
||||
|
||||
for rx_response in rx_responses_duplicate {
|
||||
assert_matches!(
|
||||
rx_response.await,
|
||||
Ok(resp) => {
|
||||
let sc_network::config::OutgoingResponse {
|
||||
result,
|
||||
reputation_changes,
|
||||
sent_feedback: _,
|
||||
} = resp;
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?reputation_changes,
|
||||
"Received reputation changes."
|
||||
);
|
||||
// We don't punish on that.
|
||||
assert_eq!(reputation_changes.len(), 0);
|
||||
|
||||
assert_matches!(result, Err(()));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Delay::new(BATCH_COLLECTING_INTERVAL).await;
|
||||
gum::trace!("Batch should be ready now (only duplicates have been added)");
|
||||
|
||||
let pending_confirmation = assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::DisputeCoordinator(
|
||||
DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt: _,
|
||||
session,
|
||||
statements,
|
||||
pending_confirmation: Some(pending_confirmation),
|
||||
}
|
||||
) => {
|
||||
assert_eq!(session, MOCK_SESSION_INDEX);
|
||||
assert_eq!(statements.len(), 3);
|
||||
pending_confirmation
|
||||
}
|
||||
);
|
||||
pending_confirmation.send(ImportStatementsResult::ValidImport).unwrap();
|
||||
|
||||
for rx_response in rx_responses {
|
||||
assert_matches!(
|
||||
rx_response.await,
|
||||
Ok(resp) => {
|
||||
let sc_network::config::OutgoingResponse {
|
||||
result,
|
||||
reputation_changes: _,
|
||||
sent_feedback,
|
||||
} = resp;
|
||||
|
||||
let result = result.unwrap();
|
||||
let decoded =
|
||||
<DisputeResponse as Decode>::decode(&mut result.as_slice()).unwrap();
|
||||
|
||||
assert!(decoded == DisputeResponse::Confirmed);
|
||||
if let Some(sent_feedback) = sent_feedback {
|
||||
sent_feedback.send(()).unwrap();
|
||||
}
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Valid import happened."
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
gum::trace!(target: LOG_TARGET, "Concluding.");
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receive_rate_limit_is_enforced() {
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
mut req_cfg: RequestResponseConfig| async move {
|
||||
let req_tx = req_cfg.inbound_queue.as_mut().unwrap();
|
||||
let _ = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
|
||||
// Initial request should get forwarded immediately:
|
||||
nested_network_dispute_request(
|
||||
&mut handle,
|
||||
req_tx,
|
||||
MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Alice),
|
||||
message.clone().into(),
|
||||
ImportStatementsResult::ValidImport,
|
||||
true,
|
||||
move |_handle, _req_tx, _message| ready(()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut rx_responses = Vec::new();
|
||||
|
||||
let peer = MOCK_AUTHORITY_DISCOVERY.get_peer_id_by_authority(Sr25519Keyring::Bob);
|
||||
|
||||
let message = make_dispute_message(candidate.clone(), BOB_INDEX, FERDIE_INDEX);
|
||||
rx_responses.push(send_network_dispute_request(req_tx, peer, message.clone().into()).await);
|
||||
|
||||
let message = make_dispute_message(candidate.clone(), CHARLIE_INDEX, FERDIE_INDEX);
|
||||
rx_responses.push(send_network_dispute_request(req_tx, peer, message.clone().into()).await);
|
||||
|
||||
gum::trace!("Import one too much:");
|
||||
|
||||
let message = make_dispute_message(candidate.clone(), CHARLIE_INDEX, ALICE_INDEX);
|
||||
let rx_response_flood =
|
||||
send_network_dispute_request(req_tx, peer, message.clone().into()).await;
|
||||
|
||||
assert_matches!(
|
||||
rx_response_flood.await,
|
||||
Ok(resp) => {
|
||||
let sc_network::config::OutgoingResponse {
|
||||
result,
|
||||
reputation_changes: _,
|
||||
sent_feedback: _,
|
||||
} = resp;
|
||||
// Received error because of flood.
|
||||
assert!(!result.is_ok());
|
||||
}
|
||||
);
|
||||
gum::trace!("Need to wait 2 patch intervals:");
|
||||
Delay::new(BATCH_COLLECTING_INTERVAL).await;
|
||||
Delay::new(BATCH_COLLECTING_INTERVAL).await;
|
||||
|
||||
gum::trace!("Batch should be ready now");
|
||||
|
||||
let pending_confirmation = assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::DisputeCoordinator(
|
||||
DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt: _,
|
||||
session,
|
||||
statements,
|
||||
pending_confirmation: Some(pending_confirmation),
|
||||
}
|
||||
) => {
|
||||
assert_eq!(session, MOCK_SESSION_INDEX);
|
||||
// Only 3 as fourth was flood:
|
||||
assert_eq!(statements.len(), 3);
|
||||
pending_confirmation
|
||||
}
|
||||
);
|
||||
pending_confirmation.send(ImportStatementsResult::ValidImport).unwrap();
|
||||
|
||||
for rx_response in rx_responses {
|
||||
assert_matches!(
|
||||
rx_response.await,
|
||||
Ok(resp) => {
|
||||
let sc_network::config::OutgoingResponse {
|
||||
result,
|
||||
reputation_changes: _,
|
||||
sent_feedback,
|
||||
} = resp;
|
||||
|
||||
let result = result.unwrap();
|
||||
let decoded =
|
||||
<DisputeResponse as Decode>::decode(&mut result.as_slice()).unwrap();
|
||||
|
||||
assert!(decoded == DisputeResponse::Confirmed);
|
||||
if let Some(sent_feedback) = sent_feedback {
|
||||
sent_feedback.send(()).unwrap();
|
||||
}
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Valid import happened."
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
gum::trace!(target: LOG_TARGET, "Concluding.");
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_dispute_gets_cleaned_up() {
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>, _| async move {
|
||||
let old_head = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
handle
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeDistributionMessage::SendDispute(message.clone()),
|
||||
})
|
||||
.await;
|
||||
|
||||
let expected_receivers = {
|
||||
let info = &MOCK_SESSION_INFO;
|
||||
info.discovery_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|a| a != &Sr25519Keyring::Ferdie.public().into())
|
||||
.collect()
|
||||
// All validators are also authorities in the first session, so we are
|
||||
// done here.
|
||||
};
|
||||
check_sent_requests(&mut handle, expected_receivers, false).await;
|
||||
|
||||
// Give tasks a chance to finish:
|
||||
Delay::new(Duration::from_millis(20)).await;
|
||||
|
||||
activate_leaf(
|
||||
&mut handle,
|
||||
Hash::random(),
|
||||
Some(old_head),
|
||||
MOCK_SESSION_INDEX,
|
||||
None,
|
||||
// No disputes any more:
|
||||
BTreeMap::new(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Yield, so subsystem can make progress:
|
||||
Delay::new(Duration::from_millis(2)).await;
|
||||
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispute_retries_and_works_across_session_boundaries() {
|
||||
sp_tracing::try_init_simple();
|
||||
let test = |mut handle: TestSubsystemContextHandle<DisputeDistributionMessage>, _| async move {
|
||||
let old_head = handle_subsystem_startup(&mut handle, None).await;
|
||||
|
||||
let relay_parent = Hash::random();
|
||||
let candidate = make_candidate_receipt(relay_parent);
|
||||
let message = make_dispute_message(candidate.clone(), ALICE_INDEX, FERDIE_INDEX);
|
||||
handle
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeDistributionMessage::SendDispute(message.clone()),
|
||||
})
|
||||
.await;
|
||||
|
||||
let expected_receivers: HashSet<_> = {
|
||||
let info = &MOCK_SESSION_INFO;
|
||||
info.discovery_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|a| a != &Sr25519Keyring::Ferdie.public().into())
|
||||
.collect()
|
||||
// All validators are also authorities in the first session, so we are
|
||||
// done here.
|
||||
};
|
||||
// Requests don't get confirmed - dispute is carried over to next session.
|
||||
check_sent_requests(&mut handle, expected_receivers.clone(), false).await;
|
||||
|
||||
// Give tasks a chance to finish:
|
||||
Delay::new(Duration::from_millis(20)).await;
|
||||
|
||||
// Trigger retry:
|
||||
let old_head2 = Hash::random();
|
||||
activate_leaf(
|
||||
&mut handle,
|
||||
old_head2,
|
||||
Some(old_head),
|
||||
MOCK_SESSION_INDEX,
|
||||
None,
|
||||
BTreeMap::from([((MOCK_SESSION_INDEX, candidate.hash()), DisputeStatus::Active)]),
|
||||
)
|
||||
.await;
|
||||
|
||||
check_sent_requests(&mut handle, expected_receivers.clone(), false).await;
|
||||
// Give tasks a chance to finish:
|
||||
Delay::new(Duration::from_millis(20)).await;
|
||||
|
||||
// Session change:
|
||||
activate_leaf(
|
||||
&mut handle,
|
||||
Hash::random(),
|
||||
Some(old_head2),
|
||||
MOCK_NEXT_SESSION_INDEX,
|
||||
Some(MOCK_NEXT_SESSION_INFO.clone()),
|
||||
BTreeMap::from([((MOCK_SESSION_INDEX, candidate.hash()), DisputeStatus::Active)]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let expected_receivers = {
|
||||
let validator_count = MOCK_SESSION_INFO.validators.len();
|
||||
let old_validators = MOCK_SESSION_INFO
|
||||
.discovery_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.take(validator_count)
|
||||
.filter(|a| *a != *FERDIE_DISCOVERY_KEY);
|
||||
|
||||
MOCK_NEXT_SESSION_INFO
|
||||
.discovery_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|a| *a != *FERDIE_DISCOVERY_KEY)
|
||||
.chain(old_validators)
|
||||
.collect()
|
||||
};
|
||||
check_sent_requests(&mut handle, expected_receivers, true).await;
|
||||
|
||||
conclude(&mut handle).await;
|
||||
};
|
||||
test_harness(test);
|
||||
}
|
||||
|
||||
async fn send_network_dispute_request(
|
||||
req_tx: &mut async_channel::Sender<sc_network::config::IncomingRequest>,
|
||||
peer: PeerId,
|
||||
message: DisputeRequest,
|
||||
) -> oneshot::Receiver<sc_network::config::OutgoingResponse> {
|
||||
let (pending_response, rx_response) = oneshot::channel();
|
||||
let req =
|
||||
sc_network::config::IncomingRequest { peer, payload: message.encode(), pending_response };
|
||||
req_tx.send(req).await.unwrap();
|
||||
rx_response
|
||||
}
|
||||
|
||||
/// Send request and handle its reactions.
|
||||
///
|
||||
/// Passed in function will be called while votes are still being imported.
|
||||
async fn nested_network_dispute_request<'a, F, O>(
|
||||
handle: &'a mut TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
req_tx: &'a mut async_channel::Sender<sc_network::config::IncomingRequest>,
|
||||
peer: PeerId,
|
||||
message: DisputeRequest,
|
||||
import_result: ImportStatementsResult,
|
||||
need_session_info: bool,
|
||||
inner: F,
|
||||
) where
|
||||
F: FnOnce(
|
||||
&'a mut TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
&'a mut async_channel::Sender<sc_network::config::IncomingRequest>,
|
||||
DisputeRequest,
|
||||
) -> O
|
||||
+ 'a,
|
||||
O: Future<Output = ()> + 'a,
|
||||
{
|
||||
let rx_response = send_network_dispute_request(req_tx, peer, message.clone().into()).await;
|
||||
|
||||
if need_session_info {
|
||||
// Subsystem might need `SessionInfo` for determining indices:
|
||||
match handle.recv().await {
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::SessionInfo(_, tx),
|
||||
)) => {
|
||||
tx.send(Ok(Some(MOCK_SESSION_INFO.clone())))
|
||||
.expect("Receiver should stay alive.");
|
||||
},
|
||||
unexpected => panic!("Unexpected message {:?}", unexpected),
|
||||
}
|
||||
match handle.recv().await {
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::SessionExecutorParams(_, tx),
|
||||
)) => {
|
||||
tx.send(Ok(Some(ExecutorParams::default())))
|
||||
.expect("Receiver should stay alive.");
|
||||
},
|
||||
unexpected => panic!("Unexpected message {:?}", unexpected),
|
||||
}
|
||||
|
||||
match handle.recv().await {
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::NodeFeatures(_, si_tx),
|
||||
)) => {
|
||||
si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
|
||||
},
|
||||
unexpected => panic!("Unexpected message {:?}", unexpected),
|
||||
}
|
||||
}
|
||||
|
||||
// Import should get initiated:
|
||||
let pending_confirmation = assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::DisputeCoordinator(
|
||||
DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt,
|
||||
session,
|
||||
statements,
|
||||
pending_confirmation: Some(pending_confirmation),
|
||||
}
|
||||
) => {
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
assert_eq!(session, MOCK_SESSION_INDEX);
|
||||
assert_eq!(candidate_hash, message.0.candidate_receipt.hash());
|
||||
assert_eq!(statements.len(), 2);
|
||||
pending_confirmation
|
||||
}
|
||||
);
|
||||
|
||||
// Do the inner thing:
|
||||
inner(handle, req_tx, message).await;
|
||||
|
||||
// Confirm import
|
||||
pending_confirmation.send(import_result).unwrap();
|
||||
|
||||
assert_matches!(
|
||||
rx_response.await,
|
||||
Ok(resp) => {
|
||||
let sc_network::config::OutgoingResponse {
|
||||
result,
|
||||
reputation_changes,
|
||||
sent_feedback,
|
||||
} = resp;
|
||||
|
||||
match import_result {
|
||||
ImportStatementsResult::ValidImport => {
|
||||
let result = result.unwrap();
|
||||
let decoded =
|
||||
<DisputeResponse as Decode>::decode(&mut result.as_slice()).unwrap();
|
||||
|
||||
assert!(decoded == DisputeResponse::Confirmed);
|
||||
if let Some(sent_feedback) = sent_feedback {
|
||||
sent_feedback.send(()).unwrap();
|
||||
}
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Valid import happened."
|
||||
);
|
||||
|
||||
}
|
||||
ImportStatementsResult::InvalidImport => {
|
||||
// Peer should get punished:
|
||||
assert_eq!(reputation_changes.len(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async fn conclude(handle: &mut TestSubsystemContextHandle<DisputeDistributionMessage>) {
|
||||
// No more messages should be in the queue:
|
||||
poll_fn(|ctx| {
|
||||
let fut = handle.recv();
|
||||
pin_mut!(fut);
|
||||
// No requests should be initiated, as there is no longer any dispute active:
|
||||
assert_matches!(fut.poll(ctx), Poll::Pending, "No requests expected");
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
|
||||
handle.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
}
|
||||
|
||||
/// Pass a `new_session` if you expect the subsystem to retrieve `SessionInfo` when given the
|
||||
/// `session_index`.
|
||||
async fn activate_leaf(
|
||||
handle: &mut TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
activate: Hash,
|
||||
deactivate: Option<Hash>,
|
||||
session_index: SessionIndex,
|
||||
// New session if we expect the subsystem to request it.
|
||||
new_session: Option<SessionInfo>,
|
||||
// Currently active disputes to send to the subsystem.
|
||||
active_disputes: BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>,
|
||||
) {
|
||||
handle
|
||||
.send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate {
|
||||
activated: Some(new_leaf(activate, 10)),
|
||||
deactivated: deactivate.into_iter().collect(),
|
||||
})))
|
||||
.await;
|
||||
assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
h,
|
||||
RuntimeApiRequest::SessionIndexForChild(tx)
|
||||
)) => {
|
||||
assert_eq!(h, activate);
|
||||
tx.send(Ok(session_index)).expect("Receiver should stay alive.");
|
||||
}
|
||||
);
|
||||
|
||||
if let Some(session_info) = new_session {
|
||||
assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
h,
|
||||
RuntimeApiRequest::SessionInfo(session_idx, tx)
|
||||
)) => {
|
||||
assert_eq!(h, activate);
|
||||
assert_eq!(session_index, session_idx);
|
||||
tx.send(Ok(Some(session_info))).expect("Receiver should stay alive.");
|
||||
}
|
||||
);
|
||||
assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
h,
|
||||
RuntimeApiRequest::SessionExecutorParams(session_idx, tx)
|
||||
)) => {
|
||||
assert_eq!(h, activate);
|
||||
assert_eq!(session_index, session_idx);
|
||||
tx.send(Ok(Some(ExecutorParams::default()))).expect("Receiver should stay alive.");
|
||||
}
|
||||
);
|
||||
assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::RuntimeApi(
|
||||
RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
|
||||
) => {
|
||||
si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::ActiveDisputes(tx)) => {
|
||||
tx.send(active_disputes).expect("Receiver should stay alive.");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Check whether sent network bridge requests match the expectation.
|
||||
async fn check_sent_requests(
|
||||
handle: &mut TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
expected_receivers: HashSet<AuthorityDiscoveryId>,
|
||||
confirm_receive: bool,
|
||||
) {
|
||||
let expected_receivers: HashSet<_> =
|
||||
expected_receivers.into_iter().map(Recipient::Authority).collect();
|
||||
|
||||
// Sends to concerned validators:
|
||||
assert_matches!(
|
||||
handle.recv().await,
|
||||
AllMessages::NetworkBridgeTx(
|
||||
NetworkBridgeTxMessage::SendRequests(reqs, IfDisconnected::ImmediateError)
|
||||
) => {
|
||||
let reqs: Vec<_> = reqs.into_iter().map(|r|
|
||||
assert_matches!(
|
||||
r,
|
||||
Requests::DisputeSendingV1(req) => {req}
|
||||
)
|
||||
)
|
||||
.collect();
|
||||
|
||||
let receivers_raw: Vec<_> = reqs.iter().map(|r| r.peer.clone()).collect();
|
||||
let receivers: HashSet<_> = receivers_raw.clone().clone().into_iter().collect();
|
||||
assert_eq!(receivers_raw.len(), receivers.len(), "No duplicates are expected.");
|
||||
assert_eq!(receivers.len(), expected_receivers.len());
|
||||
assert_eq!(receivers, expected_receivers);
|
||||
if confirm_receive {
|
||||
for req in reqs {
|
||||
req.pending_response.send(
|
||||
Ok((DisputeResponse::Confirmed.encode(), ProtocolName::from("")))
|
||||
)
|
||||
.expect("Subsystem should be listening for a response.");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize subsystem and return request sender needed for sending incoming requests to the
|
||||
/// subsystem.
|
||||
async fn handle_subsystem_startup(
|
||||
handle: &mut TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
ongoing_dispute: Option<CandidateHash>,
|
||||
) -> Hash {
|
||||
let relay_parent = Hash::random();
|
||||
activate_leaf(
|
||||
handle,
|
||||
relay_parent,
|
||||
None,
|
||||
MOCK_SESSION_INDEX,
|
||||
Some(MOCK_SESSION_INFO.clone()),
|
||||
ongoing_dispute
|
||||
.into_iter()
|
||||
.map(|c| ((MOCK_SESSION_INDEX, c), DisputeStatus::Active))
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
relay_parent
|
||||
}
|
||||
|
||||
/// Launch subsystem and provided test function
|
||||
///
|
||||
/// which simulates the overseer.
|
||||
fn test_harness<TestFn, Fut>(test: TestFn)
|
||||
where
|
||||
TestFn: FnOnce(
|
||||
TestSubsystemContextHandle<DisputeDistributionMessage>,
|
||||
RequestResponseConfig,
|
||||
) -> Fut,
|
||||
Fut: Future<Output = ()>,
|
||||
{
|
||||
sp_tracing::try_init_simple();
|
||||
let keystore = make_ferdie_keystore();
|
||||
|
||||
let genesis_hash = Hash::repeat_byte(0xff);
|
||||
let req_protocol_names = ReqProtocolNames::new(&genesis_hash, None);
|
||||
let (req_receiver, req_cfg) = IncomingRequest::get_config_receiver::<
|
||||
Block,
|
||||
sc_network::NetworkWorker<Block, Hash>,
|
||||
>(&req_protocol_names);
|
||||
let subsystem = DisputeDistributionSubsystem::new(
|
||||
keystore,
|
||||
req_receiver,
|
||||
MOCK_AUTHORITY_DISCOVERY.clone(),
|
||||
Metrics::new_dummy(),
|
||||
);
|
||||
|
||||
let subsystem = |ctx| async {
|
||||
match subsystem.run(ctx).await {
|
||||
Ok(()) => {},
|
||||
Err(fatal) => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?fatal,
|
||||
"Dispute distribution exited with fatal error."
|
||||
);
|
||||
},
|
||||
}
|
||||
};
|
||||
subsystem_test_harness(|handle| test(handle, req_cfg), subsystem);
|
||||
}
|
||||
Reference in New Issue
Block a user